merge bitcoin#22392: use LIEF for ELF security & symbol checks

This commit is contained in:
Kittywhiskers Van Gogh 2023-05-13 15:23:21 +00:00
parent d28ba33136
commit 110dbf82e8
8 changed files with 142 additions and 547 deletions

View File

@ -57,8 +57,7 @@ DIST_SHARE = \
BIN_CHECKS=$(top_srcdir)/contrib/devtools/symbol-check.py \ BIN_CHECKS=$(top_srcdir)/contrib/devtools/symbol-check.py \
$(top_srcdir)/contrib/devtools/security-check.py \ $(top_srcdir)/contrib/devtools/security-check.py \
$(top_srcdir)/contrib/devtools/utils.py \ $(top_srcdir)/contrib/devtools/utils.py
$(top_srcdir)/contrib/devtools/pixie.py
WINDOWS_PACKAGING = $(top_srcdir)/share/pixmaps/dash.ico \ WINDOWS_PACKAGING = $(top_srcdir)/share/pixmaps/dash.ico \
$(top_srcdir)/share/pixmaps/nsis-header.bmp \ $(top_srcdir)/share/pixmaps/nsis-header.bmp \
@ -343,14 +342,14 @@ clean-local: clean-docs
test-security-check: test-security-check:
if TARGET_DARWIN if TARGET_DARWIN
$(AM_V_at) CC='$(CC)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-security-check.py TestSecurityChecks.test_MACHO $(AM_V_at) CC='$(CC)' CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' LDFLAGS='$(LDFLAGS)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-security-check.py TestSecurityChecks.test_MACHO
$(AM_V_at) CC='$(CC)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-symbol-check.py TestSymbolChecks.test_MACHO $(AM_V_at) CC='$(CC)' CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' LDFLAGS='$(LDFLAGS)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-symbol-check.py TestSymbolChecks.test_MACHO
endif endif
if TARGET_WINDOWS if TARGET_WINDOWS
$(AM_V_at) CC='$(CC)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-security-check.py TestSecurityChecks.test_PE $(AM_V_at) CC='$(CC)' CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' LDFLAGS='$(LDFLAGS)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-security-check.py TestSecurityChecks.test_PE
$(AM_V_at) CC='$(CC)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-symbol-check.py TestSymbolChecks.test_PE $(AM_V_at) CC='$(CC)' CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' LDFLAGS='$(LDFLAGS)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-symbol-check.py TestSymbolChecks.test_PE
endif endif
if TARGET_LINUX if TARGET_LINUX
$(AM_V_at) CC='$(CC)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-security-check.py TestSecurityChecks.test_ELF $(AM_V_at) CC='$(CC)' CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' LDFLAGS='$(LDFLAGS)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-security-check.py TestSecurityChecks.test_ELF
$(AM_V_at) CC='$(CC)' CPPFILT='$(CPPFILT)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-symbol-check.py TestSymbolChecks.test_ELF $(AM_V_at) CC='$(CC)' CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' LDFLAGS='$(LDFLAGS)' $(PYTHON) $(top_srcdir)/contrib/devtools/test-symbol-check.py TestSymbolChecks.test_ELF
endif endif

View File

@ -106,7 +106,6 @@ AC_PATH_PROG([GIT], [git])
AC_PATH_PROG(CCACHE,ccache) AC_PATH_PROG(CCACHE,ccache)
AC_PATH_PROG(XGETTEXT,xgettext) AC_PATH_PROG(XGETTEXT,xgettext)
AC_PATH_PROG(HEXDUMP,hexdump) AC_PATH_PROG(HEXDUMP,hexdump)
AC_PATH_TOOL(CPPFILT, c++filt)
AC_PATH_TOOL(OBJCOPY, objcopy) AC_PATH_TOOL(OBJCOPY, objcopy)
AC_PATH_TOOL(DSYMUTIL, dsymutil) AC_PATH_TOOL(DSYMUTIL, dsymutil)
AC_PATH_PROG(DOXYGEN, doxygen) AC_PATH_PROG(DOXYGEN, doxygen)

