#!/usr/bin/python2
'''
Perform basic ELF security checks on a series of executables .
Exit status will be 0 if successful , and the program will be silent .
Otherwise the exit status will be 1 and it will log which executables failed which checks .
Needs ` readelf ` ( for ELF ) and ` objdump ` ( for PE ) .
'''
from __future__ import division , print_function
import subprocess
import sys
import os
READELF_CMD = os . getenv ( ' READELF ' , ' /usr/bin/readelf ' )
OBJDUMP_CMD = os . getenv ( ' OBJDUMP ' , ' /usr/bin/objdump ' )
def check_ELF_PIE ( executable ) :
'''
Check for position independent executable ( PIE ) , allowing for address space randomization .
'''
p = subprocess . Popen ( [ READELF_CMD , ' -h ' , ' -W ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE )
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
ok = False
for line in stdout . split ( ' \n ' ) :
line = line . split ( )
if len ( line ) > = 2 and line [ 0 ] == ' Type: ' and line [ 1 ] == ' DYN ' :
ok = True
return ok
def get_ELF_program_headers ( executable ) :
''' Return type and flags for ELF program headers '''
p = subprocess . Popen ( [ READELF_CMD , ' -l ' , ' -W ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE )
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
in_headers = False
count = 0
headers = [ ]
for line in stdout . split ( ' \n ' ) :
if line . startswith ( ' Program Headers: ' ) :
in_headers = True
if line == ' ' :
in_headers = False
if in_headers :
if count == 1 : # header line
ofs_typ = line . find ( ' Type ' )
ofs_offset = line . find ( ' Offset ' )
ofs_flags = line . find ( ' Flg ' )
ofs_align = line . find ( ' Align ' )
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 ) :
if typ == ' GNU_STACK ' :
have_gnu_stack = True
if ' W ' in flags and ' E ' in flags : # section is both writable and executable
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.
# See also http://permalink.gmane.org/gmane.comp.gnu.binutils/71347
if typ == ' GNU_RELRO ' :
have_gnu_relro = True
have_bindnow = False
p = subprocess . Popen ( [ READELF_CMD , ' -d ' , ' -W ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE )
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
for line in stdout . split ( ' \n ' ) :
tokens = line . split ( )
if len ( tokens ) > 1 and tokens [ 1 ] == ' (BIND_NOW) ' :
have_bindnow = True
return have_gnu_relro and have_bindnow
def check_ELF_Canary ( executable ) :
'''
Check for use of stack canary
'''
p = subprocess . Popen ( [ READELF_CMD , ' --dyn-syms ' , ' -W ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE )
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
ok = False
for line in stdout . split ( ' \n ' ) :
if ' __stack_chk_fail ' in line :
ok = True
return ok
def get_PE_dll_characteristics ( executable ) :
'''
Get PE DllCharacteristics bits
'''
p = subprocess . Popen ( [ OBJDUMP_CMD , ' -x ' , executable ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , stdin = subprocess . PIPE )
( stdout , stderr ) = p . communicate ( )
if p . returncode :
raise IOError ( ' Error opening file ' )
for line in stdout . split ( ' \n ' ) :
tokens = line . split ( )
if len ( tokens ) > = 2 and tokens [ 0 ] == ' DllCharacteristics ' :
return int ( tokens [ 1 ] , 16 )
return 0
def check_PE_PIE ( executable ) :
''' PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR) '''
return bool ( get_PE_dll_characteristics ( executable ) & 0x40 )
def check_PE_NX ( executable ) :
''' NX: DllCharacteristics bit 0x100 signifies nxcompat (DEP) '''
return bool ( get_PE_dll_characteristics ( executable ) & 0x100 )
CHECKS = {
' ELF ' : [
( ' PIE ' , check_ELF_PIE ) ,
( ' NX ' , check_ELF_NX ) ,
( ' RELRO ' , check_ELF_RELRO ) ,
( ' Canary ' , check_ELF_Canary )
] ,
' PE ' : [
( ' PIE ' , check_PE_PIE ) ,
( ' NX ' , check_PE_NX )
]
}
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 '
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 = [ ]
for ( name , func ) in CHECKS [ etype ] :
if not func ( filename ) :
failed . append ( name )
if failed :
print ( ' %s : failed %s ' % ( filename , ' ' . join ( failed ) ) )
retval = 1
except IOError :
print ( ' %s : cannot open ' % filename )
retval = 1
exit ( retval )