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