2017-12-12 20:47:24 +01:00
#!/usr/bin/env python3
2018-07-27 00:36:45 +02:00
# Copyright (c) 2015-2018 The Bitcoin Core developers
2016-09-11 23:32:22 +02:00
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
2015-10-19 14:53:56 +02:00
'''
2020-01-02 07:42:21 +01:00
Perform basic security checks on a series of executables .
2016-01-08 22:31:42 +01:00
Exit status will be 0 if successful , and the program will be silent .
2015-10-19 14:53:56 +02:00
Otherwise the exit status will be 1 and it will log which executables failed which checks .
2020-01-02 07:42:21 +01:00
Needs ` readelf ` ( for ELF ) , ` objdump ` ( for PE ) and ` otool ` ( for MACHO ) .
2015-10-19 14:53:56 +02:00
'''
import subprocess
import sys
import os
READELF_CMD = os . getenv ( ' READELF ' , ' /usr/bin/readelf ' )
OBJDUMP_CMD = os . getenv ( ' OBJDUMP ' , ' /usr/bin/objdump ' )
2020-01-02 07:42:21 +01:00
OTOOL_CMD = os . getenv ( ' OTOOL ' , ' /usr/bin/otool ' )
2018-07-26 11:09:49 +02:00
NONFATAL = { } # checks which are non-fatal for now but only generate a warning
2015-10-19 14:53:56 +02:00
def check_ELF_PIE ( executable ) :
'''
Check for position independent executable ( PIE ) , allowing for address space randomization .
'''
2018-03-28 19:37:45 +02:00
p = subprocess . Popen ( [ READELF_CMD , ' -h ' , ' -W ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE , universal_newlines = True )
2015-10-19 14:53:56 +02:00
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
ok = False
2018-03-28 19:37:45 +02:00
for line in stdout . splitlines ( ) :
2015-10-19 14:53:56 +02:00
line = line . split ( )
2018-03-28 19:37:45 +02:00
if len ( line ) > = 2 and line [ 0 ] == ' Type: ' and line [ 1 ] == ' DYN ' :
2015-10-19 14:53:56 +02:00
ok = True
return ok
def get_ELF_program_headers ( executable ) :
''' Return type and flags for ELF program headers '''
2018-03-28 19:37:45 +02:00
p = subprocess . Popen ( [ READELF_CMD , ' -l ' , ' -W ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE , universal_newlines = True )
2015-10-19 14:53:56 +02:00
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
in_headers = False
count = 0
headers = [ ]
2018-03-28 19:37:45 +02:00
for line in stdout . splitlines ( ) :
if line . startswith ( ' Program Headers: ' ) :
2015-10-19 14:53:56 +02:00
in_headers = True
2018-03-28 19:37:45 +02:00
if line == ' ' :
2015-10-19 14:53:56 +02:00
in_headers = False
if in_headers :
if count == 1 : # header line
2018-03-28 19:37:45 +02:00
ofs_typ = line . find ( ' Type ' )
ofs_offset = line . find ( ' Offset ' )
ofs_flags = line . find ( ' Flg ' )
ofs_align = line . find ( ' Align ' )
2015-10-19 14:53:56 +02:00
if ofs_typ == - 1 or ofs_offset == - 1 or ofs_flags == - 1 or ofs_align == - 1 :
raise ValueError ( ' Cannot parse elfread -lW output ' )
elif count > 1 :
typ = line [ ofs_typ : ofs_offset ] . rstrip ( )
flags = line [ ofs_flags : ofs_align ] . rstrip ( )
headers . append ( ( typ , flags ) )
count + = 1
return headers
def check_ELF_NX ( executable ) :
'''
Check that no sections are writable and executable ( including the stack )
'''
have_wx = False
have_gnu_stack = False
for ( typ , flags ) in get_ELF_program_headers ( executable ) :
2018-03-28 19:37:45 +02:00
if typ == ' GNU_STACK ' :
2015-10-19 14:53:56 +02:00
have_gnu_stack = True
2018-03-28 19:37:45 +02:00
if ' W ' in flags and ' E ' in flags : # section is both writable and executable
2015-10-19 14:53:56 +02:00
have_wx = True
return have_gnu_stack and not have_wx
def check_ELF_RELRO ( executable ) :
'''
Check for read - only relocations .
GNU_RELRO program header must exist
Dynamic section must have BIND_NOW flag
'''
have_gnu_relro = False
for ( typ , flags ) in get_ELF_program_headers ( executable ) :
# Note: not checking flags == 'R': here as linkers set the permission differently
# This does not affect security: the permission flags of the GNU_RELRO program header are ignored, the PT_LOAD header determines the effective permissions.
# However, the dynamic linker need to write to this area so these are RW.
# Glibc itself takes care of mprotecting this area R after relocations are finished.
2018-11-30 14:51:55 +01:00
# See also https://marc.info/?l=binutils&m=1498883354122353
2018-03-28 19:37:45 +02:00
if typ == ' GNU_RELRO ' :
2015-10-19 14:53:56 +02:00
have_gnu_relro = True
have_bindnow = False
2018-03-28 19:37:45 +02:00
p = subprocess . Popen ( [ READELF_CMD , ' -d ' , ' -W ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE , universal_newlines = True )
2015-10-19 14:53:56 +02:00
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
2018-03-28 19:37:45 +02:00
for line in stdout . splitlines ( ) :
2015-10-19 14:53:56 +02:00
tokens = line . split ( )
2018-07-12 18:16:01 +02:00
if len ( tokens ) > 1 and tokens [ 1 ] == ' (BIND_NOW) ' or ( len ( tokens ) > 2 and tokens [ 1 ] == ' (FLAGS) ' and ' BIND_NOW ' in tokens [ 2 : ] ) :
2015-10-19 14:53:56 +02:00
have_bindnow = True
return have_gnu_relro and have_bindnow
def check_ELF_Canary ( executable ) :
'''
Check for use of stack canary
'''
2018-03-28 19:37:45 +02:00
p = subprocess . Popen ( [ READELF_CMD , ' --dyn-syms ' , ' -W ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE , universal_newlines = True )
2015-10-19 14:53:56 +02:00
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
ok = False
2018-03-28 19:37:45 +02:00
for line in stdout . splitlines ( ) :
if ' __stack_chk_fail ' in line :
2015-10-19 14:53:56 +02:00
ok = True
return ok
def get_PE_dll_characteristics ( executable ) :
'''
2016-06-23 16:52:12 +02:00
Get PE DllCharacteristics bits .
Returns a tuple ( arch , bits ) where arch is ' i386:x86-64 ' or ' i386 '
and bits is the DllCharacteristics value .
2015-10-19 14:53:56 +02:00
'''
2018-03-28 19:37:45 +02:00
p = subprocess . Popen ( [ OBJDUMP_CMD , ' -x ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE , universal_newlines = True )
2015-10-19 14:53:56 +02:00
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
2016-06-23 16:52:12 +02:00
arch = ' '
bits = 0
2018-03-28 19:37:45 +02:00
for line in stdout . splitlines ( ) :
2015-10-19 14:53:56 +02:00
tokens = line . split ( )
2016-06-23 16:52:12 +02:00
if len ( tokens ) > = 2 and tokens [ 0 ] == ' architecture: ' :
arch = tokens [ 1 ] . rstrip ( ' , ' )
2015-10-19 14:53:56 +02:00
if len ( tokens ) > = 2 and tokens [ 0 ] == ' DllCharacteristics ' :
2016-06-23 16:52:12 +02:00
bits = int ( tokens [ 1 ] , 16 )
return ( arch , bits )
2015-10-19 14:53:56 +02:00
2016-06-23 16:52:12 +02:00
IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA = 0x0020
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE = 0x0040
IMAGE_DLL_CHARACTERISTICS_NX_COMPAT = 0x0100
2015-10-19 14:53:56 +02:00
2016-06-23 16:52:12 +02:00
def check_PE_DYNAMIC_BASE ( executable ) :
2015-10-19 14:53:56 +02:00
''' PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR) '''
2016-06-23 16:52:12 +02:00
( arch , bits ) = get_PE_dll_characteristics ( executable )
reqbits = IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE
return ( bits & reqbits ) == reqbits
# On 64 bit, must support high-entropy 64-bit address space layout randomization in addition to DYNAMIC_BASE
# to have secure ASLR.
def check_PE_HIGH_ENTROPY_VA ( executable ) :
''' PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR '''
( arch , bits ) = get_PE_dll_characteristics ( executable )
2018-05-10 17:28:27 +02:00
if arch == ' i386:x86-64 ' :
2016-06-23 16:52:12 +02:00
reqbits = IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA
else : # Unnecessary on 32-bit
assert ( arch == ' i386 ' )
reqbits = 0
return ( bits & reqbits ) == reqbits
2015-10-19 14:53:56 +02:00
def check_PE_NX ( executable ) :
''' NX: DllCharacteristics bit 0x100 signifies nxcompat (DEP) '''
2016-06-23 16:52:12 +02:00
( arch , bits ) = get_PE_dll_characteristics ( executable )
return ( bits & IMAGE_DLL_CHARACTERISTICS_NX_COMPAT ) == IMAGE_DLL_CHARACTERISTICS_NX_COMPAT
2015-10-19 14:53:56 +02:00
2020-01-02 07:42:21 +01:00
def get_MACHO_executable_flags ( executable ) :
p = subprocess . Popen ( [ OTOOL_CMD , ' -vh ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE , universal_newlines = True )
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
flags = [ ]
for line in stdout . splitlines ( ) :
tokens = line . split ( )
# filter first two header lines
if ' magic ' in tokens or ' Mach ' in tokens :
continue
# filter ncmds and sizeofcmds values
flags + = [ t for t in tokens if not t . isdigit ( ) ]
return flags
def check_MACHO_PIE ( executable ) - > bool :
'''
Check for position independent executable ( PIE ) , allowing for address space randomization .
'''
flags = get_MACHO_executable_flags ( executable )
if ' PIE ' in flags :
return True
return False
2015-10-19 14:53:56 +02:00
CHECKS = {
' ELF ' : [
( ' PIE ' , check_ELF_PIE ) ,
( ' NX ' , check_ELF_NX ) ,
( ' RELRO ' , check_ELF_RELRO ) ,
( ' Canary ' , check_ELF_Canary )
] ,
' PE ' : [
2016-06-23 16:52:12 +02:00
( ' DYNAMIC_BASE ' , check_PE_DYNAMIC_BASE ) ,
( ' HIGH_ENTROPY_VA ' , check_PE_HIGH_ENTROPY_VA ) ,
2015-10-19 14:53:56 +02:00
( ' NX ' , check_PE_NX )
2020-01-02 07:42:21 +01:00
] ,
' MACHO ' : [
( ' PIE ' , check_MACHO_PIE ) ,
2015-10-19 14:53:56 +02:00
]
}
def identify_executable ( executable ) :
with open ( filename , ' rb ' ) as f :
magic = f . read ( 4 )
if magic . startswith ( b ' MZ ' ) :
return ' PE '
elif magic . startswith ( b ' \x7f ELF ' ) :
return ' ELF '
2020-01-02 07:42:21 +01:00
elif magic . startswith ( b ' \xcf \xfa ' ) :
return ' MACHO '
2015-10-19 14:53:56 +02:00
return None
if __name__ == ' __main__ ' :
retval = 0
for filename in sys . argv [ 1 : ] :
try :
etype = identify_executable ( filename )
if etype is None :
print ( ' %s : unknown format ' % filename )
retval = 1
continue
failed = [ ]
2016-06-23 16:52:12 +02:00
warning = [ ]
2015-10-19 14:53:56 +02:00
for ( name , func ) in CHECKS [ etype ] :
if not func ( filename ) :
2016-06-23 16:52:12 +02:00
if name in NONFATAL :
warning . append ( name )
else :
failed . append ( name )
2015-10-19 14:53:56 +02:00
if failed :
print ( ' %s : failed %s ' % ( filename , ' ' . join ( failed ) ) )
retval = 1
2016-06-23 16:52:12 +02:00
if warning :
print ( ' %s : warning %s ' % ( filename , ' ' . join ( warning ) ) )
2015-10-19 14:53:56 +02:00
except IOError :
print ( ' %s : cannot open ' % filename )
retval = 1
2017-07-09 22:57:02 +02:00
sys . exit ( retval )
2015-10-19 14:53:56 +02:00