diff --git a/contrib/devtools/pixie.py b/contrib/devtools/pixie.py index 34125d785..024dd0aa3 100644 --- a/contrib/devtools/pixie.py +++ b/contrib/devtools/pixie.py @@ -1,467 +1,467 @@ #!/usr/bin/env python3 # Copyright (c) 2020 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. """ Compact, self-contained ELF implementation for bitcoin-abc security checks. """ import struct import types from typing import Dict, List, Optional, Tuple, Union # you can find all these values in elf.h EI_NIDENT = 16 # Byte indices in e_ident EI_CLASS = 4 # ELFCLASSxx EI_DATA = 5 # ELFDATAxxxx ELFCLASS32 = 1 # 32-bit ELFCLASS64 = 2 # 64-bit ELFDATA2LSB = 1 # little endian ELFDATA2MSB = 2 # big endian # relevant values for e_machine EM_386 = 3 EM_PPC64 = 21 EM_ARM = 40 EM_AARCH64 = 183 EM_X86_64 = 62 EM_RISCV = 243 # relevant values for e_type ET_DYN = 3 # relevant values for sh_type SHT_PROGBITS = 1 SHT_STRTAB = 3 SHT_DYNAMIC = 6 SHT_DYNSYM = 11 SHT_GNU_verneed = 0x6FFFFFFE SHT_GNU_versym = 0x6FFFFFFF # relevant values for p_type PT_LOAD = 1 PT_GNU_STACK = 0x6474E551 PT_GNU_RELRO = 0x6474E552 # relevant values for p_flags PF_X = 1 << 0 PF_W = 1 << 1 PF_R = 1 << 2 # relevant values for d_tag DT_NEEDED = 1 DT_FLAGS = 30 # relevant values of `d_un.d_val' in the DT_FLAGS entry DF_BIND_NOW = 0x00000008 # relevant d_tags with string payload STRING_TAGS = {DT_NEEDED} # rrlevant values for ST_BIND subfield of st_info (symbol binding) STB_LOCAL = 0 class ELFRecord(types.SimpleNamespace): """Unified parsing for ELF records.""" def __init__( self, data: bytes, offset: int, eh: "ELFHeader", total_size: Optional[int] ) -> None: hdr_struct = self.STRUCT[eh.ei_class][0][eh.ei_data] if total_size is not None and hdr_struct.size > total_size: raise ValueError( f"{self.__class__.__name__} header size too small ({total_size} <" f" {hdr_struct.size})" ) for field, value in zip( self.STRUCT[eh.ei_class][1], hdr_struct.unpack(data[offset : offset + hdr_struct.size]), ): setattr(self, field, value) def BiStruct(chars: str) -> Dict[int, struct.Struct]: """Compile a struct parser for both endians.""" return { ELFDATA2LSB: struct.Struct("<" + chars), ELFDATA2MSB: struct.Struct(">" + chars), } class ELFHeader(ELFRecord): FIELDS = [ "e_type", "e_machine", "e_version", "e_entry", "e_phoff", "e_shoff", "e_flags", "e_ehsize", "e_phentsize", "e_phnum", "e_shentsize", "e_shnum", "e_shstrndx", ] STRUCT = { ELFCLASS32: (BiStruct("HHIIIIIHHHHHH"), FIELDS), ELFCLASS64: (BiStruct("HHIQQQIHHHHHH"), FIELDS), } def __init__(self, data: bytes, offset: int) -> None: self.e_ident = data[offset : offset + EI_NIDENT] if self.e_ident[0:4] != b"\x7fELF": raise ValueError("invalid ELF magic") self.ei_class = self.e_ident[EI_CLASS] self.ei_data = self.e_ident[EI_DATA] super().__init__(data, offset + EI_NIDENT, self, None) def __repr__(self) -> str: return ( f"Header(e_ident={self.e_ident!r}, e_type={self.e_type}," f" e_machine={self.e_machine}, e_version={self.e_version}," f" e_entry={self.e_entry}, e_phoff={self.e_phoff}, e_shoff={self.e_shoff}," f" e_flags={self.e_flags}, e_ehsize={self.e_ehsize}," f" e_phentsize={self.e_phentsize}, e_phnum={self.e_phnum}," f" e_shentsize={self.e_shentsize}, e_shnum={self.e_shnum}," f" e_shstrndx={self.e_shstrndx})" ) class Section(ELFRecord): name: Optional[bytes] = None FIELDS = [ "sh_name", "sh_type", "sh_flags", "sh_addr", "sh_offset", "sh_size", "sh_link", "sh_info", "sh_addralign", "sh_entsize", ] STRUCT = { ELFCLASS32: (BiStruct("IIIIIIIIII"), FIELDS), ELFCLASS64: (BiStruct("IIQQQQIIQQ"), FIELDS), } def __init__(self, data: bytes, offset: int, eh: ELFHeader) -> None: super().__init__(data, offset, eh, eh.e_shentsize) self._data = data def __repr__(self) -> str: return ( f"Section(sh_name={self.sh_name}({self.name!r})," f" sh_type=0x{self.sh_type:x}, sh_flags={self.sh_flags}," f" sh_addr=0x{self.sh_addr:x}, sh_offset=0x{self.sh_offset:x}," f" sh_size={self.sh_size}, sh_link={self.sh_link}, sh_info={self.sh_info}," f" sh_addralign={self.sh_addralign}, sh_entsize={self.sh_entsize})" ) def contents(self) -> bytes: """Return section contents.""" return self._data[self.sh_offset : self.sh_offset + self.sh_size] class ProgramHeader(ELFRecord): STRUCT = { # different ELF classes have the same fields, but in a different order to # optimize space versus alignment ELFCLASS32: ( BiStruct("IIIIIIII"), [ "p_type", "p_offset", "p_vaddr", "p_paddr", "p_filesz", "p_memsz", "p_flags", "p_align", ], ), ELFCLASS64: ( BiStruct("IIQQQQQQ"), [ "p_type", "p_flags", "p_offset", "p_vaddr", "p_paddr", "p_filesz", "p_memsz", "p_align", ], ), } def __init__(self, data: bytes, offset: int, eh: ELFHeader) -> None: super().__init__(data, offset, eh, eh.e_phentsize) def __repr__(self) -> str: return ( f"ProgramHeader(p_type={self.p_type}, p_offset={self.p_offset}," f" p_vaddr={self.p_vaddr}, p_paddr={self.p_paddr}," f" p_filesz={self.p_filesz}, p_memsz={self.p_memsz}," f" p_flags={self.p_flags}, p_align={self.p_align})" ) class Symbol(ELFRecord): STRUCT = { # different ELF classes have the same fields, but in a different order to # optimize space versus alignment ELFCLASS32: ( BiStruct("IIIBBH"), ["st_name", "st_value", "st_size", "st_info", "st_other", "st_shndx"], ), ELFCLASS64: ( BiStruct("IBBHQQ"), ["st_name", "st_info", "st_other", "st_shndx", "st_value", "st_size"], ), } def __init__( self, data: bytes, offset: int, eh: ELFHeader, symtab: Section, strings: bytes, version: Optional[bytes], ) -> None: super().__init__(data, offset, eh, symtab.sh_entsize) self.name = _lookup_string(strings, self.st_name) self.version = version def __repr__(self) -> str: return ( f"Symbol(st_name={self.st_name}({self.name!r}), st_value={self.st_value}," f" st_size={self.st_size}, st_info={self.st_info}," f" st_other={self.st_other}, st_shndx={self.st_shndx}," f" version={self.version!r})" ) @property def is_import(self) -> bool: """Returns whether the symbol is an imported symbol.""" return self.st_bind != STB_LOCAL and self.st_shndx == 0 @property def is_export(self) -> bool: """Returns whether the symbol is an exported symbol.""" return self.st_bind != STB_LOCAL and self.st_shndx != 0 @property def st_bind(self) -> int: """Returns STB_*.""" return self.st_info >> 4 class Verneed(ELFRecord): DEF = (BiStruct("HHIII"), ["vn_version", "vn_cnt", "vn_file", "vn_aux", "vn_next"]) STRUCT = {ELFCLASS32: DEF, ELFCLASS64: DEF} def __init__(self, data: bytes, offset: int, eh: ELFHeader) -> None: super().__init__(data, offset, eh, None) def __repr__(self) -> str: return ( f"Verneed(vn_version={self.vn_version}, vn_cnt={self.vn_cnt}," f" vn_file={self.vn_file}, vn_aux={self.vn_aux}, vn_next={self.vn_next})" ) class Vernaux(ELFRecord): DEF = ( BiStruct("IHHII"), ["vna_hash", "vna_flags", "vna_other", "vna_name", "vna_next"], ) STRUCT = {ELFCLASS32: DEF, ELFCLASS64: DEF} def __init__(self, data: bytes, offset: int, eh: ELFHeader, strings: bytes) -> None: super().__init__(data, offset, eh, None) self.name = _lookup_string(strings, self.vna_name) def __repr__(self) -> str: return ( f"Veraux(vna_hash={self.vna_hash}, vna_flags={self.vna_flags}," f" vna_other={self.vna_other}, vna_name={self.vna_name}({self.name!r})," f" vna_next={self.vna_next})" ) class DynTag(ELFRecord): STRUCT = { ELFCLASS32: (BiStruct("II"), ["d_tag", "d_val"]), ELFCLASS64: (BiStruct("QQ"), ["d_tag", "d_val"]), } def __init__( self, data: bytes, offset: int, eh: ELFHeader, section: Section ) -> None: super().__init__(data, offset, eh, section.sh_entsize) def __repr__(self) -> str: return f"DynTag(d_tag={self.d_tag}, d_val={self.d_val})" def _lookup_string(data: bytes, index: int) -> bytes: """Look up string by offset in ELF string table.""" endx = data.find(b"\x00", index) assert endx != -1 return data[index:endx] # .gnu_version section has a single 16-bit integer per symbol in the linked section VERSYM_S = BiStruct("H") def _parse_symbol_table( section: Section, strings: bytes, eh: ELFHeader, versym: bytes, verneed: Dict[int, bytes], ) -> List[Symbol]: """Parse symbol table, return a list of symbols.""" data = section.contents() symbols = [] versym_iter = (verneed.get(v[0]) for v in VERSYM_S[eh.ei_data].iter_unpack(versym)) for ofs, version in zip(range(0, len(data), section.sh_entsize), versym_iter): symbols.append(Symbol(data, ofs, eh, section, strings, version)) return symbols def _parse_verneed(section: Section, strings: bytes, eh: ELFHeader) -> Dict[int, bytes]: """Parse .gnu.version_r section, return a dictionary of {versym: 'GLIBC_...'}.""" data = section.contents() ofs = 0 result = {} while True: verneed = Verneed(data, ofs, eh) - aofs = verneed.vn_aux + aofs = ofs + verneed.vn_aux while True: vernaux = Vernaux(data, aofs, eh, strings) result[vernaux.vna_other] = vernaux.name if not vernaux.vna_next: break aofs += vernaux.vna_next if not verneed.vn_next: break ofs += verneed.vn_next return result def _parse_dyn_tags( section: Section, strings: bytes, eh: ELFHeader ) -> List[Tuple[int, Union[bytes, int]]]: """Parse dynamic tags. Return array of tuples.""" data = section.contents() ofs = 0 result = [] for ofs in range(0, len(data), section.sh_entsize): tag = DynTag(data, ofs, eh, section) val = ( _lookup_string(strings, tag.d_val) if tag.d_tag in STRING_TAGS else tag.d_val ) result.append((tag.d_tag, val)) return result class ELFFile: sections: List[Section] program_headers: List[ProgramHeader] dyn_symbols: List[Symbol] dyn_tags: List[Tuple[int, Union[bytes, int]]] def __init__(self, data: bytes) -> None: self.data = data self.hdr = ELFHeader(self.data, 0) self._load_sections() self._load_program_headers() self._load_dyn_symbols() self._load_dyn_tags() self._section_to_segment_mapping() def _load_sections(self) -> None: self.sections = [] for idx in range(self.hdr.e_shnum): offset = self.hdr.e_shoff + idx * self.hdr.e_shentsize self.sections.append(Section(self.data, offset, self.hdr)) shstr = self.sections[self.hdr.e_shstrndx].contents() for section in self.sections: section.name = _lookup_string(shstr, section.sh_name) def _load_program_headers(self) -> None: self.program_headers = [] for idx in range(self.hdr.e_phnum): offset = self.hdr.e_phoff + idx * self.hdr.e_phentsize self.program_headers.append(ProgramHeader(self.data, offset, self.hdr)) def _load_dyn_symbols(self) -> None: # first, load 'verneed' section verneed = None for section in self.sections: if section.sh_type == SHT_GNU_verneed: # associated string table strtab = self.sections[section.sh_link].contents() assert verneed is None # only one section of this kind please verneed = _parse_verneed(section, strtab, self.hdr) assert verneed is not None # then, correlate GNU versym sections with dynamic symbol sections versym = {} for section in self.sections: if section.sh_type == SHT_GNU_versym: versym[section.sh_link] = section # finally, load dynsym sections self.dyn_symbols = [] for idx, section in enumerate(self.sections): if section.sh_type == SHT_DYNSYM: # find dynamic symbol tables # associated string table strtab_data = self.sections[section.sh_link].contents() versym_data = versym[idx].contents() # associated symbol version table self.dyn_symbols += _parse_symbol_table( section, strtab_data, self.hdr, versym_data, verneed ) def _load_dyn_tags(self) -> None: self.dyn_tags = [] for idx, section in enumerate(self.sections): if section.sh_type == SHT_DYNAMIC: # find dynamic tag tables # associated string table strtab = self.sections[section.sh_link].contents() self.dyn_tags += _parse_dyn_tags(section, strtab, self.hdr) def _section_to_segment_mapping(self) -> None: for ph in self.program_headers: ph.sections = [] for section in self.sections: if ph.p_vaddr <= section.sh_addr < (ph.p_vaddr + ph.p_memsz): ph.sections.append(section) def query_dyn_tags(self, tag_in: int) -> List[Union[int, bytes]]: """Return the values of all dyn tags with the specified tag.""" return [val for (tag, val) in self.dyn_tags if tag == tag_in] def load(filename: str) -> ELFFile: with open(filename, "rb") as f: data = f.read() return ELFFile(data) diff --git a/contrib/devtools/symbol-check.py b/contrib/devtools/symbol-check.py index 40f374997..479ff536a 100755 --- a/contrib/devtools/symbol-check.py +++ b/contrib/devtools/symbol-check.py @@ -1,366 +1,370 @@ #!/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. Example usage: find contrib/gitian-builder/build -type f -executable | xargs python3 contrib/devtools/symbol-check.py """ import subprocess import sys from typing import Optional import lief import pixie from utils import determine_wellknown_cmd # 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": (2, 28), "LIBATOMIC": (1, 0)} +MAX_VERSIONS = { + "GCC": (8, 3, 0), + "GLIBC": { + pixie.EM_386: (2, 28), + pixie.EM_X86_64: (2, 28), + pixie.EM_ARM: (2, 28), + pixie.EM_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", } -ARCH_MIN_GLIBC_VER = { - pixie.EM_386: (2, 1), - pixie.EM_X86_64: (2, 2, 5), - pixie.EM_ARM: (2, 4), - pixie.EM_AARCH64: (2, 17), -} - 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", } class CPPFilt(object): """ Demangle C++ symbol names. Use a pipe to the 'c++filt' command. """ def __init__(self): self.proc = subprocess.Popen( determine_wellknown_cmd("CPPFILT", "c++filt"), stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True, ) def __call__(self, mangled): self.proc.stdin.write(mangled + "\n") self.proc.stdin.flush() return self.proc.stdout.readline().rstrip() def close(self): self.proc.stdin.close() self.proc.stdout.close() self.proc.wait() def check_version(max_versions, version, arch) -> bool: if "_" in version: (lib, _, ver) = version.rpartition("_") else: lib = version ver = "0" ver = tuple([int(x) for x in ver.split(".")]) if lib not in max_versions: return False - return ( - ver <= max_versions[lib] or lib == "GLIBC" and ver <= ARCH_MIN_GLIBC_VER[arch] - ) + if isinstance(max_versions[lib], tuple): + return ver <= max_versions[lib] + else: + return ver <= max_versions[lib][arch] def check_imported_symbols(filename) -> bool: elf = pixie.load(filename) cppfilt = CPPFilt() ok = True for symbol in elf.dyn_symbols: if not symbol.is_import: continue sym = symbol.name.decode() version = symbol.version.decode() if symbol.version is not None else None if version and not check_version(MAX_VERSIONS, version, elf.hdr.e_machine): print( f"{filename}: symbol {cppfilt(sym)} from unsupported version {version}" ) ok = False return ok def check_exported_symbols(filename) -> bool: elf = pixie.load(filename) cppfilt = CPPFilt() ok = True for symbol in elf.dyn_symbols: if not symbol.is_export: continue sym = symbol.name.decode() if sym in IGNORE_EXPORTS: continue print(f"{filename}: export of symbol {cppfilt(sym)} not allowed") ok = False return ok def check_ELF_libraries(filename) -> bool: ok = True elf = pixie.load(filename) for library_name in elf.query_dyn_tags(pixie.DT_NEEDED): assert isinstance(library_name, bytes) if library_name.decode() not in ELF_ALLOWED_LIBRARIES: print(f"{filename}: NEEDED library {library_name.decode()} is not allowed") ok = False return ok def check_MACHO_libraries(filename) -> bool: ok: bool = True 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 check_MACHO_min_os(filename) -> bool: binary = lief.parse(filename) return binary.build_version.minos == [10, 15, 0] def check_MACHO_sdk(filename) -> bool: binary = lief.parse(filename) return binary.build_version.sdk == [10, 15, 6] def check_PE_libraries(filename) -> bool: ok: bool = True binary = lief.parse(filename) 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(filename) -> 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(filename) -> bool: binary = lief.parse(filename) expected_interpreter = ELF_INTERPRETER_NAMES[binary.header.machine_type][ binary.abstract.header.endianness ] return binary.concrete.interpreter == expected_interpreter CHECKS = { "ELF": [ ("IMPORTED_SYMBOLS", check_imported_symbols), ("EXPORTED_SYMBOLS", check_exported_symbols), ("LIBRARY_DEPENDENCIES", check_ELF_libraries), ("INTERPRETER_NAME", check_ELF_interpreter), ], "MACHO": [ ("DYNAMIC_LIBRARIES", check_MACHO_libraries), ("MIN_OS", check_MACHO_min_os), ("SDK", check_MACHO_sdk), ], "PE": [ ("DYNAMIC_LIBRARIES", check_PE_libraries), ("SUBSYSTEM_VERSION", check_PE_subsystem_version), ], } def identify_executable(filename) -> Optional[str]: with open(filename, "rb") as f: magic = f.read(4) if magic.startswith(b"MZ"): return "PE" elif magic.startswith(b"\x7fELF"): return "ELF" elif magic.startswith(b"\xcf\xfa"): return "MACHO" return None if __name__ == "__main__": retval = 0 for filename in sys.argv[1:]: try: etype = identify_executable(filename) if etype is None: print(f"{filename}: unknown format") retval = 1 continue failed = [] for name, func in CHECKS[etype]: if not func(filename): 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)