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