aboutsummaryrefslogtreecommitdiff
path: root/contrib/devtools
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/devtools')
-rw-r--r--contrib/devtools/README.md3
-rwxr-xr-xcontrib/devtools/security-check.py147
-rwxr-xr-xcontrib/devtools/symbol-check.py57
-rwxr-xr-xcontrib/devtools/test-security-check.py23
-rwxr-xr-xcontrib/devtools/test-symbol-check.py26
5 files changed, 103 insertions, 153 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..d740a94560 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,44 +202,22 @@ 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_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
@@ -265,7 +252,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 +261,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..106dfd2c5a 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