symbol-check.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2014 Wladimir J. van der Laan
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  '''
   6  A script to check that release executables only contain certain symbols
   7  and are only linked against allowed libraries.
   8  
   9  Example usage:
  10  
  11      find ../path/to/binaries -type f -executable | xargs python3 contrib/devtools/symbol-check.py
  12  '''
  13  import sys
  14  
  15  import lief
  16  
  17  # Debian 11 (Bullseye) EOL: 2026. https://wiki.debian.org/LTS
  18  #
  19  # - libgcc version 10.2.1 (https://packages.debian.org/bullseye/libgcc-s1)
  20  # - libc version 2.31 (https://packages.debian.org/source/bullseye/glibc)
  21  #
  22  # Ubuntu 20.04 (Focal) EOL: 2030. https://wiki.ubuntu.com/ReleaseTeam
  23  #
  24  # - libgcc version 10.5.0 (https://packages.ubuntu.com/focal/libgcc1)
  25  # - libc version 2.31 (https://packages.ubuntu.com/focal/libc6)
  26  #
  27  # CentOS Stream 9 EOL: 2027. https://www.centos.org/cl-vs-cs/#end-of-life
  28  #
  29  # - libgcc version 12.2.1 (https://mirror.stream.centos.org/9-stream/AppStream/x86_64/os/Packages/)
  30  # - libc version 2.34 (https://mirror.stream.centos.org/9-stream/AppStream/x86_64/os/Packages/)
  31  #
  32  # See https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html for more info.
  33  
  34  MAX_VERSIONS = {
  35  'GCC':       (4,3,0),
  36  'GLIBC': {
  37      lief.ELF.ARCH.x86_64: (2,31),
  38      lief.ELF.ARCH.ARM:    (2,31),
  39      lief.ELF.ARCH.AARCH64:(2,31),
  40      lief.ELF.ARCH.PPC64:  (2,31),
  41      lief.ELF.ARCH.RISCV:  (2,31),
  42  },
  43  'LIBATOMIC': (1,0),
  44  'V':         (0,5,0),  # xkb (limenka-qt only)
  45  }
  46  
  47  # Ignore symbols that are exported as part of every executable
  48  IGNORE_EXPORTS = {
  49  'environ', '_environ', '__environ', '_fini', '_init', 'stdin',
  50  'stdout', 'stderr',
  51  }
  52  
  53  # Expected linker-loader names can be found here:
  54  # https://sourceware.org/glibc/wiki/ABIList?action=recall&rev=16
  55  ELF_INTERPRETER_NAMES: dict[lief.ELF.ARCH, dict[lief.ENDIANNESS, str]] = {
  56      lief.ELF.ARCH.x86_64:  {
  57          lief.ENDIANNESS.LITTLE: "/lib64/ld-linux-x86-64.so.2",
  58      },
  59      lief.ELF.ARCH.ARM:     {
  60          lief.ENDIANNESS.LITTLE: "/lib/ld-linux-armhf.so.3",
  61      },
  62      lief.ELF.ARCH.AARCH64: {
  63          lief.ENDIANNESS.LITTLE: "/lib/ld-linux-aarch64.so.1",
  64      },
  65      lief.ELF.ARCH.PPC64:   {
  66          lief.ENDIANNESS.BIG: "/lib64/ld64.so.1",
  67          lief.ENDIANNESS.LITTLE: "/lib64/ld64.so.2",
  68      },
  69      lief.ELF.ARCH.RISCV:    {
  70          lief.ENDIANNESS.LITTLE: "/lib/ld-linux-riscv64-lp64d.so.1",
  71      },
  72  }
  73  
  74  ELF_ABIS: dict[lief.ELF.ARCH, dict[lief.ENDIANNESS, list[int]]] = {
  75      lief.ELF.ARCH.x86_64: {
  76          lief.ENDIANNESS.LITTLE: [3,2,0],
  77      },
  78      lief.ELF.ARCH.ARM: {
  79          lief.ENDIANNESS.LITTLE: [3,2,0],
  80      },
  81      lief.ELF.ARCH.AARCH64: {
  82          lief.ENDIANNESS.LITTLE: [3,7,0],
  83      },
  84      lief.ELF.ARCH.PPC64: {
  85          lief.ENDIANNESS.LITTLE: [3,10,0],
  86          lief.ENDIANNESS.BIG: [3,2,0],
  87      },
  88      lief.ELF.ARCH.RISCV: {
  89          lief.ENDIANNESS.LITTLE: [4,15,0],
  90      },
  91  }
  92  
  93  # Allowed NEEDED libraries
  94  ELF_ALLOWED_LIBRARIES = {
  95  # limenkad and limenka-qt
  96  'libgcc_s.so.1', # GCC base support
  97  'libc.so.6', # C library
  98  'libpthread.so.0', # threading
  99  'libm.so.6', # math library
 100  'libatomic.so.1',
 101  'ld-linux-x86-64.so.2', # 64-bit dynamic linker
 102  'ld-linux.so.2', # 32-bit dynamic linker
 103  'ld-linux-aarch64.so.1', # 64-bit ARM dynamic linker
 104  'ld-linux-armhf.so.3', # 32-bit ARM dynamic linker
 105  'ld64.so.1', # POWER64 ABIv1 dynamic linker
 106  'ld64.so.2', # POWER64 ABIv2 dynamic linker
 107  'ld-linux-riscv64-lp64d.so.1', # 64-bit RISC-V dynamic linker
 108  # limenka-qt only
 109  'libxcb.so.1', # part of X11
 110  'libxkbcommon.so.0', # keyboard keymapping
 111  'libxkbcommon-x11.so.0', # keyboard keymapping
 112  'libfontconfig.so.1', # font support
 113  'libfreetype.so.6', # font parsing
 114  'libdl.so.2', # programming interface to dynamic linker
 115  'libxcb-icccm.so.4',
 116  'libxcb-image.so.0',
 117  'libxcb-shm.so.0',
 118  'libxcb-keysyms.so.1',
 119  'libxcb-randr.so.0',
 120  'libxcb-render-util.so.0',
 121  'libxcb-render.so.0',
 122  'libxcb-shape.so.0',
 123  'libxcb-sync.so.1',
 124  'libxcb-xfixes.so.0',
 125  'libxcb-xinerama.so.0',
 126  'libxcb-xkb.so.1',
 127  }
 128  
 129  MACHO_ALLOWED_LIBRARIES = {
 130  # limenkad and limenka-qt
 131  'libc++.1.dylib', # C++ Standard Library
 132  'libSystem.B.dylib', # libc, libm, libpthread, libinfo
 133  # limenka-qt only
 134  'AppKit', # user interface
 135  'ApplicationServices', # common application tasks.
 136  'Carbon', # deprecated c back-compat API
 137  'ColorSync',
 138  'CoreFoundation', # low level func, data types
 139  'CoreGraphics', # 2D rendering
 140  'CoreServices', # operating system services
 141  'CoreText', # interface for laying out text and handling fonts.
 142  'CoreVideo', # video processing
 143  'Foundation', # base layer functionality for apps/frameworks
 144  'ImageIO', # read and write image file formats.
 145  'IOKit', # user-space access to hardware devices and drivers.
 146  'IOSurface', # cross process image/drawing buffers
 147  'libobjc.A.dylib', # Objective-C runtime library
 148  'Metal', # 3D graphics
 149  'Security', # access control and authentication
 150  'QuartzCore', # animation
 151  }
 152  
 153  PE_ALLOWED_LIBRARIES = {libname.lower() for libname in (
 154  'ADVAPI32.dll', # security & registry
 155  'IPHLPAPI.DLL', # IP helper API
 156  'KERNEL32.dll', # win32 base APIs
 157  'msvcrt.dll', # C standard library for MSVC
 158  'SHELL32.dll', # shell API
 159  'WS2_32.dll', # sockets
 160  # limenka-qt only
 161  'dwmapi.dll', # desktop window manager
 162  'GDI32.dll', # graphics device interface
 163  'IMM32.dll', # input method editor
 164  'NETAPI32.dll', # network management
 165  'ole32.dll', # component object model
 166  'OLEAUT32.dll', # OLE Automation API
 167  'SHLWAPI.dll', # light weight shell API
 168  'USER32.dll', # user interface
 169  'USERENV.dll', # user management
 170  'UxTheme.dll', # visual style
 171  'VERSION.dll', # version checking
 172  'WINMM.dll', # WinMM audio API
 173  'WTSAPI32.dll', # Remote Desktop
 174  )}
 175  
 176  def check_version(max_versions, version, arch) -> bool:
 177      (lib, _, ver) = version.rpartition('_')
 178      ver = tuple([int(x) for x in ver.split('.')])
 179      if not lib in max_versions:
 180          return False
 181      if isinstance(max_versions[lib], tuple):
 182          return ver <= max_versions[lib]
 183      else:
 184          return ver <= max_versions[lib][arch]
 185  
 186  def check_imported_symbols(binary) -> bool:
 187      ok: bool = True
 188  
 189      for symbol in binary.imported_symbols:
 190          if not symbol.imported:
 191              continue
 192  
 193          version = symbol.symbol_version if symbol.has_version else None
 194  
 195          if version:
 196              aux_version = version.symbol_version_auxiliary.name if version.has_auxiliary_version else None
 197              if aux_version and not check_version(MAX_VERSIONS, aux_version, binary.header.machine_type):
 198                  print(f'{filename}: symbol {symbol.name} from unsupported version {version}')
 199                  ok = False
 200      return ok
 201  
 202  def check_exported_symbols(binary) -> bool:
 203      ok: bool = True
 204  
 205      for symbol in binary.dynamic_symbols:
 206          if not symbol.exported:
 207              continue
 208          name = symbol.name
 209          if binary.header.machine_type == lief.ELF.ARCH.RISCV or name in IGNORE_EXPORTS:
 210              continue
 211          print(f'{binary.name}: export of symbol {name} not allowed!')
 212          ok = False
 213      return ok
 214  
 215  def check_RUNPATH(binary) -> bool:
 216      assert binary.get(lief.ELF.DYNAMIC_TAGS.RUNPATH) is None
 217      assert binary.get(lief.ELF.DYNAMIC_TAGS.RPATH) is None
 218      return True
 219  
 220  def check_ELF_libraries(binary) -> bool:
 221      ok: bool = True
 222      for library in binary.libraries:
 223          if library not in ELF_ALLOWED_LIBRARIES:
 224              print(f'{filename}: {library} is not in ALLOWED_LIBRARIES!')
 225              ok = False
 226      return ok
 227  
 228  def check_MACHO_libraries(binary) -> bool:
 229      ok: bool = True
 230      for dylib in binary.libraries:
 231          split = dylib.name.split('/')
 232          if split[-1] not in MACHO_ALLOWED_LIBRARIES:
 233              print(f'{split[-1]} is not in ALLOWED_LIBRARIES!')
 234              ok = False
 235      return ok
 236  
 237  def check_MACHO_min_os(binary) -> bool:
 238      if binary.build_version.minos == [13,0,0]:
 239          return True
 240      return False
 241  
 242  def check_MACHO_sdk(binary) -> bool:
 243      if binary.build_version.sdk == [14, 0, 0]:
 244          return True
 245      return False
 246  
 247  def check_MACHO_lld(binary) -> bool:
 248      if binary.build_version.tools[0].version == [18, 1, 8]:
 249          return True
 250      return False
 251  
 252  def check_PE_libraries(binary) -> bool:
 253      ok: bool = True
 254      for dylib in binary.libraries:
 255          if dylib.lower() not in PE_ALLOWED_LIBRARIES:
 256              print(f'{dylib} is not in ALLOWED_LIBRARIES!')
 257              ok = False
 258      return ok
 259  
 260  def check_PE_subsystem_version(binary) -> bool:
 261      major: int = binary.optional_header.major_subsystem_version
 262      minor: int = binary.optional_header.minor_subsystem_version
 263      if major == 6 and minor == 2:
 264          return True
 265      return False
 266  
 267  def check_ELF_interpreter(binary) -> bool:
 268      expected_interpreter = ELF_INTERPRETER_NAMES[binary.header.machine_type][binary.abstract.header.endianness]
 269  
 270      return binary.concrete.interpreter == expected_interpreter
 271  
 272  def check_ELF_ABI(binary) -> bool:
 273      expected_abi = ELF_ABIS[binary.header.machine_type][binary.abstract.header.endianness]
 274      note = binary.concrete.get(lief.ELF.NOTE_TYPES.ABI_TAG)
 275      assert note.details.abi == lief.ELF.NOTE_ABIS.LINUX
 276      return note.details.version == expected_abi
 277  
 278  CHECKS = {
 279  lief.EXE_FORMATS.ELF: [
 280      ('IMPORTED_SYMBOLS', check_imported_symbols),
 281      ('EXPORTED_SYMBOLS', check_exported_symbols),
 282      ('LIBRARY_DEPENDENCIES', check_ELF_libraries),
 283      ('INTERPRETER_NAME', check_ELF_interpreter),
 284      ('ABI', check_ELF_ABI),
 285      ('RUNPATH', check_RUNPATH),
 286  ],
 287  lief.EXE_FORMATS.MACHO: [
 288      ('DYNAMIC_LIBRARIES', check_MACHO_libraries),
 289      ('MIN_OS', check_MACHO_min_os),
 290      ('SDK', check_MACHO_sdk),
 291      ('LLD', check_MACHO_lld),
 292  ],
 293  lief.EXE_FORMATS.PE: [
 294      ('DYNAMIC_LIBRARIES', check_PE_libraries),
 295      ('SUBSYSTEM_VERSION', check_PE_subsystem_version),
 296  ]
 297  }
 298  
 299  if __name__ == '__main__':
 300      retval: int = 0
 301      for filename in sys.argv[1:]:
 302          binary = lief.parse(filename)
 303          etype = binary.format
 304  
 305          failed: list[str] = []
 306          for (name, func) in CHECKS[etype]:
 307              if not func(binary):
 308                  failed.append(name)
 309          if failed:
 310              print(f'{filename}: failed {" ".join(failed)}')
 311              retval = 1
 312      sys.exit(retval)
 313