security-check.py raw

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