scripts: use LIEF for ELF checks in symbol-check.py

Co-authored-by: Carl Dong <contact@carldong.me>
This commit is contained in:
fanquake 2021-07-02 14:22:04 +08:00
parent 610a8a8e39
commit 309eac9019
No known key found for this signature in database
GPG Key ID: 2EEB9F5CC09526C1
5 changed files with 38 additions and 72 deletions

View File

@ -376,5 +376,5 @@ if TARGET_WINDOWS
endif
if TARGET_LINUX
$(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)' CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' LDFLAGS='$(LDFLAGS)' 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

View File

@ -113,7 +113,6 @@ AC_PATH_PROG([GIT], [git])
AC_PATH_PROG(CCACHE,ccache)
AC_PATH_PROG(XGETTEXT,xgettext)
AC_PATH_PROG(HEXDUMP,hexdump)
AC_PATH_TOOL(CPPFILT, c++filt)
AC_PATH_TOOL(OBJCOPY, objcopy)
AC_PATH_PROG(DOXYGEN, doxygen)
AM_CONDITIONAL([HAVE_DOXYGEN], [test -n "$DOXYGEN"])

View File

@ -10,14 +10,14 @@ Example usage:
find ../path/to/binaries -type f -executable | xargs python3 contrib/devtools/symbol-check.py
'''
import subprocess
import sys
from typing import List, Optional
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 8 (Jessie) EOL: 2020. https://wiki.debian.org/DebianReleases#Production_Releases
#
@ -43,12 +43,12 @@ from utils import determine_wellknown_cmd
MAX_VERSIONS = {
'GCC': (4,8,0),
'GLIBC': {
pixie.EM_386: (2,17),
pixie.EM_X86_64: (2,17),
pixie.EM_ARM: (2,17),
pixie.EM_AARCH64:(2,17),
pixie.EM_PPC64: (2,17),
pixie.EM_RISCV: (2,27),
lief.ELF.ARCH.i386: (2,17),
lief.ELF.ARCH.x86_64: (2,17),
lief.ELF.ARCH.ARM: (2,17),
lief.ELF.ARCH.AARCH64:(2,17),
lief.ELF.ARCH.PPC64: (2,17),
LIEF_ELF_ARCH_RISCV: (2,27),
},
'LIBATOMIC': (1,0),
'V': (0,5,0), # xkb (bitcoin-qt only)
@ -58,7 +58,8 @@ MAX_VERSIONS = {
# Ignore symbols that are exported as part of every executable
IGNORE_EXPORTS = {
'_edata', '_end', '__end__', '_init', '__bss_start', '__bss_start__', '_bss_end__', '__bss_end__', '_fini', '_IO_stdin_used', 'stdin', 'stdout', 'stderr',
'_edata', '_end', '__end__', '_init', '__bss_start', '__bss_start__', '_bss_end__',
'__bss_end__', '_fini', '_IO_stdin_used', 'stdin', 'stdout', 'stderr',
'environ', '_environ', '__environ',
}
@ -133,31 +134,8 @@ PE_ALLOWED_LIBRARIES = {
'WTSAPI32.dll',
}
class CPPFilt(object):
'''
Demangle C++ symbol names.
Use a pipe to the 'c++filt' command.
'''
def __init__(self):
self.proc = subprocess.Popen(determine_wellknown_cmd('CPPFILT', 'c++filt'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True)
def __call__(self, mangled):
self.proc.stdin.write(mangled + '\n')
self.proc.stdin.flush()
return self.proc.stdout.readline().rstrip()
def close(self):
self.proc.stdin.close()
self.proc.stdout.close()
self.proc.wait()
def check_version(max_versions, version, arch) -> bool:
if '_' in version:
(lib, _, ver) = version.rpartition('_')
else:
lib = version
ver = '0'
(lib, _, ver) = version.rpartition('_')
ver = tuple([int(x) for x in ver.split('.')])
if not lib in max_versions:
return False
@ -167,41 +145,42 @@ def check_version(max_versions, version, arch) -> bool:
return ver <= max_versions[lib][arch]
def check_imported_symbols(filename) -> bool:
elf = pixie.load(filename)
cppfilt = CPPFilt()
ok: bool = True
binary = lief.parse(filename)
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('{}: symbol {} from unsupported version {}'.format(filename, cppfilt(sym), version))
ok = False
version = symbol.symbol_version if symbol.has_version else None
if version:
aux_version = version.symbol_version_auxiliary.name if version.has_auxiliary_version else None
if aux_version and not check_version(MAX_VERSIONS, aux_version, binary.header.machine_type):
print(f'{filename}: symbol {symbol.name} from unsupported version {version}')
ok = False
return ok
def check_exported_symbols(filename) -> bool:
elf = pixie.load(filename)
cppfilt = CPPFilt()
ok: bool = True
for symbol in elf.dyn_symbols:
if not symbol.is_export:
binary = lief.parse(filename)
for symbol in binary.dynamic_symbols:
if not symbol.exported:
continue
sym = symbol.name.decode()
if elf.hdr.e_machine == pixie.EM_RISCV or sym in IGNORE_EXPORTS:
name = symbol.name
if binary.header.machine_type == LIEF_ELF_ARCH_RISCV or name in IGNORE_EXPORTS:
continue
print('{}: export of symbol {} not allowed'.format(filename, cppfilt(sym)))
print(f'{filename}: export of symbol {name} not allowed!')
ok = False
return ok
def check_ELF_libraries(filename) -> bool:
ok: bool = 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('{}: NEEDED library {} is not allowed'.format(filename, library_name.decode()))
binary = lief.parse(filename)
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

View File

@ -60,7 +60,7 @@ class TestSymbolChecks(unittest.TestCase):
''')
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'))
# -lutil is part of the libc6 package so a safe bet that it's installed
@ -79,7 +79,7 @@ class TestSymbolChecks(unittest.TestCase):
''')
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'))
# finally, check a simple conforming binary

View File

@ -808,20 +808,8 @@ clean-local:
$(AM_V_GEN) $(WINDRES) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(CPPFLAGS) -DWINDRES_PREPROC -i $< -o $@
check-symbols: $(bin_PROGRAMS)
if TARGET_DARWIN
@echo "Checking macOS dynamic libraries..."
@echo "Running symbol and dynamic library checks..."
$(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)
if HARDEN