mirror of
https://github.com/dashpay/dash.git
synced 2024-12-24 19:42:46 +01:00
merge bitcoin#20434: Parse ELF directly for symbol and security checks
This commit is contained in:
parent
ef69317b59
commit
29c885c9f7
@ -55,7 +55,8 @@ DIST_SHARE = \
|
|||||||
$(top_srcdir)/share/rpcauth
|
$(top_srcdir)/share/rpcauth
|
||||||
|
|
||||||
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/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 \
|
||||||
|
@ -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(READELF, readelf)
|
|
||||||
AC_PATH_TOOL(CPPFILT, c++filt)
|
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)
|
||||||
|
323
contrib/devtools/pixie.py
Normal file
323
contrib/devtools/pixie.py
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
#!/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 = 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)
|
@ -6,15 +6,15 @@
|
|||||||
Perform basic security checks on a series of executables.
|
Perform basic security checks on a series of executables.
|
||||||
Exit status will be 0 if successful, and the program will be silent.
|
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.
|
||||||
Needs `readelf` (for ELF), `objdump` (for PE) and `otool` (for MACHO).
|
Needs `objdump` (for PE) and `otool` (for MACHO).
|
||||||
'''
|
'''
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
READELF_CMD = os.getenv('READELF', '/usr/bin/readelf')
|
import pixie
|
||||||
|
|
||||||
OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump')
|
OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump')
|
||||||
OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool')
|
OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool')
|
||||||
|
|
||||||
@ -26,75 +26,20 @@ def check_ELF_PIE(executable) -> bool:
|
|||||||
'''
|
'''
|
||||||
Check for position independent executable (PIE), allowing for address space randomization.
|
Check for position independent executable (PIE), allowing for address space randomization.
|
||||||
'''
|
'''
|
||||||
stdout = run_command([READELF_CMD, '-h', '-W', executable])
|
elf = pixie.load(executable)
|
||||||
|
return elf.hdr.e_type == pixie.ET_DYN
|
||||||
ok = False
|
|
||||||
for line in stdout.splitlines():
|
|
||||||
tokens = line.split()
|
|
||||||
if len(line)>=2 and tokens[0] == 'Type:' and tokens[1] == 'DYN':
|
|
||||||
ok = True
|
|
||||||
return ok
|
|
||||||
|
|
||||||
def get_ELF_program_headers(executable):
|
|
||||||
'''Return type and flags for ELF program headers'''
|
|
||||||
stdout = run_command([READELF_CMD, '-l', '-W', executable])
|
|
||||||
|
|
||||||
in_headers = False
|
|
||||||
headers = []
|
|
||||||
for line in stdout.splitlines():
|
|
||||||
if line.startswith('Program Headers:'):
|
|
||||||
in_headers = True
|
|
||||||
count = 0
|
|
||||||
if line == '':
|
|
||||||
in_headers = False
|
|
||||||
if in_headers:
|
|
||||||
if count == 1: # header line
|
|
||||||
header = [x.strip() for x in line.split()]
|
|
||||||
ofs_typ = header.index('Type')
|
|
||||||
ofs_flags = header.index('Flg')
|
|
||||||
# assert readelf output is what we expect
|
|
||||||
if ofs_typ == -1 or ofs_flags == -1:
|
|
||||||
raise ValueError('Cannot parse elfread -lW output')
|
|
||||||
elif count > 1:
|
|
||||||
splitline = [x.strip() for x in line.split()]
|
|
||||||
typ = splitline[ofs_typ]
|
|
||||||
if not typ.startswith('[R'): # skip [Requesting ...]
|
|
||||||
splitline = [x.strip() for x in line.split()]
|
|
||||||
flags = splitline[ofs_flags]
|
|
||||||
# check for 'R', ' E'
|
|
||||||
if splitline[ofs_flags + 1] is 'E':
|
|
||||||
flags += ' E'
|
|
||||||
headers.append((typ, flags, []))
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
if line.startswith(' Section to Segment mapping:'):
|
|
||||||
in_mapping = True
|
|
||||||
count = 0
|
|
||||||
if line == '':
|
|
||||||
in_mapping = False
|
|
||||||
if in_mapping:
|
|
||||||
if count == 1: # header line
|
|
||||||
ofs_segment = line.find('Segment')
|
|
||||||
ofs_sections = line.find('Sections...')
|
|
||||||
if ofs_segment == -1 or ofs_sections == -1:
|
|
||||||
raise ValueError('Cannot parse elfread -lW output')
|
|
||||||
elif count > 1:
|
|
||||||
segment = int(line[ofs_segment:ofs_sections].strip())
|
|
||||||
sections = line[ofs_sections:].strip().split()
|
|
||||||
headers[segment][2].extend(sections)
|
|
||||||
count += 1
|
|
||||||
return headers
|
|
||||||
|
|
||||||
def check_ELF_NX(executable) -> bool:
|
def check_ELF_NX(executable) -> bool:
|
||||||
'''
|
'''
|
||||||
Check that no sections are writable and executable (including the stack)
|
Check that no sections are writable and executable (including the stack)
|
||||||
'''
|
'''
|
||||||
|
elf = pixie.load(executable)
|
||||||
have_wx = False
|
have_wx = False
|
||||||
have_gnu_stack = False
|
have_gnu_stack = False
|
||||||
for (typ, flags, _) in get_ELF_program_headers(executable):
|
for ph in elf.program_headers:
|
||||||
if typ == 'GNU_STACK':
|
if ph.p_type == pixie.PT_GNU_STACK:
|
||||||
have_gnu_stack = True
|
have_gnu_stack = True
|
||||||
if 'W' in flags and 'E' in flags: # section is both writable and executable
|
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
|
have_wx = True
|
||||||
return have_gnu_stack and not have_wx
|
return have_gnu_stack and not have_wx
|
||||||
|
|
||||||
@ -104,35 +49,34 @@ def check_ELF_RELRO(executable) -> bool:
|
|||||||
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 (typ, flags, _) in get_ELF_program_headers(executable):
|
for ph in elf.program_headers:
|
||||||
# Note: not checking flags == '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 typ == 'GNU_RELRO':
|
if ph.p_type == pixie.PT_GNU_RELRO:
|
||||||
have_gnu_relro = True
|
have_gnu_relro = True
|
||||||
|
|
||||||
have_bindnow = False
|
have_bindnow = False
|
||||||
stdout = run_command([READELF_CMD, '-d', '-W', executable])
|
for flags in elf.query_dyn_tags(pixie.DT_FLAGS):
|
||||||
|
assert isinstance(flags, int)
|
||||||
for line in stdout.splitlines():
|
if flags & pixie.DF_BIND_NOW:
|
||||||
tokens = line.split()
|
|
||||||
if len(tokens)>1 and tokens[1] == '(BIND_NOW)' or (len(tokens)>2 and tokens[1] == '(FLAGS)' and 'BIND_NOW' in tokens[2:]):
|
|
||||||
have_bindnow = True
|
have_bindnow = True
|
||||||
|
|
||||||
return have_gnu_relro and have_bindnow
|
return have_gnu_relro and have_bindnow
|
||||||
|
|
||||||
def check_ELF_Canary(executable) -> bool:
|
def check_ELF_Canary(executable) -> bool:
|
||||||
'''
|
'''
|
||||||
Check for use of stack canary
|
Check for use of stack canary
|
||||||
'''
|
'''
|
||||||
stdout = run_command([READELF_CMD, '--dyn-syms', '-W', executable])
|
elf = pixie.load(executable)
|
||||||
|
|
||||||
ok = False
|
ok = False
|
||||||
for line in stdout.splitlines():
|
for symbol in elf.dyn_symbols:
|
||||||
if '__stack_chk_fail' in line:
|
if symbol.name == b'__stack_chk_fail':
|
||||||
ok = True
|
ok = True
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
@ -142,48 +86,55 @@ def check_ELF_separate_code(executable):
|
|||||||
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 = pixie.PF_R
|
||||||
|
W = pixie.PF_W
|
||||||
|
E = pixie.PF_X
|
||||||
EXPECTED_FLAGS = {
|
EXPECTED_FLAGS = {
|
||||||
# Read + execute
|
# Read + execute
|
||||||
'.init': 'R E',
|
b'.init': R | E,
|
||||||
'.plt': 'R E',
|
b'.plt': R | E,
|
||||||
'.plt.got': 'R E',
|
b'.plt.got': R | E,
|
||||||
'.plt.sec': 'R E',
|
b'.plt.sec': R | E,
|
||||||
'.text': 'R E',
|
b'.text': R | E,
|
||||||
'.fini': 'R E',
|
b'.fini': R | E,
|
||||||
# Read-only data
|
# Read-only data
|
||||||
'.interp': 'R',
|
b'.interp': R,
|
||||||
'.note.gnu.property': 'R',
|
b'.note.gnu.property': R,
|
||||||
'.note.gnu.build-id': 'R',
|
b'.note.gnu.build-id': R,
|
||||||
'.note.ABI-tag': 'R',
|
b'.note.ABI-tag': R,
|
||||||
'.gnu.hash': 'R',
|
b'.gnu.hash': R,
|
||||||
'.dynsym': 'R',
|
b'.dynsym': R,
|
||||||
'.dynstr': 'R',
|
b'.dynstr': R,
|
||||||
'.gnu.version': 'R',
|
b'.gnu.version': R,
|
||||||
'.gnu.version_r': 'R',
|
b'.gnu.version_r': R,
|
||||||
'.rela.dyn': 'R',
|
b'.rela.dyn': R,
|
||||||
'.rela.plt': 'R',
|
b'.rela.plt': R,
|
||||||
'.rodata': 'R',
|
b'.rodata': R,
|
||||||
'.eh_frame_hdr': 'R',
|
b'.eh_frame_hdr': R,
|
||||||
'.eh_frame': 'R',
|
b'.eh_frame': R,
|
||||||
'.qtmetadata': 'R',
|
b'.qtmetadata': R,
|
||||||
'.gcc_except_table': 'R',
|
b'.gcc_except_table': R,
|
||||||
'.stapsdt.base': 'R',
|
b'.stapsdt.base': R,
|
||||||
# Writable data
|
# Writable data
|
||||||
'.init_array': 'RW',
|
b'.init_array': R | W,
|
||||||
'.fini_array': 'RW',
|
b'.fini_array': R | W,
|
||||||
'.dynamic': 'RW',
|
b'.dynamic': R | W,
|
||||||
'.got': 'RW',
|
b'.got': R | W,
|
||||||
'.data': 'RW',
|
b'.data': R | W,
|
||||||
'.bss': 'RW',
|
b'.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,
|
# 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 (typ, flags, sections) in get_ELF_program_headers(executable):
|
for ph in elf.program_headers:
|
||||||
if typ == 'LOAD':
|
if ph.p_type == pixie.PT_LOAD:
|
||||||
for section in sections:
|
for section in ph.sections:
|
||||||
assert(section not in flags_per_section)
|
assert(section.name not in flags_per_section)
|
||||||
flags_per_section[section] = flags
|
flags_per_section[section.name] = ph.p_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():
|
||||||
@ -236,7 +187,7 @@ def check_PE_NX(executable) -> bool:
|
|||||||
def get_MACHO_executable_flags(executable) -> List[str]:
|
def get_MACHO_executable_flags(executable) -> List[str]:
|
||||||
stdout = run_command([OTOOL_CMD, '-vh', executable])
|
stdout = run_command([OTOOL_CMD, '-vh', executable])
|
||||||
|
|
||||||
flags = []
|
flags: List[str] = []
|
||||||
for line in stdout.splitlines():
|
for line in stdout.splitlines():
|
||||||
tokens = line.split()
|
tokens = line.split()
|
||||||
# filter first two header lines
|
# filter first two header lines
|
||||||
|
@ -12,10 +12,11 @@ Example usage:
|
|||||||
find ../gitian-builder/build -type f -executable | xargs python3 contrib/devtools/symbol-check.py
|
find ../gitian-builder/build -type f -executable | xargs python3 contrib/devtools/symbol-check.py
|
||||||
'''
|
'''
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import pixie
|
||||||
|
|
||||||
# Debian 8 (Jessie) EOL: 2020. https://wiki.debian.org/DebianReleases#Production_Releases
|
# Debian 8 (Jessie) EOL: 2020. https://wiki.debian.org/DebianReleases#Production_Releases
|
||||||
#
|
#
|
||||||
@ -52,7 +53,6 @@ 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',
|
||||||
}
|
}
|
||||||
READELF_CMD = os.getenv('READELF', '/usr/bin/readelf')
|
|
||||||
CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt')
|
CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt')
|
||||||
OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool')
|
OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool')
|
||||||
|
|
||||||
@ -69,6 +69,8 @@ ELF_ALLOWED_LIBRARIES = {
|
|||||||
'ld-linux.so.2', # 32-bit dynamic linker
|
'ld-linux.so.2', # 32-bit dynamic linker
|
||||||
'ld-linux-aarch64.so.1', # 64-bit ARM dynamic linker
|
'ld-linux-aarch64.so.1', # 64-bit ARM dynamic linker
|
||||||
'ld-linux-armhf.so.3', # 32-bit ARM dynamic linker
|
'ld-linux-armhf.so.3', # 32-bit ARM dynamic linker
|
||||||
|
'ld64.so.1', # POWER64 ABIv1 dynamic linker
|
||||||
|
'ld64.so.2', # POWER64 ABIv2 dynamic linker
|
||||||
'ld-linux-riscv64-lp64d.so.1', # 64-bit RISC-V dynamic linker
|
'ld-linux-riscv64-lp64d.so.1', # 64-bit RISC-V dynamic linker
|
||||||
# dash-qt only
|
# dash-qt only
|
||||||
'libxcb.so.1', # part of X11
|
'libxcb.so.1', # part of X11
|
||||||
@ -79,11 +81,12 @@ ELF_ALLOWED_LIBRARIES = {
|
|||||||
'libdl.so.2' # programming interface to dynamic linker
|
'libdl.so.2' # programming interface to dynamic linker
|
||||||
}
|
}
|
||||||
ARCH_MIN_GLIBC_VER = {
|
ARCH_MIN_GLIBC_VER = {
|
||||||
'80386': (2,1),
|
pixie.EM_386: (2,1),
|
||||||
'X86-64': (2,2,5),
|
pixie.EM_X86_64: (2,2,5),
|
||||||
'ARM': (2,4),
|
pixie.EM_ARM: (2,4),
|
||||||
'AArch64':(2,17),
|
pixie.EM_AARCH64:(2,17),
|
||||||
'RISC-V': (2,27)
|
pixie.EM_PPC64: (2,17),
|
||||||
|
pixie.EM_RISCV: (2,27)
|
||||||
}
|
}
|
||||||
|
|
||||||
MACHO_ALLOWED_LIBRARIES = {
|
MACHO_ALLOWED_LIBRARIES = {
|
||||||
@ -128,29 +131,6 @@ class CPPFilt(object):
|
|||||||
self.proc.stdout.close()
|
self.proc.stdout.close()
|
||||||
self.proc.wait()
|
self.proc.wait()
|
||||||
|
|
||||||
def read_symbols(executable, imports=True) -> List[Tuple[str, str, str]]:
|
|
||||||
'''
|
|
||||||
Parse an ELF executable and return a list of (symbol,version, arch) tuples
|
|
||||||
for dynamic, imported symbols.
|
|
||||||
'''
|
|
||||||
p = subprocess.Popen([READELF_CMD, '--dyn-syms', '-W', '-h', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
|
|
||||||
(stdout, stderr) = p.communicate()
|
|
||||||
if p.returncode:
|
|
||||||
raise IOError('Could not read symbols for {}: {}'.format(executable, stderr.strip()))
|
|
||||||
syms = []
|
|
||||||
for line in stdout.splitlines():
|
|
||||||
line = line.split()
|
|
||||||
if 'Machine:' in line:
|
|
||||||
arch = line[-1]
|
|
||||||
if len(line)>7 and re.match('[0-9]+:$', line[0]):
|
|
||||||
(sym, _, version) = line[7].partition('@')
|
|
||||||
is_import = line[6] == 'UND'
|
|
||||||
if version.startswith('@'):
|
|
||||||
version = version[1:]
|
|
||||||
if is_import == imports:
|
|
||||||
syms.append((sym, version, arch))
|
|
||||||
return syms
|
|
||||||
|
|
||||||
def check_version(max_versions, version, arch) -> bool:
|
def check_version(max_versions, version, arch) -> bool:
|
||||||
if '_' in version:
|
if '_' in version:
|
||||||
(lib, _, ver) = version.rpartition('_')
|
(lib, _, ver) = version.rpartition('_')
|
||||||
@ -162,36 +142,30 @@ def check_version(max_versions, version, arch) -> bool:
|
|||||||
return False
|
return False
|
||||||
return ver <= max_versions[lib] or lib == 'GLIBC' and ver <= ARCH_MIN_GLIBC_VER[arch]
|
return ver <= max_versions[lib] or lib == 'GLIBC' and ver <= ARCH_MIN_GLIBC_VER[arch]
|
||||||
|
|
||||||
def elf_read_libraries(filename) -> List[str]:
|
|
||||||
p = subprocess.Popen([READELF_CMD, '-d', '-W', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
|
|
||||||
(stdout, stderr) = p.communicate()
|
|
||||||
if p.returncode:
|
|
||||||
raise IOError('Error opening file')
|
|
||||||
libraries = []
|
|
||||||
for line in stdout.splitlines():
|
|
||||||
tokens = line.split()
|
|
||||||
if len(tokens)>2 and tokens[1] == '(NEEDED)':
|
|
||||||
match = re.match(r'^Shared library: \[(.*)\]$', ' '.join(tokens[2:]))
|
|
||||||
if match:
|
|
||||||
libraries.append(match.group(1))
|
|
||||||
else:
|
|
||||||
raise ValueError('Unparseable (NEEDED) specification')
|
|
||||||
return libraries
|
|
||||||
|
|
||||||
def check_imported_symbols(filename) -> bool:
|
def check_imported_symbols(filename) -> bool:
|
||||||
|
elf = pixie.load(filename)
|
||||||
cppfilt = CPPFilt()
|
cppfilt = CPPFilt()
|
||||||
ok = True
|
ok = True
|
||||||
for sym, version, arch in read_symbols(filename, True):
|
|
||||||
if version and not check_version(MAX_VERSIONS, version, arch):
|
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('{}: symbol {} from unsupported version {}'.format(filename, cppfilt(sym), version))
|
print('{}: symbol {} from unsupported version {}'.format(filename, cppfilt(sym), version))
|
||||||
ok = False
|
ok = False
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
def check_exported_symbols(filename) -> bool:
|
def check_exported_symbols(filename) -> bool:
|
||||||
|
elf = pixie.load(filename)
|
||||||
cppfilt = CPPFilt()
|
cppfilt = CPPFilt()
|
||||||
ok = True
|
ok = True
|
||||||
for sym,version,arch in read_symbols(filename, False):
|
for symbol in elf.dyn_symbols:
|
||||||
if arch == 'RISC-V' or sym in IGNORE_EXPORTS:
|
if not symbol.is_export:
|
||||||
|
continue
|
||||||
|
sym = symbol.name.decode()
|
||||||
|
if elf.hdr.e_machine == pixie.EM_RISCV or sym in IGNORE_EXPORTS:
|
||||||
continue
|
continue
|
||||||
print('{}: export of symbol {} not allowed'.format(filename, cppfilt(sym)))
|
print('{}: export of symbol {} not allowed'.format(filename, cppfilt(sym)))
|
||||||
ok = False
|
ok = False
|
||||||
@ -199,9 +173,11 @@ def check_exported_symbols(filename) -> bool:
|
|||||||
|
|
||||||
def check_ELF_libraries(filename) -> bool:
|
def check_ELF_libraries(filename) -> bool:
|
||||||
ok = True
|
ok = True
|
||||||
for library_name in elf_read_libraries(filename):
|
elf = pixie.load(filename)
|
||||||
if library_name not in ELF_ALLOWED_LIBRARIES:
|
for library_name in elf.query_dyn_tags(pixie.DT_NEEDED):
|
||||||
print('{}: NEEDED library {} is not allowed'.format(filename, library_name))
|
assert(isinstance(library_name, bytes))
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -928,13 +928,13 @@ endif
|
|||||||
|
|
||||||
if GLIBC_BACK_COMPAT
|
if GLIBC_BACK_COMPAT
|
||||||
@echo "Checking glibc back compat..."
|
@echo "Checking glibc back compat..."
|
||||||
$(AM_V_at) READELF=$(READELF) CPPFILT=$(CPPFILT) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS)
|
$(AM_V_at) CPPFILT=$(CPPFILT) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
check-security: $(bin_PROGRAMS)
|
check-security: $(bin_PROGRAMS)
|
||||||
if HARDEN
|
if HARDEN
|
||||||
@echo "Checking binary security..."
|
@echo "Checking binary security..."
|
||||||
$(AM_V_at) READELF=$(READELF) OBJDUMP=$(OBJDUMP) OTOOL=$(OTOOL) $(PYTHON) $(top_srcdir)/contrib/devtools/security-check.py $(bin_PROGRAMS)
|
$(AM_V_at) OBJDUMP=$(OBJDUMP) OTOOL=$(OTOOL) $(PYTHON) $(top_srcdir)/contrib/devtools/security-check.py $(bin_PROGRAMS)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user