merge bitcoin#21664: use LIEF for macOS and Windows symbol & security checks

This commit is contained in:
Kittywhiskers Van Gogh 2023-05-13 17:44:39 +00:00
parent 101cb67433
commit ef9300ad63
9 changed files with 88 additions and 134 deletions

View File

@ -8,7 +8,7 @@ export LC_ALL=C.UTF-8
export CONTAINER_NAME=ci_macos export CONTAINER_NAME=ci_macos
export HOST=x86_64-apple-darwin19 export HOST=x86_64-apple-darwin19
export PIP_PACKAGES="zmq" export PIP_PACKAGES="zmq lief"
export RUN_UNIT_TESTS=true export RUN_UNIT_TESTS=true
export RUN_INTEGRATION_TESTS=false export RUN_INTEGRATION_TESTS=false
export RUN_SECURITY_TESTS="true" export RUN_SECURITY_TESTS="true"

View File

@ -68,6 +68,9 @@ if [[ $DOCKER_NAME_TAG == centos* ]]; then
elif [ "$CI_USE_APT_INSTALL" != "no" ]; then elif [ "$CI_USE_APT_INSTALL" != "no" ]; then
${CI_RETRY_EXE} DOCKER_EXEC apt-get update ${CI_RETRY_EXE} DOCKER_EXEC apt-get update
${CI_RETRY_EXE} DOCKER_EXEC apt-get install --no-install-recommends --no-upgrade -y $PACKAGES $DOCKER_PACKAGES ${CI_RETRY_EXE} DOCKER_EXEC apt-get install --no-install-recommends --no-upgrade -y $PACKAGES $DOCKER_PACKAGES
if [ -n "$PIP_PACKAGES" ]; then
${CI_RETRY_EXE} pip3 install --user $PIP_PACKAGES
fi
fi fi
if [ "$TRAVIS_OS_NAME" == "osx" ]; then if [ "$TRAVIS_OS_NAME" == "osx" ]; then

View File

@ -37,6 +37,7 @@ RUN pip3 install \
codespell==1.17.1 \ codespell==1.17.1 \
flake8==3.8.3 \ flake8==3.8.3 \
jinja2 \ jinja2 \
lief==0.11.4 \
pyzmq \ pyzmq \
vulture==2.3 \ vulture==2.3 \
yq \ yq \

View File

