aboutsummaryrefslogtreecommitdiff
path: root/contrib/devtools/security-check.py
diff options
context:
space:
mode:
authorWladimir J. van der Laan <laanwj@gmail.com>2015-10-19 14:53:56 +0200
committerWladimir J. van der Laan <laanwj@gmail.com>2015-10-22 03:35:38 +0200
commit579b863cd7586b98974484ad55e19be2a54d241d (patch)
tree0a5e4661d767cd45c7c3bfb7d2cf75bb6bc5d831 /contrib/devtools/security-check.py
parenta1d623da3ea4c73605d27703bc966ed8109d231e (diff)
devtools: Add security-check.py
Perform the following ELF security checks: - PIE: Check for position independent executable (PIE), allowing for address space randomization - NX: Check that no sections are writable and executable (including the stack) - RELRO: Check for read-only relocations, binding at startup - Canary: Check for use of stack canary Also add a check to symbol-check.py that checks that only the subset of allowed libraries is imported (to avoid incompatibilities).
Diffstat (limited to 'contrib/devtools/security-check.py')
-rwxr-xr-xcontrib/devtools/security-check.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/contrib/devtools/security-check.py b/contrib/devtools/security-check.py
new file mode 100755
index 0000000000..e96eaa9c38
--- /dev/null
+++ b/contrib/devtools/security-check.py
@@ -0,0 +1,181 @@
+#!/usr/bin/python2
+'''
+Perform basic ELF security checks on a series of executables.
+Exit status will be 0 if succesful, 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'\x7fELF'):
+ 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)
+