diff options
Diffstat (limited to 'contrib/devtools')
-rw-r--r-- | contrib/devtools/README.md | 3 | ||||
-rwxr-xr-x | contrib/devtools/security-check.py | 147 | ||||
-rwxr-xr-x | contrib/devtools/symbol-check.py | 82 | ||||
-rwxr-xr-x | contrib/devtools/test-security-check.py | 23 | ||||
-rwxr-xr-x | contrib/devtools/test-symbol-check.py | 61 |
5 files changed, 158 insertions, 158 deletions
diff --git a/contrib/devtools/README.md b/contrib/devtools/README.md index bdff7a84b0..1fa850af1a 100644 --- a/contrib/devtools/README.md +++ b/contrib/devtools/README.md @@ -7,7 +7,8 @@ clang-format-diff.py A script to format unified git diffs according to [.clang-format](../../src/.clang-format). -Requires `clang-format`, installed e.g. via `brew install clang-format` on macOS. +Requires `clang-format`, installed e.g. via `brew install clang-format` on macOS, +or `sudo apt install clang-format` on Debian/Ubuntu. For instance, to format the last commit with 0 lines of context, the script should be called from the git root folder as follows. diff --git a/contrib/devtools/security-check.py b/contrib/devtools/security-check.py index 7b09c42fde..0b59d8eada 100755 --- a/contrib/devtools/security-check.py +++ b/contrib/devtools/security-check.py @@ -6,22 +6,13 @@ Perform basic 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 `objdump` (for PE) and `otool` (for MACHO). ''' -import subprocess import sys -import os from typing import List, Optional +import lief import pixie -OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump') -OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool') - -def run_command(command) -> str: - p = subprocess.run(command, stdout=subprocess.PIPE, check=True, universal_newlines=True) - return p.stdout - def check_ELF_PIE(executable) -> bool: ''' Check for position independent executable (PIE), allowing for address space randomization. @@ -143,112 +134,72 @@ def check_ELF_separate_code(executable): return False return True -def get_PE_dll_characteristics(executable) -> int: - '''Get PE DllCharacteristics bits''' - stdout = run_command([OBJDUMP_CMD, '-x', executable]) - - bits = 0 - for line in stdout.splitlines(): - tokens = line.split() - if len(tokens)>=2 and tokens[0] == 'DllCharacteristics': - bits = int(tokens[1],16) - return bits - -IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA = 0x0020 -IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE = 0x0040 -IMAGE_DLL_CHARACTERISTICS_NX_COMPAT = 0x0100 - def check_PE_DYNAMIC_BASE(executable) -> bool: '''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)''' - bits = get_PE_dll_characteristics(executable) - return (bits & IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE) == IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE + binary = lief.parse(executable) + return lief.PE.DLL_CHARACTERISTICS.DYNAMIC_BASE in binary.optional_header.dll_characteristics_lists # 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) -> bool: '''PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR''' - bits = get_PE_dll_characteristics(executable) - return (bits & IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA) == IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA + binary = lief.parse(executable) + return lief.PE.DLL_CHARACTERISTICS.HIGH_ENTROPY_VA in binary.optional_header.dll_characteristics_lists def check_PE_RELOC_SECTION(executable) -> bool: '''Check for a reloc section. This is required for functional ASLR.''' - stdout = run_command([OBJDUMP_CMD, '-h', executable]) + binary = lief.parse(executable) + return binary.has_relocations - for line in stdout.splitlines(): - if '.reloc' in line: - return True - return False - -def check_PE_NX(executable) -> bool: - '''NX: DllCharacteristics bit 0x100 signifies nxcompat (DEP)''' - bits = get_PE_dll_characteristics(executable) - return (bits & IMAGE_DLL_CHARACTERISTICS_NX_COMPAT) == IMAGE_DLL_CHARACTERISTICS_NX_COMPAT - -def get_MACHO_executable_flags(executable) -> List[str]: - stdout = run_command([OTOOL_CMD, '-vh', executable]) +def check_MACHO_NOUNDEFS(executable) -> bool: + ''' + Check for no undefined references. + ''' + binary = lief.parse(executable) + return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS) - flags: List[str] = [] - 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_LAZY_BINDINGS(executable) -> bool: + ''' + Check for no lazy bindings. + We don't use or check for MH_BINDATLOAD. See #18295. + ''' + binary = lief.parse(executable) + return binary.dyld_info.lazy_bind == (0,0) -def check_MACHO_PIE(executable) -> bool: +def check_MACHO_Canary(executable) -> bool: ''' - Check for position independent executable (PIE), allowing for address space randomization. + Check for use of stack canary ''' - flags = get_MACHO_executable_flags(executable) - if 'PIE' in flags: - return True - return False + binary = lief.parse(executable) + return binary.has_symbol('___stack_chk_fail') -def check_MACHO_NOUNDEFS(executable) -> bool: +def check_PIE(executable) -> bool: ''' - Check for no undefined references. + Check for position independent executable (PIE), + allowing for address space randomization. ''' - flags = get_MACHO_executable_flags(executable) - if 'NOUNDEFS' in flags: - return True - return False + binary = lief.parse(executable) + return binary.is_pie -def check_MACHO_NX(executable) -> bool: +def check_NX(executable) -> bool: ''' Check for no stack execution ''' - flags = get_MACHO_executable_flags(executable) - if 'ALLOW_STACK_EXECUTION' in flags: - return False - return True + binary = lief.parse(executable) + return binary.has_nx -def check_MACHO_LAZY_BINDINGS(executable) -> bool: +def check_control_flow(executable) -> bool: ''' - Check for no lazy bindings. - We don't use or check for MH_BINDATLOAD. See #18295. + Check for control flow instrumentation ''' - stdout = run_command([OTOOL_CMD, '-l', executable]) + binary = lief.parse(executable) - for line in stdout.splitlines(): - tokens = line.split() - if 'lazy_bind_off' in tokens or 'lazy_bind_size' in tokens: - if tokens[1] != '0': - return False - return True + content = binary.get_content_from_virtual_address(binary.entrypoint, 4, lief.Binary.VA_TYPES.AUTO) -def check_MACHO_Canary(executable) -> bool: - ''' - Check for use of stack canary - ''' - stdout = run_command([OTOOL_CMD, '-Iv', executable]) + if content == [243, 15, 30, 250]: # endbr64 + return True + return False - ok = False - for line in stdout.splitlines(): - if '___stack_chk_fail' in line: - ok = True - return ok CHECKS = { 'ELF': [ @@ -259,17 +210,19 @@ CHECKS = { ('separate_code', check_ELF_separate_code), ], 'PE': [ + ('PIE', check_PIE), ('DYNAMIC_BASE', check_PE_DYNAMIC_BASE), ('HIGH_ENTROPY_VA', check_PE_HIGH_ENTROPY_VA), - ('NX', check_PE_NX), + ('NX', check_NX), ('RELOC_SECTION', check_PE_RELOC_SECTION) ], 'MACHO': [ - ('PIE', check_MACHO_PIE), + ('PIE', check_PIE), ('NOUNDEFS', check_MACHO_NOUNDEFS), - ('NX', check_MACHO_NX), + ('NX', check_NX), ('LAZY_BINDINGS', check_MACHO_LAZY_BINDINGS), - ('Canary', check_MACHO_Canary) + ('Canary', check_MACHO_Canary), + ('CONTROL_FLOW', check_control_flow), ] } @@ -285,24 +238,24 @@ def identify_executable(executable) -> Optional[str]: return None if __name__ == '__main__': - retval = 0 + retval: int = 0 for filename in sys.argv[1:]: try: etype = identify_executable(filename) if etype is None: - print('%s: unknown format' % filename) + print(f'{filename}: unknown format') retval = 1 continue - failed = [] + failed: List[str] = [] for (name, func) in CHECKS[etype]: if not func(filename): failed.append(name) if failed: - print('%s: failed %s' % (filename, ' '.join(failed))) + print(f'{filename}: failed {" ".join(failed)}') retval = 1 except IOError: - print('%s: cannot open' % filename) + print(f'{filename}: cannot open') retval = 1 sys.exit(retval) diff --git a/contrib/devtools/symbol-check.py b/contrib/devtools/symbol-check.py index b30ed62521..7a5a42c5d2 100755 --- a/contrib/devtools/symbol-check.py +++ b/contrib/devtools/symbol-check.py @@ -15,6 +15,7 @@ import sys import os from typing import List, Optional +import lief import pixie # Debian 8 (Jessie) EOL: 2020. https://wiki.debian.org/DebianReleases#Production_Releases @@ -52,8 +53,6 @@ IGNORE_EXPORTS = { 'environ', '_environ', '__environ', } CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt') -OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump') -OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool') # Allowed NEEDED libraries ELF_ALLOWED_LIBRARIES = { @@ -73,6 +72,8 @@ ELF_ALLOWED_LIBRARIES = { 'ld-linux-riscv64-lp64d.so.1', # 64-bit RISC-V dynamic linker # bitcoin-qt only 'libxcb.so.1', # part of X11 +'libxkbcommon.so.0', # keyboard keymapping +'libxkbcommon-x11.so.0', # keyboard keymapping 'libfontconfig.so.1', # font support 'libfreetype.so.6', # font parsing 'libdl.so.2' # programming interface to dynamic linker @@ -98,10 +99,15 @@ MACHO_ALLOWED_LIBRARIES = { 'CoreGraphics', # 2D rendering 'CoreServices', # operating system services 'CoreText', # interface for laying out text and handling fonts. +'CoreVideo', # video processing 'Foundation', # base layer functionality for apps/frameworks 'ImageIO', # read and write image file formats. 'IOKit', # user-space access to hardware devices and drivers. +'IOSurface', # cross process image/drawing buffers 'libobjc.A.dylib', # Objective-C runtime library +'Metal', # 3D graphics +'Security', # access control and authentication +'QuartzCore', # animation } PE_ALLOWED_LIBRARIES = { @@ -116,12 +122,15 @@ PE_ALLOWED_LIBRARIES = { 'dwmapi.dll', # desktop window manager 'GDI32.dll', # graphics device interface 'IMM32.dll', # input method editor +'NETAPI32.dll', 'ole32.dll', # component object model 'OLEAUT32.dll', # OLE Automation API 'SHLWAPI.dll', # light weight shell API +'USERENV.dll', 'UxTheme.dll', 'VERSION.dll', # version checking 'WINMM.dll', # WinMM audio API +'WTSAPI32.dll', } class CPPFilt(object): @@ -193,47 +202,45 @@ def check_ELF_libraries(filename) -> bool: ok = False return ok -def macho_read_libraries(filename) -> List[str]: - p = subprocess.Popen([OTOOL_CMD, '-L', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True) - (stdout, stderr) = p.communicate() - if p.returncode: - raise IOError('Error opening file') - libraries = [] - for line in stdout.splitlines(): - tokens = line.split() - if len(tokens) == 1: # skip executable name - continue - libraries.append(tokens[0].split('/')[-1]) - return libraries - def check_MACHO_libraries(filename) -> bool: ok: bool = True - for dylib in macho_read_libraries(filename): - if dylib not in MACHO_ALLOWED_LIBRARIES: - print('{} is not in ALLOWED_LIBRARIES!'.format(dylib)) + binary = lief.parse(filename) + for dylib in binary.libraries: + split = dylib.name.split('/') + if split[-1] not in MACHO_ALLOWED_LIBRARIES: + print(f'{split[-1]} is not in ALLOWED_LIBRARIES!') ok = False return ok -def pe_read_libraries(filename) -> List[str]: - p = subprocess.Popen([OBJDUMP_CMD, '-x', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True) - (stdout, stderr) = p.communicate() - if p.returncode: - raise IOError('Error opening file') - libraries = [] - for line in stdout.splitlines(): - if 'DLL Name:' in line: - tokens = line.split(': ') - libraries.append(tokens[1]) - return libraries +def check_MACHO_min_os(filename) -> bool: + binary = lief.parse(filename) + if binary.build_version.minos == [10,14,0]: + return True + return False + +def check_MACHO_sdk(filename) -> bool: + binary = lief.parse(filename) + if binary.build_version.sdk == [10, 15, 6]: + return True + return False def check_PE_libraries(filename) -> bool: ok: bool = True - for dylib in pe_read_libraries(filename): + binary = lief.parse(filename) + for dylib in binary.libraries: if dylib not in PE_ALLOWED_LIBRARIES: - print('{} is not in ALLOWED_LIBRARIES!'.format(dylib)) + print(f'{dylib} is not in ALLOWED_LIBRARIES!') ok = False return ok +def check_PE_subsystem_version(filename) -> bool: + binary = lief.parse(filename) + major: int = binary.optional_header.major_subsystem_version + minor: int = binary.optional_header.minor_subsystem_version + if major == 6 and minor == 1: + return True + return False + CHECKS = { 'ELF': [ ('IMPORTED_SYMBOLS', check_imported_symbols), @@ -241,10 +248,13 @@ CHECKS = { ('LIBRARY_DEPENDENCIES', check_ELF_libraries) ], 'MACHO': [ - ('DYNAMIC_LIBRARIES', check_MACHO_libraries) + ('DYNAMIC_LIBRARIES', check_MACHO_libraries), + ('MIN_OS', check_MACHO_min_os), + ('SDK', check_MACHO_sdk), ], 'PE' : [ - ('DYNAMIC_LIBRARIES', check_PE_libraries) + ('DYNAMIC_LIBRARIES', check_PE_libraries), + ('SUBSYSTEM_VERSION', check_PE_subsystem_version), ] } @@ -265,7 +275,7 @@ if __name__ == '__main__': try: etype = identify_executable(filename) if etype is None: - print('{}: unknown format'.format(filename)) + print(f'{filename}: unknown format') retval = 1 continue @@ -274,9 +284,9 @@ if __name__ == '__main__': if not func(filename): failed.append(name) if failed: - print('{}: failed {}'.format(filename, ' '.join(failed))) + print(f'{filename}: failed {" ".join(failed)}') retval = 1 except IOError: - print('{}: cannot open'.format(filename)) + print(f'{filename}: cannot open') retval = 1 sys.exit(retval) diff --git a/contrib/devtools/test-security-check.py b/contrib/devtools/test-security-check.py index ec2d886653..c079fe5b4d 100755 --- a/contrib/devtools/test-security-check.py +++ b/contrib/devtools/test-security-check.py @@ -5,6 +5,7 @@ ''' Test script for security-check.py ''' +import os import subprocess import unittest @@ -19,6 +20,10 @@ def write_testcode(filename): } ''') +def clean_files(source, executable): + os.remove(source) + os.remove(executable) + def call_security_check(cc, source, executable, options): subprocess.run([cc,source,'-o',executable] + options, check=True) p = subprocess.run(['./contrib/devtools/security-check.py',executable], stdout=subprocess.PIPE, universal_newlines=True) @@ -44,6 +49,8 @@ class TestSecurityChecks(unittest.TestCase): self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-zrelro','-Wl,-z,now','-pie','-fPIE', '-Wl,-z,separate-code']), (0, '')) + clean_files(source, executable) + def test_PE(self): source = 'test1.c' executable = 'test1.exe' @@ -61,6 +68,8 @@ class TestSecurityChecks(unittest.TestCase): self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--dynamicbase','-Wl,--high-entropy-va','-pie','-fPIE']), (0, '')) + clean_files(source, executable) + def test_MACHO(self): source = 'test1.c' executable = 'test1' @@ -68,18 +77,22 @@ class TestSecurityChecks(unittest.TestCase): write_testcode(source) self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-flat_namespace','-Wl,-allow_stack_execute','-fno-stack-protector']), - (1, executable+': failed PIE NOUNDEFS NX LAZY_BINDINGS Canary')) + (1, executable+': failed PIE NOUNDEFS NX LAZY_BINDINGS Canary CONTROL_FLOW')) self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-flat_namespace','-Wl,-allow_stack_execute','-fstack-protector-all']), - (1, executable+': failed PIE NOUNDEFS NX LAZY_BINDINGS')) + (1, executable+': failed PIE NOUNDEFS NX LAZY_BINDINGS CONTROL_FLOW')) self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-flat_namespace','-fstack-protector-all']), - (1, executable+': failed PIE NOUNDEFS LAZY_BINDINGS')) + (1, executable+': failed PIE NOUNDEFS LAZY_BINDINGS CONTROL_FLOW')) self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-fstack-protector-all']), - (1, executable+': failed PIE LAZY_BINDINGS')) + (1, executable+': failed PIE LAZY_BINDINGS CONTROL_FLOW')) self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-bind_at_load','-fstack-protector-all']), + (1, executable+': failed PIE CONTROL_FLOW')) + self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-bind_at_load','-fstack-protector-all', '-fcf-protection=full']), (1, executable+': failed PIE')) - self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-pie','-Wl,-bind_at_load','-fstack-protector-all']), + self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-pie','-Wl,-bind_at_load','-fstack-protector-all', '-fcf-protection=full']), (0, '')) + clean_files(source, executable) + if __name__ == '__main__': unittest.main() diff --git a/contrib/devtools/test-symbol-check.py b/contrib/devtools/test-symbol-check.py index 18ed7d61e0..6ce2fa3560 100755 --- a/contrib/devtools/test-symbol-check.py +++ b/contrib/devtools/test-symbol-check.py @@ -5,47 +5,43 @@ ''' Test script for symbol-check.py ''' +import os import subprocess import unittest def call_symbol_check(cc, source, executable, options): subprocess.run([cc,source,'-o',executable] + options, check=True) p = subprocess.run(['./contrib/devtools/symbol-check.py',executable], stdout=subprocess.PIPE, universal_newlines=True) + os.remove(source) + os.remove(executable) return (p.returncode, p.stdout.rstrip()) -def get_machine(cc): - p = subprocess.run([cc,'-dumpmachine'], stdout=subprocess.PIPE, universal_newlines=True) - return p.stdout.rstrip() - class TestSymbolChecks(unittest.TestCase): def test_ELF(self): source = 'test1.c' executable = 'test1' cc = 'gcc' - # there's no way to do this test for RISC-V at the moment; bionic's libc is 2.27 - # and we allow all symbols from 2.27. - if 'riscv' in get_machine(cc): - self.skipTest("test not available for RISC-V") - - # memfd_create was introduced in GLIBC 2.27, so is newer than the upper limit of - # all but RISC-V but still available on bionic + # renameat2 was introduced in GLIBC 2.28, so is newer than the upper limit + # of glibc for all platforms with open(source, 'w', encoding="utf8") as f: f.write(''' #define _GNU_SOURCE - #include <sys/mman.h> + #include <stdio.h> + #include <linux/fs.h> - int memfd_create(const char *name, unsigned int flags); + int renameat2(int olddirfd, const char *oldpath, + int newdirfd, const char *newpath, unsigned int flags); int main() { - memfd_create("test", 0); + renameat2(0, "test", 0, "test_", RENAME_EXCHANGE); return 0; } ''') self.assertEqual(call_symbol_check(cc, source, executable, []), - (1, executable + ': symbol memfd_create from unsupported version GLIBC_2.27\n' + + (1, executable + ': symbol renameat2 from unsupported version GLIBC_2.28\n' + executable + ': failed IMPORTED_SYMBOLS')) # -lutil is part of the libc6 package so a safe bet that it's installed @@ -102,7 +98,7 @@ class TestSymbolChecks(unittest.TestCase): self.assertEqual(call_symbol_check(cc, source, executable, ['-lexpat']), (1, 'libexpat.1.dylib is not in ALLOWED_LIBRARIES!\n' + - executable + ': failed DYNAMIC_LIBRARIES')) + f'{executable}: failed DYNAMIC_LIBRARIES MIN_OS SDK')) source = 'test2.c' executable = 'test2' @@ -118,7 +114,20 @@ class TestSymbolChecks(unittest.TestCase): ''') self.assertEqual(call_symbol_check(cc, source, executable, ['-framework', 'CoreGraphics']), - (0, '')) + (1, f'{executable}: failed MIN_OS SDK')) + + source = 'test3.c' + executable = 'test3' + with open(source, 'w', encoding="utf8") as f: + f.write(''' + int main() + { + return 0; + } + ''') + + self.assertEqual(call_symbol_check(cc, source, executable, ['-mmacosx-version-min=10.14']), + (1, f'{executable}: failed SDK')) def test_PE(self): source = 'test1.c' @@ -136,12 +145,26 @@ class TestSymbolChecks(unittest.TestCase): } ''') - self.assertEqual(call_symbol_check(cc, source, executable, ['-lpdh']), + self.assertEqual(call_symbol_check(cc, source, executable, ['-lpdh', '-Wl,--major-subsystem-version', '-Wl,6', '-Wl,--minor-subsystem-version', '-Wl,1']), (1, 'pdh.dll is not in ALLOWED_LIBRARIES!\n' + executable + ': failed DYNAMIC_LIBRARIES')) source = 'test2.c' executable = 'test2.exe' + + with open(source, 'w', encoding="utf8") as f: + f.write(''' + int main() + { + return 0; + } + ''') + + self.assertEqual(call_symbol_check(cc, source, executable, ['-Wl,--major-subsystem-version', '-Wl,9', '-Wl,--minor-subsystem-version', '-Wl,9']), + (1, executable + ': failed SUBSYSTEM_VERSION')) + + source = 'test3.c' + executable = 'test3.exe' with open(source, 'w', encoding="utf8") as f: f.write(''' #include <windows.h> @@ -153,7 +176,7 @@ class TestSymbolChecks(unittest.TestCase): } ''') - self.assertEqual(call_symbol_check(cc, source, executable, ['-lole32']), + self.assertEqual(call_symbol_check(cc, source, executable, ['-lole32', '-Wl,--major-subsystem-version', '-Wl,6', '-Wl,--minor-subsystem-version', '-Wl,1']), (0, '')) |