security-check.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2015-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  '''
   6  Perform basic security checks on a series of executables.
   7  Exit status will be 0 if successful, and the program will be silent.
   8  Otherwise the exit status will be 1 and it will log which executables failed which checks.
   9  
  10  Example usage:
  11  
  12      find ../path/to/guix/binaries -type f -executable | xargs python3 contrib/guix/security-check.py
  13  '''
  14  import re
  15  import sys
  16  
  17  import lief
  18  
  19  def check_ELF_RELRO(binary) -> bool:
  20      '''
  21      Check for read-only relocations.
  22      GNU_RELRO program header must exist
  23      Dynamic section must have BIND_NOW flag
  24      '''
  25      have_gnu_relro = False
  26      for segment in binary.segments:
  27          # Note: not checking p_flags == PF_R: here as linkers set the permission differently
  28          # This does not affect security: the permission flags of the GNU_RELRO program
  29          # header are ignored, the PT_LOAD header determines the effective permissions.
  30          # However, the dynamic linker need to write to this area so these are RW.
  31          # Glibc itself takes care of mprotecting this area R after relocations are finished.
  32          # See also https://marc.info/?l=binutils&m=1498883354122353
  33          if segment.type == lief.ELF.Segment.TYPE.GNU_RELRO:
  34              have_gnu_relro = True
  35  
  36      have_bindnow = False
  37      try:
  38          flags = binary.get(lief.ELF.DynamicEntry.TAG.FLAGS)
  39          if flags.has(lief.ELF.DynamicEntryFlags.FLAG.BIND_NOW):
  40              have_bindnow = True
  41      except Exception:
  42          have_bindnow = False
  43  
  44      return have_gnu_relro and have_bindnow
  45  
  46  def check_ELF_CANARY(binary) -> bool:
  47      '''
  48      Check for use of stack canary
  49      '''
  50      return binary.has_symbol('__stack_chk_fail')
  51  
  52  def check_ELF_SEPARATE_CODE(binary):
  53      '''
  54      Check that sections are appropriately separated in virtual memory,
  55      based on their permissions. This checks for missing -Wl,-z,separate-code
  56      and potentially other problems.
  57      '''
  58      R = lief.ELF.Segment.FLAGS.R
  59      W = lief.ELF.Segment.FLAGS.W
  60      E = lief.ELF.Segment.FLAGS.X
  61      EXPECTED_FLAGS = {
  62          # Read + execute
  63          '.init': R | E,
  64          '.plt': R | E,
  65          '.plt.got': R | E,
  66          '.plt.sec': R | E,
  67          '.text': R | E,
  68          '.fini': R | E,
  69          # Read-only data
  70          '.interp': R,
  71          '.note.gnu.property': R,
  72          '.note.gnu.build-id': R,
  73          '.note.ABI-tag': R,
  74          '.gnu.hash': R,
  75          '.dynsym': R,
  76          '.dynstr': R,
  77          '.gnu.version': R,
  78          '.gnu.version_r': R,
  79          '.rela.dyn': R,
  80          '.rela.plt': R,
  81          '.rodata': R,
  82          '.eh_frame_hdr': R,
  83          '.eh_frame': R,
  84          '.qtmetadata': R,
  85          '.gcc_except_table': R,
  86          '.stapsdt.base': R,
  87          # Writable data
  88          '.init_array': R | W,
  89          '.fini_array': R | W,
  90          '.dynamic': R | W,
  91          '.got': R | W,
  92          '.data': R | W,
  93          '.bss': R | W,
  94      }
  95      if binary.header.machine_type == lief.ELF.ARCH.PPC64:
  96          # .plt is RW on ppc64 even with separate-code
  97          EXPECTED_FLAGS['.plt'] = R | W
  98      # For all LOAD program headers get mapping to the list of sections,
  99      # and for each section, remember the flags of the associated program header.
 100      flags_per_section = {}
 101      for segment in binary.segments:
 102          if segment.type == lief.ELF.Segment.TYPE.LOAD:
 103              for section in segment.sections:
 104                  flags_per_section[section.name] = segment.flags
 105      # Spot-check ELF LOAD program header flags per section
 106      # If these sections exist, check them against the expected R/W/E flags
 107      for (section, flags) in flags_per_section.items():
 108          if section in EXPECTED_FLAGS:
 109              if int(EXPECTED_FLAGS[section]) != int(flags):
 110                  return False
 111      return True
 112  
 113  def check_ELF_CONTROL_FLOW(binary) -> bool:
 114      '''
 115      Check for control flow instrumentation
 116      '''
 117      main = binary.get_function_address('main')
 118      content = binary.get_content_from_virtual_address(main, 4, lief.Binary.VA_TYPES.AUTO)
 119  
 120      if content.tolist() == [243, 15, 30, 250]: # endbr64
 121          return True
 122      return False
 123  
 124  def check_ELF_FORTIFY(binary) -> bool:
 125  
 126      # bitcoin wrapper does not currently contain any fortified functions
 127      if '--monolithic' in binary.strings:
 128          return True
 129  
 130      chk_funcs = set()
 131  
 132      for sym in binary.imported_symbols:
 133          match = re.search(r'__[a-z]*_chk', sym.name)
 134          if match:
 135              chk_funcs.add(match.group(0))
 136  
 137      # ignore stack-protector
 138      chk_funcs.discard('__stack_chk')
 139  
 140      return len(chk_funcs) >= 1
 141  
 142  def check_PE_DYNAMIC_BASE(binary) -> bool:
 143      '''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)'''
 144      return lief.PE.OptionalHeader.DLL_CHARACTERISTICS.DYNAMIC_BASE in binary.optional_header.dll_characteristics_lists
 145  
 146  # Must support high-entropy 64-bit address space layout randomization
 147  # in addition to DYNAMIC_BASE to have secure ASLR.
 148  def check_PE_HIGH_ENTROPY_VA(binary) -> bool:
 149      '''PIE: DllCharacteristics bit 0x20 signifies high-entropy ASLR'''
 150      return lief.PE.OptionalHeader.DLL_CHARACTERISTICS.HIGH_ENTROPY_VA in binary.optional_header.dll_characteristics_lists
 151  
 152  def check_PE_RELOC_SECTION(binary) -> bool:
 153      '''Check for a reloc section. This is required for functional ASLR.'''
 154      return binary.has_relocations
 155  
 156  def check_PE_CONTROL_FLOW(binary) -> bool:
 157      '''
 158      Check for control flow instrumentation
 159      '''
 160      main = binary.get_symbol('main').value
 161  
 162      section_addr = binary.section_from_rva(main).virtual_address
 163      virtual_address = binary.optional_header.imagebase + section_addr + main
 164  
 165      content = binary.get_content_from_virtual_address(virtual_address, 4, lief.Binary.VA_TYPES.VA)
 166  
 167      if content.tolist() == [243, 15, 30, 250]: # endbr64
 168          return True
 169      return False
 170  
 171  def check_PE_CANARY(binary) -> bool:
 172      '''
 173      Check for use of stack canary
 174      '''
 175      return binary.has_symbol('__stack_chk_fail')
 176  
 177  def check_MACHO_NOUNDEFS(binary) -> bool:
 178      '''
 179      Check for no undefined references.
 180      '''
 181      return binary.header.has(lief.MachO.Header.FLAGS.NOUNDEFS)
 182  
 183  def check_MACHO_FIXUP_CHAINS(binary) -> bool:
 184      '''
 185      Check for use of chained fixups.
 186      '''
 187      return binary.has_dyld_chained_fixups
 188  
 189  def check_MACHO_CANARY(binary) -> bool:
 190      '''
 191      Check for use of stack canary
 192      '''
 193      return binary.has_symbol('___stack_chk_fail')
 194  
 195  def check_PIE(binary) -> bool:
 196      '''
 197      Check for position independent executable (PIE),
 198      allowing for address space randomization.
 199      '''
 200      return binary.is_pie
 201  
 202  def check_NX(binary) -> bool:
 203      '''
 204      Check for no stack execution
 205      '''
 206  
 207      # binary.has_nx checks are only for the stack, but MachO binaries might
 208      # have executable heaps.
 209      if binary.format == lief.Binary.FORMATS.MACHO:
 210          return binary.concrete.has_nx_stack and binary.concrete.has_nx_heap
 211      else:
 212          return binary.has_nx
 213  
 214  def check_MACHO_CONTROL_FLOW(binary) -> bool:
 215      '''
 216      Check for control flow instrumentation
 217      '''
 218      content = binary.get_content_from_virtual_address(binary.entrypoint, 4, lief.Binary.VA_TYPES.AUTO)
 219  
 220      if content.tolist() == [243, 15, 30, 250]: # endbr64
 221          return True
 222      return False
 223  
 224  def check_MACHO_BRANCH_PROTECTION(binary) -> bool:
 225      '''
 226      Check for branch protection instrumentation
 227      '''
 228      content = binary.get_content_from_virtual_address(binary.entrypoint, 4, lief.Binary.VA_TYPES.AUTO)
 229  
 230      if content.tolist() == [95, 36, 3, 213]: # bti
 231          return True
 232      return False
 233  
 234  BASE_ELF = [
 235      ('FORTIFY', check_ELF_FORTIFY),
 236      ('PIE', check_PIE),
 237      ('NX', check_NX),
 238      ('RELRO', check_ELF_RELRO),
 239      ('CANARY', check_ELF_CANARY),
 240      ('SEPARATE_CODE', check_ELF_SEPARATE_CODE),
 241  ]
 242  
 243  BASE_PE = [
 244      ('PIE', check_PIE),
 245      ('DYNAMIC_BASE', check_PE_DYNAMIC_BASE),
 246      ('HIGH_ENTROPY_VA', check_PE_HIGH_ENTROPY_VA),
 247      ('NX', check_NX),
 248      ('RELOC_SECTION', check_PE_RELOC_SECTION),
 249      ('CONTROL_FLOW', check_PE_CONTROL_FLOW),
 250      ('CANARY', check_PE_CANARY),
 251  ]
 252  
 253  BASE_MACHO = [
 254      ('NOUNDEFS', check_MACHO_NOUNDEFS),
 255      ('CANARY', check_MACHO_CANARY),
 256      ('FIXUP_CHAINS', check_MACHO_FIXUP_CHAINS),
 257  ]
 258  
 259  CHECKS = {
 260      lief.Binary.FORMATS.ELF: {
 261          lief.Header.ARCHITECTURES.X86_64: BASE_ELF + [('CONTROL_FLOW', check_ELF_CONTROL_FLOW)],
 262          lief.Header.ARCHITECTURES.ARM: BASE_ELF,
 263          lief.Header.ARCHITECTURES.ARM64: BASE_ELF,
 264          lief.Header.ARCHITECTURES.PPC64: BASE_ELF,
 265          lief.Header.ARCHITECTURES.RISCV: BASE_ELF,
 266      },
 267      lief.Binary.FORMATS.PE: {
 268          lief.Header.ARCHITECTURES.X86_64: BASE_PE,
 269      },
 270      lief.Binary.FORMATS.MACHO: {
 271          lief.Header.ARCHITECTURES.X86_64: BASE_MACHO + [('PIE', check_PIE),
 272                                                ('NX', check_NX),
 273                                                ('CONTROL_FLOW', check_MACHO_CONTROL_FLOW)],
 274          lief.Header.ARCHITECTURES.ARM64: BASE_MACHO + [('BRANCH_PROTECTION', check_MACHO_BRANCH_PROTECTION)],
 275      }
 276  }
 277  
 278  if __name__ == '__main__':
 279      retval: int = 0
 280      for filename in sys.argv[1:]:
 281          binary = lief.parse(filename)
 282  
 283          etype = binary.format
 284          arch = binary.abstract.header.architecture
 285  
 286          failed: list[str] = []
 287          for (name, func) in CHECKS[etype][arch]:
 288              if not func(binary):
 289                  failed.append(name)
 290          if failed:
 291              print(f'{filename}: failed {" ".join(failed)}')
 292              retval = 1
 293      sys.exit(retval)
 294