diff --git a/contrib/devtools/pixie.py b/contrib/devtools/pixie.py deleted file mode 100644 --- a/contrib/devtools/pixie.py +++ /dev/null @@ -1,467 +0,0 @@ -#!/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 = 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/security-check.py b/contrib/devtools/security-check.py --- a/contrib/devtools/security-check.py +++ b/contrib/devtools/security-check.py @@ -8,46 +8,19 @@ Otherwise the exit status will be 1 and it will log which executables failed which checks. """ import sys -from typing import List, Optional +from typing import List import lief -import pixie -def check_ELF_PIE(executable) -> bool: - """ - Check for position independent executable (PIE), allowing for address space - randomization. - """ - elf = pixie.load(executable) - return elf.hdr.e_type == pixie.ET_DYN - - -def check_ELF_NX(executable) -> bool: - """ - Check that no sections are writable and executable (including the stack) - """ - elf = pixie.load(executable) - have_wx = False - have_gnu_stack = False - for ph in elf.program_headers: - if ph.p_type == pixie.PT_GNU_STACK: - have_gnu_stack = True - # section is both writable and executable - if (ph.p_flags & pixie.PF_W) != 0 and (ph.p_flags & pixie.PF_X) != 0: - have_wx = True - return have_gnu_stack and not have_wx - - -def check_ELF_RELRO(executable) -> bool: +def check_ELF_RELRO(binary) -> bool: """ Check for read-only relocations. GNU_RELRO program header must exist Dynamic section must have BIND_NOW flag """ - elf = pixie.load(executable) have_gnu_relro = False - for ph in elf.program_headers: + 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 @@ -55,96 +28,89 @@ # 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 ph.p_type == pixie.PT_GNU_RELRO: + if segment.type == lief.ELF.SEGMENT_TYPES.GNU_RELRO: have_gnu_relro = True have_bindnow = False - for flags in elf.query_dyn_tags(pixie.DT_FLAGS): - assert isinstance(flags, int) - if flags & pixie.DF_BIND_NOW: + 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(executable) -> bool: +def check_ELF_Canary(binary) -> bool: """ Check for use of stack canary """ - elf = pixie.load(executable) - for symbol in elf.dyn_symbols: - if symbol.name == b"__stack_chk_fail": - return True - return False + return binary.has_symbol("__stack_chk_fail") -def check_ELF_separate_code(executable): +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. """ - elf = pixie.load(executable) - R = pixie.PF_R - W = pixie.PF_W - E = pixie.PF_X + R = lief.ELF.SEGMENT_FLAGS.R + W = lief.ELF.SEGMENT_FLAGS.W + E = lief.ELF.SEGMENT_FLAGS.X EXPECTED_FLAGS = { # Read + execute - b".init": R | E, - b".plt": R | E, - b".plt.got": R | E, - b".plt.sec": R | E, - b".text": R | E, - b".fini": R | E, + ".init": R | E, + ".plt": R | E, + ".plt.got": R | E, + ".plt.sec": R | E, + ".text": R | E, + ".fini": R | E, # Read-only data - b".interp": R, - b".note.gnu.property": R, - b".note.gnu.build-id": R, - b".note.ABI-tag": R, - b".gnu.hash": R, - b".dynsym": R, - b".dynstr": R, - b".gnu.version": R, - b".gnu.version_r": R, - b".rela.dyn": R, - b".rela.plt": R, - b".rodata": R, - b".eh_frame_hdr": R, - b".eh_frame": R, - b".qtmetadata": R, - b".gcc_except_table": R, - b".stapsdt.base": R, + ".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 - b".init_array": R | W, - b".fini_array": R | W, - b".dynamic": R | W, - b".got": R | W, - b".data": R | W, - b".bss": R | W, + ".init_array": R | W, + ".fini_array": R | W, + ".dynamic": R | W, + ".got": R | W, + ".data": R | W, + ".bss": R | W, } - if elf.hdr.e_machine == pixie.EM_PPC64: - # .plt is RW on ppc64 even with separate-code - EXPECTED_FLAGS[b".plt"] = 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 ph in elf.program_headers: - if ph.p_type == pixie.PT_LOAD: - for section in ph.sections: + 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] = ph.p_flags + 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 EXPECTED_FLAGS[section] != flags: + if int(EXPECTED_FLAGS[section]) != int(flags): return False return True -def check_PE_DYNAMIC_BASE(executable) -> bool: +def check_PE_DYNAMIC_BASE(binary) -> bool: """PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)""" - binary = lief.parse(executable) return ( lief.PE.DLL_CHARACTERISTICS.DYNAMIC_BASE in binary.optional_header.dll_characteristics_lists @@ -155,58 +121,52 @@ # in addition to DYNAMIC_BASE to have secure ASLR. -def check_PE_HIGH_ENTROPY_VA(executable) -> bool: +def check_PE_HIGH_ENTROPY_VA(binary) -> bool: """PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR""" - 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: +def check_PE_RELOC_SECTION(binary) -> bool: """Check for a reloc section. This is required for functional ASLR.""" - binary = lief.parse(executable) return binary.has_relocations -def check_MACHO_NOUNDEFS(executable) -> bool: +def check_MACHO_NOUNDEFS(binary) -> bool: """ Check for no undefined references. """ - binary = lief.parse(executable) return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS) -def check_MACHO_Canary(executable) -> bool: +def check_MACHO_Canary(binary) -> bool: """ Check for use of stack canary """ - binary = lief.parse(executable) return binary.has_symbol("___stack_chk_fail") -def check_PIE(executable) -> bool: +def check_PIE(binary) -> bool: """ Check for position independent executable (PIE), allowing for address space randomization. """ - binary = lief.parse(executable) return binary.is_pie -def check_NX(executable) -> bool: +def check_NX(binary) -> bool: """ Check for no stack execution """ - binary = lief.parse(executable) return binary.has_nx CHECKS = { "ELF": [ - ("PIE", check_ELF_PIE), - ("NX", check_ELF_NX), + ("PIE", check_PIE), + ("NX", check_NX), ("RELRO", check_ELF_RELRO), ("Canary", check_ELF_Canary), ("separate_code", check_ELF_separate_code), @@ -227,31 +187,20 @@ } -def identify_executable(executable) -> Optional[str]: - with open(executable, "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: int = 0 for filename in sys.argv[1:]: try: - etype = identify_executable(filename) - if etype is None: - print(f"{filename}: unknown format") + binary = lief.parse(filename) + etype = binary.format.name + if etype == lief.EXE_FORMATS.UNKNOWN: + print(f"{filename}: unknown executable format") retval = 1 continue failed: List[str] = [] for name, func in CHECKS[etype]: - if not func(filename): + if not func(binary): failed.append(name) if failed: print(f"{filename}: failed {' '.join(failed)}") diff --git a/contrib/devtools/symbol-check.py b/contrib/devtools/symbol-check.py --- a/contrib/devtools/symbol-check.py +++ b/contrib/devtools/symbol-check.py @@ -10,13 +10,9 @@ 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 # @@ -32,10 +28,10 @@ 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), + 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) @@ -184,38 +180,8 @@ } -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" + (lib, _, ver) = version.rpartition("_") ver = tuple([int(x) for x in ver.split(".")]) if lib not in max_versions: return False @@ -225,53 +191,57 @@ return ver <= max_versions[lib][arch] -def check_imported_symbols(filename) -> bool: - elf = pixie.load(filename) - cppfilt = CPPFilt() +def check_imported_symbols(binary) -> bool: ok = True - for symbol in elf.dyn_symbols: - if not symbol.is_import: + for symbol in binary.imported_symbols: + if not symbol.imported: 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}" + + 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 ) - ok = False + 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(filename) -> bool: - elf = pixie.load(filename) - cppfilt = CPPFilt() +def check_exported_symbols(binary) -> bool: ok = True - for symbol in elf.dyn_symbols: - if not symbol.is_export: + + for symbol in binary.dynamic_symbols: + if not symbol.exported: continue - sym = symbol.name.decode() - if sym in IGNORE_EXPORTS: + name = symbol.name + if name in IGNORE_EXPORTS: continue - print(f"{filename}: export of symbol {cppfilt(sym)} not allowed") + print(f"{binary.name}: export of symbol {name} not allowed!") ok = False return ok -def check_ELF_libraries(filename) -> bool: +def check_ELF_libraries(binary) -> 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") + 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(filename) -> bool: - ok: bool = True - binary = lief.parse(filename) +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: @@ -280,19 +250,16 @@ return ok -def check_MACHO_min_os(filename) -> bool: - binary = lief.parse(filename) +def check_MACHO_min_os(binary) -> bool: return binary.build_version.minos == [10, 15, 0] -def check_MACHO_sdk(filename) -> bool: - binary = lief.parse(filename) +def check_MACHO_sdk(binary) -> bool: return binary.build_version.sdk == [10, 15, 6] -def check_PE_libraries(filename) -> bool: +def check_PE_libraries(binary) -> 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!") @@ -300,15 +267,14 @@ return ok -def check_PE_subsystem_version(filename) -> bool: +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(filename) -> bool: - binary = lief.parse(filename) +def check_ELF_interpreter(binary) -> bool: expected_interpreter = ELF_INTERPRETER_NAMES[binary.header.machine_type][ binary.abstract.header.endianness ] @@ -335,31 +301,18 @@ } -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 + binary = lief.parse(filename) + etype = binary.format.name + if etype == lief.EXE_FORMATS.UNKNOWN: + print(f"{filename}: unknown executable format") failed = [] for name, func in CHECKS[etype]: - if not func(filename): + if not func(binary): failed.append(name) if failed: print(f'{filename}: failed {" ".join(failed)}') diff --git a/contrib/devtools/test-security-check.py b/contrib/devtools/test-security-check.py --- a/contrib/devtools/test-security-check.py +++ b/contrib/devtools/test-security-check.py @@ -8,6 +8,7 @@ import os import subprocess import unittest +from typing import List from utils import determine_wellknown_cmd @@ -30,7 +31,16 @@ def call_security_check(cc, source, executable, options): - subprocess.run([*cc, source, "-o", executable] + options, check=True) + # This should behave the same as AC_TRY_LINK, so arrange well-known flags + # in the same order as autoconf would. + # + # See the definitions for ac_link in autoconf's lib/autoconf/c.m4 file for + # reference. + env_flags: List[str] = [] + for var in ["CFLAGS", "CPPFLAGS", "LDFLAGS"]: + env_flags += filter(None, os.environ.get(var, "").split(" ")) + + subprocess.run([*cc, source, "-o", executable] + env_flags + options, check=True) p = subprocess.run( ["./contrib/devtools/security-check.py", executable], stdout=subprocess.PIPE, diff --git a/contrib/devtools/test-symbol-check.py b/contrib/devtools/test-symbol-check.py --- a/contrib/devtools/test-symbol-check.py +++ b/contrib/devtools/test-symbol-check.py @@ -14,7 +14,17 @@ def call_symbol_check(cc: List[str], source, executable, options): - subprocess.run([*cc, source, "-o", executable] + options, check=True) + # This should behave the same as AC_TRY_LINK, so arrange well-known flags + # in the same order as autoconf would. + # + # See the definitions for ac_link in autoconf's lib/autoconf/c.m4 file for + # reference. + # Arrange well-known flags in the same order as cmake would. + env_flags: List[str] = [] + for var in ["CFLAGS", "CPPFLAGS", "LDFLAGS"]: + env_flags += filter(None, os.environ.get(var, "").split(" ")) + + subprocess.run([*cc, source, "-o", executable] + env_flags + options, check=True) p = subprocess.run( ["./contrib/devtools/symbol-check.py", executable], stdout=subprocess.PIPE, @@ -22,7 +32,7 @@ ) os.remove(source) os.remove(executable) - return (p.returncode, p.stdout.rstrip()) + return p.returncode, p.stdout.rstrip() class TestSymbolChecks(unittest.TestCase): @@ -54,7 +64,7 @@ ( 1, executable - + ": symbol renameat2 from unsupported version GLIBC_2.28\n" + + ": symbol renameat2 from unsupported version GLIBC_2.28(3)\n" + executable + ": failed IMPORTED_SYMBOLS", ), @@ -81,7 +91,7 @@ ( 1, executable - + ": NEEDED library libutil.so.1 is not allowed\n" + + ": libutil.so.1 is not ALLOWED_LIBRARIES\n" + executable + ": failed LIBRARY_DEPENDENCIES", ),