|
| 1 | +#!/usr/bin/python2 |
| 2 | +''' |
| 3 | +Perform basic ELF security checks on a series of executables. |
| 4 | +Exit status will be 0 if succesful, and the program will be silent. |
| 5 | +Otherwise the exit status will be 1 and it will log which executables failed which checks. |
| 6 | +Needs `readelf` (for ELF) and `objdump` (for PE). |
| 7 | +''' |
| 8 | +from __future__ import division,print_function |
| 9 | +import subprocess |
| 10 | +import sys |
| 11 | +import os |
| 12 | + |
| 13 | +READELF_CMD = os.getenv('READELF', '/usr/bin/readelf') |
| 14 | +OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump') |
| 15 | + |
| 16 | +def check_ELF_PIE(executable): |
| 17 | + ''' |
| 18 | + Check for position independent executable (PIE), allowing for address space randomization. |
| 19 | + ''' |
| 20 | + p = subprocess.Popen([READELF_CMD, '-h', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) |
| 21 | + (stdout, stderr) = p.communicate() |
| 22 | + if p.returncode: |
| 23 | + raise IOError('Error opening file') |
| 24 | + |
| 25 | + ok = False |
| 26 | + for line in stdout.split('\n'): |
| 27 | + line = line.split() |
| 28 | + if len(line)>=2 and line[0] == 'Type:' and line[1] == 'DYN': |
| 29 | + ok = True |
| 30 | + return ok |
| 31 | + |
| 32 | +def get_ELF_program_headers(executable): |
| 33 | + '''Return type and flags for ELF program headers''' |
| 34 | + p = subprocess.Popen([READELF_CMD, '-l', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) |
| 35 | + (stdout, stderr) = p.communicate() |
| 36 | + if p.returncode: |
| 37 | + raise IOError('Error opening file') |
| 38 | + in_headers = False |
| 39 | + count = 0 |
| 40 | + headers = [] |
| 41 | + for line in stdout.split('\n'): |
| 42 | + if line.startswith('Program Headers:'): |
| 43 | + in_headers = True |
| 44 | + if line == '': |
| 45 | + in_headers = False |
| 46 | + if in_headers: |
| 47 | + if count == 1: # header line |
| 48 | + ofs_typ = line.find('Type') |
| 49 | + ofs_offset = line.find('Offset') |
| 50 | + ofs_flags = line.find('Flg') |
| 51 | + ofs_align = line.find('Align') |
| 52 | + if ofs_typ == -1 or ofs_offset == -1 or ofs_flags == -1 or ofs_align == -1: |
| 53 | + raise ValueError('Cannot parse elfread -lW output') |
| 54 | + elif count > 1: |
| 55 | + typ = line[ofs_typ:ofs_offset].rstrip() |
| 56 | + flags = line[ofs_flags:ofs_align].rstrip() |
| 57 | + headers.append((typ, flags)) |
| 58 | + count += 1 |
| 59 | + return headers |
| 60 | + |
| 61 | +def check_ELF_NX(executable): |
| 62 | + ''' |
| 63 | + Check that no sections are writable and executable (including the stack) |
| 64 | + ''' |
| 65 | + have_wx = False |
| 66 | + have_gnu_stack = False |
| 67 | + for (typ, flags) in get_ELF_program_headers(executable): |
| 68 | + if typ == 'GNU_STACK': |
| 69 | + have_gnu_stack = True |
| 70 | + if 'W' in flags and 'E' in flags: # section is both writable and executable |
| 71 | + have_wx = True |
| 72 | + return have_gnu_stack and not have_wx |
| 73 | + |
| 74 | +def check_ELF_RELRO(executable): |
| 75 | + ''' |
| 76 | + Check for read-only relocations. |
| 77 | + GNU_RELRO program header must exist |
| 78 | + Dynamic section must have BIND_NOW flag |
| 79 | + ''' |
| 80 | + have_gnu_relro = False |
| 81 | + for (typ, flags) in get_ELF_program_headers(executable): |
| 82 | + # Note: not checking flags == 'R': here as linkers set the permission differently |
| 83 | + # This does not affect security: the permission flags of the GNU_RELRO program header are ignored, the PT_LOAD header determines the effective permissions. |
| 84 | + # However, the dynamic linker need to write to this area so these are RW. |
| 85 | + # Glibc itself takes care of mprotecting this area R after relocations are finished. |
| 86 | + # See also http://permalink.gmane.org/gmane.comp.gnu.binutils/71347 |
| 87 | + if typ == 'GNU_RELRO': |
| 88 | + have_gnu_relro = True |
| 89 | + |
| 90 | + have_bindnow = False |
| 91 | + p = subprocess.Popen([READELF_CMD, '-d', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) |
| 92 | + (stdout, stderr) = p.communicate() |
| 93 | + if p.returncode: |
| 94 | + raise IOError('Error opening file') |
| 95 | + for line in stdout.split('\n'): |
| 96 | + tokens = line.split() |
| 97 | + if len(tokens)>1 and tokens[1] == '(BIND_NOW)': |
| 98 | + have_bindnow = True |
| 99 | + return have_gnu_relro and have_bindnow |
| 100 | + |
| 101 | +def check_ELF_Canary(executable): |
| 102 | + ''' |
| 103 | + Check for use of stack canary |
| 104 | + ''' |
| 105 | + p = subprocess.Popen([READELF_CMD, '--dyn-syms', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) |
| 106 | + (stdout, stderr) = p.communicate() |
| 107 | + if p.returncode: |
| 108 | + raise IOError('Error opening file') |
| 109 | + ok = False |
| 110 | + for line in stdout.split('\n'): |
| 111 | + if '__stack_chk_fail' in line: |
| 112 | + ok = True |
| 113 | + return ok |
| 114 | + |
| 115 | +def get_PE_dll_characteristics(executable): |
| 116 | + ''' |
| 117 | + Get PE DllCharacteristics bits |
| 118 | + ''' |
| 119 | + p = subprocess.Popen([OBJDUMP_CMD, '-x', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) |
| 120 | + (stdout, stderr) = p.communicate() |
| 121 | + if p.returncode: |
| 122 | + raise IOError('Error opening file') |
| 123 | + for line in stdout.split('\n'): |
| 124 | + tokens = line.split() |
| 125 | + if len(tokens)>=2 and tokens[0] == 'DllCharacteristics': |
| 126 | + return int(tokens[1],16) |
| 127 | + return 0 |
| 128 | + |
| 129 | + |
| 130 | +def check_PE_PIE(executable): |
| 131 | + '''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)''' |
| 132 | + return bool(get_PE_dll_characteristics(executable) & 0x40) |
| 133 | + |
| 134 | +def check_PE_NX(executable): |
| 135 | + '''NX: DllCharacteristics bit 0x100 signifies nxcompat (DEP)''' |
| 136 | + return bool(get_PE_dll_characteristics(executable) & 0x100) |
| 137 | + |
| 138 | +CHECKS = { |
| 139 | +'ELF': [ |
| 140 | + ('PIE', check_ELF_PIE), |
| 141 | + ('NX', check_ELF_NX), |
| 142 | + ('RELRO', check_ELF_RELRO), |
| 143 | + ('Canary', check_ELF_Canary) |
| 144 | +], |
| 145 | +'PE': [ |
| 146 | + ('PIE', check_PE_PIE), |
| 147 | + ('NX', check_PE_NX) |
| 148 | +] |
| 149 | +} |
| 150 | + |
| 151 | +def identify_executable(executable): |
| 152 | + with open(filename, 'rb') as f: |
| 153 | + magic = f.read(4) |
| 154 | + if magic.startswith(b'MZ'): |
| 155 | + return 'PE' |
| 156 | + elif magic.startswith(b'\x7fELF'): |
| 157 | + return 'ELF' |
| 158 | + return None |
| 159 | + |
| 160 | +if __name__ == '__main__': |
| 161 | + retval = 0 |
| 162 | + for filename in sys.argv[1:]: |
| 163 | + try: |
| 164 | + etype = identify_executable(filename) |
| 165 | + if etype is None: |
| 166 | + print('%s: unknown format' % filename) |
| 167 | + retval = 1 |
| 168 | + continue |
| 169 | + |
| 170 | + failed = [] |
| 171 | + for (name, func) in CHECKS[etype]: |
| 172 | + if not func(filename): |
| 173 | + failed.append(name) |
| 174 | + if failed: |
| 175 | + print('%s: failed %s' % (filename, ' '.join(failed))) |
| 176 | + retval = 1 |
| 177 | + except IOError: |
| 178 | + print('%s: cannot open' % filename) |
| 179 | + retval = 1 |
| 180 | + exit(retval) |
| 181 | + |
0 commit comments