@ -6,22 +6,13 @@
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 `objdump` (for PE) and `otool` (for MACHO).
''' '''
import subprocess
import sys import sys
import os
from typing import List, Optional from typing import List, Optional
import lief
import pixie import pixie
OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump')
OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool')
def run_command(command) -> str:
p = subprocess.run(command, stdout=subprocess.PIPE, check=True, universal_newlines=True)
return p.stdout
def check_ELF_PIE(executable) -> bool: 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.
@ -143,112 +134,59 @@ def check_ELF_separate_code(executable):
return False return False
return True return True
def get_PE_dll_characteristics(executable) -> int:
'''Get PE DllCharacteristics bits'''
stdout = run_command([OBJDUMP_CMD, '-x', executable])
bits = 0
for line in stdout.splitlines():
tokens = line.split()
if len(tokens)>=2 and tokens[0] == 'DllCharacteristics':
bits = int(tokens[1],16)
return bits
IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA = 0x0020
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE = 0x0040
IMAGE_DLL_CHARACTERISTICS_NX_COMPAT = 0x0100
def check_PE_DYNAMIC_BASE(executable) -> bool: def check_PE_DYNAMIC_BASE(executable) -> bool:
'''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)''' '''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)'''
bits = get_PE_dll_characteristics(executable) binary = lief.parse(executable)
return (bits & IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE) == IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE 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(executable) -> bool:
'''PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR''' '''PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR'''
bits = get_PE_dll_characteristics(executable) binary = lief.parse(executable)
return (bits & IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA) == IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA 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(executable) -> bool:
'''Check for a reloc section. This is required for functional ASLR.''' '''Check for a reloc section. This is required for functional ASLR.'''
stdout = run_command([OBJDUMP_CMD, '-h', executable]) binary = lief.parse(executable)
return binary.has_relocations
for line in stdout.splitlines():
if '.reloc' in line:
return True
return False
def check_PE_NX(executable) -> bool:
'''NX: DllCharacteristics bit 0x100 signifies nxcompat (DEP)'''
bits = get_PE_dll_characteristics(executable)
return (bits & IMAGE_DLL_CHARACTERISTICS_NX_COMPAT) == IMAGE_DLL_CHARACTERISTICS_NX_COMPAT
def get_MACHO_executable_flags(executable) -> List[str]:
stdout = run_command([OTOOL_CMD, '-vh', executable])
flags: List[str] = []
for line in stdout.splitlines():
tokens = line.split()
# filter first two header lines
if 'magic' in tokens or 'Mach' in tokens:
continue
# filter ncmds and sizeofcmds values
flags += [t for t in tokens if not t.isdigit()]
return flags
def check_MACHO_PIE(executable) -> bool:
'''
Check for position independent executable (PIE), allowing for address space randomization.
'''
flags = get_MACHO_executable_flags(executable)
if 'PIE' in flags:
return True
return False
def check_MACHO_NOUNDEFS(executable) -> bool: def check_MACHO_NOUNDEFS(executable) -> bool:
''' '''
Check for no undefined references. Check for no undefined references.
''' '''
flags = get_MACHO_executable_flags(executable) binary = lief.parse(executable)
if 'NOUNDEFS' in flags: return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS)
return True
return False
def check_MACHO_NX(executable) -> bool:
'''
Check for no stack execution
'''
flags = get_MACHO_executable_flags(executable)
if 'ALLOW_STACK_EXECUTION' in flags:
return False
return True
def check_MACHO_LAZY_BINDINGS(executable) -> bool: def check_MACHO_LAZY_BINDINGS(executable) -> 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.
''' '''
stdout = run_command([OTOOL_CMD, '-l', executable]) binary = lief.parse(executable)
return binary.dyld_info.lazy_bind == (0,0)
for line in stdout.splitlines():
tokens = line.split()
if 'lazy_bind_off' in tokens or 'lazy_bind_size' in tokens:
if tokens[1] != '0':
return False
return True
def check_MACHO_Canary(executable) -> bool: def check_MACHO_Canary(executable) -> bool:
''' '''
Check for use of stack canary Check for use of stack canary
''' '''
stdout = run_command([OTOOL_CMD, '-Iv', executable]) binary = lief.parse(executable)
return binary.has_symbol('___stack_chk_fail')
ok = False def check_PIE(executable) -> bool:
for line in stdout.splitlines(): '''
if '___stack_chk_fail' in line: Check for position independent executable (PIE),
ok = True allowing for address space randomization.
return ok '''
binary = lief.parse(executable)
return binary.is_pie
def check_NX(executable) -> bool:
'''
Check for no stack execution
'''
binary = lief.parse(executable)
return binary.has_nx
CHECKS = { CHECKS = {
'ELF': [ 'ELF': [
@ -259,15 +197,16 @@ CHECKS = {
('separate_code', check_ELF_separate_code), ('separate_code', check_ELF_separate_code),
], ],
'PE': [ 'PE': [
('PIE', check_PIE),
('DYNAMIC_BASE', check_PE_DYNAMIC_BASE), ('DYNAMIC_BASE', check_PE_DYNAMIC_BASE),
('HIGH_ENTROPY_VA', check_PE_HIGH_ENTROPY_VA), ('HIGH_ENTROPY_VA', check_PE_HIGH_ENTROPY_VA),
('NX', check_PE_NX), ('NX', check_NX),
('RELOC_SECTION', check_PE_RELOC_SECTION) ('RELOC_SECTION', check_PE_RELOC_SECTION)
], ],
'MACHO': [ 'MACHO': [
('PIE', check_MACHO_PIE), ('PIE', check_PIE),
('NOUNDEFS', check_MACHO_NOUNDEFS), ('NOUNDEFS', check_MACHO_NOUNDEFS),
('NX', check_MACHO_NX), ('NX', check_NX),
('LAZY_BINDINGS', check_MACHO_LAZY_BINDINGS), ('LAZY_BINDINGS', check_MACHO_LAZY_BINDINGS),
('Canary', check_MACHO_Canary) ('Canary', check_MACHO_Canary)
] ]
@ -285,24 +224,24 @@ def identify_executable(executable) -> Optional[str]:
return None return None
if __name__ == '__main__': if __name__ == '__main__':
retval = 0 retval: int = 0
for filename in sys.argv[1:]: for filename in sys.argv[1:]:
try: try:
etype = identify_executable(filename) etype = identify_executable(filename)
if etype is None: if etype is None:
print('%s: unknown format' % filename) print(f'{filename}: unknown format')
retval = 1 retval = 1
continue continue
failed = [] failed: List[str] = []
for (name, func) in CHECKS[etype]: for (name, func) in CHECKS[etype]:
if not func(filename): if not func(filename):
failed.append(name) failed.append(name)
if failed: if failed:
print('%s: failed %s' % (filename, ' '.join(failed))) print(f'{filename}: failed {" ".join(failed)}')
retval = 1 retval = 1
except IOError: except IOError:
print('%s: cannot open' % filename) print(f'{filename}: cannot open')
retval = 1 retval = 1
sys.exit(retval) sys.exit(retval)

View File

@ -13,8 +13,9 @@ Example usage:
import subprocess import subprocess
import sys import sys
import os import os
from typing import List, Optional from typing import Optional
import lief
import pixie import pixie
# Debian 9 (Stretch) EOL: 2022. https://wiki.debian.org/DebianReleases#Production_Releases # Debian 9 (Stretch) EOL: 2022. https://wiki.debian.org/DebianReleases#Production_Releases
@ -64,8 +65,6 @@ IGNORE_EXPORTS = {
'__cxa_demangle' '__cxa_demangle'
} }
CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt') CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt')
OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump')
OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool')
# Allowed NEEDED libraries # Allowed NEEDED libraries
ELF_ALLOWED_LIBRARIES = { ELF_ALLOWED_LIBRARIES = {
@ -213,44 +212,22 @@ def check_ELF_libraries(filename) -> bool:
ok = False ok = False
return ok return ok
def macho_read_libraries(filename) -> List[str]:
p = subprocess.Popen([OTOOL_CMD, '-L', 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) == 1: # skip executable name
continue
libraries.append(tokens[0].split('/')[-1])
return libraries
def check_MACHO_libraries(filename) -> bool: def check_MACHO_libraries(filename) -> bool:
ok = True ok = True
for dylib in macho_read_libraries(filename): binary = lief.parse(filename)
if dylib not in MACHO_ALLOWED_LIBRARIES: for dylib in binary.libraries:
print('{} is not in ALLOWED_LIBRARIES!'.format(dylib)) split = dylib.name.split('/')
if split[-1] not in MACHO_ALLOWED_LIBRARIES:
print(f'{split[-1]} is not in ALLOWED_LIBRARIES!')
ok = False ok = False
return ok return ok
def pe_read_libraries(filename) -> List[str]:
p = subprocess.Popen([OBJDUMP_CMD, '-x', 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():
if 'DLL Name:' in line:
tokens = line.split(': ')
libraries.append(tokens[1])
return libraries
def check_PE_libraries(filename) -> bool: def check_PE_libraries(filename) -> bool:
ok = True ok = True
for dylib in pe_read_libraries(filename): binary = lief.parse(filename)
for dylib in binary.libraries:
if dylib not in PE_ALLOWED_LIBRARIES: if dylib not in PE_ALLOWED_LIBRARIES:
print('{} is not in ALLOWED_LIBRARIES!'.format(dylib)) print(f'{dylib} is not in ALLOWED_LIBRARIES!')
ok = False ok = False
return ok return ok
@ -285,7 +262,7 @@ if __name__ == '__main__':
try: try:
etype = identify_executable(filename) etype = identify_executable(filename)
if etype is None: if etype is None:
print('{}: unknown format'.format(filename)) print(f'{filename}: unknown format')
retval = 1 retval = 1
continue continue
@ -294,9 +271,9 @@ if __name__ == '__main__':
if not func(filename): if not func(filename):
failed.append(name) failed.append(name)
if failed: if failed:
print('{}: failed {}'.format(filename, ' '.join(failed))) print(f'{filename}: failed {" ".join(failed)}')
retval = 1 retval = 1
except IOError: except IOError:
print('{}: cannot open'.format(filename)) print(f'{filename}: cannot open')
retval = 1 retval = 1
sys.exit(retval) sys.exit(retval)

View File

@ -23,6 +23,7 @@ packages:
- "patch" - "patch"
- "pkg-config" - "pkg-config"
- "python3" - "python3"
- "python3-pip"
- "libxkbcommon0" - "libxkbcommon0"
- "ccache" - "ccache"
# Cross compilation HOSTS: # Cross compilation HOSTS:
@ -109,6 +110,8 @@ script: |
done done
} }
pip3 install lief==0.11.4
# Faketime for depends so intermediate results are comparable # Faketime for depends so intermediate results are comparable
export PATH_orig=${PATH} export PATH_orig=${PATH}
create_global_faketime_wrappers "2000-01-01 12:00:00" create_global_faketime_wrappers "2000-01-01 12:00:00"

View File

@ -26,6 +26,7 @@ packages:
- "python3" - "python3"
- "python3-dev" - "python3-dev"
- "python3-setuptools" - "python3-setuptools"
- "python3-pip"
- "fonts-tuffy" - "fonts-tuffy"
- "ccache" - "ccache"
- "cmake" - "cmake"
@ -98,6 +99,8 @@ script: |
done done
} }
pip3 install lief==0.11.4
# Faketime for depends so intermediate results are comparable # Faketime for depends so intermediate results are comparable
export PATH_orig=${PATH} export PATH_orig=${PATH}
create_global_faketime_wrappers "2000-01-01 12:00:00" create_global_faketime_wrappers "2000-01-01 12:00:00"

View File

@ -22,6 +22,7 @@ packages:
- "zip" - "zip"
- "ca-certificates" - "ca-certificates"
- "python3" - "python3"
- "python3-pip"
- "ccache" - "ccache"
remotes: remotes:
- "url": "https://github.com/dashpay/dash.git" - "url": "https://github.com/dashpay/dash.git"
@ -114,6 +115,8 @@ script: |
done done
} }
pip3 install lief==0.11.4
# Faketime for depends so intermediate results are comparable # Faketime for depends so intermediate results are comparable
export PATH_orig=${PATH} export PATH_orig=${PATH}
create_global_faketime_wrappers "2000-01-01 12:00:00" create_global_faketime_wrappers "2000-01-01 12:00:00"

