mirror of
https://github.com/dogecoin/dogecoin.git
synced 2026-03-14 15:37:33 +00:00
devtools: backport lief-based security and symbol checkers
Takes the security and symbol checkers from Bitcoin Core v24.0.1 because this uses the python3 capable lief module for reading multi-platform binaries. This helps getting rid of incompatibilities when using these tools in Ubuntu releases newer than Bionic (18.04) and by using the external module, reduces risk and maintenance cost of custom code. This commit does NOT reconfigure for Dogecoin 1.14.7 parametrization Backported from state at: b3f866a8@bitcoin/bitcoin
This commit is contained in:
parent
cf79c44373
commit
20eff509e8
@ -1,216 +1,263 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (c) 2015-2016 The Bitcoin Core developers
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2015-2021 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
'''
|
||||
Perform basic ELF 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.
|
||||
Otherwise the exit status will be 1 and it will log which executables failed which checks.
|
||||
Needs `readelf` (for ELF) and `objdump` (for PE).
|
||||
'''
|
||||
from __future__ import division,print_function,unicode_literals
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
READELF_CMD = os.getenv('READELF', '/usr/bin/readelf')
|
||||
OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump')
|
||||
NONFATAL = {'HIGH_ENTROPY_VA'} # checks which are non-fatal for now but only generate a warning
|
||||
import lief #type:ignore
|
||||
|
||||
def check_ELF_PIE(executable):
|
||||
'''
|
||||
Check for position independent executable (PIE), allowing for address space randomization.
|
||||
'''
|
||||
p = subprocess.Popen([READELF_CMD, '-h', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode:
|
||||
raise IOError('Error opening file')
|
||||
|
||||
ok = False
|
||||
for line in stdout.split(b'\n'):
|
||||
line = line.split()
|
||||
if len(line)>=2 and line[0] == b'Type:' and line[1] == b'DYN':
|
||||
ok = True
|
||||
return ok
|
||||
|
||||
def get_ELF_program_headers(executable):
|
||||
'''Return type and flags for ELF program headers'''
|
||||
p = subprocess.Popen([READELF_CMD, '-l', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode:
|
||||
raise IOError('Error opening file')
|
||||
in_headers = False
|
||||
count = 0
|
||||
headers = []
|
||||
for line in stdout.split(b'\n'):
|
||||
if line.startswith(b'Program Headers:'):
|
||||
in_headers = True
|
||||
if line == b'':
|
||||
in_headers = False
|
||||
if in_headers:
|
||||
if count == 1: # header line
|
||||
ofs_typ = line.find(b'Type')
|
||||
ofs_offset = line.find(b'Offset')
|
||||
ofs_flags = line.find(b'Flg')
|
||||
ofs_align = line.find(b'Align')
|
||||
if ofs_typ == -1 or ofs_offset == -1 or ofs_flags == -1 or ofs_align == -1:
|
||||
raise ValueError('Cannot parse elfread -lW output')
|
||||
elif count > 1:
|
||||
typ = line[ofs_typ:ofs_offset].rstrip()
|
||||
flags = line[ofs_flags:ofs_align].rstrip()
|
||||
headers.append((typ, flags))
|
||||
count += 1
|
||||
return headers
|
||||
|
||||
def check_ELF_NX(executable):
|
||||
'''
|
||||
Check that no sections are writable and executable (including the stack)
|
||||
'''
|
||||
have_wx = False
|
||||
have_gnu_stack = False
|
||||
for (typ, flags) in get_ELF_program_headers(executable):
|
||||
if typ == b'GNU_STACK':
|
||||
have_gnu_stack = True
|
||||
if b'W' in flags and b'E' in flags: # section is both writable and executable
|
||||
have_wx = True
|
||||
return have_gnu_stack and not have_wx
|
||||
|
||||
def check_ELF_RELRO(executable):
|
||||
def check_ELF_RELRO(binary) -> bool:
|
||||
'''
|
||||
Check for read-only relocations.
|
||||
GNU_RELRO program header must exist
|
||||
Dynamic section must have BIND_NOW flag
|
||||
'''
|
||||
have_gnu_relro = False
|
||||
for (typ, flags) in get_ELF_program_headers(executable):
|
||||
# Note: not checking flags == 'R': here as linkers set the permission differently
|
||||
# This does not affect security: the permission flags of the GNU_RELRO program header are ignored, the PT_LOAD header determines the effective permissions.
|
||||
for segment in binary.segments:
|
||||
# Note: not checking p_flags == PF_R: here as linkers set the permission differently
|
||||
# This does not affect security: the permission flags of the GNU_RELRO program
|
||||
# header are ignored, the PT_LOAD header determines the effective permissions.
|
||||
# However, the dynamic linker need to write to this area so these are RW.
|
||||
# Glibc itself takes care of mprotecting this area R after relocations are finished.
|
||||
# See also http://permalink.gmane.org/gmane.comp.gnu.binutils/71347
|
||||
if typ == b'GNU_RELRO':
|
||||
# See also https://marc.info/?l=binutils&m=1498883354122353
|
||||
if segment.type == lief.ELF.SEGMENT_TYPES.GNU_RELRO:
|
||||
have_gnu_relro = True
|
||||
|
||||
have_bindnow = False
|
||||
p = subprocess.Popen([READELF_CMD, '-d', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode:
|
||||
raise IOError('Error opening file')
|
||||
for line in stdout.split(b'\n'):
|
||||
tokens = line.split()
|
||||
if len(tokens)>1 and tokens[1] == b'(BIND_NOW)' or (len(tokens)>2 and tokens[1] == b'(FLAGS)' and b'BIND_NOW' in tokens[2]):
|
||||
try:
|
||||
flags = binary.get(lief.ELF.DYNAMIC_TAGS.FLAGS)
|
||||
if flags.value & lief.ELF.DYNAMIC_FLAGS.BIND_NOW:
|
||||
have_bindnow = True
|
||||
except:
|
||||
have_bindnow = False
|
||||
|
||||
return have_gnu_relro and have_bindnow
|
||||
|
||||
def check_ELF_Canary(executable):
|
||||
def check_ELF_Canary(binary) -> bool:
|
||||
'''
|
||||
Check for use of stack canary
|
||||
'''
|
||||
p = subprocess.Popen([READELF_CMD, '--dyn-syms', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode:
|
||||
raise IOError('Error opening file')
|
||||
ok = False
|
||||
for line in stdout.split(b'\n'):
|
||||
if b'__stack_chk_fail' in line:
|
||||
ok = True
|
||||
return ok
|
||||
return binary.has_symbol('__stack_chk_fail')
|
||||
|
||||
def get_PE_dll_characteristics(executable):
|
||||
def check_ELF_separate_code(binary):
|
||||
'''
|
||||
Get PE DllCharacteristics bits.
|
||||
Returns a tuple (arch,bits) where arch is 'i386:x86-64' or 'i386'
|
||||
and bits is the DllCharacteristics value.
|
||||
Check that sections are appropriately separated in virtual memory,
|
||||
based on their permissions. This checks for missing -Wl,-z,separate-code
|
||||
and potentially other problems.
|
||||
'''
|
||||
p = subprocess.Popen([OBJDUMP_CMD, '-x', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode:
|
||||
raise IOError('Error opening file')
|
||||
arch = ''
|
||||
bits = 0
|
||||
for line in stdout.split('\n'):
|
||||
tokens = line.split()
|
||||
if len(tokens)>=2 and tokens[0] == 'architecture:':
|
||||
arch = tokens[1].rstrip(',')
|
||||
if len(tokens)>=2 and tokens[0] == 'DllCharacteristics':
|
||||
bits = int(tokens[1],16)
|
||||
return (arch,bits)
|
||||
R = lief.ELF.SEGMENT_FLAGS.R
|
||||
W = lief.ELF.SEGMENT_FLAGS.W
|
||||
E = lief.ELF.SEGMENT_FLAGS.X
|
||||
EXPECTED_FLAGS = {
|
||||
# Read + execute
|
||||
'.init': R | E,
|
||||
'.plt': R | E,
|
||||
'.plt.got': R | E,
|
||||
'.plt.sec': R | E,
|
||||
'.text': R | E,
|
||||
'.fini': R | E,
|
||||
# Read-only data
|
||||
'.interp': R,
|
||||
'.note.gnu.property': R,
|
||||
'.note.gnu.build-id': R,
|
||||
'.note.ABI-tag': R,
|
||||
'.gnu.hash': R,
|
||||
'.dynsym': R,
|
||||
'.dynstr': R,
|
||||
'.gnu.version': R,
|
||||
'.gnu.version_r': R,
|
||||
'.rela.dyn': R,
|
||||
'.rela.plt': R,
|
||||
'.rodata': R,
|
||||
'.eh_frame_hdr': R,
|
||||
'.eh_frame': R,
|
||||
'.qtmetadata': R,
|
||||
'.gcc_except_table': R,
|
||||
'.stapsdt.base': R,
|
||||
# Writable data
|
||||
'.init_array': R | W,
|
||||
'.fini_array': R | W,
|
||||
'.dynamic': R | W,
|
||||
'.got': R | W,
|
||||
'.data': R | W,
|
||||
'.bss': R | W,
|
||||
}
|
||||
if binary.header.machine_type == lief.ELF.ARCH.PPC64:
|
||||
# .plt is RW on ppc64 even with separate-code
|
||||
EXPECTED_FLAGS['.plt'] = R | W
|
||||
# For all LOAD program headers get mapping to the list of sections,
|
||||
# and for each section, remember the flags of the associated program header.
|
||||
flags_per_section = {}
|
||||
for segment in binary.segments:
|
||||
if segment.type == lief.ELF.SEGMENT_TYPES.LOAD:
|
||||
for section in segment.sections:
|
||||
flags_per_section[section.name] = segment.flags
|
||||
# Spot-check ELF LOAD program header flags per section
|
||||
# If these sections exist, check them against the expected R/W/E flags
|
||||
for (section, flags) in flags_per_section.items():
|
||||
if section in EXPECTED_FLAGS:
|
||||
if int(EXPECTED_FLAGS[section]) != int(flags):
|
||||
return False
|
||||
return True
|
||||
|
||||
IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA = 0x0020
|
||||
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE = 0x0040
|
||||
IMAGE_DLL_CHARACTERISTICS_NX_COMPAT = 0x0100
|
||||
def check_ELF_control_flow(binary) -> bool:
|
||||
'''
|
||||
Check for control flow instrumentation
|
||||
'''
|
||||
main = binary.get_function_address('main')
|
||||
content = binary.get_content_from_virtual_address(main, 4, lief.Binary.VA_TYPES.AUTO)
|
||||
|
||||
def check_PE_DYNAMIC_BASE(executable):
|
||||
if content == [243, 15, 30, 250]: # endbr64
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_PE_DYNAMIC_BASE(binary) -> bool:
|
||||
'''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)'''
|
||||
(arch,bits) = get_PE_dll_characteristics(executable)
|
||||
reqbits = IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE
|
||||
return (bits & reqbits) == reqbits
|
||||
return lief.PE.DLL_CHARACTERISTICS.DYNAMIC_BASE in binary.optional_header.dll_characteristics_lists
|
||||
|
||||
# On 64 bit, must support high-entropy 64-bit address space layout randomization in addition to DYNAMIC_BASE
|
||||
# to have secure ASLR.
|
||||
def check_PE_HIGH_ENTROPY_VA(executable):
|
||||
# Must support high-entropy 64-bit address space layout randomization
|
||||
# in addition to DYNAMIC_BASE to have secure ASLR.
|
||||
def check_PE_HIGH_ENTROPY_VA(binary) -> bool:
|
||||
'''PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR'''
|
||||
(arch,bits) = get_PE_dll_characteristics(executable)
|
||||
if arch == 'i386:x86-64':
|
||||
reqbits = IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA
|
||||
else: # Unnecessary on 32-bit
|
||||
assert(arch == 'i386')
|
||||
reqbits = 0
|
||||
return (bits & reqbits) == reqbits
|
||||
return lief.PE.DLL_CHARACTERISTICS.HIGH_ENTROPY_VA in binary.optional_header.dll_characteristics_lists
|
||||
|
||||
def check_PE_NX(executable):
|
||||
'''NX: DllCharacteristics bit 0x100 signifies nxcompat (DEP)'''
|
||||
(arch,bits) = get_PE_dll_characteristics(executable)
|
||||
return (bits & IMAGE_DLL_CHARACTERISTICS_NX_COMPAT) == IMAGE_DLL_CHARACTERISTICS_NX_COMPAT
|
||||
def check_PE_RELOC_SECTION(binary) -> bool:
|
||||
'''Check for a reloc section. This is required for functional ASLR.'''
|
||||
return binary.has_relocations
|
||||
|
||||
CHECKS = {
|
||||
'ELF': [
|
||||
('PIE', check_ELF_PIE),
|
||||
('NX', check_ELF_NX),
|
||||
def check_PE_control_flow(binary) -> bool:
|
||||
'''
|
||||
Check for control flow instrumentation
|
||||
'''
|
||||
main = binary.get_symbol('main').value
|
||||
|
||||
section_addr = binary.section_from_rva(main).virtual_address
|
||||
virtual_address = binary.optional_header.imagebase + section_addr + main
|
||||
|
||||
content = binary.get_content_from_virtual_address(virtual_address, 4, lief.Binary.VA_TYPES.VA)
|
||||
|
||||
if content == [243, 15, 30, 250]: # endbr64
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_MACHO_NOUNDEFS(binary) -> bool:
|
||||
'''
|
||||
Check for no undefined references.
|
||||
'''
|
||||
return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS)
|
||||
|
||||
def check_MACHO_LAZY_BINDINGS(binary) -> bool:
|
||||
'''
|
||||
Check for no lazy bindings.
|
||||
We don't use or check for MH_BINDATLOAD. See #18295.
|
||||
'''
|
||||
return binary.dyld_info.lazy_bind == (0,0)
|
||||
|
||||
def check_MACHO_Canary(binary) -> bool:
|
||||
'''
|
||||
Check for use of stack canary
|
||||
'''
|
||||
return binary.has_symbol('___stack_chk_fail')
|
||||
|
||||
def check_PIE(binary) -> bool:
|
||||
'''
|
||||
Check for position independent executable (PIE),
|
||||
allowing for address space randomization.
|
||||
'''
|
||||
return binary.is_pie
|
||||
|
||||
def check_NX(binary) -> bool:
|
||||
'''
|
||||
Check for no stack execution
|
||||
'''
|
||||
return binary.has_nx
|
||||
|
||||
def check_MACHO_control_flow(binary) -> bool:
|
||||
'''
|
||||
Check for control flow instrumentation
|
||||
'''
|
||||
content = binary.get_content_from_virtual_address(binary.entrypoint, 4, lief.Binary.VA_TYPES.AUTO)
|
||||
|
||||
if content == [243, 15, 30, 250]: # endbr64
|
||||
return True
|
||||
return False
|
||||
|
||||
BASE_ELF = [
|
||||
('PIE', check_PIE),
|
||||
('NX', check_NX),
|
||||
('RELRO', check_ELF_RELRO),
|
||||
('Canary', check_ELF_Canary)
|
||||
],
|
||||
'PE': [
|
||||
('Canary', check_ELF_Canary),
|
||||
('separate_code', check_ELF_separate_code),
|
||||
]
|
||||
|
||||
BASE_PE = [
|
||||
('PIE', check_PIE),
|
||||
('DYNAMIC_BASE', check_PE_DYNAMIC_BASE),
|
||||
('HIGH_ENTROPY_VA', check_PE_HIGH_ENTROPY_VA),
|
||||
('NX', check_PE_NX)
|
||||
('NX', check_NX),
|
||||
('RELOC_SECTION', check_PE_RELOC_SECTION),
|
||||
('CONTROL_FLOW', check_PE_control_flow),
|
||||
]
|
||||
|
||||
BASE_MACHO = [
|
||||
('NOUNDEFS', check_MACHO_NOUNDEFS),
|
||||
('LAZY_BINDINGS', check_MACHO_LAZY_BINDINGS),
|
||||
('Canary', check_MACHO_Canary),
|
||||
]
|
||||
|
||||
CHECKS = {
|
||||
lief.EXE_FORMATS.ELF: {
|
||||
lief.ARCHITECTURES.X86: BASE_ELF + [('CONTROL_FLOW', check_ELF_control_flow)],
|
||||
lief.ARCHITECTURES.ARM: BASE_ELF,
|
||||
lief.ARCHITECTURES.ARM64: BASE_ELF,
|
||||
lief.ARCHITECTURES.PPC: BASE_ELF,
|
||||
lief.ARCHITECTURES.RISCV: BASE_ELF,
|
||||
},
|
||||
lief.EXE_FORMATS.PE: {
|
||||
lief.ARCHITECTURES.X86: BASE_PE,
|
||||
},
|
||||
lief.EXE_FORMATS.MACHO: {
|
||||
lief.ARCHITECTURES.X86: BASE_MACHO + [('PIE', check_PIE),
|
||||
('NX', check_NX),
|
||||
('CONTROL_FLOW', check_MACHO_control_flow)],
|
||||
lief.ARCHITECTURES.ARM64: BASE_MACHO,
|
||||
}
|
||||
}
|
||||
|
||||
def identify_executable(executable):
|
||||
with open(filename, 'rb') as f:
|
||||
magic = f.read(4)
|
||||
if magic.startswith(b'MZ'):
|
||||
return 'PE'
|
||||
elif magic.startswith(b'\x7fELF'):
|
||||
return 'ELF'
|
||||
return None
|
||||
|
||||
if __name__ == '__main__':
|
||||
retval = 0
|
||||
retval: int = 0
|
||||
for filename in sys.argv[1:]:
|
||||
try:
|
||||
etype = identify_executable(filename)
|
||||
if etype is None:
|
||||
print('%s: unknown format' % filename)
|
||||
binary = lief.parse(filename)
|
||||
etype = binary.format
|
||||
arch = binary.abstract.header.architecture
|
||||
binary.concrete
|
||||
|
||||
if etype == lief.EXE_FORMATS.UNKNOWN:
|
||||
print(f'{filename}: unknown executable format')
|
||||
retval = 1
|
||||
continue
|
||||
|
||||
failed = []
|
||||
warning = []
|
||||
for (name, func) in CHECKS[etype]:
|
||||
if not func(filename):
|
||||
if name in NONFATAL:
|
||||
warning.append(name)
|
||||
else:
|
||||
failed.append(name)
|
||||
if failed:
|
||||
print('%s: failed %s' % (filename, ' '.join(failed)))
|
||||
if arch == lief.ARCHITECTURES.NONE:
|
||||
print(f'{filename}: unknown architecture')
|
||||
retval = 1
|
||||
if warning:
|
||||
print('%s: warning %s' % (filename, ' '.join(warning)))
|
||||
except IOError:
|
||||
print('%s: cannot open' % filename)
|
||||
retval = 1
|
||||
exit(retval)
|
||||
continue
|
||||
|
||||
failed: List[str] = []
|
||||
for (name, func) in CHECKS[etype][arch]:
|
||||
if not func(binary):
|
||||
failed.append(name)
|
||||
if failed:
|
||||
print(f'{filename}: failed {" ".join(failed)}')
|
||||
retval = 1
|
||||
except IOError:
|
||||
print(f'{filename}: cannot open')
|
||||
retval = 1
|
||||
sys.exit(retval)
|
||||
|
||||
|
||||
@ -1,166 +1,284 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2014 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.
|
||||
'''
|
||||
A script to check that the (Linux) executables produced by gitian only contain
|
||||
allowed gcc, glibc and libstdc++ version symbols. This makes sure they are
|
||||
still compatible with the minimum supported Linux distribution versions.
|
||||
A script to check that release executables only contain certain symbols
|
||||
and are only linked against allowed libraries.
|
||||
|
||||
Example usage:
|
||||
|
||||
find ../gitian-builder/build -type f -executable | xargs python contrib/devtools/symbol-check.py
|
||||
find ../path/to/binaries -type f -executable | xargs python3 contrib/devtools/symbol-check.py
|
||||
'''
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from typing import List, Dict
|
||||
|
||||
# Debian 6.0.9 (Squeeze) has:
|
||||
import lief #type:ignore
|
||||
|
||||
# Debian 9 (Stretch) EOL: 2022. https://wiki.debian.org/DebianReleases#Production_Releases
|
||||
#
|
||||
# - g++ version 4.4.5 (https://packages.debian.org/search?suite=default§ion=all&arch=any&searchon=names&keywords=g%2B%2B)
|
||||
# - libc version 2.11.3 (https://packages.debian.org/search?suite=default§ion=all&arch=any&searchon=names&keywords=libc6)
|
||||
# - libstdc++ version 4.4.5 (https://packages.debian.org/search?suite=default§ion=all&arch=any&searchon=names&keywords=libstdc%2B%2B6)
|
||||
# - g++ version 6.3.0 (https://packages.debian.org/search?suite=stretch&arch=any&searchon=names&keywords=g%2B%2B)
|
||||
# - libc version 2.24 (https://packages.debian.org/search?suite=stretch&arch=any&searchon=names&keywords=libc6)
|
||||
#
|
||||
# Ubuntu 10.04.4 (Lucid Lynx) has:
|
||||
# Ubuntu 16.04 (Xenial) EOL: 2026. https://wiki.ubuntu.com/Releases
|
||||
#
|
||||
# - g++ version 4.4.3 (http://packages.ubuntu.com/search?keywords=g%2B%2B&searchon=names&suite=lucid§ion=all)
|
||||
# - libc version 2.11.1 (http://packages.ubuntu.com/search?keywords=libc6&searchon=names&suite=lucid§ion=all)
|
||||
# - libstdc++ version 4.4.3 (http://packages.ubuntu.com/search?suite=lucid§ion=all&arch=any&keywords=libstdc%2B%2B&searchon=names)
|
||||
# - g++ version 5.3.1
|
||||
# - libc version 2.23
|
||||
#
|
||||
# Taking the minimum of these as our target.
|
||||
# CentOS Stream 8 EOL: 2024. https://wiki.centos.org/About/Product
|
||||
#
|
||||
# According to GNU ABI document (http://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html) this corresponds to:
|
||||
# GCC 4.4.0: GCC_4.4.0
|
||||
# GCC 4.4.2: GLIBCXX_3.4.13, CXXABI_1.3.3
|
||||
# (glibc) GLIBC_2_11
|
||||
# - g++ version 8.5.0 (http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/)
|
||||
# - libc version 2.28 (http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/)
|
||||
#
|
||||
# See https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html for more info.
|
||||
|
||||
MAX_VERSIONS = {
|
||||
'GCC': (4,4,0),
|
||||
'CXXABI': (1,3,3),
|
||||
'GLIBCXX': (3,4,13),
|
||||
'GLIBC': (2,11),
|
||||
'V': (0,5,0) # xkb (qt only)
|
||||
'GCC': (4,8,0),
|
||||
'GLIBC': {
|
||||
lief.ELF.ARCH.x86_64: (2,18),
|
||||
lief.ELF.ARCH.ARM: (2,18),
|
||||
lief.ELF.ARCH.AARCH64:(2,18),
|
||||
lief.ELF.ARCH.PPC64: (2,18),
|
||||
lief.ELF.ARCH.RISCV: (2,27),
|
||||
},
|
||||
'LIBATOMIC': (1,0),
|
||||
'V': (0,5,0), # xkb (bitcoin-qt only)
|
||||
}
|
||||
# See here for a description of _IO_stdin_used:
|
||||
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=634261#109
|
||||
|
||||
# Ignore symbols that are exported as part of every executable
|
||||
IGNORE_EXPORTS = {
|
||||
b'_edata', b'_end', b'_init', b'__bss_start', b'_fini', b'_IO_stdin_used',
|
||||
b'stdin', b'stdout', b'stderr'
|
||||
'environ', '_environ', '__environ', '_fini', '_init', 'stdin',
|
||||
'stdout', 'stderr',
|
||||
}
|
||||
READELF_CMD = os.getenv('READELF', '/usr/bin/readelf')
|
||||
CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt')
|
||||
|
||||
# Expected linker-loader names can be found here:
|
||||
# https://sourceware.org/glibc/wiki/ABIList?action=recall&rev=16
|
||||
ELF_INTERPRETER_NAMES: Dict[lief.ELF.ARCH, Dict[lief.ENDIANNESS, str]] = {
|
||||
lief.ELF.ARCH.x86_64: {
|
||||
lief.ENDIANNESS.LITTLE: "/lib64/ld-linux-x86-64.so.2",
|
||||
},
|
||||
lief.ELF.ARCH.ARM: {
|
||||
lief.ENDIANNESS.LITTLE: "/lib/ld-linux-armhf.so.3",
|
||||
},
|
||||
lief.ELF.ARCH.AARCH64: {
|
||||
lief.ENDIANNESS.LITTLE: "/lib/ld-linux-aarch64.so.1",
|
||||
},
|
||||
lief.ELF.ARCH.PPC64: {
|
||||
lief.ENDIANNESS.BIG: "/lib64/ld64.so.1",
|
||||
lief.ENDIANNESS.LITTLE: "/lib64/ld64.so.2",
|
||||
},
|
||||
lief.ELF.ARCH.RISCV: {
|
||||
lief.ENDIANNESS.LITTLE: "/lib/ld-linux-riscv64-lp64d.so.1",
|
||||
},
|
||||
}
|
||||
|
||||
# Allowed NEEDED libraries
|
||||
ALLOWED_LIBRARIES = {
|
||||
ELF_ALLOWED_LIBRARIES = {
|
||||
# bitcoind and bitcoin-qt
|
||||
b'libgcc_s.so.1', # GCC base support
|
||||
b'libc.so.6', # C library
|
||||
b'libpthread.so.0', # threading
|
||||
b'libanl.so.1', # DNS resolve
|
||||
b'libm.so.6', # math library
|
||||
b'librt.so.1', # real-time (clock)
|
||||
b'ld-linux-x86-64.so.2', # 64-bit dynamic linker
|
||||
b'ld-linux.so.2', # 32-bit dynamic linker
|
||||
'libgcc_s.so.1', # GCC base support
|
||||
'libc.so.6', # C library
|
||||
'libpthread.so.0', # threading
|
||||
'libm.so.6', # math library
|
||||
'librt.so.1', # real-time (clock)
|
||||
'libatomic.so.1',
|
||||
'ld-linux-x86-64.so.2', # 64-bit dynamic linker
|
||||
'ld-linux.so.2', # 32-bit dynamic linker
|
||||
'ld-linux-aarch64.so.1', # 64-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
|
||||
# bitcoin-qt only
|
||||
b'libX11-xcb.so.1', # part of X11
|
||||
b'libX11.so.6', # part of X11
|
||||
b'libxcb.so.1', # part of X11
|
||||
b'libxkbcommon.so.0', # keyboard keymapping
|
||||
b'libxkbcommon-x11.so.0', # keyboard keymapping
|
||||
b'libfontconfig.so.1', # font support
|
||||
b'libfreetype.so.6', # font parsing
|
||||
b'libdl.so.2' # programming interface to dynamic linker
|
||||
'libxcb.so.1', # part of X11
|
||||
'libxkbcommon.so.0', # keyboard keymapping
|
||||
'libxkbcommon-x11.so.0', # keyboard keymapping
|
||||
'libfontconfig.so.1', # font support
|
||||
'libfreetype.so.6', # font parsing
|
||||
'libdl.so.2', # programming interface to dynamic linker
|
||||
'libxcb-icccm.so.4',
|
||||
'libxcb-image.so.0',
|
||||
'libxcb-shm.so.0',
|
||||
'libxcb-keysyms.so.1',
|
||||
'libxcb-randr.so.0',
|
||||
'libxcb-render-util.so.0',
|
||||
'libxcb-render.so.0',
|
||||
'libxcb-shape.so.0',
|
||||
'libxcb-sync.so.1',
|
||||
'libxcb-xfixes.so.0',
|
||||
'libxcb-xinerama.so.0',
|
||||
'libxcb-xkb.so.1',
|
||||
}
|
||||
|
||||
class CPPFilt(object):
|
||||
'''
|
||||
Demangle C++ symbol names.
|
||||
MACHO_ALLOWED_LIBRARIES = {
|
||||
# bitcoind and bitcoin-qt
|
||||
'libc++.1.dylib', # C++ Standard Library
|
||||
'libSystem.B.dylib', # libc, libm, libpthread, libinfo
|
||||
# bitcoin-qt only
|
||||
'AppKit', # user interface
|
||||
'ApplicationServices', # common application tasks.
|
||||
'Carbon', # deprecated c back-compat API
|
||||
'ColorSync',
|
||||
'CoreFoundation', # low level func, data types
|
||||
'CoreGraphics', # 2D rendering
|
||||
'CoreServices', # operating system services
|
||||
'CoreText', # interface for laying out text and handling fonts.
|
||||
'CoreVideo', # video processing
|
||||
'Foundation', # base layer functionality for apps/frameworks
|
||||
'ImageIO', # read and write image file formats.
|
||||
'IOKit', # user-space access to hardware devices and drivers.
|
||||
'IOSurface', # cross process image/drawing buffers
|
||||
'libobjc.A.dylib', # Objective-C runtime library
|
||||
'Metal', # 3D graphics
|
||||
'Security', # access control and authentication
|
||||
'QuartzCore', # animation
|
||||
}
|
||||
|
||||
Use a pipe to the 'c++filt' command.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.proc = subprocess.Popen(CPPFILT_CMD, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
PE_ALLOWED_LIBRARIES = {
|
||||
'ADVAPI32.dll', # security & registry
|
||||
'IPHLPAPI.DLL', # IP helper API
|
||||
'KERNEL32.dll', # win32 base APIs
|
||||
'msvcrt.dll', # C standard library for MSVC
|
||||
'SHELL32.dll', # shell API
|
||||
'USER32.dll', # user interface
|
||||
'WS2_32.dll', # sockets
|
||||
# bitcoin-qt only
|
||||
'dwmapi.dll', # desktop window manager
|
||||
'GDI32.dll', # graphics device interface
|
||||
'IMM32.dll', # input method editor
|
||||
'NETAPI32.dll',
|
||||
'ole32.dll', # component object model
|
||||
'OLEAUT32.dll', # OLE Automation API
|
||||
'SHLWAPI.dll', # light weight shell API
|
||||
'USERENV.dll',
|
||||
'UxTheme.dll',
|
||||
'VERSION.dll', # version checking
|
||||
'WINMM.dll', # WinMM audio API
|
||||
'WTSAPI32.dll',
|
||||
}
|
||||
|
||||
def __call__(self, mangled):
|
||||
self.proc.stdin.write(mangled + b'\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 read_symbols(executable, imports=True):
|
||||
'''
|
||||
Parse an ELF executable and return a list of (symbol,version) tuples
|
||||
for dynamic, imported symbols.
|
||||
'''
|
||||
p = subprocess.Popen([READELF_CMD, '--dyn-syms', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode:
|
||||
raise IOError('Could not read symbols for %s: %s' % (executable, stderr.strip()))
|
||||
syms = []
|
||||
for line in stdout.split(b'\n'):
|
||||
line = line.split()
|
||||
if len(line)>7 and re.match(b'[0-9]+:$', line[0]):
|
||||
(sym, _, version) = line[7].partition(b'@')
|
||||
is_import = line[6] == b'UND'
|
||||
if version.startswith(b'@'):
|
||||
version = version[1:]
|
||||
if is_import == imports:
|
||||
syms.append((sym, version))
|
||||
return syms
|
||||
|
||||
def check_version(max_versions, version):
|
||||
if b'_' in version:
|
||||
(lib, _, ver) = version.rpartition(b'_')
|
||||
else:
|
||||
lib = version
|
||||
ver = '0'
|
||||
ver = tuple([int(x) for x in ver.split(b'.')])
|
||||
def check_version(max_versions, version, arch) -> bool:
|
||||
(lib, _, ver) = version.rpartition('_')
|
||||
ver = tuple([int(x) for x in ver.split('.')])
|
||||
if not lib in max_versions:
|
||||
return False
|
||||
return ver <= max_versions[lib]
|
||||
if isinstance(max_versions[lib], tuple):
|
||||
return ver <= max_versions[lib]
|
||||
else:
|
||||
return ver <= max_versions[lib][arch]
|
||||
|
||||
def read_libraries(filename):
|
||||
p = subprocess.Popen([READELF_CMD, '-d', '-W', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode:
|
||||
raise IOError('Error opening file')
|
||||
libraries = []
|
||||
for line in stdout.split(b'\n'):
|
||||
tokens = line.split()
|
||||
if len(tokens)>2 and tokens[1] == b'(NEEDED)':
|
||||
match = re.match(b'^Shared library: \[(.*)\]$', b' '.join(tokens[2:]))
|
||||
if match:
|
||||
libraries.append(match.group(1))
|
||||
else:
|
||||
raise ValueError('Unparseable (NEEDED) specification')
|
||||
return libraries
|
||||
def check_imported_symbols(binary) -> bool:
|
||||
ok: bool = True
|
||||
|
||||
for symbol in binary.imported_symbols:
|
||||
if not symbol.imported:
|
||||
continue
|
||||
|
||||
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(binary) -> bool:
|
||||
ok: bool = True
|
||||
|
||||
for symbol in binary.dynamic_symbols:
|
||||
if not symbol.exported:
|
||||
continue
|
||||
name = symbol.name
|
||||
if binary.header.machine_type == lief.ELF.ARCH.RISCV or name in IGNORE_EXPORTS:
|
||||
continue
|
||||
print(f'{binary.name}: export of symbol {name} not allowed!')
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
def check_ELF_libraries(binary) -> bool:
|
||||
ok: bool = True
|
||||
for library in binary.libraries:
|
||||
if library not in ELF_ALLOWED_LIBRARIES:
|
||||
print(f'{filename}: {library} is not in ALLOWED_LIBRARIES!')
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
def check_MACHO_libraries(binary) -> bool:
|
||||
ok: bool = True
|
||||
for dylib in binary.libraries:
|
||||
split = dylib.name.split('/')
|
||||
if split[-1] not in MACHO_ALLOWED_LIBRARIES:
|
||||
print(f'{split[-1]} is not in ALLOWED_LIBRARIES!')
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
def check_MACHO_min_os(binary) -> bool:
|
||||
if binary.build_version.minos == [10,15,0]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_MACHO_sdk(binary) -> bool:
|
||||
if binary.build_version.sdk == [11, 0, 0]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_PE_libraries(binary) -> bool:
|
||||
ok: bool = True
|
||||
for dylib in binary.libraries:
|
||||
if dylib not in PE_ALLOWED_LIBRARIES:
|
||||
print(f'{dylib} is not in ALLOWED_LIBRARIES!')
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
def check_PE_subsystem_version(binary) -> bool:
|
||||
major: int = binary.optional_header.major_subsystem_version
|
||||
minor: int = binary.optional_header.minor_subsystem_version
|
||||
if major == 6 and minor == 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_ELF_interpreter(binary) -> bool:
|
||||
expected_interpreter = ELF_INTERPRETER_NAMES[binary.header.machine_type][binary.abstract.header.endianness]
|
||||
|
||||
return binary.concrete.interpreter == expected_interpreter
|
||||
|
||||
CHECKS = {
|
||||
lief.EXE_FORMATS.ELF: [
|
||||
('IMPORTED_SYMBOLS', check_imported_symbols),
|
||||
('EXPORTED_SYMBOLS', check_exported_symbols),
|
||||
('LIBRARY_DEPENDENCIES', check_ELF_libraries),
|
||||
('INTERPRETER_NAME', check_ELF_interpreter),
|
||||
],
|
||||
lief.EXE_FORMATS.MACHO: [
|
||||
('DYNAMIC_LIBRARIES', check_MACHO_libraries),
|
||||
('MIN_OS', check_MACHO_min_os),
|
||||
('SDK', check_MACHO_sdk),
|
||||
],
|
||||
lief.EXE_FORMATS.PE: [
|
||||
('DYNAMIC_LIBRARIES', check_PE_libraries),
|
||||
('SUBSYSTEM_VERSION', check_PE_subsystem_version),
|
||||
]
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
cppfilt = CPPFilt()
|
||||
retval = 0
|
||||
retval: int = 0
|
||||
for filename in sys.argv[1:]:
|
||||
# Check imported symbols
|
||||
for sym,version in read_symbols(filename, True):
|
||||
if version and not check_version(MAX_VERSIONS, version):
|
||||
print('%s: symbol %s from unsupported version %s' % (filename, cppfilt(sym).decode('utf-8'), version.decode('utf-8')))
|
||||
try:
|
||||
binary = lief.parse(filename)
|
||||
etype = binary.format
|
||||
if etype == lief.EXE_FORMATS.UNKNOWN:
|
||||
print(f'{filename}: unknown executable format')
|
||||
retval = 1
|
||||
# Check exported symbols
|
||||
for sym,version in read_symbols(filename, False):
|
||||
if sym in IGNORE_EXPORTS:
|
||||
continue
|
||||
print('%s: export of symbol %s not allowed' % (filename, cppfilt(sym).decode('utf-8')))
|
||||
retval = 1
|
||||
# Check dependency libraries
|
||||
for library_name in read_libraries(filename):
|
||||
if library_name not in ALLOWED_LIBRARIES:
|
||||
print('%s: NEEDED library %s is not allowed' % (filename, library_name.decode('utf-8')))
|
||||
retval = 1
|
||||
|
||||
exit(retval)
|
||||
failed: List[str] = []
|
||||
for (name, func) in CHECKS[etype]:
|
||||
if not func(binary):
|
||||
failed.append(name)
|
||||
if failed:
|
||||
print(f'{filename}: failed {" ".join(failed)}')
|
||||
retval = 1
|
||||
except IOError:
|
||||
print(f'{filename}: cannot open')
|
||||
retval = 1
|
||||
sys.exit(retval)
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
#!/usr/bin/env python2
|
||||
# Copyright (c) 2015-2016 The Bitcoin Core developers
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2015-2021 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
'''
|
||||
Test script for security-check.py
|
||||
'''
|
||||
from __future__ import division,print_function
|
||||
import lief #type:ignore
|
||||
import os
|
||||
import subprocess
|
||||
from typing import List
|
||||
import unittest
|
||||
|
||||
from utils import determine_wellknown_cmd
|
||||
|
||||
def write_testcode(filename):
|
||||
with open(filename, 'w') as f:
|
||||
with open(filename, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
#include <stdio.h>
|
||||
int main()
|
||||
@ -20,43 +24,128 @@ def write_testcode(filename):
|
||||
}
|
||||
''')
|
||||
|
||||
def clean_files(source, executable):
|
||||
os.remove(source)
|
||||
os.remove(executable)
|
||||
|
||||
def call_security_check(cc, source, executable, options):
|
||||
subprocess.check_call([cc,source,'-o',executable] + options)
|
||||
p = subprocess.Popen(['./security-check.py',executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
return (p.returncode, stdout.rstrip())
|
||||
# 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)
|
||||
return (p.returncode, p.stdout.rstrip())
|
||||
|
||||
def get_arch(cc, source, executable):
|
||||
subprocess.run([*cc, source, '-o', executable], check=True)
|
||||
binary = lief.parse(executable)
|
||||
arch = binary.abstract.header.architecture
|
||||
os.remove(executable)
|
||||
return arch
|
||||
|
||||
class TestSecurityChecks(unittest.TestCase):
|
||||
def test_ELF(self):
|
||||
source = 'test1.c'
|
||||
executable = 'test1'
|
||||
cc = 'gcc'
|
||||
cc = determine_wellknown_cmd('CC', 'gcc')
|
||||
write_testcode(source)
|
||||
arch = get_arch(cc, source, executable)
|
||||
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-zexecstack','-fno-stack-protector','-Wl,-znorelro']),
|
||||
(1, executable+': failed PIE NX RELRO Canary'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fno-stack-protector','-Wl,-znorelro']),
|
||||
(1, executable+': failed PIE RELRO Canary'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-znorelro']),
|
||||
(1, executable+': failed PIE RELRO'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-znorelro','-pie','-fPIE']),
|
||||
(1, executable+': failed RELRO'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-zrelro','-Wl,-z,now','-pie','-fPIE']),
|
||||
(0, ''))
|
||||
if arch == lief.ARCHITECTURES.X86:
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-zexecstack','-fno-stack-protector','-Wl,-znorelro','-no-pie','-fno-PIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed PIE NX RELRO Canary CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fno-stack-protector','-Wl,-znorelro','-no-pie','-fno-PIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed PIE RELRO Canary CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-znorelro','-no-pie','-fno-PIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed PIE RELRO CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-znorelro','-pie','-fPIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed RELRO CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-zrelro','-Wl,-z,now','-pie','-fPIE', '-Wl,-z,noseparate-code']),
|
||||
(1, executable+': failed separate_code CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-zrelro','-Wl,-z,now','-pie','-fPIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-zrelro','-Wl,-z,now','-pie','-fPIE', '-Wl,-z,separate-code', '-fcf-protection=full']),
|
||||
(0, ''))
|
||||
else:
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-zexecstack','-fno-stack-protector','-Wl,-znorelro','-no-pie','-fno-PIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed PIE NX RELRO Canary'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fno-stack-protector','-Wl,-znorelro','-no-pie','-fno-PIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed PIE RELRO Canary'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-znorelro','-no-pie','-fno-PIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed PIE RELRO'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-znorelro','-pie','-fPIE', '-Wl,-z,separate-code']),
|
||||
(1, executable+': failed RELRO'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-zrelro','-Wl,-z,now','-pie','-fPIE', '-Wl,-z,noseparate-code']),
|
||||
(1, executable+': failed separate_code'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-zrelro','-Wl,-z,now','-pie','-fPIE', '-Wl,-z,separate-code']),
|
||||
(0, ''))
|
||||
|
||||
clean_files(source, executable)
|
||||
|
||||
def test_PE(self):
|
||||
source = 'test1.c'
|
||||
executable = 'test1.exe'
|
||||
cc = 'i686-w64-mingw32-gcc'
|
||||
cc = determine_wellknown_cmd('CC', 'x86_64-w64-mingw32-gcc')
|
||||
write_testcode(source)
|
||||
|
||||
self.assertEqual(call_security_check(cc, source, executable, []),
|
||||
(1, executable+': failed PIE NX'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat']),
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--disable-nxcompat','-Wl,--disable-reloc-section','-Wl,--disable-dynamicbase','-Wl,--disable-high-entropy-va','-no-pie','-fno-PIE']),
|
||||
(1, executable+': failed PIE DYNAMIC_BASE HIGH_ENTROPY_VA NX RELOC_SECTION CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--disable-reloc-section','-Wl,--disable-dynamicbase','-Wl,--disable-high-entropy-va','-no-pie','-fno-PIE']),
|
||||
(1, executable+': failed PIE DYNAMIC_BASE HIGH_ENTROPY_VA RELOC_SECTION CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--enable-reloc-section','-Wl,--disable-dynamicbase','-Wl,--disable-high-entropy-va','-no-pie','-fno-PIE']),
|
||||
(1, executable+': failed PIE DYNAMIC_BASE HIGH_ENTROPY_VA CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--enable-reloc-section','-Wl,--disable-dynamicbase','-Wl,--disable-high-entropy-va','-pie','-fPIE']),
|
||||
(1, executable+': failed PIE DYNAMIC_BASE HIGH_ENTROPY_VA CONTROL_FLOW')) # -pie -fPIE does nothing unless --dynamicbase is also supplied
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--enable-reloc-section','-Wl,--dynamicbase','-Wl,--disable-high-entropy-va','-pie','-fPIE']),
|
||||
(1, executable+': failed HIGH_ENTROPY_VA CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--enable-reloc-section','-Wl,--dynamicbase','-Wl,--high-entropy-va','-pie','-fPIE']),
|
||||
(1, executable+': failed CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--enable-reloc-section','-Wl,--dynamicbase','-Wl,--high-entropy-va','-pie','-fPIE', '-fcf-protection=full']),
|
||||
(0, ''))
|
||||
|
||||
clean_files(source, executable)
|
||||
|
||||
def test_MACHO(self):
|
||||
source = 'test1.c'
|
||||
executable = 'test1'
|
||||
cc = determine_wellknown_cmd('CC', 'clang')
|
||||
write_testcode(source)
|
||||
arch = get_arch(cc, source, executable)
|
||||
|
||||
if arch == lief.ARCHITECTURES.X86:
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-flat_namespace','-Wl,-allow_stack_execute','-fno-stack-protector']),
|
||||
(1, executable+': failed NOUNDEFS LAZY_BINDINGS Canary PIE NX CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-flat_namespace','-Wl,-allow_stack_execute','-fstack-protector-all']),
|
||||
(1, executable+': failed NOUNDEFS LAZY_BINDINGS PIE NX CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-flat_namespace','-fstack-protector-all']),
|
||||
(1, executable+': failed NOUNDEFS LAZY_BINDINGS PIE CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-fstack-protector-all']),
|
||||
(1, executable+': failed LAZY_BINDINGS PIE CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-bind_at_load','-fstack-protector-all']),
|
||||
(1, executable+': failed PIE CONTROL_FLOW'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-no_pie','-Wl,-bind_at_load','-fstack-protector-all', '-fcf-protection=full']),
|
||||
(1, executable+': failed PIE'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--dynamicbase']),
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-pie','-Wl,-bind_at_load','-fstack-protector-all', '-fcf-protection=full']),
|
||||
(0, ''))
|
||||
else:
|
||||
# arm64 darwin doesn't support non-PIE binaries, control flow or executable stacks
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-flat_namespace','-fno-stack-protector']),
|
||||
(1, executable+': failed NOUNDEFS LAZY_BINDINGS Canary'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-flat_namespace','-fstack-protector-all']),
|
||||
(1, executable+': failed NOUNDEFS LAZY_BINDINGS'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-fstack-protector-all']),
|
||||
(1, executable+': failed LAZY_BINDINGS'))
|
||||
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-bind_at_load','-fstack-protector-all']),
|
||||
(0, ''))
|
||||
|
||||
|
||||
clean_files(source, executable)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
204
contrib/devtools/test-symbol-check.py
Executable file
204
contrib/devtools/test-symbol-check.py
Executable file
@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2020-2021 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
'''
|
||||
Test script for symbol-check.py
|
||||
'''
|
||||
import os
|
||||
import subprocess
|
||||
from typing import List
|
||||
import unittest
|
||||
|
||||
from utils import determine_wellknown_cmd
|
||||
|
||||
def call_symbol_check(cc: List[str], source, executable, options):
|
||||
# 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)
|
||||
os.remove(source)
|
||||
os.remove(executable)
|
||||
return (p.returncode, p.stdout.rstrip())
|
||||
|
||||
def get_machine(cc: List[str]):
|
||||
p = subprocess.run([*cc,'-dumpmachine'], stdout=subprocess.PIPE, universal_newlines=True)
|
||||
return p.stdout.rstrip()
|
||||
|
||||
class TestSymbolChecks(unittest.TestCase):
|
||||
def test_ELF(self):
|
||||
source = 'test1.c'
|
||||
executable = 'test1'
|
||||
cc = determine_wellknown_cmd('CC', 'gcc')
|
||||
|
||||
# there's no way to do this test for RISC-V at the moment; we build for
|
||||
# RISC-V in a glibc 2.27 environment and we allow all symbols from 2.27.
|
||||
if 'riscv' in get_machine(cc):
|
||||
self.skipTest("test not available for RISC-V")
|
||||
|
||||
# nextup was introduced in GLIBC 2.24, so is newer than our supported
|
||||
# glibc (2.18), and available in our release build environment (2.24).
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
#define _GNU_SOURCE
|
||||
#include <math.h>
|
||||
|
||||
double nextup(double x);
|
||||
|
||||
int main()
|
||||
{
|
||||
nextup(3.14);
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, ['-lm']),
|
||||
(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
|
||||
# it's also out of context enough that it's unlikely to ever become a real dependency
|
||||
source = 'test2.c'
|
||||
executable = 'test2'
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
#include <utmp.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
login(0);
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, ['-lutil']),
|
||||
(1, executable + ': libutil.so.1 is not in ALLOWED_LIBRARIES!\n' +
|
||||
executable + ': failed LIBRARY_DEPENDENCIES'))
|
||||
|
||||
# finally, check a simple conforming binary
|
||||
source = 'test3.c'
|
||||
executable = 'test3'
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
#include <stdio.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("42");
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, []),
|
||||
(0, ''))
|
||||
|
||||
def test_MACHO(self):
|
||||
source = 'test1.c'
|
||||
executable = 'test1'
|
||||
cc = determine_wellknown_cmd('CC', 'clang')
|
||||
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
#include <expat.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
XML_ExpatVersion();
|
||||
return 0;
|
||||
}
|
||||
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, ['-lexpat', '-Wl,-platform_version','-Wl,macos', '-Wl,11.4', '-Wl,11.4']),
|
||||
(1, 'libexpat.1.dylib is not in ALLOWED_LIBRARIES!\n' +
|
||||
f'{executable}: failed DYNAMIC_LIBRARIES MIN_OS SDK'))
|
||||
|
||||
source = 'test2.c'
|
||||
executable = 'test2'
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
CGMainDisplayID();
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, ['-framework', 'CoreGraphics', '-Wl,-platform_version','-Wl,macos', '-Wl,11.4', '-Wl,11.4']),
|
||||
(1, f'{executable}: failed MIN_OS SDK'))
|
||||
|
||||
source = 'test3.c'
|
||||
executable = 'test3'
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
int main()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, ['-Wl,-platform_version','-Wl,macos', '-Wl,10.15', '-Wl,11.4']),
|
||||
(1, f'{executable}: failed SDK'))
|
||||
|
||||
def test_PE(self):
|
||||
source = 'test1.c'
|
||||
executable = 'test1.exe'
|
||||
cc = determine_wellknown_cmd('CC', 'x86_64-w64-mingw32-gcc')
|
||||
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
#include <pdh.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
PdhConnectMachineA(NULL);
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, ['-lpdh', '-Wl,--major-subsystem-version', '-Wl,6', '-Wl,--minor-subsystem-version', '-Wl,1']),
|
||||
(1, 'pdh.dll is not in ALLOWED_LIBRARIES!\n' +
|
||||
executable + ': failed DYNAMIC_LIBRARIES'))
|
||||
|
||||
source = 'test2.c'
|
||||
executable = 'test2.exe'
|
||||
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
int main()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, ['-Wl,--major-subsystem-version', '-Wl,9', '-Wl,--minor-subsystem-version', '-Wl,9']),
|
||||
(1, executable + ': failed SUBSYSTEM_VERSION'))
|
||||
|
||||
source = 'test3.c'
|
||||
executable = 'test3.exe'
|
||||
with open(source, 'w', encoding="utf8") as f:
|
||||
f.write('''
|
||||
#include <combaseapi.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
CoFreeUnusedLibrariesEx(0,0);
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
|
||||
self.assertEqual(call_symbol_check(cc, source, executable, ['-lole32', '-Wl,--major-subsystem-version', '-Wl,6', '-Wl,--minor-subsystem-version', '-Wl,1']),
|
||||
(0, ''))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
22
contrib/devtools/utils.py
Executable file
22
contrib/devtools/utils.py
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2021 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
'''
|
||||
Common utility functions
|
||||
'''
|
||||
import shutil
|
||||
import sys
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
|
||||
def determine_wellknown_cmd(envvar, progname) -> List[str]:
|
||||
maybe_env = os.getenv(envvar)
|
||||
maybe_which = shutil.which(progname)
|
||||
if maybe_env:
|
||||
return maybe_env.split(' ') # Well-known vars are often meant to be word-split
|
||||
elif maybe_which:
|
||||
return [ maybe_which ]
|
||||
else:
|
||||
sys.exit(f"{progname} not found")
|
||||
Loading…
x
Reference in New Issue
Block a user