View File

@ -1,323 +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-core security checks.
'''
import struct
import types
from typing import Dict, List, Optional, Union, Tuple
# 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} < {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}, e_machine={self.e_machine}, e_version={self.e_version}, e_entry={self.e_entry}, e_phoff={self.e_phoff}, e_shoff={self.e_shoff}, e_flags={self.e_flags}, e_ehsize={self.e_ehsize}, e_phentsize={self.e_phentsize}, e_phnum={self.e_phnum}, e_shentsize={self.e_shentsize}, e_shnum={self.e_shnum}, 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}), sh_type=0x{self.sh_type:x}, sh_flags={self.sh_flags}, sh_addr=0x{self.sh_addr:x}, sh_offset=0x{self.sh_offset:x}, sh_size={self.sh_size}, sh_link={self.sh_link}, sh_info={self.sh_info}, 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}, p_vaddr={self.p_vaddr}, p_paddr={self.p_paddr}, p_filesz={self.p_filesz}, p_memsz={self.p_memsz}, 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}, st_size={self.st_size}, st_info={self.st_info}, st_other={self.st_other}, st_shndx={self.st_shndx}, 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}, 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}, vna_other={self.vna_other}, vna_name={self.vna_name}({self.name!r}), 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]
VERSYM_S = BiStruct('H') # .gnu_version section has a single 16-bit integer per symbol in the linked section
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:
strtab = self.sections[section.sh_link].contents() # associated string table
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
strtab_data = self.sections[section.sh_link].contents() # associated string table
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
strtab = self.sections[section.sh_link].contents() # associated string table
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)

View File

@ -8,192 +8,155 @@ 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. Otherwise the exit status will be 1 and it will log which executables failed which checks.
''' '''
import sys import sys
from typing import List, Optional from typing import List
import lief import lief
import pixie
def check_ELF_PIE(executable) -> bool: def check_ELF_RELRO(binary) -> 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
if (ph.p_flags & pixie.PF_W) != 0 and (ph.p_flags & pixie.PF_X) != 0: # section is both writable and executable
have_wx = True
return have_gnu_stack and not have_wx
def check_ELF_RELRO(executable) -> bool:
''' '''
Check for read-only relocations. Check for read-only relocations.
GNU_RELRO program header must exist GNU_RELRO program header must exist
Dynamic section must have BIND_NOW flag Dynamic section must have BIND_NOW flag
''' '''
elf = pixie.load(executable)
have_gnu_relro = False 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 # 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 # This does not affect security: the permission flags of the GNU_RELRO program
# header are ignored, the PT_LOAD header determines the effective permissions. # 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. # 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. # Glibc itself takes care of mprotecting this area R after relocations are finished.
# See also https://marc.info/?l=binutils&m=1498883354122353 # 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_gnu_relro = True
have_bindnow = False have_bindnow = False
for flags in elf.query_dyn_tags(pixie.DT_FLAGS): try:
assert isinstance(flags, int) flags = binary.get(lief.ELF.DYNAMIC_TAGS.FLAGS)
if flags & pixie.DF_BIND_NOW: if flags.value & lief.ELF.DYNAMIC_FLAGS.BIND_NOW:
have_bindnow = True have_bindnow = True
except:
have_bindnow = False
return have_gnu_relro and have_bindnow 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 Check for use of stack canary
''' '''
elf = pixie.load(executable) return binary.has_symbol('__stack_chk_fail')
ok = False
for symbol in elf.dyn_symbols:
if symbol.name == b'__stack_chk_fail':
ok = True
return ok
def check_ELF_separate_code(executable): def check_ELF_separate_code(binary):
''' '''
Check that sections are appropriately separated in virtual memory, Check that sections are appropriately separated in virtual memory,
based on their permissions. This checks for missing -Wl,-z,separate-code based on their permissions. This checks for missing -Wl,-z,separate-code
and potentially other problems. and potentially other problems.
''' '''
elf = pixie.load(executable) R = lief.ELF.SEGMENT_FLAGS.R
R = pixie.PF_R W = lief.ELF.SEGMENT_FLAGS.W
W = pixie.PF_W E = lief.ELF.SEGMENT_FLAGS.X
E = pixie.PF_X
EXPECTED_FLAGS = { EXPECTED_FLAGS = {
# Read + execute # Read + execute
b'.init': R | E, '.init': R | E,
b'.plt': R | E, '.plt': R | E,
b'.plt.got': R | E, '.plt.got': R | E,
b'.plt.sec': R | E, '.plt.sec': R | E,
b'.text': R | E, '.text': R | E,
b'.fini': R | E, '.fini': R | E,
# Read-only data # Read-only data
b'.interp': R, '.interp': R,
b'.note.gnu.property': R, '.note.gnu.property': R,
b'.note.gnu.build-id': R, '.note.gnu.build-id': R,
b'.note.ABI-tag': R, '.note.ABI-tag': R,
b'.gnu.hash': R, '.gnu.hash': R,
b'.dynsym': R, '.dynsym': R,
b'.dynstr': R, '.dynstr': R,
b'.gnu.version': R, '.gnu.version': R,
b'.gnu.version_r': R, '.gnu.version_r': R,
b'.rela.dyn': R, '.rela.dyn': R,
b'.rela.plt': R, '.rela.plt': R,
b'.rodata': R, '.rodata': R,
b'.eh_frame_hdr': R, '.eh_frame_hdr': R,
b'.eh_frame': R, '.eh_frame': R,
b'.qtmetadata': R, '.qtmetadata': R,
b'.gcc_except_table': R, '.gcc_except_table': R,
b'.stapsdt.base': R, '.stapsdt.base': R,
# Writable data # Writable data
b'.init_array': R | W, '.init_array': R | W,
b'.fini_array': R | W, '.fini_array': R | W,
b'.dynamic': R | W, '.dynamic': R | W,
b'.got': R | W, '.got': R | W,
b'.data': R | W, '.data': R | W,
b'.bss': R | W, '.bss': R | W,
} }
if elf.hdr.e_machine == pixie.EM_PPC64: if binary.header.machine_type == lief.ELF.ARCH.PPC64:
# .plt is RW on ppc64 even with separate-code # .plt is RW on ppc64 even with separate-code
EXPECTED_FLAGS[b'.plt'] = R | W EXPECTED_FLAGS['.plt'] = R | W
# For all LOAD program headers get mapping to the list of sections, # For all LOAD program headers get mapping to the list of sections,
# and for each section, remember the flags of the associated program header. # and for each section, remember the flags of the associated program header.
flags_per_section = {} flags_per_section = {}
for ph in elf.program_headers: for segment in binary.segments:
if ph.p_type == pixie.PT_LOAD: if segment.type == lief.ELF.SEGMENT_TYPES.LOAD:
for section in ph.sections: for section in segment.sections:
assert(section.name not in flags_per_section) 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 # Spot-check ELF LOAD program header flags per section
# If these sections exist, check them against the expected R/W/E flags # If these sections exist, check them against the expected R/W/E flags
for (section, flags) in flags_per_section.items(): for (section, flags) in flags_per_section.items():
if section in EXPECTED_FLAGS: if section in EXPECTED_FLAGS:
if EXPECTED_FLAGS[section] != flags: if int(EXPECTED_FLAGS[section]) != int(flags):
return False return False
return True return True
def check_PE_DYNAMIC_BASE(executable) -> bool: def check_PE_DYNAMIC_BASE(binary) -> bool:
'''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)''' '''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 return lief.PE.DLL_CHARACTERISTICS.DYNAMIC_BASE in binary.optional_header.dll_characteristics_lists
# Must support high-entropy 64-bit address space layout randomization # Must support high-entropy 64-bit address space layout randomization
# in addition to DYNAMIC_BASE to have secure ASLR. # 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''' '''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 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.''' '''Check for a reloc section. This is required for functional ASLR.'''
binary = lief.parse(executable)
return binary.has_relocations return binary.has_relocations
def check_MACHO_NOUNDEFS(executable) -> bool: def check_MACHO_NOUNDEFS(binary) -> bool:
''' '''
Check for no undefined references. Check for no undefined references.
''' '''
binary = lief.parse(executable)
return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS) return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS)
def check_MACHO_LAZY_BINDINGS(executable) -> bool: def check_MACHO_LAZY_BINDINGS(binary) -> bool:
''' '''
Check for no lazy bindings. Check for no lazy bindings.
We don't use or check for MH_BINDATLOAD. See #18295. We don't use or check for MH_BINDATLOAD. See #18295.
''' '''
binary = lief.parse(executable)
return binary.dyld_info.lazy_bind == (0,0) return binary.dyld_info.lazy_bind == (0,0)
def check_MACHO_Canary(executable) -> bool: def check_MACHO_Canary(binary) -> bool:
''' '''
Check for use of stack canary Check for use of stack canary
''' '''
binary = lief.parse(executable)
return binary.has_symbol('___stack_chk_fail') return binary.has_symbol('___stack_chk_fail')
def check_PIE(executable) -> bool: def check_PIE(binary) -> bool:
''' '''
Check for position independent executable (PIE), Check for position independent executable (PIE),
allowing for address space randomization. allowing for address space randomization.
''' '''
binary = lief.parse(executable)
return binary.is_pie return binary.is_pie
def check_NX(executable) -> bool: def check_NX(binary) -> bool:
''' '''
Check for no stack execution Check for no stack execution
''' '''
binary = lief.parse(executable)
return binary.has_nx return binary.has_nx
def check_control_flow(executable) -> bool: def check_control_flow(binary) -> bool:
''' '''
Check for control flow instrumentation Check for control flow instrumentation
''' '''
binary = lief.parse(executable)
content = binary.get_content_from_virtual_address(binary.entrypoint, 4, lief.Binary.VA_TYPES.AUTO) content = binary.get_content_from_virtual_address(binary.entrypoint, 4, lief.Binary.VA_TYPES.AUTO)
if content == [243, 15, 30, 250]: # endbr64 if content == [243, 15, 30, 250]: # endbr64
@ -203,8 +166,8 @@ def check_control_flow(executable) -> bool:
CHECKS = { CHECKS = {
'ELF': [ 'ELF': [
('PIE', check_ELF_PIE), ('PIE', check_PIE),
('NX', check_ELF_NX), ('NX', check_NX),
('RELRO', check_ELF_RELRO), ('RELRO', check_ELF_RELRO),
('Canary', check_ELF_Canary), ('Canary', check_ELF_Canary),
('separate_code', check_ELF_separate_code), ('separate_code', check_ELF_separate_code),
@ -226,30 +189,20 @@ CHECKS = {
] ]
} }
def identify_executable(executable) -> 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__': if __name__ == '__main__':
retval: int = 0 retval: int = 0
for filename in sys.argv[1:]: for filename in sys.argv[1:]:
try: try:
etype = identify_executable(filename) binary = lief.parse(filename)
if etype is None: etype = binary.format.name
print(f'{filename}: unknown format') if etype == lief.EXE_FORMATS.UNKNOWN:
print(f'{filename}: unknown executable format')
retval = 1 retval = 1
continue continue
failed: List[str] = [] failed: List[str] = []
for (name, func) in CHECKS[etype]: for (name, func) in CHECKS[etype]:
if not func(filename): if not func(binary):
failed.append(name) failed.append(name)
if failed: if failed:
print(f'{filename}: failed {" ".join(failed)}') print(f'{filename}: failed {" ".join(failed)}')

View File

@ -10,14 +10,13 @@ Example usage:
find ../path/to/binaries -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 subprocess
import sys import sys
from typing import Optional
import lief import lief
import pixie
from utils import determine_wellknown_cmd # temporary constant, to be replaced with lief.ELF.ARCH.RISCV
# https://github.com/lief-project/LIEF/pull/562
LIEF_ELF_ARCH_RISCV = lief.ELF.ARCH(243)
# Debian 9 (Stretch) EOL: 2022. https://wiki.debian.org/DebianReleases#Production_Releases # Debian 9 (Stretch) EOL: 2022. https://wiki.debian.org/DebianReleases#Production_Releases
# #
@ -45,12 +44,12 @@ from utils import determine_wellknown_cmd
MAX_VERSIONS = { MAX_VERSIONS = {
'GCC': (4,8,0), 'GCC': (4,8,0),
'GLIBC': { 'GLIBC': {
pixie.EM_386: (2,28), lief.ELF.ARCH.i386: (2,28),
pixie.EM_X86_64: (2,18), lief.ELF.ARCH.x86_64: (2,18),
pixie.EM_ARM: (2,28), lief.ELF.ARCH.ARM: (2,28),
pixie.EM_AARCH64:(2,18), lief.ELF.ARCH.AARCH64:(2,18),
pixie.EM_PPC64: (2,18), lief.ELF.ARCH.PPC64: (2,18),
pixie.EM_RISCV: (2,27), LIEF_ELF_ARCH_RISCV: (2,27),
}, },
'LIBATOMIC': (1,0), 'LIBATOMIC': (1,0),
'V': (0,5,0), # xkb (bitcoin-qt only) 'V': (0,5,0), # xkb (bitcoin-qt only)
@ -60,7 +59,8 @@ MAX_VERSIONS = {
# Ignore symbols that are exported as part of every executable # Ignore symbols that are exported as part of every executable
IGNORE_EXPORTS = { IGNORE_EXPORTS = {
'_edata', '_end', '__end__', '_init', '__bss_start', '__bss_start__', '_bss_end__', '__bss_end__', '_fini', '_IO_stdin_used', 'stdin', 'stdout', 'stderr', '_edata', '_end', '__end__', '_init', '__bss_start', '__bss_start__', '_bss_end__',
'__bss_end__', '_fini', '_IO_stdin_used', 'stdin', 'stdout', 'stderr',
'environ', '_environ', '__environ', 'environ', '_environ', '__environ',
# Used in stacktraces.cpp # Used in stacktraces.cpp
'__cxa_demangle' '__cxa_demangle'
@ -140,31 +140,8 @@ PE_ALLOWED_LIBRARIES = {
'WTSAPI32.dll', '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: def check_version(max_versions, version, arch) -> bool:
if '_' in version: (lib, _, ver) = version.rpartition('_')
(lib, _, ver) = version.rpartition('_')
else:
lib = version
ver = '0'
ver = tuple([int(x) for x in ver.split('.')]) ver = tuple([int(x) for x in ver.split('.')])
if not lib in max_versions: if not lib in max_versions:
return False return False
@ -173,48 +150,45 @@ def check_version(max_versions, version, arch) -> bool:
else: else:
return ver <= max_versions[lib][arch] return ver <= max_versions[lib][arch]
def check_imported_symbols(filename) -> bool: def check_imported_symbols(binary) -> bool:
elf = pixie.load(filename)
cppfilt = CPPFilt()
ok = True ok = True
for symbol in elf.dyn_symbols: for symbol in binary.imported_symbols:
if not symbol.is_import: if not symbol.imported:
continue continue
sym = symbol.name.decode()
version = symbol.version.decode() if symbol.version is not None else None version = symbol.symbol_version if symbol.has_version else None
if version and not check_version(MAX_VERSIONS, version, elf.hdr.e_machine):
print('{}: symbol {} from unsupported version {}'.format(filename, cppfilt(sym), version)) if version:
ok = False 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 {version}')
ok = False
return ok return ok
def check_exported_symbols(filename) -> bool: def check_exported_symbols(binary) -> bool:
elf = pixie.load(filename)
cppfilt = CPPFilt()
ok = True ok = True
for symbol in elf.dyn_symbols:
if not symbol.is_export: for symbol in binary.dynamic_symbols:
if not symbol.exported:
continue continue
sym = symbol.name.decode() name = symbol.name
if elf.hdr.e_machine == pixie.EM_RISCV or sym in IGNORE_EXPORTS: if binary.header.machine_type == LIEF_ELF_ARCH_RISCV or name in IGNORE_EXPORTS:
continue continue
print('{}: export of symbol {} not allowed'.format(filename, cppfilt(sym))) print(f'{binary.name}: export of symbol {name} not allowed!')
ok = False ok = False
return ok return ok
def check_ELF_libraries(filename) -> bool: def check_ELF_libraries(binary) -> bool:
ok = True ok = True
elf = pixie.load(filename) for library in binary.libraries:
for library_name in elf.query_dyn_tags(pixie.DT_NEEDED): if library not in ELF_ALLOWED_LIBRARIES:
assert(isinstance(library_name, bytes)) print(f'{filename}: {library} is not in ALLOWED_LIBRARIES!')
if library_name.decode() not in ELF_ALLOWED_LIBRARIES:
print('{}: NEEDED library {} is not allowed'.format(filename, library_name.decode()))
ok = False ok = False
return ok return ok
def check_MACHO_libraries(filename) -> bool: def check_MACHO_libraries(binary) -> bool:
ok = True ok = True
binary = lief.parse(filename)
for dylib in binary.libraries: for dylib in binary.libraries:
split = dylib.name.split('/') split = dylib.name.split('/')
if split[-1] not in MACHO_ALLOWED_LIBRARIES: if split[-1] not in MACHO_ALLOWED_LIBRARIES:
@ -222,29 +196,25 @@ def check_MACHO_libraries(filename) -> bool:
ok = False ok = False
return ok return ok
def check_MACHO_min_os(filename) -> bool: def check_MACHO_min_os(binary) -> bool:
binary = lief.parse(filename)
if binary.build_version.minos == [10,15,0]: if binary.build_version.minos == [10,15,0]:
return True return True
return False return False
def check_MACHO_sdk(filename) -> bool: def check_MACHO_sdk(binary) -> bool:
binary = lief.parse(filename)
if binary.build_version.sdk == [10, 15, 6]: if binary.build_version.sdk == [10, 15, 6]:
return True return True
return False return False
def check_PE_libraries(filename) -> bool: def check_PE_libraries(binary) -> bool:
ok = True ok = True
binary = lief.parse(filename)
for dylib in binary.libraries: for dylib in binary.libraries:
if dylib not in PE_ALLOWED_LIBRARIES: if dylib not in PE_ALLOWED_LIBRARIES:
print(f'{dylib} is not in ALLOWED_LIBRARIES!') print(f'{dylib} is not in ALLOWED_LIBRARIES!')
ok = False ok = False
return ok 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 major: int = binary.optional_header.major_subsystem_version
minor: int = binary.optional_header.minor_subsystem_version minor: int = binary.optional_header.minor_subsystem_version
if major == 6 and minor == 1: if major == 6 and minor == 1:
@ -268,30 +238,20 @@ CHECKS = {
] ]
} }
def identify_executable(executable) -> 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__': if __name__ == '__main__':
retval = 0 retval = 0
for filename in sys.argv[1:]: for filename in sys.argv[1:]:
try: try:
etype = identify_executable(filename) binary = lief.parse(filename)
if etype is None: etype = binary.format.name
print(f'{filename}: unknown format') if etype == lief.EXE_FORMATS.UNKNOWN:
print(f'{filename}: unknown executable format')
retval = 1 retval = 1
continue continue
failed = [] failed = []
for (name, func) in CHECKS[etype]: for (name, func) in CHECKS[etype]:
if not func(filename): if not func(binary):
failed.append(name) failed.append(name)
if failed: if failed:
print(f'{filename}: failed {" ".join(failed)}') print(f'{filename}: failed {" ".join(failed)}')

View File

@ -7,6 +7,7 @@ Test script for security-check.py
''' '''
import os import os
import subprocess import subprocess
from typing import List
import unittest import unittest
from utils import determine_wellknown_cmd from utils import determine_wellknown_cmd
@ -27,7 +28,16 @@ def clean_files(source, executable):
os.remove(executable) os.remove(executable)
def call_security_check(cc, source, executable, options): 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, universal_newlines=True) p = subprocess.run(['./contrib/devtools/security-check.py',executable], stdout=subprocess.PIPE, universal_newlines=True)
return (p.returncode, p.stdout.rstrip()) return (p.returncode, p.stdout.rstrip())

View File

@ -13,7 +13,16 @@ import unittest
from utils import determine_wellknown_cmd from utils import determine_wellknown_cmd
def call_symbol_check(cc: List[str], source, executable, options): 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.
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, universal_newlines=True) p = subprocess.run(['./contrib/devtools/symbol-check.py',executable], stdout=subprocess.PIPE, universal_newlines=True)
os.remove(source) os.remove(source)
os.remove(executable) os.remove(executable)
@ -56,7 +65,7 @@ class TestSymbolChecks(unittest.TestCase):
''') ''')
self.assertEqual(call_symbol_check(cc, source, executable, ['-lm']), self.assertEqual(call_symbol_check(cc, source, executable, ['-lm']),
(1, executable + ': symbol nextup from unsupported version GLIBC_2.24\n' + (1, executable + ': symbol nextup from unsupported version GLIBC_2.24(3)\n' +
executable + ': failed IMPORTED_SYMBOLS')) executable + ': failed IMPORTED_SYMBOLS'))
# -lutil is part of the libc6 package so a safe bet that it's installed # -lutil is part of the libc6 package so a safe bet that it's installed
@ -75,7 +84,7 @@ class TestSymbolChecks(unittest.TestCase):
''') ''')
self.assertEqual(call_symbol_check(cc, source, executable, ['-lutil']), self.assertEqual(call_symbol_check(cc, source, executable, ['-lutil']),
(1, executable + ': NEEDED library libutil.so.1 is not allowed\n' + (1, executable + ': libutil.so.1 is not in ALLOWED_LIBRARIES!\n' +
executable + ': failed LIBRARY_DEPENDENCIES')) executable + ': failed LIBRARY_DEPENDENCIES'))
# finally, check a simple conforming binary # finally, check a simple conforming binary

View File

@ -933,20 +933,8 @@ clean-local:
$(AM_V_GEN) $(WINDRES) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(CPPFLAGS) -DWINDRES_PREPROC -i $< -o $@ $(AM_V_GEN) $(WINDRES) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(CPPFLAGS) -DWINDRES_PREPROC -i $< -o $@
check-symbols: $(bin_PROGRAMS) check-symbols: $(bin_PROGRAMS)
if TARGET_DARWIN @echo "Running symbol and dynamic library checks..."
@echo "Checking macOS dynamic libraries..."
$(AM_V_at) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS) $(AM_V_at) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS)
endif
if TARGET_WINDOWS
@echo "Checking Windows dynamic libraries..."
$(AM_V_at) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS)
endif
if TARGET_LINUX
@echo "Checking glibc back compat..."
$(AM_V_at) CPPFILT='$(CPPFILT)' $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS)
endif
check-security: $(bin_PROGRAMS) check-security: $(bin_PROGRAMS)
if HARDEN if HARDEN