mirror of
https://github.com/dashpay/dash.git
synced 2024-12-26 12:32:48 +01:00
67ceda1b5a
5ca90f8b598978437340bb8467f527b9edfb2bbf scripts: add MACHO lazy bindings check to security-check.py (fanquake)
Pull request description:
This is a slightly belated follow up to #17686 and some discussion with Cory. It's not entirely clear if we should make this change due to the way the macOS dynamic loader appears to work. However I'm opening this for some discussion. Also related to #17768.
#### Issue:
[`LD64`](https://opensource.apple.com/source/ld64/) doesn't set the [MH_BINDATLOAD](https://opensource.apple.com/source/xnu/xnu-6153.11.26/EXTERNAL_HEADERS/mach-o/loader.h.auto.html) bit in the header of MACHO executables, when building with `-bind_at_load`. This is in contradiction to the [documentation](https://opensource.apple.com/source/ld64/ld64-450.3/doc/man/man1/ld.1.auto.html):
```bash
-bind_at_load
Sets a bit in the mach header of the resulting binary which tells dyld to
bind all symbols when the binary is loaded, rather than lazily.
```
The [`ld` in Apples cctools](https://opensource.apple.com/source/cctools/cctools-927.0.2/ld/layout.c.auto.html) does set the bit, however the [cctools-port](https://github.com/tpoechtrager/cctools-port/) that we use for release builds, bundles `LD64`.
However; even if the linker hasn't set that bit, the dynamic loader ([`dyld`](https://opensource.apple.com/source/dyld/)) doesn't seem to ever check for it, and from what I understand, it looks at a different part of the header when determining whether to lazily load symbols.
Note that our release binaries are currently working as expected, and no lazy loading occurs.
#### Example:
Using a small program, we can observe the behaviour of the dynamic loader.
Conducted using:
```bash
clang++ --version
Apple clang version 11.0.0 (clang-1100.0.33.17)
Target: x86_64-apple-darwin18.7.0
ld -v
@(#)PROGRAM:ld PROJECT:ld64-530
BUILD 18:57:17 Dec 13 2019
LTO support using: LLVM version 11.0.0, (clang-1100.0.33.17) (static support for 23, runtime is 23)
TAPI support using: Apple TAPI version 11.0.0 (tapi-1100.0.11)
```
```cpp
#include <iostream>
int main() {
std::cout << "Hello World!\n";
return 0;
}
```
Compile and check the MACHO header:
```bash
clang++ test.cpp -o test
otool -vh test
...
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 16 1424 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
# Run and dump dynamic loader bindings:
DYLD_PRINT_BINDINGS=1 DYLD_PRINT_TO_FILE=no_bind.txt ./test
Hello World!
```
Recompile with `-bind_at_load`. Note still no `BINDATLOAD` flag:
```bash
clang++ test.cpp -o test -Wl,-bind_at_load
otool -vh test
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 16 1424 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
...
DYLD_PRINT_BINDINGS=1 DYLD_PRINT_TO_FILE=bind.txt ./test
Hello World!
```
If we diff the outputs, you can see that `dyld` doesn't perform any lazy bindings when the binary is compiled with `-bind_at_load`, even if the `BINDATLOAD` flag is not set:
```diff
@@ -1,11 +1,27 @@
+dyld: bind: test:0x103EDF030 = libc++.1.dylib:__ZNKSt3__16locale9use_facetERNS0_2idE, *0x103EDF030 = 0x7FFF70C9FA58
+dyld: bind: test:0x103EDF038 = libc++.1.dylib:__ZNKSt3__18ios_base6getlocEv, *0x103EDF038 = 0x7FFF70CA12C2
+dyld: bind: test:0x103EDF068 = libc++.1.dylib:__ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryC1ERS3_, *0x103EDF068 = 0x7FFF70CA12B6
+dyld: bind: test:0x103EDF070 = libc++.1.dylib:__ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryD1Ev, *0x103EDF070 = 0x7FFF70CA1528
+dyld: bind: test:0x103EDF080 = libc++.1.dylib:__ZNSt3__16localeD1Ev, *0x103EDF080 = 0x7FFF70C9FAE6
<trim>
-dyld: lazy bind: test:0x10D4AC0C8 = libsystem_platform.dylib:_strlen, *0x10D4AC0C8 = 0x7FFF73C5C6E0
-dyld: lazy bind: test:0x10D4AC068 = libc++.1.dylib:__ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryC1ERS3_, *0x10D4AC068 = 0x7FFF70CA12B6
-dyld: lazy bind: test:0x10D4AC038 = libc++.1.dylib:__ZNKSt3__18ios_base6getlocEv, *0x10D4AC038 = 0x7FFF70CA12C2
-dyld: lazy bind: test:0x10D4AC030 = libc++.1.dylib:__ZNKSt3__16locale9use_facetERNS0_2idE, *0x10D4AC030 = 0x7FFF70C9FA58
-dyld: lazy bind: test:0x10D4AC080 = libc++.1.dylib:__ZNSt3__16localeD1Ev, *0x10D4AC080 = 0x7FFF70C9FAE6
-dyld: lazy bind: test:0x10D4AC070 = libc++.1.dylib:__ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryD1Ev, *0x10D4AC070 = 0x7FFF70CA1528
```
Note: `dyld` also has a `DYLD_BIND_AT_LAUNCH=1` environment variable, that when set, will force any lazy bindings to be non-lazy:
```bash
dyld: forced lazy bind: test:0x10BEC8068 = libc++.1.dylib:__ZNSt3__113basic_ostream
```
#### Thoughts:
After looking at the dyld source, I can't find any checks for `MH_BINDATLOAD`. You can see the flags it does check for, such as MH_PIE or MH_BIND_TO_WEAK [here](https://opensource.apple.com/source/dyld/dyld-732.8/src/ImageLoaderMachO.cpp.auto.html).
It seems that the lazy binding of any symbols depends on whether or not [lazy_bind_size](https://opensource.apple.com/source/xnu/xnu-6153.11.26/EXTERNAL_HEADERS/mach-o/loader.h.auto.html) from the `LC_DYLD_INFO_ONLY` load command is > 0. Which was mentioned in [#17686](https://github.com/bitcoin/bitcoin/pull/17686#issue-350216254).
#### Changes:
This PR is one of [Corys commits](7b6ba26178
), that I've rebased and modified to make build. I've also included an addition to the `security-check.py` script to check for the flag.
However, given the above, I'm not entirely sure this patch is the correct approach. If the linker no-longer inserts it, and the dynamic loader doesn't look for it, there might be little benefit to setting it. Or, maybe this is an oversight from Apple and needs some upstream discussion. Looking for some thoughts / Concept ACK/NACK.
One alternate approach we could take is to drop the patch and modify security-check.py to look for `lazy_bind_size` == 0 in the `LC_DYLD_INFO_ONLY` load command, using `otool -l`.
ACKs for top commit:
theuni:
ACK 5ca90f8b598978437340bb8467f527b9edfb2bbf
Tree-SHA512: 444022ea9d19ed74dd06dc2ab3857a9c23fbc2f6475364e8552d761b712d684b3a7114d144f20de42328d1a99403b48667ba96885121392affb2e05b834b6e1c
285 lines
10 KiB
Python
Executable File
285 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2015-2016 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 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), `objdump` (for PE) and `otool` (for MACHO).
|
|
'''
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
|
|
READELF_CMD = os.getenv('READELF', '/usr/bin/readelf')
|
|
OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump')
|
|
OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool')
|
|
NONFATAL = {} # checks which are non-fatal for now but only generate a warning
|
|
|
|
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, universal_newlines=True)
|
|
(stdout, stderr) = p.communicate()
|
|
if p.returncode:
|
|
raise IOError('Error opening file')
|
|
|
|
ok = False
|
|
for line in stdout.splitlines():
|
|
line = line.split()
|
|
if len(line)>=2 and line[0] == 'Type:' and line[1] == '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, universal_newlines=True)
|
|
(stdout, stderr) = p.communicate()
|
|
if p.returncode:
|
|
raise IOError('Error opening file')
|
|
in_headers = False
|
|
count = 0
|
|
headers = []
|
|
for line in stdout.splitlines():
|
|
if line.startswith('Program Headers:'):
|
|
in_headers = True
|
|
if line == '':
|
|
in_headers = False
|
|
if in_headers:
|
|
if count == 1: # header line
|
|
ofs_typ = line.find('Type')
|
|
ofs_offset = line.find('Offset')
|
|
ofs_flags = line.find('Flg')
|
|
ofs_align = line.find('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 == 'GNU_STACK':
|
|
have_gnu_stack = True
|
|
if 'W' in flags and '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):
|
|
'''
|
|
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.
|
|
# 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 https://marc.info/?l=binutils&m=1498883354122353
|
|
if typ == '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, universal_newlines=True)
|
|
(stdout, stderr) = p.communicate()
|
|
if p.returncode:
|
|
raise IOError('Error opening file')
|
|
for line in stdout.splitlines():
|
|
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
|
|
return have_gnu_relro and have_bindnow
|
|
|
|
def check_ELF_Canary(executable):
|
|
'''
|
|
Check for use of stack canary
|
|
'''
|
|
p = subprocess.Popen([READELF_CMD, '--dyn-syms', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
|
|
(stdout, stderr) = p.communicate()
|
|
if p.returncode:
|
|
raise IOError('Error opening file')
|
|
ok = False
|
|
for line in stdout.splitlines():
|
|
if '__stack_chk_fail' in line:
|
|
ok = True
|
|
return ok
|
|
|
|
def get_PE_dll_characteristics(executable):
|
|
'''
|
|
Get PE DllCharacteristics bits.
|
|
Returns a tuple (arch,bits) where arch is 'i386:x86-64' or 'i386'
|
|
and bits is the DllCharacteristics value.
|
|
'''
|
|
p = subprocess.Popen([OBJDUMP_CMD, '-x', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
|
|
(stdout, stderr) = p.communicate()
|
|
if p.returncode:
|
|
raise IOError('Error opening file')
|
|
arch = ''
|
|
bits = 0
|
|
for line in stdout.splitlines():
|
|
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)
|
|
|
|
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):
|
|
'''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)'''
|
|
(arch,bits) = get_PE_dll_characteristics(executable)
|
|
reqbits = IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE
|
|
return (bits & reqbits) == reqbits
|
|
|
|
# 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):
|
|
'''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
|
|
|
|
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 get_MACHO_executable_flags(executable):
|
|
p = subprocess.Popen([OTOOL_CMD, '-vh', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
|
|
(stdout, stderr) = p.communicate()
|
|
if p.returncode:
|
|
raise IOError('Error opening file')
|
|
|
|
flags = []
|
|
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:
|
|
'''
|
|
Check for no undefined references.
|
|
'''
|
|
flags = get_MACHO_executable_flags(executable)
|
|
if 'NOUNDEFS' in flags:
|
|
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:
|
|
'''
|
|
Check for no lazy bindings.
|
|
We don't use or check for MH_BINDATLOAD. See #18295.
|
|
'''
|
|
p = subprocess.Popen([OTOOL_CMD, '-l', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
|
|
(stdout, stderr) = p.communicate()
|
|
if p.returncode:
|
|
raise IOError('Error opening file')
|
|
|
|
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
|
|
|
|
CHECKS = {
|
|
'ELF': [
|
|
('PIE', check_ELF_PIE),
|
|
('NX', check_ELF_NX),
|
|
('RELRO', check_ELF_RELRO),
|
|
('Canary', check_ELF_Canary)
|
|
],
|
|
'PE': [
|
|
('DYNAMIC_BASE', check_PE_DYNAMIC_BASE),
|
|
('HIGH_ENTROPY_VA', check_PE_HIGH_ENTROPY_VA),
|
|
('NX', check_PE_NX)
|
|
],
|
|
'MACHO': [
|
|
('PIE', check_MACHO_PIE),
|
|
('NOUNDEFS', check_MACHO_NOUNDEFS),
|
|
('NX', check_MACHO_NX),
|
|
('LAZY_BINDINGS', check_MACHO_LAZY_BINDINGS)
|
|
]
|
|
}
|
|
|
|
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'
|
|
elif magic.startswith(b'\xcf\xfa'):
|
|
return 'MACHO'
|
|
return None
|
|
|
|
if __name__ == '__main__':
|
|
retval = 0
|
|
for filename in sys.argv[1:]:
|
|
try:
|
|
etype = identify_executable(filename)
|
|
if etype is None:
|
|
print('%s: unknown format' % filename)
|
|
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)))
|
|
retval = 1
|
|
if warning:
|
|
print('%s: warning %s' % (filename, ' '.join(warning)))
|
|
except IOError:
|
|
print('%s: cannot open' % filename)
|
|
retval = 1
|
|
sys.exit(retval)
|
|
|