Changeset View
Changeset View
Standalone View
Standalone View
contrib/devtools/security-check.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2015-2017 The Bitcoin Core developers | # Copyright (c) 2015-2017 The Bitcoin Core developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
""" | """ | ||||
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. | ||||
""" | """ | ||||
import sys | import sys | ||||
from typing import List, Optional | from typing import List | ||||
import lief | import lief | ||||
import pixie | |||||
def check_ELF_PIE(executable) -> bool: | def check_ELF_RELRO(binary) -> bool: | ||||
""" | |||||
Check for position independent executable (PIE), allowing for address space | |||||
randomization. | |||||
""" | |||||
elf = pixie.load(executable) | |||||
return elf.hdr.e_type == pixie.ET_DYN | |||||
def check_ELF_NX(executable) -> bool: | |||||
""" | |||||
Check that no sections are writable and executable (including the stack) | |||||
""" | |||||
elf = pixie.load(executable) | |||||
have_wx = False | |||||
have_gnu_stack = False | |||||
for ph in elf.program_headers: | |||||
if ph.p_type == pixie.PT_GNU_STACK: | |||||
have_gnu_stack = True | |||||
# section is both writable and executable | |||||
if (ph.p_flags & pixie.PF_W) != 0 and (ph.p_flags & pixie.PF_X) != 0: | |||||
have_wx = True | |||||
return have_gnu_stack and not have_wx | |||||
def check_ELF_RELRO(executable) -> bool: | |||||
""" | """ | ||||
Check for read-only relocations. | Check for read-only relocations. | ||||
GNU_RELRO program header must exist | GNU_RELRO program header must exist | ||||
Dynamic section must have BIND_NOW flag | Dynamic section must have BIND_NOW flag | ||||
""" | """ | ||||
elf = pixie.load(executable) | |||||
have_gnu_relro = False | have_gnu_relro = False | ||||
for ph in elf.program_headers: | for segment in binary.segments: | ||||
# Note: not checking p_flags == PF_R: here as linkers set the permission | # Note: not checking p_flags == PF_R: here as linkers set the permission | ||||
# differently. This does not affect security: the permission flags of the | # differently. This does not affect security: the permission flags of the | ||||
# GNU_RELRO program header are ignored, the PT_LOAD header determines the | # GNU_RELRO program header are ignored, the PT_LOAD header determines the | ||||
# effective permissions. | # effective permissions. | ||||
# However, the dynamic linker need to write to this area so these are RW. | # However, the dynamic linker need to write to this area so these are RW. | ||||
# Glibc itself takes care of mprotecting this area R after relocations are | # Glibc itself takes care of mprotecting this area R after relocations are | ||||
# finished. See also https://marc.info/?l=binutils&m=1498883354122353 | # finished. See also https://marc.info/?l=binutils&m=1498883354122353 | ||||
if ph.p_type == pixie.PT_GNU_RELRO: | if segment.type == lief.ELF.SEGMENT_TYPES.GNU_RELRO: | ||||
have_gnu_relro = True | have_gnu_relro = True | ||||
have_bindnow = False | have_bindnow = False | ||||
for flags in elf.query_dyn_tags(pixie.DT_FLAGS): | try: | ||||
assert isinstance(flags, int) | flags = binary.get(lief.ELF.DYNAMIC_TAGS.FLAGS) | ||||
if flags & pixie.DF_BIND_NOW: | if flags.value & lief.ELF.DYNAMIC_FLAGS.BIND_NOW: | ||||
have_bindnow = True | have_bindnow = True | ||||
except Exception: | |||||
have_bindnow = False | |||||
return have_gnu_relro and have_bindnow | return have_gnu_relro and have_bindnow | ||||
def check_ELF_Canary(executable) -> bool: | def check_ELF_Canary(binary) -> bool: | ||||
""" | """ | ||||
Check for use of stack canary | Check for use of stack canary | ||||
""" | """ | ||||
elf = pixie.load(executable) | return binary.has_symbol("__stack_chk_fail") | ||||
for symbol in elf.dyn_symbols: | |||||
if symbol.name == b"__stack_chk_fail": | |||||
return True | |||||
return False | |||||
def check_ELF_separate_code(executable): | def check_ELF_separate_code(binary): | ||||
""" | """ | ||||
Check that sections are appropriately separated in virtual memory, | Check that sections are appropriately separated in virtual memory, | ||||
based on their permissions. This checks for missing -Wl,-z,separate-code | based on their permissions. This checks for missing -Wl,-z,separate-code | ||||
and potentially other problems. | and potentially other problems. | ||||
""" | """ | ||||
elf = pixie.load(executable) | R = lief.ELF.SEGMENT_FLAGS.R | ||||
R = pixie.PF_R | W = lief.ELF.SEGMENT_FLAGS.W | ||||
W = pixie.PF_W | E = lief.ELF.SEGMENT_FLAGS.X | ||||
E = pixie.PF_X | |||||
EXPECTED_FLAGS = { | EXPECTED_FLAGS = { | ||||
# Read + execute | # Read + execute | ||||
b".init": R | E, | ".init": R | E, | ||||
b".plt": R | E, | ".plt": R | E, | ||||
b".plt.got": R | E, | ".plt.got": R | E, | ||||
b".plt.sec": R | E, | ".plt.sec": R | E, | ||||
b".text": R | E, | ".text": R | E, | ||||
b".fini": R | E, | ".fini": R | E, | ||||
# Read-only data | # Read-only data | ||||
b".interp": R, | ".interp": R, | ||||
b".note.gnu.property": R, | ".note.gnu.property": R, | ||||
b".note.gnu.build-id": R, | ".note.gnu.build-id": R, | ||||
b".note.ABI-tag": R, | ".note.ABI-tag": R, | ||||
b".gnu.hash": R, | ".gnu.hash": R, | ||||
b".dynsym": R, | ".dynsym": R, | ||||
b".dynstr": R, | ".dynstr": R, | ||||
b".gnu.version": R, | ".gnu.version": R, | ||||
b".gnu.version_r": R, | ".gnu.version_r": R, | ||||
b".rela.dyn": R, | ".rela.dyn": R, | ||||
b".rela.plt": R, | ".rela.plt": R, | ||||
b".rodata": R, | ".rodata": R, | ||||
b".eh_frame_hdr": R, | ".eh_frame_hdr": R, | ||||
b".eh_frame": R, | ".eh_frame": R, | ||||
b".qtmetadata": R, | ".qtmetadata": R, | ||||
b".gcc_except_table": R, | ".gcc_except_table": R, | ||||
b".stapsdt.base": R, | ".stapsdt.base": R, | ||||
# Writable data | # Writable data | ||||
b".init_array": R | W, | ".init_array": R | W, | ||||
b".fini_array": R | W, | ".fini_array": R | W, | ||||
b".dynamic": R | W, | ".dynamic": R | W, | ||||
b".got": R | W, | ".got": R | W, | ||||
b".data": R | W, | ".data": R | W, | ||||
b".bss": R | W, | ".bss": R | W, | ||||
} | } | ||||
if elf.hdr.e_machine == pixie.EM_PPC64: | |||||
# .plt is RW on ppc64 even with separate-code | |||||
EXPECTED_FLAGS[b".plt"] = R | W | |||||
# For all LOAD program headers get mapping to the list of sections, | # For all LOAD program headers get mapping to the list of sections, | ||||
# and for each section, remember the flags of the associated program header. | # and for each section, remember the flags of the associated program header. | ||||
flags_per_section = {} | flags_per_section = {} | ||||
for ph in elf.program_headers: | for segment in binary.segments: | ||||
if ph.p_type == pixie.PT_LOAD: | if segment.type == lief.ELF.SEGMENT_TYPES.LOAD: | ||||
for section in ph.sections: | for section in segment.sections: | ||||
assert section.name not in flags_per_section | assert section.name not in flags_per_section | ||||
flags_per_section[section.name] = ph.p_flags | flags_per_section[section.name] = segment.flags | ||||
# Spot-check ELF LOAD program header flags per section | # Spot-check ELF LOAD program header flags per section | ||||
# If these sections exist, check them against the expected R/W/E flags | # If these sections exist, check them against the expected R/W/E flags | ||||
for section, flags in flags_per_section.items(): | for section, flags in flags_per_section.items(): | ||||
if section in EXPECTED_FLAGS: | if section in EXPECTED_FLAGS: | ||||
if EXPECTED_FLAGS[section] != flags: | if int(EXPECTED_FLAGS[section]) != int(flags): | ||||
return False | return False | ||||
return True | return True | ||||
def check_PE_DYNAMIC_BASE(executable) -> bool: | def check_PE_DYNAMIC_BASE(binary) -> bool: | ||||
"""PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)""" | """PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)""" | ||||
binary = lief.parse(executable) | |||||
return ( | return ( | ||||
lief.PE.DLL_CHARACTERISTICS.DYNAMIC_BASE | lief.PE.DLL_CHARACTERISTICS.DYNAMIC_BASE | ||||
in binary.optional_header.dll_characteristics_lists | 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(binary) -> bool: | ||||
"""PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR""" | """PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR""" | ||||
binary = lief.parse(executable) | |||||
return ( | return ( | ||||
lief.PE.DLL_CHARACTERISTICS.HIGH_ENTROPY_VA | lief.PE.DLL_CHARACTERISTICS.HIGH_ENTROPY_VA | ||||
in binary.optional_header.dll_characteristics_lists | in binary.optional_header.dll_characteristics_lists | ||||
) | ) | ||||
def check_PE_RELOC_SECTION(executable) -> bool: | def check_PE_RELOC_SECTION(binary) -> bool: | ||||
"""Check for a reloc section. This is required for functional ASLR.""" | """Check for a reloc section. This is required for functional ASLR.""" | ||||
binary = lief.parse(executable) | |||||
return binary.has_relocations | return binary.has_relocations | ||||
def check_MACHO_NOUNDEFS(executable) -> bool: | def check_MACHO_NOUNDEFS(binary) -> bool: | ||||
""" | """ | ||||
Check for no undefined references. | Check for no undefined references. | ||||
""" | """ | ||||
binary = lief.parse(executable) | |||||
return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS) | return binary.header.has(lief.MachO.HEADER_FLAGS.NOUNDEFS) | ||||
def check_MACHO_Canary(executable) -> bool: | def check_MACHO_Canary(binary) -> bool: | ||||
""" | """ | ||||
Check for use of stack canary | Check for use of stack canary | ||||
""" | """ | ||||
binary = lief.parse(executable) | |||||
return binary.has_symbol("___stack_chk_fail") | return binary.has_symbol("___stack_chk_fail") | ||||
def check_PIE(executable) -> bool: | def check_PIE(binary) -> bool: | ||||
""" | """ | ||||
Check for position independent executable (PIE), | Check for position independent executable (PIE), | ||||
allowing for address space randomization. | allowing for address space randomization. | ||||
""" | """ | ||||
binary = lief.parse(executable) | |||||
return binary.is_pie | return binary.is_pie | ||||
def check_NX(executable) -> bool: | def check_NX(binary) -> bool: | ||||
""" | """ | ||||
Check for no stack execution | Check for no stack execution | ||||
""" | """ | ||||
binary = lief.parse(executable) | |||||
return binary.has_nx | return binary.has_nx | ||||
CHECKS = { | CHECKS = { | ||||
"ELF": [ | "ELF": [ | ||||
("PIE", check_ELF_PIE), | ("PIE", check_PIE), | ||||
("NX", check_ELF_NX), | ("NX", check_NX), | ||||
("RELRO", check_ELF_RELRO), | ("RELRO", check_ELF_RELRO), | ||||
("Canary", check_ELF_Canary), | ("Canary", check_ELF_Canary), | ||||
("separate_code", check_ELF_separate_code), | ("separate_code", check_ELF_separate_code), | ||||
], | ], | ||||
"PE": [ | "PE": [ | ||||
("PIE", check_PIE), | ("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_NX), | ("NX", check_NX), | ||||
("RELOC_SECTION", check_PE_RELOC_SECTION), | ("RELOC_SECTION", check_PE_RELOC_SECTION), | ||||
], | ], | ||||
"MACHO": [ | "MACHO": [ | ||||
("PIE", check_PIE), | ("PIE", check_PIE), | ||||
("NOUNDEFS", check_MACHO_NOUNDEFS), | ("NOUNDEFS", check_MACHO_NOUNDEFS), | ||||
("NX", check_NX), | ("NX", check_NX), | ||||
("Canary", check_MACHO_Canary), | ("Canary", check_MACHO_Canary), | ||||
], | ], | ||||
} | } | ||||
def identify_executable(executable) -> Optional[str]: | |||||
with open(executable, "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__": | if __name__ == "__main__": | ||||
retval: int = 0 | retval: int = 0 | ||||
for filename in sys.argv[1:]: | for filename in sys.argv[1:]: | ||||
try: | try: | ||||
etype = identify_executable(filename) | binary = lief.parse(filename) | ||||
if etype is None: | etype = binary.format.name | ||||
print(f"{filename}: unknown format") | if etype == lief.EXE_FORMATS.UNKNOWN: | ||||
print(f"{filename}: unknown executable format") | |||||
retval = 1 | retval = 1 | ||||
continue | continue | ||||
failed: List[str] = [] | failed: List[str] = [] | ||||
for name, func in CHECKS[etype]: | for name, func in CHECKS[etype]: | ||||
if not func(filename): | if not func(binary): | ||||
failed.append(name) | failed.append(name) | ||||
if failed: | if failed: | ||||
print(f"{filename}: failed {' '.join(failed)}") | print(f"{filename}: failed {' '.join(failed)}") | ||||
retval = 1 | retval = 1 | ||||
except IOError: | except IOError: | ||||
print(f"{filename}: cannot open") | print(f"{filename}: cannot open") | ||||
retval = 1 | retval = 1 | ||||
sys.exit(retval) | sys.exit(retval) |