View File

@ -580,6 +580,29 @@ inspecting signatures in Mach-O binaries.")
(package-with-extra-patches glibc-2.27 (package-with-extra-patches glibc-2.27
(search-our-patches "glibc-2.27-riscv64-Use-__has_include__-to-include-asm-syscalls.h.patch"))) (search-our-patches "glibc-2.27-riscv64-Use-__has_include__-to-include-asm-syscalls.h.patch")))
(define-public lief
(package
(name "python-lief")
(version "0.11.4")
(source
(origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/lief-project/LIEF.git")
(commit version)))
(file-name (git-file-name name version))
(sha256
(base32
"0h4kcwr9z478almjqhmils8imfpflzk0r7d05g4xbkdyknn162qf"))))
(build-system python-build-system)
(native-inputs
`(("cmake" ,cmake)))
(home-page "https://github.com/lief-project/LIEF")
(synopsis "Library to Instrument Executable Formats")
(description "Python library to to provide a cross platform library which can
parse, modify and abstract ELF, PE and MachO formats.")
(license license:asl2.0)))
(packages->manifest (packages->manifest
(append (append
(list ;; The Basics (list ;; The Basics
@ -616,6 +639,8 @@ inspecting signatures in Mach-O binaries.")
python-3 python-3
;; Git ;; Git
git git
;; Tests
lief
;; Native gcc 7 toolchain ;; Native gcc 7 toolchain
gcc-toolchain-7 gcc-toolchain-7
(list gcc-toolchain-7 "static")) (list gcc-toolchain-7 "static"))