diff --git a/contrib/devtools/security-check.py b/contrib/devtools/security-check.py index 646fa279a..910ba683e 100755 --- a/contrib/devtools/security-check.py +++ b/contrib/devtools/security-check.py @@ -1,211 +1,233 @@ #!/usr/bin/env python3 # Copyright (c) 2015-2017 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ 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. """ import sys from typing import List import lief def check_ELF_RELRO(binary) -> bool: """ Check for read-only relocations. GNU_RELRO program header must exist Dynamic section must have BIND_NOW flag """ have_gnu_relro = False for segment in binary.segments: # Note: not checking p_flags == PF_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 https://marc.info/?l=binutils&m=1498883354122353 if segment.type == lief.ELF.SEGMENT_TYPES.GNU_RELRO: have_gnu_relro = True have_bindnow = False try: flags = binary.get(lief.ELF.DYNAMIC_TAGS.FLAGS) if flags.value & lief.ELF.DYNAMIC_FLAGS.BIND_NOW: have_bindnow = True except Exception: have_bindnow = False return have_gnu_relro and have_bindnow def check_ELF_Canary(binary) -> bool: """ Check for use of stack canary """ return binary.has_symbol("__stack_chk_fail") def check_ELF_separate_code(binary): """ Check that sections are appropriately separated in virtual memory, based on their permissions. This checks for missing -Wl,-z,separate-code and potentially other problems. """ R = lief.ELF.SEGMENT_FLAGS.R W = lief.ELF.SEGMENT_FLAGS.W E = lief.ELF.SEGMENT_FLAGS.X EXPECTED_FLAGS = { # Read + execute ".init": R | E, ".plt": R | E, ".plt.got": R | E, ".plt.sec": R | E, ".text": R | E, ".fini": R | E, # Read-only data ".interp": R, ".note.gnu.property": R, ".note.gnu.build-id": R, ".note.ABI-tag": R, ".gnu.hash": R, ".dynsym": R, ".dynstr": R, ".gnu.version": R, ".gnu.version_r": R, ".rela.dyn": R, ".rela.plt": R, ".rodata": R, ".eh_frame_hdr": R, ".eh_frame": R, ".qtmetadata": R, ".gcc_except_table": R, ".stapsdt.base": R, # Writable data ".init_array": R | W, ".fini_array": R | W, ".dynamic": R | W, ".got": R | W, ".data": R | W, ".bss": R | W, } # For all LOAD program headers get mapping to the list of sections, # and for each section, remember the flags of the associated program header. flags_per_section = {} for segment in binary.segments: if segment.type == lief.ELF.SEGMENT_TYPES.LOAD: for section in segment.sections: assert section.name not in flags_per_section flags_per_section[section.name] = segment.flags # Spot-check ELF LOAD program header flags per section # If these sections exist, check them against the expected R/W/E flags for section, flags in flags_per_section.items(): if section in EXPECTED_FLAGS: if int(EXPECTED_FLAGS[section]) != int(flags): return False return True def check_PE_DYNAMIC_BASE(binary) -> bool: """PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)""" 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(binary) -> bool: """PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR""" return ( lief.PE.DLL_CHARACTERISTICS.HIGH_ENTROPY_VA in binary.optional_header.dll_characteristics_lists ) def check_PE_RELOC_SECTION(binary) -> bool: """Check for a reloc section. This is required for functional ASLR.""" return binary.has_relocations def check_MACHO_NOUNDEFS(binary) -> bool: """ Check for no undefined references. """ return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS) def check_MACHO_Canary(binary) -> bool: """ Check for use of stack canary """ return binary.has_symbol("___stack_chk_fail") def check_PIE(binary) -> bool: """ Check for position independent executable (PIE), allowing for address space randomization. """ return binary.is_pie def check_NX(binary) -> bool: """ Check for no stack execution """ return binary.has_nx +BASE_ELF = [ + ("PIE", check_PIE), + ("NX", check_NX), + ("RELRO", check_ELF_RELRO), + ("Canary", check_ELF_Canary), + ("separate_code", check_ELF_separate_code), +] + +BASE_PE = [ + ("PIE", check_PIE), + ("DYNAMIC_BASE", check_PE_DYNAMIC_BASE), + ("HIGH_ENTROPY_VA", check_PE_HIGH_ENTROPY_VA), + ("NX", check_NX), + ("RELOC_SECTION", check_PE_RELOC_SECTION), +] + +BASE_MACHO = [ + ("PIE", check_PIE), + ("NOUNDEFS", check_MACHO_NOUNDEFS), + ("NX", check_NX), + ("Canary", check_MACHO_Canary), +] + CHECKS = { - "ELF": [ - ("PIE", check_PIE), - ("NX", check_NX), - ("RELRO", check_ELF_RELRO), - ("Canary", check_ELF_Canary), - ("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_NX), - ("RELOC_SECTION", check_PE_RELOC_SECTION), - ], - "MACHO": [ - ("PIE", check_PIE), - ("NOUNDEFS", check_MACHO_NOUNDEFS), - ("NX", check_NX), - ("Canary", check_MACHO_Canary), - ], + lief.EXE_FORMATS.ELF: { + lief.ARCHITECTURES.X86: BASE_ELF, + lief.ARCHITECTURES.ARM: BASE_ELF, + lief.ARCHITECTURES.ARM64: BASE_ELF, + }, + lief.EXE_FORMATS.PE: { + lief.ARCHITECTURES.X86: BASE_PE, + }, + lief.EXE_FORMATS.MACHO: { + lief.ARCHITECTURES.X86: BASE_MACHO, + }, } if __name__ == "__main__": retval: int = 0 for filename in sys.argv[1:]: try: binary = lief.parse(filename) - etype = binary.format.name + etype = binary.format + arch = binary.abstract.header.architecture + binary.concrete + if etype == lief.EXE_FORMATS.UNKNOWN: print(f"{filename}: unknown executable format") retval = 1 continue + if arch == lief.ARCHITECTURES.NONE: + print(f"{filename}: unknown architecture") + retval = 1 + continue + failed: List[str] = [] - for name, func in CHECKS[etype]: + for name, func in CHECKS[etype][arch]: if not func(binary): failed.append(name) if failed: print(f"{filename}: failed {' '.join(failed)}") retval = 1 except IOError: 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 8db6d82e2..0abcad587 100755 --- a/contrib/devtools/symbol-check.py +++ b/contrib/devtools/symbol-check.py @@ -1,323 +1,323 @@ #!/usr/bin/env python3 # Copyright (c) 2014 Wladimir J. van der Laan # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ -A script to check that the executables produced by gitian only contain -certain symbols and are only linked against allowed libraries. +A script to check that release executables only contain certain symbols +and are only linked against allowed libraries. Example usage: - find contrib/gitian-builder/build -type f -executable | xargs python3 contrib/devtools/symbol-check.py + find ../path/to/binaries -type f -executable | xargs python3 contrib/devtools/symbol-check.py """ import sys import lief # Debian 10 (Buster) EOL: 2024. https://wiki.debian.org/LTS # # - libgcc version 8.3.0 (https://packages.debian.org/search?suite=buster&arch=any&searchon=names&keywords=libgcc1) # - libc version 2.28 (https://packages.debian.org/search?suite=buster&arch=any&searchon=names&keywords=libc6) # # CentOS Stream 8 EOL: 2024. https://wiki.centos.org/About/Product # # - libgcc version 8.5.0 (http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/) # - libc version 2.28 (http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/) # # See https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html for more info. MAX_VERSIONS = { "GCC": (8, 3, 0), "GLIBC": { lief.ELF.ARCH.i386: (2, 28), lief.ELF.ARCH.x86_64: (2, 28), lief.ELF.ARCH.ARM: (2, 28), lief.ELF.ARCH.AARCH64: (2, 28), }, "LIBATOMIC": (1, 0), "V": (0, 5, 0), # xkb (bitcoin-qt only) } # See here for a description of _IO_stdin_used: # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=634261#109 # Ignore symbols that are exported as part of every executable IGNORE_EXPORTS = { "_edata", "_end", "__end__", "_init", "__bss_start", "__bss_start__", "_bss_end__", "__bss_end__", "_fini", "_IO_stdin_used", "stdin", "stdout", "stderr", # Jemalloc exported symbols "__malloc_hook", "malloc", "calloc", "malloc_usable_size", "__free_hook", "free", "__realloc_hook", "realloc", "__memalign_hook", "memalign", "posix_memalign", "aligned_alloc", "valloc", # Figure out why we get these symbols exported on xenial. "_ZNKSt5ctypeIcE8do_widenEc", "in6addr_any", "optarg", "_ZNSt16_Sp_counted_baseILN9__gnu_cxx12_Lock_policyE2EE10_M_destroyEv", } # Expected linker-loader names can be found here: # https://sourceware.org/glibc/wiki/ABIList?action=recall&rev=16 ELF_INTERPRETER_NAMES = { lief.ELF.ARCH.i386: { lief.ENDIANNESS.LITTLE: "/lib/ld-linux.so.2", }, lief.ELF.ARCH.x86_64: { lief.ENDIANNESS.LITTLE: "/lib64/ld-linux-x86-64.so.2", }, lief.ELF.ARCH.ARM: { lief.ENDIANNESS.LITTLE: "/lib/ld-linux-armhf.so.3", }, lief.ELF.ARCH.AARCH64: { lief.ENDIANNESS.LITTLE: "/lib/ld-linux-aarch64.so.1", }, } # Allowed NEEDED libraries ELF_ALLOWED_LIBRARIES = { # bitcoind and bitcoin-qt "libgcc_s.so.1", # GCC base support "libc.so.6", # C library "libpthread.so.0", # threading "libanl.so.1", # DNS resolve "libm.so.6", # math library "librt.so.1", # real-time (clock) "libatomic.so.1", "ld-linux-x86-64.so.2", # 64-bit dynamic linker "ld-linux.so.2", # 32-bit dynamic linker "ld-linux-aarch64.so.1", # 64-bit ARM dynamic linker "ld-linux-armhf.so.3", # 32-bit ARM 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 "libdl.so.2", # programming interface to dynamic linker "libxcb-icccm.so.4", "libxcb-image.so.0", "libxcb-shm.so.0", "libxcb-keysyms.so.1", "libxcb-randr.so.0", "libxcb-render-util.so.0", "libxcb-render.so.0", "libxcb-shape.so.0", "libxcb-sync.so.1", "libxcb-xfixes.so.0", "libxcb-xinerama.so.0", "libxcb-xkb.so.1", } MACHO_ALLOWED_LIBRARIES = { # bitcoind and bitcoin-qt "libc++.1.dylib", # C++ Standard Library "libSystem.B.dylib", # libc, libm, libpthread, libinfo # bitcoin-qt only "AppKit", # user interface "ApplicationServices", # common application tasks. "Carbon", # deprecated c back-compat API "ColorSync", "CFNetwork", # network services and changes in network configurations "CoreFoundation", # low level func, data types "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 "SystemConfiguration", # access network configuration settings "GSS", } PE_ALLOWED_LIBRARIES = { "ADVAPI32.dll", # security & registry "IPHLPAPI.DLL", # IP helper API "KERNEL32.dll", # win32 base APIs "msvcrt.dll", # C standard library for MSVC "SHELL32.dll", # shell API "USER32.dll", # user interface "WS2_32.dll", # sockets # bitcoin-qt only "dwmapi.dll", # desktop window manager "CRYPT32.dll", # openssl "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", } def check_version(max_versions, version, arch) -> bool: (lib, _, ver) = version.rpartition("_") ver = tuple([int(x) for x in ver.split(".")]) if lib not in max_versions: return False if isinstance(max_versions[lib], tuple): return ver <= max_versions[lib] else: return ver <= max_versions[lib][arch] def check_imported_symbols(binary) -> bool: ok = True for symbol in binary.imported_symbols: if not symbol.imported: continue version = symbol.symbol_version if symbol.has_version else None if version: aux_version = ( version.symbol_version_auxiliary.name if version.has_auxiliary_version else None ) if aux_version and not check_version( MAX_VERSIONS, aux_version, binary.header.machine_type ): print( f"{filename}: symbol {symbol.name} from unsupported version" f" {version}" ) ok = False return ok def check_exported_symbols(binary) -> bool: ok = True for symbol in binary.dynamic_symbols: if not symbol.exported: continue name = symbol.name if name in IGNORE_EXPORTS: continue print(f"{binary.name}: export of symbol {name} not allowed!") ok = False return ok def check_ELF_libraries(binary) -> bool: ok = True for library in binary.libraries: if library not in ELF_ALLOWED_LIBRARIES: print(f"{filename}: {library} is not in ALLOWED_LIBRARIES!") ok = False return ok def check_MACHO_libraries(binary) -> bool: ok = True 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 check_MACHO_min_os(binary) -> bool: return binary.build_version.minos == [10, 15, 0] def check_MACHO_sdk(binary) -> bool: return binary.build_version.sdk == [10, 15, 6] def check_PE_libraries(binary) -> bool: ok: bool = True for dylib in binary.libraries: if dylib not in PE_ALLOWED_LIBRARIES: print(f"{dylib} is not in ALLOWED_LIBRARIES!") ok = False return ok def check_PE_subsystem_version(binary) -> bool: binary = lief.parse(filename) major: int = binary.optional_header.major_subsystem_version minor: int = binary.optional_header.minor_subsystem_version return major == 6 and minor == 1 def check_ELF_interpreter(binary) -> bool: expected_interpreter = ELF_INTERPRETER_NAMES[binary.header.machine_type][ binary.abstract.header.endianness ] return binary.concrete.interpreter == expected_interpreter CHECKS = { - "ELF": [ + lief.EXE_FORMATS.ELF: [ ("IMPORTED_SYMBOLS", check_imported_symbols), ("EXPORTED_SYMBOLS", check_exported_symbols), ("LIBRARY_DEPENDENCIES", check_ELF_libraries), ("INTERPRETER_NAME", check_ELF_interpreter), ], - "MACHO": [ + lief.EXE_FORMATS.MACHO: [ ("DYNAMIC_LIBRARIES", check_MACHO_libraries), ("MIN_OS", check_MACHO_min_os), ("SDK", check_MACHO_sdk), ], - "PE": [ + lief.EXE_FORMATS.PE: [ ("DYNAMIC_LIBRARIES", check_PE_libraries), ("SUBSYSTEM_VERSION", check_PE_subsystem_version), ], } if __name__ == "__main__": retval = 0 for filename in sys.argv[1:]: try: binary = lief.parse(filename) - etype = binary.format.name + etype = binary.format if etype == lief.EXE_FORMATS.UNKNOWN: print(f"{filename}: unknown executable format") failed = [] for name, func in CHECKS[etype]: if not func(binary): failed.append(name) if failed: print(f'{filename}: failed {" ".join(failed)}') retval = 1 except IOError: print(f"{filename}: cannot open") retval = 1 sys.exit(retval)