pybinaryguard 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pybinaryguard/__init__.py +78 -0
- pybinaryguard/__main__.py +7 -0
- pybinaryguard/_compat/__init__.py +0 -0
- pybinaryguard/agent/__init__.py +63 -0
- pybinaryguard/agent/guard.py +232 -0
- pybinaryguard/agent/recommender.py +283 -0
- pybinaryguard/agent/schema.py +200 -0
- pybinaryguard/agent/simulator.py +430 -0
- pybinaryguard/agent/tool_interface.py +474 -0
- pybinaryguard/analyzers/__init__.py +209 -0
- pybinaryguard/analyzers/base.py +40 -0
- pybinaryguard/analyzers/dependency_analyzer.py +336 -0
- pybinaryguard/analyzers/elf_analyzer.py +754 -0
- pybinaryguard/analyzers/symbol_analyzer.py +280 -0
- pybinaryguard/analyzers/wheel_analyzer.py +308 -0
- pybinaryguard/cli/__init__.py +5 -0
- pybinaryguard/cli/commands.py +414 -0
- pybinaryguard/cli/formatters.py +720 -0
- pybinaryguard/cli/main.py +250 -0
- pybinaryguard/diagnostics/__init__.py +13 -0
- pybinaryguard/diagnostics/explainer.py +356 -0
- pybinaryguard/diagnostics/findings.py +146 -0
- pybinaryguard/diagnostics/suggestions.py +508 -0
- pybinaryguard/frameworks/__init__.py +20 -0
- pybinaryguard/frameworks/onnxruntime.py +214 -0
- pybinaryguard/frameworks/pytorch.py +223 -0
- pybinaryguard/frameworks/tensorflow.py +266 -0
- pybinaryguard/frameworks/tensorrt.py +189 -0
- pybinaryguard/models/__init__.py +19 -0
- pybinaryguard/models/enums.py +102 -0
- pybinaryguard/models/finding.py +109 -0
- pybinaryguard/models/package.py +121 -0
- pybinaryguard/models/system.py +118 -0
- pybinaryguard/plugins/__init__.py +19 -0
- pybinaryguard/plugins/contrib/__init__.py +7 -0
- pybinaryguard/plugins/contrib/gstreamer.py +109 -0
- pybinaryguard/plugins/contrib/jetson.py +385 -0
- pybinaryguard/plugins/contrib/opencv.py +306 -0
- pybinaryguard/plugins/contrib/tensorrt.py +426 -0
- pybinaryguard/plugins/hooks.py +273 -0
- pybinaryguard/plugins/loader.py +190 -0
- pybinaryguard/predictor/__init__.py +23 -0
- pybinaryguard/predictor/dependency_graph.py +226 -0
- pybinaryguard/predictor/linker_simulator.py +161 -0
- pybinaryguard/predictor/predictor.py +144 -0
- pybinaryguard/predictor/resolver.py +282 -0
- pybinaryguard/probes/__init__.py +73 -0
- pybinaryguard/probes/base.py +45 -0
- pybinaryguard/probes/board_probe.py +248 -0
- pybinaryguard/probes/cpu_probe.py +176 -0
- pybinaryguard/probes/glibc_probe.py +215 -0
- pybinaryguard/probes/gpu_probe.py +430 -0
- pybinaryguard/probes/library_probe.py +132 -0
- pybinaryguard/probes/os_probe.py +227 -0
- pybinaryguard/probes/python_probe.py +135 -0
- pybinaryguard/probes/toolchain_probe.py +89 -0
- pybinaryguard/probes/venv_probe.py +111 -0
- pybinaryguard/profiles/__init__.py +12 -0
- pybinaryguard/profiles/engine.py +343 -0
- pybinaryguard/rules/__init__.py +24 -0
- pybinaryguard/rules/base.py +57 -0
- pybinaryguard/rules/builtin/__init__.py +136 -0
- pybinaryguard/rules/builtin/arch_rules.py +86 -0
- pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
- pybinaryguard/rules/builtin/container_rules.py +170 -0
- pybinaryguard/rules/builtin/cpu_rules.py +204 -0
- pybinaryguard/rules/builtin/cuda_rules.py +760 -0
- pybinaryguard/rules/builtin/dependency_rules.py +278 -0
- pybinaryguard/rules/builtin/framework_rules.py +252 -0
- pybinaryguard/rules/builtin/glibc_rules.py +320 -0
- pybinaryguard/rules/builtin/numpy_rules.py +110 -0
- pybinaryguard/rules/builtin/predictive_rules.py +165 -0
- pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
- pybinaryguard/rules/builtin/source_build_rules.py +150 -0
- pybinaryguard/rules/builtin/venv_rules.py +159 -0
- pybinaryguard/rules/engine.py +123 -0
- pybinaryguard/scanner.py +904 -0
- pybinaryguard/scoring/__init__.py +19 -0
- pybinaryguard/scoring/engine.py +416 -0
- pybinaryguard/snapshot/__init__.py +20 -0
- pybinaryguard/snapshot/generator.py +168 -0
- pybinaryguard/snapshot/lockfile.py +152 -0
- pybinaryguard/snapshot/verifier.py +315 -0
- pybinaryguard/validators/__init__.py +7 -0
- pybinaryguard/validators/import_validator.py +231 -0
- pybinaryguard-1.0.0.dist-info/METADATA +876 -0
- pybinaryguard-1.0.0.dist-info/RECORD +91 -0
- pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
- pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
- pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
- pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""ELF binary analyzer with a minimal pure-Python ELF parser.
|
|
2
|
+
|
|
3
|
+
This module provides ``MinimalELFParser`` -- a zero-dependency ELF reader
|
|
4
|
+
that extracts only the metadata PyBinaryGuard needs -- and ``ELFAnalyzer``,
|
|
5
|
+
which walks a package's installed files and populates
|
|
6
|
+
``PackageBinaryInfo.shared_objects``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import struct
|
|
14
|
+
from typing import BinaryIO, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from pybinaryguard.analyzers.base import AnalyzerBase
|
|
17
|
+
from pybinaryguard.models.enums import Architecture
|
|
18
|
+
from pybinaryguard.models.package import PackageBinaryInfo, SharedObjectInfo
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# ELF constants
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_ELF_MAGIC = b"\x7fELF"
|
|
27
|
+
|
|
28
|
+
# EI_CLASS
|
|
29
|
+
_ELFCLASS32 = 1
|
|
30
|
+
_ELFCLASS64 = 2
|
|
31
|
+
|
|
32
|
+
# EI_DATA
|
|
33
|
+
_ELFDATA2LSB = 1 # little-endian
|
|
34
|
+
_ELFDATA2MSB = 2 # big-endian
|
|
35
|
+
|
|
36
|
+
# Section header types
|
|
37
|
+
_SHT_NOTE = 7
|
|
38
|
+
_SHT_DYNAMIC = 6
|
|
39
|
+
_SHT_DYNSYM = 11
|
|
40
|
+
_SHT_STRTAB = 3
|
|
41
|
+
_SHT_GNU_VERNEED = 0x6FFFFFFE
|
|
42
|
+
|
|
43
|
+
# Dynamic tag types
|
|
44
|
+
_DT_NULL = 0
|
|
45
|
+
_DT_NEEDED = 1
|
|
46
|
+
_DT_STRTAB = 5
|
|
47
|
+
_DT_STRSZ = 10
|
|
48
|
+
_DT_SONAME = 14
|
|
49
|
+
_DT_RPATH = 15
|
|
50
|
+
_DT_RUNPATH = 29
|
|
51
|
+
|
|
52
|
+
# Note types
|
|
53
|
+
_NT_GNU_BUILD_ID = 3
|
|
54
|
+
|
|
55
|
+
# e_machine -> Architecture mapping
|
|
56
|
+
_EM_TO_ARCH: Dict[int, Architecture] = {
|
|
57
|
+
3: Architecture.I686, # EM_386
|
|
58
|
+
21: Architecture.PPC64LE, # EM_PPC64
|
|
59
|
+
22: Architecture.S390X, # EM_S390
|
|
60
|
+
40: Architecture.ARMV7L, # EM_ARM
|
|
61
|
+
62: Architecture.X86_64, # EM_X86_64
|
|
62
|
+
183: Architecture.AARCH64, # EM_AARCH64
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Program header types
|
|
66
|
+
_PT_DYNAMIC = 2
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ELFParseError(Exception):
|
|
70
|
+
"""Raised when a file is not a valid ELF binary or is truncated."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MinimalELFParser:
|
|
74
|
+
"""Minimal pure-Python ELF parser.
|
|
75
|
+
|
|
76
|
+
Extracts only the subset of ELF metadata needed by PyBinaryGuard:
|
|
77
|
+
|
|
78
|
+
* ELF class (32/64), endianness, ``e_machine``
|
|
79
|
+
* ``DT_NEEDED``, ``DT_SONAME``, ``DT_RPATH``, ``DT_RUNPATH``
|
|
80
|
+
* GNU version requirements (``SHT_GNU_VERNEED``)
|
|
81
|
+
* GNU build-id (``NT_GNU_BUILD_ID``)
|
|
82
|
+
|
|
83
|
+
The parser reads *only* the sections it needs -- it never loads the
|
|
84
|
+
entire file into memory.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
path:
|
|
89
|
+
Absolute path to the ELF file.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, path: str) -> None:
|
|
93
|
+
self._path = path
|
|
94
|
+
self._ei_class: int = 0 # 1=32, 2=64
|
|
95
|
+
self._ei_data: int = 0 # 1=LE, 2=BE
|
|
96
|
+
self._e_machine: int = 0
|
|
97
|
+
self._e_type: int = 0
|
|
98
|
+
|
|
99
|
+
# Section headers (offset, size, type, name_index, sh_link, sh_info, sh_entsize)
|
|
100
|
+
self._sections: List[Dict[str, int]] = []
|
|
101
|
+
self._shstrtab_offset: int = 0
|
|
102
|
+
self._shstrtab_size: int = 0
|
|
103
|
+
|
|
104
|
+
# Program headers
|
|
105
|
+
self._phdrs: List[Dict[str, int]] = []
|
|
106
|
+
|
|
107
|
+
# Extracted dynamic entries
|
|
108
|
+
self._dt_needed_offsets: List[int] = []
|
|
109
|
+
self._dt_soname_offset: Optional[int] = None
|
|
110
|
+
self._dt_rpath_offset: Optional[int] = None
|
|
111
|
+
self._dt_runpath_offset: Optional[int] = None
|
|
112
|
+
self._dynstr_offset: int = 0
|
|
113
|
+
self._dynstr_size: int = 0
|
|
114
|
+
|
|
115
|
+
# Resolved strings
|
|
116
|
+
self._needed: List[str] = []
|
|
117
|
+
self._soname: Optional[str] = None
|
|
118
|
+
self._rpath: Optional[str] = None
|
|
119
|
+
self._runpath: Optional[str] = None
|
|
120
|
+
|
|
121
|
+
# Version requirements: list of (library, version_string)
|
|
122
|
+
self._version_requirements: List[Tuple[str, str]] = []
|
|
123
|
+
|
|
124
|
+
# Build-id hex string
|
|
125
|
+
self._build_id: Optional[str] = None
|
|
126
|
+
|
|
127
|
+
self._parsed = False
|
|
128
|
+
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
# Struct helpers
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def _endian_prefix(self) -> str:
|
|
134
|
+
"""Return struct byte-order prefix for the ELF endianness."""
|
|
135
|
+
return "<" if self._ei_data == _ELFDATA2LSB else ">"
|
|
136
|
+
|
|
137
|
+
def _ptr_fmt(self) -> str:
|
|
138
|
+
"""Return the struct format character for an address/offset."""
|
|
139
|
+
return "Q" if self._ei_class == _ELFCLASS64 else "I"
|
|
140
|
+
|
|
141
|
+
def _ptr_size(self) -> int:
|
|
142
|
+
return 8 if self._ei_class == _ELFCLASS64 else 4
|
|
143
|
+
|
|
144
|
+
def _read_at(self, f: BinaryIO, offset: int, size: int) -> bytes:
|
|
145
|
+
"""Seek to *offset* and read exactly *size* bytes."""
|
|
146
|
+
f.seek(offset)
|
|
147
|
+
data = f.read(size)
|
|
148
|
+
if len(data) < size:
|
|
149
|
+
raise ELFParseError(
|
|
150
|
+
f"Truncated read at offset {offset}: expected {size} bytes, got {len(data)}"
|
|
151
|
+
)
|
|
152
|
+
return data
|
|
153
|
+
|
|
154
|
+
def _read_string(self, f: BinaryIO, strtab_offset: int, str_offset: int) -> str:
|
|
155
|
+
"""Read a null-terminated string from a string table."""
|
|
156
|
+
f.seek(strtab_offset + str_offset)
|
|
157
|
+
chunks: List[bytes] = []
|
|
158
|
+
while True:
|
|
159
|
+
byte = f.read(1)
|
|
160
|
+
if not byte or byte == b"\x00":
|
|
161
|
+
break
|
|
162
|
+
chunks.append(byte)
|
|
163
|
+
return b"".join(chunks).decode("ascii", errors="replace")
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# Header parsing
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def _parse_ident(self, f: BinaryIO) -> None:
|
|
170
|
+
"""Parse the 16-byte ELF identification."""
|
|
171
|
+
ident = self._read_at(f, 0, 16)
|
|
172
|
+
if ident[:4] != _ELF_MAGIC:
|
|
173
|
+
raise ELFParseError(f"Not an ELF file: {self._path}")
|
|
174
|
+
|
|
175
|
+
self._ei_class = ident[4]
|
|
176
|
+
if self._ei_class not in (_ELFCLASS32, _ELFCLASS64):
|
|
177
|
+
raise ELFParseError(f"Unknown ELF class: {self._ei_class}")
|
|
178
|
+
|
|
179
|
+
self._ei_data = ident[5]
|
|
180
|
+
if self._ei_data not in (_ELFDATA2LSB, _ELFDATA2MSB):
|
|
181
|
+
raise ELFParseError(f"Unknown ELF data encoding: {self._ei_data}")
|
|
182
|
+
|
|
183
|
+
def _parse_header(self, f: BinaryIO) -> None:
|
|
184
|
+
"""Parse the ELF header (after ident)."""
|
|
185
|
+
ep = self._endian_prefix()
|
|
186
|
+
|
|
187
|
+
# e_type (2 bytes at offset 16), e_machine (2 bytes at offset 18)
|
|
188
|
+
hdr_common = self._read_at(f, 16, 4)
|
|
189
|
+
self._e_type, self._e_machine = struct.unpack(ep + "HH", hdr_common)
|
|
190
|
+
|
|
191
|
+
if self._ei_class == _ELFCLASS64:
|
|
192
|
+
# 64-bit header layout (after e_ident[16]):
|
|
193
|
+
# e_type(2) e_machine(2) e_version(4) e_entry(8) e_phoff(8)
|
|
194
|
+
# e_shoff(8) e_flags(4) e_ehsize(2) e_phentsize(2) e_phnum(2)
|
|
195
|
+
# e_shentsize(2) e_shnum(2) e_shstrndx(2)
|
|
196
|
+
data = self._read_at(f, 16, 48) # offsets 16..63
|
|
197
|
+
(
|
|
198
|
+
_e_type, _e_machine, _e_version,
|
|
199
|
+
_e_entry, e_phoff, e_shoff,
|
|
200
|
+
_e_flags, _e_ehsize,
|
|
201
|
+
e_phentsize, e_phnum,
|
|
202
|
+
e_shentsize, e_shnum, e_shstrndx,
|
|
203
|
+
) = struct.unpack(ep + "HHIQQQIHHHHHH", data)
|
|
204
|
+
else:
|
|
205
|
+
# 32-bit header layout (after e_ident[16]):
|
|
206
|
+
# e_type(2) e_machine(2) e_version(4) e_entry(4) e_phoff(4)
|
|
207
|
+
# e_shoff(4) e_flags(4) e_ehsize(2) e_phentsize(2) e_phnum(2)
|
|
208
|
+
# e_shentsize(2) e_shnum(2) e_shstrndx(2)
|
|
209
|
+
data = self._read_at(f, 16, 36) # offsets 16..51
|
|
210
|
+
(
|
|
211
|
+
_e_type, _e_machine, _e_version,
|
|
212
|
+
_e_entry, e_phoff, e_shoff,
|
|
213
|
+
_e_flags, _e_ehsize,
|
|
214
|
+
e_phentsize, e_phnum,
|
|
215
|
+
e_shentsize, e_shnum, e_shstrndx,
|
|
216
|
+
) = struct.unpack(ep + "HHIIIIIHHHHHH", data)
|
|
217
|
+
|
|
218
|
+
self._e_shoff = e_shoff
|
|
219
|
+
self._e_shentsize = e_shentsize
|
|
220
|
+
self._e_shnum = e_shnum
|
|
221
|
+
self._e_shstrndx = e_shstrndx
|
|
222
|
+
self._e_phoff = e_phoff
|
|
223
|
+
self._e_phentsize = e_phentsize
|
|
224
|
+
self._e_phnum = e_phnum
|
|
225
|
+
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
# Program headers
|
|
228
|
+
# ------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
def _parse_program_headers(self, f: BinaryIO) -> None:
|
|
231
|
+
"""Parse all program headers to locate the PT_DYNAMIC segment."""
|
|
232
|
+
ep = self._endian_prefix()
|
|
233
|
+
for i in range(self._e_phnum):
|
|
234
|
+
offset = self._e_phoff + i * self._e_phentsize
|
|
235
|
+
if self._ei_class == _ELFCLASS64:
|
|
236
|
+
# Elf64_Phdr: p_type(4) p_flags(4) p_offset(8) p_vaddr(8)
|
|
237
|
+
# p_paddr(8) p_filesz(8) p_memsz(8) p_align(8)
|
|
238
|
+
data = self._read_at(f, offset, 56)
|
|
239
|
+
(
|
|
240
|
+
p_type, p_flags, p_offset, p_vaddr,
|
|
241
|
+
p_paddr, p_filesz, p_memsz, p_align,
|
|
242
|
+
) = struct.unpack(ep + "IIQQQQQQ", data)
|
|
243
|
+
else:
|
|
244
|
+
# Elf32_Phdr: p_type(4) p_offset(4) p_vaddr(4) p_paddr(4)
|
|
245
|
+
# p_filesz(4) p_memsz(4) p_flags(4) p_align(4)
|
|
246
|
+
data = self._read_at(f, offset, 32)
|
|
247
|
+
(
|
|
248
|
+
p_type, p_offset, p_vaddr, p_paddr,
|
|
249
|
+
p_filesz, p_memsz, p_flags, p_align,
|
|
250
|
+
) = struct.unpack(ep + "IIIIIIII", data)
|
|
251
|
+
self._phdrs.append({
|
|
252
|
+
"p_type": p_type,
|
|
253
|
+
"p_offset": p_offset,
|
|
254
|
+
"p_vaddr": p_vaddr,
|
|
255
|
+
"p_filesz": p_filesz,
|
|
256
|
+
"p_memsz": p_memsz,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
# Section headers
|
|
261
|
+
# ------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def _parse_section_headers(self, f: BinaryIO) -> None:
|
|
264
|
+
"""Parse all section headers."""
|
|
265
|
+
ep = self._endian_prefix()
|
|
266
|
+
|
|
267
|
+
for i in range(self._e_shnum):
|
|
268
|
+
offset = self._e_shoff + i * self._e_shentsize
|
|
269
|
+
|
|
270
|
+
if self._ei_class == _ELFCLASS64:
|
|
271
|
+
# Elf64_Shdr: sh_name(4) sh_type(4) sh_flags(8) sh_addr(8)
|
|
272
|
+
# sh_offset(8) sh_size(8) sh_link(4) sh_info(4)
|
|
273
|
+
# sh_addralign(8) sh_entsize(8)
|
|
274
|
+
data = self._read_at(f, offset, 64)
|
|
275
|
+
(
|
|
276
|
+
sh_name, sh_type, sh_flags, sh_addr,
|
|
277
|
+
sh_offset, sh_size, sh_link, sh_info,
|
|
278
|
+
sh_addralign, sh_entsize,
|
|
279
|
+
) = struct.unpack(ep + "IIQQQQIIqq", data)
|
|
280
|
+
else:
|
|
281
|
+
# Elf32_Shdr: sh_name(4) sh_type(4) sh_flags(4) sh_addr(4)
|
|
282
|
+
# sh_offset(4) sh_size(4) sh_link(4) sh_info(4)
|
|
283
|
+
# sh_addralign(4) sh_entsize(4)
|
|
284
|
+
data = self._read_at(f, offset, 40)
|
|
285
|
+
(
|
|
286
|
+
sh_name, sh_type, sh_flags, sh_addr,
|
|
287
|
+
sh_offset, sh_size, sh_link, sh_info,
|
|
288
|
+
sh_addralign, sh_entsize,
|
|
289
|
+
) = struct.unpack(ep + "IIIIIIIIII", data)
|
|
290
|
+
|
|
291
|
+
self._sections.append({
|
|
292
|
+
"sh_name": sh_name,
|
|
293
|
+
"sh_type": sh_type,
|
|
294
|
+
"sh_flags": sh_flags,
|
|
295
|
+
"sh_addr": sh_addr,
|
|
296
|
+
"sh_offset": sh_offset,
|
|
297
|
+
"sh_size": sh_size,
|
|
298
|
+
"sh_link": sh_link,
|
|
299
|
+
"sh_info": sh_info,
|
|
300
|
+
"sh_addralign": sh_addralign,
|
|
301
|
+
"sh_entsize": sh_entsize,
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
# Load the section header string table so we can resolve names
|
|
305
|
+
if 0 <= self._e_shstrndx < len(self._sections):
|
|
306
|
+
shstrtab = self._sections[self._e_shstrndx]
|
|
307
|
+
self._shstrtab_offset = shstrtab["sh_offset"]
|
|
308
|
+
self._shstrtab_size = shstrtab["sh_size"]
|
|
309
|
+
|
|
310
|
+
def _section_name(self, f: BinaryIO, name_offset: int) -> str:
|
|
311
|
+
"""Resolve a section name from the section header string table."""
|
|
312
|
+
if self._shstrtab_offset == 0:
|
|
313
|
+
return ""
|
|
314
|
+
return self._read_string(f, self._shstrtab_offset, name_offset)
|
|
315
|
+
|
|
316
|
+
# ------------------------------------------------------------------
|
|
317
|
+
# Dynamic section parsing
|
|
318
|
+
# ------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
def _parse_dynamic_section(self, f: BinaryIO) -> None:
|
|
321
|
+
"""Parse the SHT_DYNAMIC section to extract DT_NEEDED, etc.
|
|
322
|
+
|
|
323
|
+
We locate the dynamic string table (DT_STRTAB/DT_STRSZ) and then
|
|
324
|
+
find the .dynstr section that covers that address range.
|
|
325
|
+
"""
|
|
326
|
+
ep = self._endian_prefix()
|
|
327
|
+
ptr = self._ptr_fmt()
|
|
328
|
+
ps = self._ptr_size()
|
|
329
|
+
|
|
330
|
+
dyn_section = None
|
|
331
|
+
for sec in self._sections:
|
|
332
|
+
if sec["sh_type"] == _SHT_DYNAMIC:
|
|
333
|
+
dyn_section = sec
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
if dyn_section is None:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Each dynamic entry is two pointer-width values: d_tag, d_un
|
|
340
|
+
entry_size = ps * 2
|
|
341
|
+
fmt = ep + ptr + ptr
|
|
342
|
+
count = dyn_section["sh_size"] // entry_size
|
|
343
|
+
|
|
344
|
+
dyn_data = self._read_at(f, dyn_section["sh_offset"], dyn_section["sh_size"])
|
|
345
|
+
|
|
346
|
+
dt_strtab_vaddr: int = 0
|
|
347
|
+
dt_strsz: int = 0
|
|
348
|
+
|
|
349
|
+
for i in range(count):
|
|
350
|
+
d_tag, d_val = struct.unpack_from(fmt, dyn_data, i * entry_size)
|
|
351
|
+
if d_tag == _DT_NULL:
|
|
352
|
+
break
|
|
353
|
+
elif d_tag == _DT_NEEDED:
|
|
354
|
+
self._dt_needed_offsets.append(d_val)
|
|
355
|
+
elif d_tag == _DT_SONAME:
|
|
356
|
+
self._dt_soname_offset = d_val
|
|
357
|
+
elif d_tag == _DT_RPATH:
|
|
358
|
+
self._dt_rpath_offset = d_val
|
|
359
|
+
elif d_tag == _DT_RUNPATH:
|
|
360
|
+
self._dt_runpath_offset = d_val
|
|
361
|
+
elif d_tag == _DT_STRTAB:
|
|
362
|
+
dt_strtab_vaddr = d_val
|
|
363
|
+
elif d_tag == _DT_STRSZ:
|
|
364
|
+
dt_strsz = d_val
|
|
365
|
+
|
|
366
|
+
# Resolve DT_STRTAB virtual address to a file offset by finding the
|
|
367
|
+
# matching section. The dynamic string table section (usually
|
|
368
|
+
# .dynstr) has type SHT_STRTAB and its sh_addr matches DT_STRTAB.
|
|
369
|
+
# If we can't resolve via sections, try program headers.
|
|
370
|
+
self._dynstr_offset = 0
|
|
371
|
+
self._dynstr_size = dt_strsz
|
|
372
|
+
|
|
373
|
+
if dt_strtab_vaddr != 0:
|
|
374
|
+
# First try: match a STRTAB section by virtual address
|
|
375
|
+
for sec in self._sections:
|
|
376
|
+
if sec["sh_type"] == _SHT_STRTAB and sec["sh_addr"] == dt_strtab_vaddr:
|
|
377
|
+
self._dynstr_offset = sec["sh_offset"]
|
|
378
|
+
if dt_strsz == 0:
|
|
379
|
+
self._dynstr_size = sec["sh_size"]
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
# Fallback: use the linked section from the dynamic section header
|
|
383
|
+
if self._dynstr_offset == 0 and dyn_section["sh_link"] < len(self._sections):
|
|
384
|
+
linked = self._sections[dyn_section["sh_link"]]
|
|
385
|
+
self._dynstr_offset = linked["sh_offset"]
|
|
386
|
+
if self._dynstr_size == 0:
|
|
387
|
+
self._dynstr_size = linked["sh_size"]
|
|
388
|
+
|
|
389
|
+
# Last resort: compute offset from PT_LOAD segments
|
|
390
|
+
if self._dynstr_offset == 0:
|
|
391
|
+
self._dynstr_offset = self._vaddr_to_offset(dt_strtab_vaddr)
|
|
392
|
+
|
|
393
|
+
def _vaddr_to_offset(self, vaddr: int) -> int:
|
|
394
|
+
"""Convert a virtual address to a file offset using program headers.
|
|
395
|
+
|
|
396
|
+
Iterates over program headers to find a LOAD segment that contains
|
|
397
|
+
*vaddr* and computes the corresponding file offset. Returns 0 if
|
|
398
|
+
no matching segment is found.
|
|
399
|
+
"""
|
|
400
|
+
# PT_LOAD = 1
|
|
401
|
+
for phdr in self._phdrs:
|
|
402
|
+
if phdr["p_type"] != 1: # PT_LOAD
|
|
403
|
+
continue
|
|
404
|
+
seg_vaddr = phdr["p_vaddr"]
|
|
405
|
+
seg_memsz = phdr["p_memsz"]
|
|
406
|
+
if seg_vaddr <= vaddr < seg_vaddr + seg_memsz:
|
|
407
|
+
return phdr["p_offset"] + (vaddr - seg_vaddr)
|
|
408
|
+
return 0
|
|
409
|
+
|
|
410
|
+
def _resolve_dynamic_strings(self, f: BinaryIO) -> None:
|
|
411
|
+
"""Read actual string values from the dynstr table."""
|
|
412
|
+
if self._dynstr_offset == 0:
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
for off in self._dt_needed_offsets:
|
|
416
|
+
self._needed.append(self._read_string(f, self._dynstr_offset, off))
|
|
417
|
+
|
|
418
|
+
if self._dt_soname_offset is not None:
|
|
419
|
+
self._soname = self._read_string(f, self._dynstr_offset, self._dt_soname_offset)
|
|
420
|
+
|
|
421
|
+
if self._dt_rpath_offset is not None:
|
|
422
|
+
self._rpath = self._read_string(f, self._dynstr_offset, self._dt_rpath_offset)
|
|
423
|
+
|
|
424
|
+
if self._dt_runpath_offset is not None:
|
|
425
|
+
self._runpath = self._read_string(f, self._dynstr_offset, self._dt_runpath_offset)
|
|
426
|
+
|
|
427
|
+
# ------------------------------------------------------------------
|
|
428
|
+
# GNU version requirements (SHT_GNU_VERNEED)
|
|
429
|
+
# ------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
def _parse_verneed(self, f: BinaryIO) -> None:
|
|
432
|
+
"""Parse .gnu.version_r to extract GLIBC/GLIBCXX version strings.
|
|
433
|
+
|
|
434
|
+
The section contains a linked list of ``Elfxx_Verneed`` entries.
|
|
435
|
+
Each entry has a linked list of ``Elfxx_Vernaux`` sub-entries that
|
|
436
|
+
carry the actual version string indices.
|
|
437
|
+
|
|
438
|
+
Verneed layout (both 32- and 64-bit have same sizes):
|
|
439
|
+
vn_version (2) -- always 1
|
|
440
|
+
vn_cnt (2) -- number of Vernaux entries
|
|
441
|
+
vn_file (4) -- offset into linked string table for library name
|
|
442
|
+
vn_aux (4) -- byte offset to first Vernaux
|
|
443
|
+
vn_next (4) -- byte offset to next Verneed (0 = last)
|
|
444
|
+
|
|
445
|
+
Vernaux layout:
|
|
446
|
+
vna_hash (4)
|
|
447
|
+
vna_flags (2)
|
|
448
|
+
vna_other (2)
|
|
449
|
+
vna_name (4) -- offset into linked string table for version string
|
|
450
|
+
vna_next (4) -- byte offset to next Vernaux (0 = last)
|
|
451
|
+
"""
|
|
452
|
+
ep = self._endian_prefix()
|
|
453
|
+
|
|
454
|
+
for sec in self._sections:
|
|
455
|
+
if sec["sh_type"] != _SHT_GNU_VERNEED:
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
# The linked string table is indicated by sh_link
|
|
459
|
+
strtab_sec_idx = sec["sh_link"]
|
|
460
|
+
if strtab_sec_idx >= len(self._sections):
|
|
461
|
+
continue
|
|
462
|
+
strtab_offset = self._sections[strtab_sec_idx]["sh_offset"]
|
|
463
|
+
|
|
464
|
+
sec_data = self._read_at(f, sec["sh_offset"], sec["sh_size"])
|
|
465
|
+
pos = 0
|
|
466
|
+
|
|
467
|
+
while pos < len(sec_data):
|
|
468
|
+
if pos + 16 > len(sec_data):
|
|
469
|
+
break
|
|
470
|
+
|
|
471
|
+
vn_version, vn_cnt, vn_file, vn_aux, vn_next = struct.unpack_from(
|
|
472
|
+
ep + "HHIII", sec_data, pos
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
lib_name = self._read_string(f, strtab_offset, vn_file)
|
|
476
|
+
|
|
477
|
+
# Walk Vernaux entries
|
|
478
|
+
aux_pos = pos + vn_aux
|
|
479
|
+
for _ in range(vn_cnt):
|
|
480
|
+
if aux_pos + 16 > len(sec_data):
|
|
481
|
+
break
|
|
482
|
+
vna_hash, vna_flags, vna_other, vna_name, vna_next = struct.unpack_from(
|
|
483
|
+
ep + "IHHII", sec_data, aux_pos
|
|
484
|
+
)
|
|
485
|
+
ver_str = self._read_string(f, strtab_offset, vna_name)
|
|
486
|
+
self._version_requirements.append((lib_name, ver_str))
|
|
487
|
+
|
|
488
|
+
if vna_next == 0:
|
|
489
|
+
break
|
|
490
|
+
aux_pos += vna_next
|
|
491
|
+
|
|
492
|
+
if vn_next == 0:
|
|
493
|
+
break
|
|
494
|
+
pos += vn_next
|
|
495
|
+
|
|
496
|
+
# ------------------------------------------------------------------
|
|
497
|
+
# GNU build-id (SHT_NOTE with name "GNU\0" and type NT_GNU_BUILD_ID)
|
|
498
|
+
# ------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
def _parse_build_id(self, f: BinaryIO) -> None:
|
|
501
|
+
"""Parse .note.gnu.build-id section."""
|
|
502
|
+
ep = self._endian_prefix()
|
|
503
|
+
|
|
504
|
+
for sec in self._sections:
|
|
505
|
+
if sec["sh_type"] != _SHT_NOTE:
|
|
506
|
+
continue
|
|
507
|
+
|
|
508
|
+
# Check the section name to avoid parsing irrelevant notes
|
|
509
|
+
name = self._section_name(f, sec["sh_name"])
|
|
510
|
+
if name != ".note.gnu.build-id":
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
note_data = self._read_at(f, sec["sh_offset"], sec["sh_size"])
|
|
514
|
+
pos = 0
|
|
515
|
+
|
|
516
|
+
while pos + 12 <= len(note_data):
|
|
517
|
+
namesz, descsz, note_type = struct.unpack_from(ep + "III", note_data, pos)
|
|
518
|
+
pos += 12
|
|
519
|
+
|
|
520
|
+
# Align name and desc to 4 bytes
|
|
521
|
+
namesz_aligned = (namesz + 3) & ~3
|
|
522
|
+
descsz_aligned = (descsz + 3) & ~3
|
|
523
|
+
|
|
524
|
+
if pos + namesz_aligned + descsz_aligned > len(note_data):
|
|
525
|
+
break
|
|
526
|
+
|
|
527
|
+
note_name = note_data[pos:pos + namesz].rstrip(b"\x00")
|
|
528
|
+
pos += namesz_aligned
|
|
529
|
+
|
|
530
|
+
if note_name == b"GNU" and note_type == _NT_GNU_BUILD_ID:
|
|
531
|
+
desc = note_data[pos:pos + descsz]
|
|
532
|
+
self._build_id = desc.hex()
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
pos += descsz_aligned
|
|
536
|
+
|
|
537
|
+
# ------------------------------------------------------------------
|
|
538
|
+
# Public API
|
|
539
|
+
# ------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
def parse(self) -> Dict[str, object]:
|
|
542
|
+
"""Parse the ELF file and return extracted metadata.
|
|
543
|
+
|
|
544
|
+
Returns
|
|
545
|
+
-------
|
|
546
|
+
dict
|
|
547
|
+
Keys: ``ei_class``, ``endianness``, ``e_machine``,
|
|
548
|
+
``e_type``, ``needed``, ``soname``, ``rpath``, ``runpath``,
|
|
549
|
+
``version_requirements``, ``build_id``.
|
|
550
|
+
|
|
551
|
+
Raises
|
|
552
|
+
------
|
|
553
|
+
ELFParseError
|
|
554
|
+
If the file is not a valid ELF binary or is truncated.
|
|
555
|
+
OSError
|
|
556
|
+
If the file cannot be opened.
|
|
557
|
+
"""
|
|
558
|
+
with open(self._path, "rb") as f:
|
|
559
|
+
self._parse_ident(f)
|
|
560
|
+
self._parse_header(f)
|
|
561
|
+
self._parse_program_headers(f)
|
|
562
|
+
self._parse_section_headers(f)
|
|
563
|
+
self._parse_dynamic_section(f)
|
|
564
|
+
self._resolve_dynamic_strings(f)
|
|
565
|
+
self._parse_verneed(f)
|
|
566
|
+
self._parse_build_id(f)
|
|
567
|
+
|
|
568
|
+
self._parsed = True
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
"ei_class": 64 if self._ei_class == _ELFCLASS64 else 32,
|
|
572
|
+
"endianness": "little" if self._ei_data == _ELFDATA2LSB else "big",
|
|
573
|
+
"e_machine": self._e_machine,
|
|
574
|
+
"e_type": self._e_type,
|
|
575
|
+
"needed": list(self._needed),
|
|
576
|
+
"soname": self._soname,
|
|
577
|
+
"rpath": self._rpath,
|
|
578
|
+
"runpath": self._runpath,
|
|
579
|
+
"version_requirements": list(self._version_requirements),
|
|
580
|
+
"build_id": self._build_id,
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
def get_needed(self) -> List[str]:
|
|
584
|
+
"""Return the list of DT_NEEDED library names.
|
|
585
|
+
|
|
586
|
+
``parse()`` must be called first.
|
|
587
|
+
"""
|
|
588
|
+
return list(self._needed)
|
|
589
|
+
|
|
590
|
+
def get_version_requirements(self) -> List[Tuple[str, str]]:
|
|
591
|
+
"""Return GNU version requirements as ``(library, version)`` tuples.
|
|
592
|
+
|
|
593
|
+
``parse()`` must be called first.
|
|
594
|
+
"""
|
|
595
|
+
return list(self._version_requirements)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# ---------------------------------------------------------------------------
|
|
599
|
+
# ELF Analyzer
|
|
600
|
+
# ---------------------------------------------------------------------------
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class ELFAnalyzer(AnalyzerBase):
|
|
604
|
+
"""Analyzer that discovers and parses ELF shared objects in a package.
|
|
605
|
+
|
|
606
|
+
For each ``.so`` file found under the package's ``install_path``:
|
|
607
|
+
|
|
608
|
+
1. Parse the ELF header and dynamic section with ``MinimalELFParser``.
|
|
609
|
+
2. Populate a ``SharedObjectInfo`` instance.
|
|
610
|
+
3. Set package-level aggregates (``required_glibc``,
|
|
611
|
+
``target_architecture``, ``required_libraries``).
|
|
612
|
+
"""
|
|
613
|
+
|
|
614
|
+
name: str = "elf"
|
|
615
|
+
|
|
616
|
+
def analyze(self, package_info: PackageBinaryInfo) -> PackageBinaryInfo:
|
|
617
|
+
"""Discover and parse all ELF shared objects under *package_info.install_path*.
|
|
618
|
+
|
|
619
|
+
Parameters
|
|
620
|
+
----------
|
|
621
|
+
package_info:
|
|
622
|
+
The package descriptor to populate.
|
|
623
|
+
|
|
624
|
+
Returns
|
|
625
|
+
-------
|
|
626
|
+
PackageBinaryInfo
|
|
627
|
+
The same (mutated) instance.
|
|
628
|
+
"""
|
|
629
|
+
install_path = package_info.install_path
|
|
630
|
+
if not install_path or not os.path.isdir(install_path):
|
|
631
|
+
return package_info
|
|
632
|
+
|
|
633
|
+
so_paths = self._find_shared_objects(install_path)
|
|
634
|
+
if not so_paths:
|
|
635
|
+
return package_info
|
|
636
|
+
|
|
637
|
+
package_info.is_pure_python = False
|
|
638
|
+
all_needed: set = set() # type: ignore[type-arg]
|
|
639
|
+
max_glibc: Optional[Tuple[int, int]] = None
|
|
640
|
+
arch: Optional[Architecture] = None
|
|
641
|
+
|
|
642
|
+
for so_path in so_paths:
|
|
643
|
+
so_info = self._parse_so(so_path)
|
|
644
|
+
if so_info is None:
|
|
645
|
+
continue
|
|
646
|
+
|
|
647
|
+
package_info.shared_objects.append(so_info)
|
|
648
|
+
|
|
649
|
+
# Aggregate DT_NEEDED
|
|
650
|
+
for lib in so_info.dt_needed:
|
|
651
|
+
all_needed.add(lib)
|
|
652
|
+
|
|
653
|
+
# Track maximum GLIBC requirement
|
|
654
|
+
if so_info.required_glibc is not None:
|
|
655
|
+
if max_glibc is None or so_info.required_glibc > max_glibc:
|
|
656
|
+
max_glibc = so_info.required_glibc
|
|
657
|
+
|
|
658
|
+
# Architecture (use the first one found; they should all agree)
|
|
659
|
+
if arch is None and so_info.architecture != Architecture.UNKNOWN:
|
|
660
|
+
arch = so_info.architecture
|
|
661
|
+
|
|
662
|
+
package_info.required_libraries = all_needed
|
|
663
|
+
if max_glibc is not None:
|
|
664
|
+
package_info.required_glibc = max_glibc
|
|
665
|
+
if arch is not None:
|
|
666
|
+
package_info.target_architecture = arch
|
|
667
|
+
|
|
668
|
+
return package_info
|
|
669
|
+
|
|
670
|
+
@staticmethod
|
|
671
|
+
def _find_shared_objects(root: str) -> List[str]:
|
|
672
|
+
"""Walk *root* and collect paths to all ``.so`` files."""
|
|
673
|
+
result: List[str] = []
|
|
674
|
+
try:
|
|
675
|
+
for dirpath, _dirnames, filenames in os.walk(root):
|
|
676
|
+
for fname in filenames:
|
|
677
|
+
if fname.endswith(".so") or ".so." in fname:
|
|
678
|
+
result.append(os.path.join(dirpath, fname))
|
|
679
|
+
except OSError as exc:
|
|
680
|
+
logger.debug("Error walking %s: %s", root, exc)
|
|
681
|
+
return result
|
|
682
|
+
|
|
683
|
+
@staticmethod
|
|
684
|
+
def _parse_so(path: str) -> Optional[SharedObjectInfo]:
|
|
685
|
+
"""Parse a single .so file and return its ``SharedObjectInfo``.
|
|
686
|
+
|
|
687
|
+
Returns ``None`` if the file cannot be parsed (e.g. not ELF, or I/O
|
|
688
|
+
error).
|
|
689
|
+
"""
|
|
690
|
+
try:
|
|
691
|
+
parser = MinimalELFParser(path)
|
|
692
|
+
info = parser.parse()
|
|
693
|
+
except (ELFParseError, OSError) as exc:
|
|
694
|
+
logger.debug("Skipping %s: %s", path, exc)
|
|
695
|
+
return None
|
|
696
|
+
|
|
697
|
+
architecture = _EM_TO_ARCH.get(info["e_machine"], Architecture.UNKNOWN) # type: ignore[arg-type]
|
|
698
|
+
|
|
699
|
+
# Compute the maximum GLIBC version from version requirements
|
|
700
|
+
max_glibc: Optional[Tuple[int, int]] = None
|
|
701
|
+
version_req_strs: List[str] = []
|
|
702
|
+
for lib, ver_str in info["version_requirements"]: # type: ignore[union-attr]
|
|
703
|
+
version_req_strs.append(f"{lib}({ver_str})")
|
|
704
|
+
if ver_str.startswith("GLIBC_"):
|
|
705
|
+
glibc_ver = _parse_glibc_version(ver_str)
|
|
706
|
+
if glibc_ver is not None:
|
|
707
|
+
if max_glibc is None or glibc_ver > max_glibc:
|
|
708
|
+
max_glibc = glibc_ver
|
|
709
|
+
|
|
710
|
+
# Detect max GLIBCXX
|
|
711
|
+
max_glibcxx: Optional[str] = None
|
|
712
|
+
for lib, ver_str in info["version_requirements"]: # type: ignore[union-attr]
|
|
713
|
+
if ver_str.startswith("GLIBCXX_"):
|
|
714
|
+
if max_glibcxx is None or ver_str > max_glibcxx:
|
|
715
|
+
max_glibcxx = ver_str
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
file_size = os.path.getsize(path)
|
|
719
|
+
except OSError:
|
|
720
|
+
file_size = 0
|
|
721
|
+
|
|
722
|
+
return SharedObjectInfo(
|
|
723
|
+
path=path,
|
|
724
|
+
filename=os.path.basename(path),
|
|
725
|
+
architecture=architecture,
|
|
726
|
+
elf_class=info["ei_class"], # type: ignore[arg-type]
|
|
727
|
+
endianness=info["endianness"], # type: ignore[arg-type]
|
|
728
|
+
dt_needed=info["needed"], # type: ignore[arg-type]
|
|
729
|
+
dt_soname=info["soname"], # type: ignore[arg-type]
|
|
730
|
+
dt_rpath=info["rpath"], # type: ignore[arg-type]
|
|
731
|
+
dt_runpath=info["runpath"], # type: ignore[arg-type]
|
|
732
|
+
required_glibc=max_glibc,
|
|
733
|
+
required_glibcxx=max_glibcxx,
|
|
734
|
+
gnu_version_requirements=version_req_strs,
|
|
735
|
+
build_id=info["build_id"], # type: ignore[arg-type]
|
|
736
|
+
file_size=file_size,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _parse_glibc_version(version_string: str) -> Optional[Tuple[int, int]]:
|
|
741
|
+
"""Parse a version string like ``GLIBC_2.17`` into ``(2, 17)``.
|
|
742
|
+
|
|
743
|
+
Returns ``None`` if the string cannot be parsed.
|
|
744
|
+
"""
|
|
745
|
+
prefix = "GLIBC_"
|
|
746
|
+
if not version_string.startswith(prefix):
|
|
747
|
+
return None
|
|
748
|
+
parts = version_string[len(prefix):].split(".")
|
|
749
|
+
if len(parts) < 2:
|
|
750
|
+
return None
|
|
751
|
+
try:
|
|
752
|
+
return (int(parts[0]), int(parts[1]))
|
|
753
|
+
except (ValueError, IndexError):
|
|
754
|
+
return None
|