codevanta 0.1.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.
- codevanta/__init__.py +1 -0
- codevanta/analysis/__init__.py +1 -0
- codevanta/analysis/binary_diff.py +1168 -0
- codevanta/analysis/callgraph.py +542 -0
- codevanta/analysis/cfg.py +1201 -0
- codevanta/analysis/dataflow.py +762 -0
- codevanta/analysis/debugger.py +289 -0
- codevanta/analysis/debuginfo.py +304 -0
- codevanta/analysis/decompile.py +1094 -0
- codevanta/analysis/decompilers/__init__.py +250 -0
- codevanta/analysis/decompilers/angr_backend.py +251 -0
- codevanta/analysis/decompilers/base.py +278 -0
- codevanta/analysis/decompilers/ghidra_backend.py +316 -0
- codevanta/analysis/dynamic.py +756 -0
- codevanta/analysis/emulate.py +1349 -0
- codevanta/analysis/evidence_bundle.py +736 -0
- codevanta/analysis/function_hash.py +562 -0
- codevanta/analysis/graph_export.py +441 -0
- codevanta/analysis/html_view.py +695 -0
- codevanta/analysis/jump_tables.py +546 -0
- codevanta/analysis/solver.py +715 -0
- codevanta/analysis/symbolic.py +1809 -0
- codevanta/analysis/trace.py +646 -0
- codevanta/analysis/type_propagation.py +1055 -0
- codevanta/analysis/type_recovery.py +1214 -0
- codevanta/analysis/vtables.py +572 -0
- codevanta/analysis/xrefs.py +434 -0
- codevanta/api/__init__.py +1 -0
- codevanta/cli/__init__.py +1 -0
- codevanta/cli/main.py +3038 -0
- codevanta/core/__init__.py +31 -0
- codevanta/core/disassembler.py +121 -0
- codevanta/core/entropy.py +237 -0
- codevanta/core/functions.py +176 -0
- codevanta/core/loader.py +1351 -0
- codevanta/core/report.py +306 -0
- codevanta/core/strings.py +109 -0
- codevanta/evidence/__init__.py +7 -0
- codevanta/evidence/confidence.py +36 -0
- codevanta/evidence/models.py +43 -0
- codevanta/evidence/provenance.py +37 -0
- codevanta/gui/__init__.py +46 -0
- codevanta/gui/layout.py +1384 -0
- codevanta/gui/server.py +1305 -0
- codevanta/ir/__init__.py +6 -0
- codevanta/ir/adapters/__init__.py +31 -0
- codevanta/ir/base.py +31 -0
- codevanta/ir/instruction.py +42 -0
- codevanta/mcp_server/__init__.py +5 -0
- codevanta/mcp_server/server.py +710 -0
- codevanta/mcp_server/tools.py +1364 -0
- codevanta/plugins/__init__.py +748 -0
- codevanta/setup/__init__.py +28 -0
- codevanta/setup/apply.py +22 -0
- codevanta/setup/clients.py +285 -0
- codevanta/setup/plan.py +79 -0
- codevanta/setup/verify.py +43 -0
- codevanta/storage/__init__.py +1 -0
- codevanta/storage/session.py +514 -0
- codevanta-0.1.0.dist-info/METADATA +688 -0
- codevanta-0.1.0.dist-info/RECORD +65 -0
- codevanta-0.1.0.dist-info/WHEEL +5 -0
- codevanta-0.1.0.dist-info/entry_points.txt +2 -0
- codevanta-0.1.0.dist-info/licenses/LICENSE +21 -0
- codevanta-0.1.0.dist-info/top_level.txt +1 -0
codevanta/core/loader.py
ADDED
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
"""Binary loading and metadata extraction backed by LIEF.
|
|
2
|
+
|
|
3
|
+
The loader normalizes PE, ELF, and Mach-O binaries into :class:`BinaryInfo`.
|
|
4
|
+
All addresses exposed by ``BinaryInfo`` are absolute virtual addresses (VA);
|
|
5
|
+
PE RVAs are rebased with the image base so downstream modules can treat every
|
|
6
|
+
format uniformly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import struct
|
|
13
|
+
from dataclasses import dataclass, field, replace
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import lief
|
|
18
|
+
|
|
19
|
+
from codevanta.evidence.confidence import Confidence
|
|
20
|
+
from codevanta.evidence.models import Evidence
|
|
21
|
+
|
|
22
|
+
try: # keep expected parser warnings (e.g. section-only ELF) out of stderr
|
|
23
|
+
lief.logging.set_level(lief.logging.LEVEL.ERROR)
|
|
24
|
+
except Exception: # pragma: no cover - LIEF build without logging controls
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BinaryLoadError(Exception):
|
|
30
|
+
"""Raised when a file cannot be loaded as a supported binary."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---- fat (universal) Mach-O container --------------------------------------
|
|
34
|
+
|
|
35
|
+
# A universal binary wraps several thin Mach-O images behind one of these
|
|
36
|
+
# magics. The 8-byte header (magic + nfat_arch) is always followed by one
|
|
37
|
+
# ``fat_arch`` entry per slice; the "_64" variants use 64-bit offset/size
|
|
38
|
+
# fields and the "CIGAM" variants store the fields in little-endian order.
|
|
39
|
+
FAT_MAGIC = 0xCAFEBABE
|
|
40
|
+
FAT_CIGAM = 0xBEBAFECA
|
|
41
|
+
FAT_MAGIC_64 = 0xCAFEBABF
|
|
42
|
+
FAT_CIGAM_64 = 0xBFBAFECA
|
|
43
|
+
|
|
44
|
+
# magic -> (magic name, 64-bit table entries, struct byte order)
|
|
45
|
+
_FAT_MAGICS: dict[int, tuple[str, bool, str]] = {
|
|
46
|
+
FAT_MAGIC: ("FAT_MAGIC", False, ">"),
|
|
47
|
+
FAT_CIGAM: ("FAT_CIGAM", False, "<"),
|
|
48
|
+
FAT_MAGIC_64: ("FAT_MAGIC_64", True, ">"),
|
|
49
|
+
FAT_CIGAM_64: ("FAT_CIGAM_64", True, "<"),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_FAT_HEADER_SIZE = 8
|
|
53
|
+
_FAT_ARCH_SIZE = 20 # fat_arch: cputype, cpusubtype, offset, size, align
|
|
54
|
+
_FAT_ARCH_64_SIZE = 32 # fat_arch_64: 64-bit offset/size plus a reserved field
|
|
55
|
+
|
|
56
|
+
# Universal binaries in the wild carry at most a handful of slices (2-4 is
|
|
57
|
+
# typical). ``nfat_arch`` is attacker-controlled, so the table is hard-capped:
|
|
58
|
+
# without this bound a crafted 512 MiB file could declare hundreds of thousands
|
|
59
|
+
# of entries and burn seconds of parsing time plus proportional memory.
|
|
60
|
+
MAX_FAT_SLICES = 32
|
|
61
|
+
|
|
62
|
+
# ``fat_arch.align`` is an exponent (``1 << align``), never larger than 2**20
|
|
63
|
+
# in real toolchain output. Anything above that would let a file demand absurd
|
|
64
|
+
# slice alignment, so it is rejected rather than fed into ``1 << align``.
|
|
65
|
+
MAX_FAT_ALIGN = 20
|
|
66
|
+
|
|
67
|
+
# CPU_ARCH_ABI64 marks the 64-bit dialect of a CPU family (x86_64, arm64, ...).
|
|
68
|
+
_CPU_ARCH_ABI64 = 0x01000000
|
|
69
|
+
_CPU_TYPE_NAMES = {
|
|
70
|
+
0x00000007: "x86",
|
|
71
|
+
0x01000007: "x86_64",
|
|
72
|
+
0x0000000C: "arm",
|
|
73
|
+
0x0100000C: "aarch64",
|
|
74
|
+
0x0200000C: "aarch64_32",
|
|
75
|
+
0x00000012: "ppc",
|
|
76
|
+
0x01000012: "ppc64",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cpu_type_name(cputype: int) -> str:
|
|
81
|
+
"""Stable, human-readable name for a Mach-O ``cputype``."""
|
|
82
|
+
return _CPU_TYPE_NAMES.get(cputype, f"cputype_{cputype:#x}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# LIEF ``CPU_TYPE`` member name -> numeric cputype, used only when the enum
|
|
86
|
+
# cannot be cast to ``int`` (older/limited LIEF builds).
|
|
87
|
+
_CPU_TYPE_ALIASES = {
|
|
88
|
+
"I386": 0x00000007,
|
|
89
|
+
"X86": 0x00000007,
|
|
90
|
+
"X86_64": 0x01000007,
|
|
91
|
+
"AMD64": 0x01000007,
|
|
92
|
+
"ARM": 0x0000000C,
|
|
93
|
+
"AARCH64": 0x0100000C,
|
|
94
|
+
"ARM64": 0x0100000C,
|
|
95
|
+
"ARM64E": 0x0100000C,
|
|
96
|
+
"ARM64_32": 0x0200000C,
|
|
97
|
+
"POWERPC": 0x00000012,
|
|
98
|
+
"POWERPC64": 0x01000012,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Selection rule tokens, kept stable for reports and tests.
|
|
103
|
+
RULE_PREFER_64BIT = "prefer_64bit"
|
|
104
|
+
RULE_FIRST_BY_OFFSET = "first_by_offset"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class FatArch:
|
|
109
|
+
"""One ``fat_arch`` entry of a fat Mach-O architecture table."""
|
|
110
|
+
|
|
111
|
+
index: int
|
|
112
|
+
cputype: int
|
|
113
|
+
cpusubtype: int
|
|
114
|
+
offset: int
|
|
115
|
+
size: int
|
|
116
|
+
align: int
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def is_64bit(self) -> bool:
|
|
120
|
+
return bool(self.cputype & _CPU_ARCH_ABI64)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def arch_name(self) -> str:
|
|
124
|
+
return _cpu_type_name(self.cputype)
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> dict[str, Any]:
|
|
127
|
+
return {
|
|
128
|
+
"index": self.index,
|
|
129
|
+
"arch": self.arch_name,
|
|
130
|
+
"cputype": f"0x{self.cputype:x}",
|
|
131
|
+
"cpusubtype": self.cpusubtype,
|
|
132
|
+
"offset": f"0x{self.offset:x}",
|
|
133
|
+
"size": self.size,
|
|
134
|
+
"align": self.align,
|
|
135
|
+
"is_64bit": self.is_64bit,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class FatContainer:
|
|
141
|
+
"""A parsed fat (universal) Mach-O container plus the selected slice."""
|
|
142
|
+
|
|
143
|
+
magic: int
|
|
144
|
+
wide_entries: bool
|
|
145
|
+
arches: tuple[FatArch, ...]
|
|
146
|
+
slice: FatArch
|
|
147
|
+
rule: str
|
|
148
|
+
reason: str
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def magic_name(self) -> str:
|
|
152
|
+
return _FAT_MAGICS[self.magic][0]
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def slice_count(self) -> int:
|
|
156
|
+
return len(self.arches)
|
|
157
|
+
|
|
158
|
+
def evidence_extra(self) -> dict[str, Any]:
|
|
159
|
+
"""Structured container metadata for :attr:`BinaryInfo.evidence`."""
|
|
160
|
+
return {
|
|
161
|
+
"fat": True,
|
|
162
|
+
"fat_magic": f"0x{self.magic:08x}",
|
|
163
|
+
"fat_magic_name": self.magic_name,
|
|
164
|
+
"fat_arch_64": self.wide_entries,
|
|
165
|
+
"slice_index": self.slice.index,
|
|
166
|
+
"slice_count": self.slice_count,
|
|
167
|
+
"slice_arch": self.slice.arch_name,
|
|
168
|
+
"slice_cputype": f"0x{self.slice.cputype:x}",
|
|
169
|
+
"slice_offset": self.slice.offset,
|
|
170
|
+
"slice_size": self.slice.size,
|
|
171
|
+
"selection_rule": self.rule,
|
|
172
|
+
"selection_reason": self.reason,
|
|
173
|
+
"available_arches": [arch.arch_name for arch in self.arches],
|
|
174
|
+
# The table is already bounded by MAX_FAT_SLICES, but the evidence
|
|
175
|
+
# list is capped explicitly so reports stay bounded by construction
|
|
176
|
+
# even if that limit is ever raised.
|
|
177
|
+
"available_slices": [
|
|
178
|
+
arch.to_dict() for arch in self.arches[:MAX_FAT_SLICES]
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class SectionInfo:
|
|
185
|
+
"""A section with a known file mapping."""
|
|
186
|
+
|
|
187
|
+
name: str
|
|
188
|
+
virtual_address: int
|
|
189
|
+
size: int
|
|
190
|
+
offset: int
|
|
191
|
+
raw_size: int = 0
|
|
192
|
+
entropy: float | None = None
|
|
193
|
+
is_executable: bool = False
|
|
194
|
+
is_writable: bool = False
|
|
195
|
+
evidence: Evidence | None = None
|
|
196
|
+
|
|
197
|
+
def to_dict(self) -> dict[str, Any]:
|
|
198
|
+
data: dict[str, Any] = {
|
|
199
|
+
"name": self.name,
|
|
200
|
+
"virtual_address": f"0x{self.virtual_address:x}",
|
|
201
|
+
"size": self.size,
|
|
202
|
+
"raw_size": self.raw_size,
|
|
203
|
+
"offset": f"0x{self.offset:x}",
|
|
204
|
+
"entropy": self.entropy,
|
|
205
|
+
"executable": self.is_executable,
|
|
206
|
+
"writable": self.is_writable,
|
|
207
|
+
}
|
|
208
|
+
if self.evidence is not None:
|
|
209
|
+
data["evidence"] = self.evidence.to_dict()
|
|
210
|
+
return data
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True)
|
|
214
|
+
class ImportInfo:
|
|
215
|
+
"""An imported symbol."""
|
|
216
|
+
|
|
217
|
+
name: str
|
|
218
|
+
library: str | None = None
|
|
219
|
+
address: int | None = None
|
|
220
|
+
evidence: Evidence | None = None
|
|
221
|
+
|
|
222
|
+
def to_dict(self) -> dict[str, Any]:
|
|
223
|
+
data: dict[str, Any] = {"name": self.name}
|
|
224
|
+
if self.library:
|
|
225
|
+
data["library"] = self.library
|
|
226
|
+
if self.address is not None:
|
|
227
|
+
data["address"] = f"0x{self.address:x}"
|
|
228
|
+
if self.evidence is not None:
|
|
229
|
+
data["evidence"] = self.evidence.to_dict()
|
|
230
|
+
return data
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True)
|
|
234
|
+
class ExportInfo:
|
|
235
|
+
"""An exported symbol, with forwarding information when present."""
|
|
236
|
+
|
|
237
|
+
name: str
|
|
238
|
+
address: int | None
|
|
239
|
+
ordinal: int | None = None
|
|
240
|
+
forwarded_to: str | None = None
|
|
241
|
+
evidence: Evidence | None = None
|
|
242
|
+
|
|
243
|
+
def to_dict(self) -> dict[str, Any]:
|
|
244
|
+
data: dict[str, Any] = {"name": self.name}
|
|
245
|
+
if self.address is not None:
|
|
246
|
+
data["address"] = f"0x{self.address:x}"
|
|
247
|
+
if self.ordinal is not None:
|
|
248
|
+
data["ordinal"] = self.ordinal
|
|
249
|
+
if self.forwarded_to:
|
|
250
|
+
data["forwarded_to"] = self.forwarded_to
|
|
251
|
+
if self.evidence is not None:
|
|
252
|
+
data["evidence"] = self.evidence.to_dict()
|
|
253
|
+
return data
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True)
|
|
257
|
+
class SymbolInfo:
|
|
258
|
+
"""A symbol from a symbol table (ELF symtab/dynsym, PE COFF)."""
|
|
259
|
+
|
|
260
|
+
name: str
|
|
261
|
+
address: int
|
|
262
|
+
size: int = 0
|
|
263
|
+
kind: str = "unknown"
|
|
264
|
+
is_function: bool = False
|
|
265
|
+
evidence: Evidence | None = None
|
|
266
|
+
|
|
267
|
+
def to_dict(self) -> dict[str, Any]:
|
|
268
|
+
data: dict[str, Any] = {
|
|
269
|
+
"name": self.name,
|
|
270
|
+
"address": f"0x{self.address:x}",
|
|
271
|
+
"size": self.size,
|
|
272
|
+
"kind": self.kind,
|
|
273
|
+
"is_function": self.is_function,
|
|
274
|
+
}
|
|
275
|
+
if self.evidence is not None:
|
|
276
|
+
data["evidence"] = self.evidence.to_dict()
|
|
277
|
+
return data
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass
|
|
281
|
+
class BinaryInfo:
|
|
282
|
+
"""Normalized metadata for a loaded binary. All addresses are VA."""
|
|
283
|
+
|
|
284
|
+
path: Path
|
|
285
|
+
format: str
|
|
286
|
+
architecture: str
|
|
287
|
+
bitness: int
|
|
288
|
+
endianness: str
|
|
289
|
+
entry_point: int | None
|
|
290
|
+
image_base: int
|
|
291
|
+
sha256: str
|
|
292
|
+
file_size: int
|
|
293
|
+
sections: list[SectionInfo] = field(default_factory=list)
|
|
294
|
+
imports: list[ImportInfo] = field(default_factory=list)
|
|
295
|
+
exports: list[ExportInfo] = field(default_factory=list)
|
|
296
|
+
symbols: list[SymbolInfo] = field(default_factory=list)
|
|
297
|
+
is_stripped: bool = False
|
|
298
|
+
warnings: list[str] = field(default_factory=list)
|
|
299
|
+
# Container-level provenance (currently the fat Mach-O slice selection).
|
|
300
|
+
evidence: Evidence | None = None
|
|
301
|
+
|
|
302
|
+
def to_dict(self) -> dict[str, Any]:
|
|
303
|
+
data: dict[str, Any] = {
|
|
304
|
+
"path": str(self.path),
|
|
305
|
+
"format": self.format,
|
|
306
|
+
"architecture": self.architecture,
|
|
307
|
+
"bitness": self.bitness,
|
|
308
|
+
"endianness": self.endianness,
|
|
309
|
+
"entry_point": f"0x{self.entry_point:x}" if self.entry_point is not None else None,
|
|
310
|
+
"image_base": f"0x{self.image_base:x}",
|
|
311
|
+
"sha256": self.sha256,
|
|
312
|
+
"file_size": self.file_size,
|
|
313
|
+
"is_stripped": self.is_stripped,
|
|
314
|
+
"sections": [s.to_dict() for s in self.sections],
|
|
315
|
+
"imports": [i.to_dict() for i in self.imports],
|
|
316
|
+
"exports": [e.to_dict() for e in self.exports],
|
|
317
|
+
"symbols": [s.to_dict() for s in self.symbols],
|
|
318
|
+
"warnings": list(self.warnings),
|
|
319
|
+
}
|
|
320
|
+
if self.evidence is not None:
|
|
321
|
+
data["evidence"] = self.evidence.to_dict()
|
|
322
|
+
return data
|
|
323
|
+
|
|
324
|
+
def section_for_va(self, va: int) -> SectionInfo | None:
|
|
325
|
+
"""Return the section that contains ``va``, if any."""
|
|
326
|
+
for section in self.sections:
|
|
327
|
+
if section.size <= 0:
|
|
328
|
+
continue
|
|
329
|
+
if section.virtual_address <= va < section.virtual_address + section.size:
|
|
330
|
+
return section
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def _mapped_size(self, section: SectionInfo) -> int:
|
|
334
|
+
if section.raw_size > 0:
|
|
335
|
+
return section.raw_size
|
|
336
|
+
if self.format == "ELF":
|
|
337
|
+
return section.size
|
|
338
|
+
return 0
|
|
339
|
+
|
|
340
|
+
def va_to_offset(self, va: int) -> int | None:
|
|
341
|
+
"""Map a virtual address to a file offset using section metadata."""
|
|
342
|
+
section = self.section_for_va(va)
|
|
343
|
+
if section is None:
|
|
344
|
+
return None
|
|
345
|
+
delta = va - section.virtual_address
|
|
346
|
+
mapped = self._mapped_size(section)
|
|
347
|
+
if mapped <= 0 or delta < 0 or delta >= mapped:
|
|
348
|
+
return None
|
|
349
|
+
return section.offset + delta
|
|
350
|
+
|
|
351
|
+
def offset_to_va(self, offset: int) -> int | None:
|
|
352
|
+
"""Map a file offset to a virtual address using section metadata."""
|
|
353
|
+
for section in self.sections:
|
|
354
|
+
mapped = self._mapped_size(section)
|
|
355
|
+
if mapped <= 0:
|
|
356
|
+
continue
|
|
357
|
+
if section.offset <= offset < section.offset + mapped:
|
|
358
|
+
return section.virtual_address + (offset - section.offset)
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class BinaryLoader:
|
|
363
|
+
"""Loads PE, ELF, Mach-O, and DEX binaries and extracts structural metadata."""
|
|
364
|
+
|
|
365
|
+
MAX_FILE_SIZE = 512 * 1024 * 1024 # 512 MiB safety guard
|
|
366
|
+
|
|
367
|
+
def load(self, path: str | Path) -> BinaryInfo:
|
|
368
|
+
"""Load ``path`` and return normalized :class:`BinaryInfo`."""
|
|
369
|
+
file_path = Path(path)
|
|
370
|
+
self._validate(file_path)
|
|
371
|
+
raw = file_path.read_bytes()
|
|
372
|
+
sha256 = hashlib.sha256(raw).hexdigest()
|
|
373
|
+
|
|
374
|
+
# DEX is a bytecode container, not an executable image: LIEF only
|
|
375
|
+
# reaches it through the dedicated DEX parser, and the generic
|
|
376
|
+
# ``lief.parse`` answers None for it. The magic cannot collide with
|
|
377
|
+
# any fat/thin image magic, so checking it first is safe.
|
|
378
|
+
if raw[:4] == b"dex\n":
|
|
379
|
+
info = self._load_dex(file_path, raw)
|
|
380
|
+
info.sha256 = sha256
|
|
381
|
+
info.file_size = len(raw)
|
|
382
|
+
return info
|
|
383
|
+
|
|
384
|
+
# A fat (universal) container must be unwrapped before any of the thin
|
|
385
|
+
# parsers run: LIEF would only ever see the container magic.
|
|
386
|
+
container = self._fat_container(raw, file_path)
|
|
387
|
+
fat_offset = 0
|
|
388
|
+
if container is not None:
|
|
389
|
+
chosen = container.slice
|
|
390
|
+
payload = raw[chosen.offset : chosen.offset + chosen.size]
|
|
391
|
+
fat_offset = chosen.offset
|
|
392
|
+
try:
|
|
393
|
+
binary = lief.parse(payload)
|
|
394
|
+
except Exception as exc: # LIEF raises several native error types
|
|
395
|
+
raise BinaryLoadError(
|
|
396
|
+
f"Fat Mach-O {file_path}: slice {chosen.index} "
|
|
397
|
+
f"({chosen.arch_name}) could not be parsed: {exc}"
|
|
398
|
+
) from exc
|
|
399
|
+
if binary is None:
|
|
400
|
+
raise BinaryLoadError(
|
|
401
|
+
f"Fat Mach-O {file_path}: slice {chosen.index} "
|
|
402
|
+
f"({chosen.arch_name}) at offset 0x{chosen.offset:x} "
|
|
403
|
+
f"is not a valid thin Mach-O image"
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
try:
|
|
407
|
+
binary = lief.parse(str(file_path))
|
|
408
|
+
except Exception as exc: # LIEF raises several native error types
|
|
409
|
+
raise BinaryLoadError(f"LIEF could not parse {file_path}: {exc}") from exc
|
|
410
|
+
if binary is None:
|
|
411
|
+
raise BinaryLoadError(f"Unsupported or unreadable binary: {file_path}")
|
|
412
|
+
|
|
413
|
+
fmt = self._format_name(binary)
|
|
414
|
+
if container is not None:
|
|
415
|
+
# Everything inside a fat container must be a thin Mach-O image:
|
|
416
|
+
# an ELF or PE payload would be parsed with slice-relative offsets
|
|
417
|
+
# and silently reported as that foreign format.
|
|
418
|
+
if fmt != "MachO":
|
|
419
|
+
raise BinaryLoadError(
|
|
420
|
+
f"Fat Mach-O {file_path}: fat slice is not a Mach-O image: {fmt}"
|
|
421
|
+
)
|
|
422
|
+
# With the payload parsed, the slice header is the authority on what
|
|
423
|
+
# was actually loaded; the container table is only a claim.
|
|
424
|
+
container = self._verify_fat_slice(container, binary, file_path)
|
|
425
|
+
if fmt == "PE":
|
|
426
|
+
info = self._load_pe(binary, file_path)
|
|
427
|
+
elif fmt == "ELF":
|
|
428
|
+
info = self._load_elf(binary, file_path)
|
|
429
|
+
elif fmt == "MachO":
|
|
430
|
+
info = self._load_macho(
|
|
431
|
+
binary, file_path, fat_offset=fat_offset, container=container
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
raise BinaryLoadError(f"Unsupported format {fmt!r}: {file_path}")
|
|
435
|
+
|
|
436
|
+
info.sha256 = sha256
|
|
437
|
+
info.file_size = len(raw)
|
|
438
|
+
return info
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _validate(path: Path) -> None:
|
|
442
|
+
if not path.exists():
|
|
443
|
+
raise BinaryLoadError(f"File not found: {path}")
|
|
444
|
+
if not path.is_file():
|
|
445
|
+
raise BinaryLoadError(f"Not a regular file: {path}")
|
|
446
|
+
size = path.stat().st_size
|
|
447
|
+
if size == 0:
|
|
448
|
+
raise BinaryLoadError(f"Empty file: {path}")
|
|
449
|
+
if size > BinaryLoader.MAX_FILE_SIZE:
|
|
450
|
+
raise BinaryLoadError(
|
|
451
|
+
f"File too large ({size} bytes); limit is {BinaryLoader.MAX_FILE_SIZE} bytes"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def _format_name(binary: Any) -> str:
|
|
456
|
+
text = str(getattr(binary, "format", "")).upper()
|
|
457
|
+
if "ELF" in text:
|
|
458
|
+
return "ELF"
|
|
459
|
+
if "PE" in text:
|
|
460
|
+
return "PE"
|
|
461
|
+
if "MACHO" in text or "MACH_O" in text:
|
|
462
|
+
return "MachO"
|
|
463
|
+
type_name = type(binary).__name__.upper()
|
|
464
|
+
if "ELF" in type_name:
|
|
465
|
+
return "ELF"
|
|
466
|
+
if "PE" in type_name:
|
|
467
|
+
return "PE"
|
|
468
|
+
return "Unknown"
|
|
469
|
+
|
|
470
|
+
# ---- fat (universal) Mach-O container ----------------------------------
|
|
471
|
+
|
|
472
|
+
@classmethod
|
|
473
|
+
def _fat_container(cls, raw: bytes, path: Path) -> FatContainer | None:
|
|
474
|
+
"""Parse a fat Mach-O container header and pick one slice.
|
|
475
|
+
|
|
476
|
+
Returns ``None`` when ``raw`` does not start with a fat magic, so thin
|
|
477
|
+
Mach-O, ELF and PE inputs keep taking the unchanged path. Malformed
|
|
478
|
+
containers raise :class:`BinaryLoadError` naming the offending field.
|
|
479
|
+
"""
|
|
480
|
+
if len(raw) < len(b"\xca\xfe\xba\xbe"):
|
|
481
|
+
return None
|
|
482
|
+
magic = int.from_bytes(raw[:4], "big")
|
|
483
|
+
if magic not in _FAT_MAGICS:
|
|
484
|
+
return None
|
|
485
|
+
magic_name, wide_entries, order = _FAT_MAGICS[magic]
|
|
486
|
+
entry_size = _FAT_ARCH_64_SIZE if wide_entries else _FAT_ARCH_SIZE
|
|
487
|
+
|
|
488
|
+
if len(raw) < _FAT_HEADER_SIZE:
|
|
489
|
+
raise BinaryLoadError(
|
|
490
|
+
f"Malformed fat Mach-O {path}: nfat_arch field is truncated "
|
|
491
|
+
f"({magic_name} header needs {_FAT_HEADER_SIZE} bytes, "
|
|
492
|
+
f"file has {len(raw)})"
|
|
493
|
+
)
|
|
494
|
+
slice_count = struct.unpack_from(f"{order}I", raw, 4)[0]
|
|
495
|
+
if slice_count == 0:
|
|
496
|
+
raise BinaryLoadError(
|
|
497
|
+
f"Malformed fat Mach-O {path}: nfat_arch is zero, a fat "
|
|
498
|
+
f"{magic_name} container must hold at least one slice"
|
|
499
|
+
)
|
|
500
|
+
if slice_count > MAX_FAT_SLICES:
|
|
501
|
+
raise BinaryLoadError(
|
|
502
|
+
f"Malformed fat Mach-O {path}: nfat_arch={slice_count} exceeds "
|
|
503
|
+
f"the supported maximum of {MAX_FAT_SLICES} slices"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
table_end = _FAT_HEADER_SIZE + slice_count * entry_size
|
|
507
|
+
if table_end > len(raw):
|
|
508
|
+
raise BinaryLoadError(
|
|
509
|
+
f"Malformed fat Mach-O {path}: fat_arch table truncated "
|
|
510
|
+
f"(nfat_arch={slice_count} needs {table_end} bytes, "
|
|
511
|
+
f"file has {len(raw)})"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
format_string = f"{order}IIQQII" if wide_entries else f"{order}IIIII"
|
|
515
|
+
arches: list[FatArch] = []
|
|
516
|
+
for index in range(slice_count):
|
|
517
|
+
base = _FAT_HEADER_SIZE + index * entry_size
|
|
518
|
+
if wide_entries:
|
|
519
|
+
# fat_arch_64 carries a trailing reserved u32.
|
|
520
|
+
cputype, cpusubtype, offset, size, align = struct.unpack_from(
|
|
521
|
+
format_string, raw, base
|
|
522
|
+
)[:5]
|
|
523
|
+
else:
|
|
524
|
+
cputype, cpusubtype, offset, size, align = struct.unpack_from(
|
|
525
|
+
format_string, raw, base
|
|
526
|
+
)
|
|
527
|
+
if size == 0:
|
|
528
|
+
raise BinaryLoadError(
|
|
529
|
+
f"Malformed fat Mach-O {path}: fat_arch[{index}].size is zero"
|
|
530
|
+
)
|
|
531
|
+
if align > MAX_FAT_ALIGN:
|
|
532
|
+
raise BinaryLoadError(
|
|
533
|
+
f"Malformed fat Mach-O {path}: fat_arch[{index}].align is "
|
|
534
|
+
f"{align}, above the supported maximum of {MAX_FAT_ALIGN}"
|
|
535
|
+
)
|
|
536
|
+
alignment = 1 << align
|
|
537
|
+
if offset % alignment != 0:
|
|
538
|
+
raise BinaryLoadError(
|
|
539
|
+
f"Malformed fat Mach-O {path}: fat_arch[{index}].offset "
|
|
540
|
+
f"0x{offset:x} is not aligned to {alignment} bytes "
|
|
541
|
+
f"(align={align})"
|
|
542
|
+
)
|
|
543
|
+
if offset < table_end:
|
|
544
|
+
raise BinaryLoadError(
|
|
545
|
+
f"Malformed fat Mach-O {path}: fat_arch[{index}].offset "
|
|
546
|
+
f"0x{offset:x} points inside the fat header/table, which "
|
|
547
|
+
f"ends at 0x{table_end:x}"
|
|
548
|
+
)
|
|
549
|
+
if offset + size > len(raw):
|
|
550
|
+
raise BinaryLoadError(
|
|
551
|
+
f"Malformed fat Mach-O {path}: fat_arch[{index}].offset/size "
|
|
552
|
+
f"outside the file (offset=0x{offset:x}, size=0x{size:x}, "
|
|
553
|
+
f"file size=0x{len(raw):x})"
|
|
554
|
+
)
|
|
555
|
+
arches.append(
|
|
556
|
+
FatArch(
|
|
557
|
+
index=index,
|
|
558
|
+
cputype=cputype,
|
|
559
|
+
cpusubtype=cpusubtype,
|
|
560
|
+
offset=offset,
|
|
561
|
+
size=size,
|
|
562
|
+
align=align,
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
chosen, rule, reason = cls._select_fat_slice(arches)
|
|
567
|
+
return FatContainer(
|
|
568
|
+
magic=magic,
|
|
569
|
+
wide_entries=wide_entries,
|
|
570
|
+
arches=tuple(arches),
|
|
571
|
+
slice=chosen,
|
|
572
|
+
rule=rule,
|
|
573
|
+
reason=reason,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
@staticmethod
|
|
577
|
+
def _select_fat_slice(arches: list[FatArch]) -> tuple[FatArch, str, str]:
|
|
578
|
+
"""Pick one slice deterministically: 64-bit first, then lowest offset."""
|
|
579
|
+
wide = [arch for arch in arches if arch.is_64bit]
|
|
580
|
+
candidates = wide or arches
|
|
581
|
+
chosen = min(candidates, key=lambda arch: (arch.offset, arch.index))
|
|
582
|
+
if wide:
|
|
583
|
+
return (
|
|
584
|
+
chosen,
|
|
585
|
+
RULE_PREFER_64BIT,
|
|
586
|
+
"64-bit slice preferred over 32-bit slices, lowest file offset wins",
|
|
587
|
+
)
|
|
588
|
+
return (
|
|
589
|
+
chosen,
|
|
590
|
+
RULE_FIRST_BY_OFFSET,
|
|
591
|
+
"no 64-bit slice present, first slice by file offset wins",
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
@classmethod
|
|
595
|
+
def _verify_fat_slice(
|
|
596
|
+
cls, container: FatContainer, binary: Any, path: Path
|
|
597
|
+
) -> FatContainer:
|
|
598
|
+
"""Reconcile the container entry with the parsed thin Mach-O header.
|
|
599
|
+
|
|
600
|
+
The table entry only claims an architecture; the slice header proves it.
|
|
601
|
+
A mismatch means the bytes are not what the container advertises, so it
|
|
602
|
+
is rejected. On success the container's slice is rebuilt from the real
|
|
603
|
+
header, keeping ``slice_cputype``/``slice_arch`` evidence honest.
|
|
604
|
+
"""
|
|
605
|
+
header_cputype = cls._macho_header_cputype(binary)
|
|
606
|
+
if header_cputype is None: # LIEF build without a usable cpu_type
|
|
607
|
+
return container
|
|
608
|
+
chosen = container.slice
|
|
609
|
+
if header_cputype != chosen.cputype:
|
|
610
|
+
raise BinaryLoadError(
|
|
611
|
+
f"Fat Mach-O {path}: fat_arch[{chosen.index}].cputype "
|
|
612
|
+
f"{_cpu_type_name(chosen.cputype)} (0x{chosen.cputype:x}) does "
|
|
613
|
+
f"not match the parsed slice header cpu_type "
|
|
614
|
+
f"{_cpu_type_name(header_cputype)} (0x{header_cputype:x})"
|
|
615
|
+
)
|
|
616
|
+
return replace(container, slice=replace(chosen, cputype=header_cputype))
|
|
617
|
+
|
|
618
|
+
@staticmethod
|
|
619
|
+
def _macho_header_cputype(binary: Any) -> int | None:
|
|
620
|
+
"""Numeric ``cputype`` of a parsed Mach-O header, or ``None``."""
|
|
621
|
+
header = getattr(binary, "header", None)
|
|
622
|
+
cpu_type = getattr(header, "cpu_type", None)
|
|
623
|
+
if cpu_type is None:
|
|
624
|
+
return None
|
|
625
|
+
try:
|
|
626
|
+
return int(cpu_type)
|
|
627
|
+
except (TypeError, ValueError):
|
|
628
|
+
pass
|
|
629
|
+
# LIEF builds that expose only the enum member name.
|
|
630
|
+
name = str(getattr(cpu_type, "name", cpu_type) or "").upper()
|
|
631
|
+
return _CPU_TYPE_ALIASES.get(name)
|
|
632
|
+
|
|
633
|
+
# ---- PE ---------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
def _load_pe(self, binary: Any, path: Path) -> BinaryInfo:
|
|
636
|
+
optional = getattr(binary, "optional_header", None)
|
|
637
|
+
image_base = self._as_int(getattr(optional, "imagebase", 0))
|
|
638
|
+
entry_rva = self._as_int(getattr(optional, "addressof_entrypoint", 0))
|
|
639
|
+
entry_va = image_base + entry_rva if entry_rva else None
|
|
640
|
+
header = getattr(binary, "header", None)
|
|
641
|
+
machine = str(self._get_attr(header, "machine", "machine_type") or "")
|
|
642
|
+
arch, bitness = self._pe_arch(machine)
|
|
643
|
+
sections = [self._pe_section(s, image_base) for s in self._safe(binary, "sections")]
|
|
644
|
+
imports = self._pe_imports(binary, image_base)
|
|
645
|
+
exports = self._pe_exports(binary, image_base)
|
|
646
|
+
symbols = self._pe_symbols(binary)
|
|
647
|
+
warnings: list[str] = []
|
|
648
|
+
if not exports and not symbols:
|
|
649
|
+
warnings.append(
|
|
650
|
+
"No exports or symbols found; function discovery is limited to the entry point."
|
|
651
|
+
)
|
|
652
|
+
return BinaryInfo(
|
|
653
|
+
path=path,
|
|
654
|
+
format="PE",
|
|
655
|
+
architecture=arch,
|
|
656
|
+
bitness=bitness,
|
|
657
|
+
endianness="little",
|
|
658
|
+
entry_point=entry_va,
|
|
659
|
+
image_base=image_base,
|
|
660
|
+
sha256="",
|
|
661
|
+
file_size=0,
|
|
662
|
+
sections=sections,
|
|
663
|
+
imports=imports,
|
|
664
|
+
exports=exports,
|
|
665
|
+
symbols=symbols,
|
|
666
|
+
is_stripped=not symbols and not exports,
|
|
667
|
+
warnings=warnings,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
@staticmethod
|
|
671
|
+
def _pe_arch(machine: str) -> tuple[str, int]:
|
|
672
|
+
upper = machine.upper()
|
|
673
|
+
if "AMD64" in upper or "X86_64" in upper:
|
|
674
|
+
return "x86_64", 64
|
|
675
|
+
if "I386" in upper or "I686" in upper:
|
|
676
|
+
return "x86", 32
|
|
677
|
+
if "AARCH64" in upper or "ARM64" in upper:
|
|
678
|
+
return "aarch64", 64
|
|
679
|
+
if "ARM" in upper:
|
|
680
|
+
return "arm", 32
|
|
681
|
+
if "IA64" in upper:
|
|
682
|
+
return "ia64", 64
|
|
683
|
+
return "unknown", 0
|
|
684
|
+
|
|
685
|
+
@classmethod
|
|
686
|
+
def _pe_section(cls, section: Any, image_base: int) -> SectionInfo:
|
|
687
|
+
characteristics = cls._as_int(getattr(section, "characteristics", 0))
|
|
688
|
+
entropy = getattr(section, "entropy", None)
|
|
689
|
+
virtual_size = cls._as_int(getattr(section, "virtual_size", 0))
|
|
690
|
+
size = virtual_size or cls._as_int(getattr(section, "size", 0))
|
|
691
|
+
return SectionInfo(
|
|
692
|
+
name=str(getattr(section, "name", "") or ""),
|
|
693
|
+
virtual_address=image_base + cls._as_int(getattr(section, "virtual_address", 0)),
|
|
694
|
+
size=size,
|
|
695
|
+
offset=cls._as_int(getattr(section, "offset", 0)),
|
|
696
|
+
raw_size=cls._as_int(getattr(section, "sizeof_raw_data", 0)),
|
|
697
|
+
entropy=float(entropy) if entropy is not None else None,
|
|
698
|
+
is_executable=bool(characteristics & 0x20000000),
|
|
699
|
+
is_writable=bool(characteristics & 0x80000000),
|
|
700
|
+
evidence=Evidence(
|
|
701
|
+
source="pe.section_table",
|
|
702
|
+
method="section_header",
|
|
703
|
+
confidence=Confidence.CERTAIN,
|
|
704
|
+
address=image_base + cls._as_int(getattr(section, "virtual_address", 0)),
|
|
705
|
+
detail=f"PE section {getattr(section, 'name', '')!r}",
|
|
706
|
+
),
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
@classmethod
|
|
710
|
+
def _pe_imports(cls, binary: Any, image_base: int) -> list[ImportInfo]:
|
|
711
|
+
imports: list[ImportInfo] = []
|
|
712
|
+
for lib in cls._safe(binary, "imports"):
|
|
713
|
+
library = str(getattr(lib, "name", "") or "") or None
|
|
714
|
+
for entry in getattr(lib, "entries", []) or []:
|
|
715
|
+
name = str(getattr(entry, "name", "") or "")
|
|
716
|
+
if not name and getattr(entry, "is_ordinal", False):
|
|
717
|
+
name = f"ordinal_{cls._as_int(getattr(entry, 'ordinal', 0))}"
|
|
718
|
+
if not name:
|
|
719
|
+
continue
|
|
720
|
+
iat = getattr(entry, "iat_address", None)
|
|
721
|
+
address = image_base + cls._as_int(iat) if iat else None
|
|
722
|
+
imports.append(
|
|
723
|
+
ImportInfo(
|
|
724
|
+
name=name,
|
|
725
|
+
library=library,
|
|
726
|
+
address=address,
|
|
727
|
+
evidence=Evidence(
|
|
728
|
+
source="pe.import_table",
|
|
729
|
+
method="import_directory",
|
|
730
|
+
confidence=Confidence.HIGH,
|
|
731
|
+
address=address,
|
|
732
|
+
detail=f"imported from {library or 'unknown module'}",
|
|
733
|
+
),
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
return imports
|
|
737
|
+
|
|
738
|
+
@classmethod
|
|
739
|
+
def _pe_exports(cls, binary: Any, image_base: int) -> list[ExportInfo]:
|
|
740
|
+
exports: list[ExportInfo] = []
|
|
741
|
+
table = None
|
|
742
|
+
getter = getattr(binary, "get_export", None) or getattr(binary, "export", None)
|
|
743
|
+
if callable(getter):
|
|
744
|
+
try:
|
|
745
|
+
table = getter()
|
|
746
|
+
except Exception:
|
|
747
|
+
table = None
|
|
748
|
+
entries = list(getattr(table, "entries", []) or []) if table is not None else []
|
|
749
|
+
if entries:
|
|
750
|
+
for entry in entries:
|
|
751
|
+
name = str(getattr(entry, "name", "") or "")
|
|
752
|
+
ordinal = getattr(entry, "ordinal", None)
|
|
753
|
+
forwarded = bool(getattr(entry, "is_forwarded", False))
|
|
754
|
+
forward_to = cls._pe_forward_target(entry)
|
|
755
|
+
rva = getattr(entry, "function_rva", None)
|
|
756
|
+
if not rva:
|
|
757
|
+
rva = getattr(entry, "address", None)
|
|
758
|
+
address = image_base + cls._as_int(rva) if rva else None
|
|
759
|
+
confidence = Confidence.HIGH if address is not None else Confidence.MEDIUM
|
|
760
|
+
detail = "forwarded export" if forwarded else "exported function"
|
|
761
|
+
evidence = Evidence(
|
|
762
|
+
source="pe.export_table",
|
|
763
|
+
method="export_directory",
|
|
764
|
+
confidence=confidence,
|
|
765
|
+
address=address,
|
|
766
|
+
detail=detail,
|
|
767
|
+
)
|
|
768
|
+
exports.append(
|
|
769
|
+
ExportInfo(
|
|
770
|
+
name=name or f"ordinal_{cls._as_int(ordinal)}",
|
|
771
|
+
address=address,
|
|
772
|
+
ordinal=cls._as_int(ordinal) if ordinal is not None else None,
|
|
773
|
+
forwarded_to=forward_to,
|
|
774
|
+
evidence=evidence,
|
|
775
|
+
)
|
|
776
|
+
)
|
|
777
|
+
return exports
|
|
778
|
+
|
|
779
|
+
# Fallback for LIEF builds that only expose a name list.
|
|
780
|
+
for item in cls._safe(binary, "exported_functions"):
|
|
781
|
+
if isinstance(item, str):
|
|
782
|
+
name, raw_address = item, None
|
|
783
|
+
else:
|
|
784
|
+
name = str(getattr(item, "name", "") or "")
|
|
785
|
+
raw_address = getattr(item, "address", None)
|
|
786
|
+
if not name:
|
|
787
|
+
continue
|
|
788
|
+
address = image_base + cls._as_int(raw_address) if raw_address else None
|
|
789
|
+
evidence = Evidence(
|
|
790
|
+
source="pe.exported_functions",
|
|
791
|
+
method="export_list",
|
|
792
|
+
confidence=Confidence.HIGH if address is not None else Confidence.MEDIUM,
|
|
793
|
+
address=address,
|
|
794
|
+
detail="exported function",
|
|
795
|
+
)
|
|
796
|
+
exports.append(ExportInfo(name=name, address=address, evidence=evidence))
|
|
797
|
+
return exports
|
|
798
|
+
|
|
799
|
+
@staticmethod
|
|
800
|
+
def _pe_forward_target(entry: Any) -> str | None:
|
|
801
|
+
info = getattr(entry, "forward_information", None)
|
|
802
|
+
if info is None:
|
|
803
|
+
return None
|
|
804
|
+
library = getattr(info, "library", None)
|
|
805
|
+
function = getattr(info, "function", None)
|
|
806
|
+
if library and function:
|
|
807
|
+
return f"{library}.{function}"
|
|
808
|
+
return str(info) if info else None
|
|
809
|
+
|
|
810
|
+
@classmethod
|
|
811
|
+
def _pe_symbols(cls, binary: Any) -> list[SymbolInfo]:
|
|
812
|
+
symbols: list[SymbolInfo] = []
|
|
813
|
+
for symbol in cls._safe(binary, "symbols"):
|
|
814
|
+
name = str(getattr(symbol, "name", "") or "")
|
|
815
|
+
if not name:
|
|
816
|
+
continue
|
|
817
|
+
value = cls._as_int(getattr(symbol, "value", 0))
|
|
818
|
+
size = cls._as_int(getattr(symbol, "size", 0))
|
|
819
|
+
is_function = bool(getattr(symbol, "is_function", False))
|
|
820
|
+
evidence = Evidence(
|
|
821
|
+
source="pe.coff_symbols",
|
|
822
|
+
method="symbol_table",
|
|
823
|
+
confidence=Confidence.MEDIUM,
|
|
824
|
+
address=value or None,
|
|
825
|
+
detail="COFF symbol value may be section-relative",
|
|
826
|
+
)
|
|
827
|
+
symbols.append(
|
|
828
|
+
SymbolInfo(
|
|
829
|
+
name=name,
|
|
830
|
+
address=value,
|
|
831
|
+
size=size,
|
|
832
|
+
kind="function" if is_function else "data",
|
|
833
|
+
is_function=is_function,
|
|
834
|
+
evidence=evidence,
|
|
835
|
+
)
|
|
836
|
+
)
|
|
837
|
+
return symbols
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
# ---- ELF --------------------------------------------------------------
|
|
841
|
+
|
|
842
|
+
def _load_elf(self, binary: Any, path: Path) -> BinaryInfo:
|
|
843
|
+
header = getattr(binary, "header", None)
|
|
844
|
+
machine = str(self._get_attr(header, "machine_type", "machine") or "")
|
|
845
|
+
arch, bitness = self._elf_arch(machine)
|
|
846
|
+
class_text = str(self._get_attr(header, "identity_class", "") or "").upper()
|
|
847
|
+
if "ELF64" in class_text:
|
|
848
|
+
bitness = 64
|
|
849
|
+
elif "ELF32" in class_text:
|
|
850
|
+
bitness = 32
|
|
851
|
+
data_text = str(self._get_attr(header, "identity_data", "") or "").upper()
|
|
852
|
+
endianness = "big" if "MSB" in data_text else "little"
|
|
853
|
+
image_base = self._as_int(getattr(binary, "imagebase", 0))
|
|
854
|
+
if image_base < 0 or image_base >= (1 << 63):
|
|
855
|
+
image_base = 0
|
|
856
|
+
entry_point = self._as_int(getattr(binary, "entrypoint", 0)) or None
|
|
857
|
+
sections = [self._elf_section(s) for s in self._safe(binary, "sections")]
|
|
858
|
+
imports = self._elf_imports(binary)
|
|
859
|
+
exports = self._elf_exports(binary)
|
|
860
|
+
symbols = self._elf_symbols(binary)
|
|
861
|
+
warnings: list[str] = []
|
|
862
|
+
if not sections:
|
|
863
|
+
warnings.append("ELF has no section headers; section-based analysis is limited.")
|
|
864
|
+
if not symbols:
|
|
865
|
+
warnings.append("Binary is stripped: no symbol table found.")
|
|
866
|
+
return BinaryInfo(
|
|
867
|
+
path=path,
|
|
868
|
+
format="ELF",
|
|
869
|
+
architecture=arch,
|
|
870
|
+
bitness=bitness,
|
|
871
|
+
endianness=endianness,
|
|
872
|
+
entry_point=entry_point,
|
|
873
|
+
image_base=image_base,
|
|
874
|
+
sha256="",
|
|
875
|
+
file_size=0,
|
|
876
|
+
sections=sections,
|
|
877
|
+
imports=imports,
|
|
878
|
+
exports=exports,
|
|
879
|
+
symbols=symbols,
|
|
880
|
+
is_stripped=not symbols,
|
|
881
|
+
warnings=warnings,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
@staticmethod
|
|
885
|
+
def _elf_arch(machine: str) -> tuple[str, int]:
|
|
886
|
+
upper = machine.upper()
|
|
887
|
+
if "X86_64" in upper or "AMD64" in upper:
|
|
888
|
+
return "x86_64", 64
|
|
889
|
+
if "I386" in upper or upper.endswith("386"):
|
|
890
|
+
return "x86", 32
|
|
891
|
+
if "AARCH64" in upper or "ARM64" in upper:
|
|
892
|
+
return "aarch64", 64
|
|
893
|
+
if "ARM" in upper:
|
|
894
|
+
return "arm", 32
|
|
895
|
+
if "MIPS" in upper:
|
|
896
|
+
return "mips", 0
|
|
897
|
+
return "unknown", 0
|
|
898
|
+
|
|
899
|
+
@classmethod
|
|
900
|
+
def _elf_section(cls, section: Any) -> SectionInfo:
|
|
901
|
+
flags = cls._as_int(getattr(section, "flags", 0))
|
|
902
|
+
entropy = getattr(section, "entropy", None)
|
|
903
|
+
return SectionInfo(
|
|
904
|
+
name=str(getattr(section, "name", "") or ""),
|
|
905
|
+
virtual_address=cls._as_int(getattr(section, "virtual_address", 0)),
|
|
906
|
+
size=cls._as_int(getattr(section, "size", 0)),
|
|
907
|
+
offset=cls._as_int(getattr(section, "offset", 0)),
|
|
908
|
+
raw_size=0,
|
|
909
|
+
entropy=float(entropy) if entropy is not None else None,
|
|
910
|
+
is_executable=bool(flags & 0x4), # SHF_EXECINSTR
|
|
911
|
+
is_writable=bool(flags & 0x1), # SHF_WRITE
|
|
912
|
+
evidence=Evidence(
|
|
913
|
+
source="elf.section_table",
|
|
914
|
+
method="section_header",
|
|
915
|
+
confidence=Confidence.CERTAIN,
|
|
916
|
+
address=cls._as_int(getattr(section, "virtual_address", 0)),
|
|
917
|
+
detail=f"ELF section {getattr(section, 'name', '')!r}",
|
|
918
|
+
),
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
@classmethod
|
|
923
|
+
def _elf_imports(cls, binary: Any) -> list[ImportInfo]:
|
|
924
|
+
imports: list[ImportInfo] = []
|
|
925
|
+
for symbol in cls._safe(binary, "imported_symbols"):
|
|
926
|
+
name = str(getattr(symbol, "name", "") or "")
|
|
927
|
+
if not name:
|
|
928
|
+
continue
|
|
929
|
+
value = cls._as_int(getattr(symbol, "value", 0))
|
|
930
|
+
imports.append(
|
|
931
|
+
ImportInfo(
|
|
932
|
+
name=name,
|
|
933
|
+
address=value or None,
|
|
934
|
+
evidence=Evidence(
|
|
935
|
+
source="elf.dynsym",
|
|
936
|
+
method="imported_symbol",
|
|
937
|
+
confidence=Confidence.HIGH,
|
|
938
|
+
address=value or None,
|
|
939
|
+
detail="imported dynamic symbol",
|
|
940
|
+
),
|
|
941
|
+
)
|
|
942
|
+
)
|
|
943
|
+
return imports
|
|
944
|
+
|
|
945
|
+
@classmethod
|
|
946
|
+
def _elf_exports(cls, binary: Any) -> list[ExportInfo]:
|
|
947
|
+
exports: list[ExportInfo] = []
|
|
948
|
+
for symbol in cls._safe(binary, "exported_symbols"):
|
|
949
|
+
name = str(getattr(symbol, "name", "") or "")
|
|
950
|
+
if not name:
|
|
951
|
+
continue
|
|
952
|
+
value = cls._as_int(getattr(symbol, "value", 0))
|
|
953
|
+
evidence = Evidence(
|
|
954
|
+
source="elf.dynsym",
|
|
955
|
+
method="exported_symbol",
|
|
956
|
+
confidence=Confidence.HIGH if value else Confidence.MEDIUM,
|
|
957
|
+
address=value or None,
|
|
958
|
+
detail="exported dynamic symbol",
|
|
959
|
+
)
|
|
960
|
+
exports.append(ExportInfo(name=name, address=value or None, evidence=evidence))
|
|
961
|
+
return exports
|
|
962
|
+
|
|
963
|
+
@classmethod
|
|
964
|
+
def _elf_symbols(cls, binary: Any) -> list[SymbolInfo]:
|
|
965
|
+
symbols: list[SymbolInfo] = []
|
|
966
|
+
seen: set[tuple[str, int]] = set()
|
|
967
|
+
for symbol in cls._safe(binary, "symbols"):
|
|
968
|
+
name = str(getattr(symbol, "name", "") or "")
|
|
969
|
+
if not name:
|
|
970
|
+
continue
|
|
971
|
+
value = cls._as_int(getattr(symbol, "value", 0))
|
|
972
|
+
key = (name, value)
|
|
973
|
+
if key in seen:
|
|
974
|
+
continue
|
|
975
|
+
seen.add(key)
|
|
976
|
+
size = cls._as_int(getattr(symbol, "size", 0))
|
|
977
|
+
type_text = str(getattr(symbol, "type", "") or "")
|
|
978
|
+
is_function = "FUNC" in type_text.upper()
|
|
979
|
+
evidence = Evidence(
|
|
980
|
+
source="elf.symtab",
|
|
981
|
+
method="symbol_table",
|
|
982
|
+
confidence=Confidence.HIGH,
|
|
983
|
+
address=value or None,
|
|
984
|
+
detail="ELF symbol table entry",
|
|
985
|
+
)
|
|
986
|
+
symbols.append(
|
|
987
|
+
SymbolInfo(
|
|
988
|
+
name=name,
|
|
989
|
+
address=value,
|
|
990
|
+
size=size,
|
|
991
|
+
kind=type_text or "unknown",
|
|
992
|
+
is_function=is_function,
|
|
993
|
+
evidence=evidence,
|
|
994
|
+
)
|
|
995
|
+
)
|
|
996
|
+
return symbols
|
|
997
|
+
|
|
998
|
+
# ---- Mach-O ------------------------------------------------------------
|
|
999
|
+
|
|
1000
|
+
def _load_macho(
|
|
1001
|
+
self,
|
|
1002
|
+
binary: Any,
|
|
1003
|
+
path: Path,
|
|
1004
|
+
*,
|
|
1005
|
+
fat_offset: int = 0,
|
|
1006
|
+
container: FatContainer | None = None,
|
|
1007
|
+
) -> BinaryInfo:
|
|
1008
|
+
header = getattr(binary, "header", None)
|
|
1009
|
+
cpu = str(self._get_attr(header, "cpu_type", "") or "")
|
|
1010
|
+
arch, bitness = self._macho_arch(cpu)
|
|
1011
|
+
if bool(getattr(header, "is_64bit", False)):
|
|
1012
|
+
bitness = 64
|
|
1013
|
+
elif bool(getattr(header, "is_32bit", False)):
|
|
1014
|
+
bitness = 32
|
|
1015
|
+
magic_text = str(self._get_attr(header, "magic", "") or "").upper()
|
|
1016
|
+
endianness = "big" if "CIGAM" in magic_text else "little"
|
|
1017
|
+
image_base = self._as_int(getattr(binary, "imagebase", 0))
|
|
1018
|
+
entry_point = self._as_int(getattr(binary, "entrypoint", 0)) or None
|
|
1019
|
+
# Section offsets from LIEF are relative to the parsed slice; shift them
|
|
1020
|
+
# so they stay valid offsets into the outer fat container.
|
|
1021
|
+
sections = [
|
|
1022
|
+
self._macho_section(s, fat_offset) for s in self._safe(binary, "sections")
|
|
1023
|
+
]
|
|
1024
|
+
imports = self._macho_imports(binary)
|
|
1025
|
+
exports = self._macho_exports(binary)
|
|
1026
|
+
symbols = self._macho_symbols(binary, sections)
|
|
1027
|
+
warnings: list[str] = []
|
|
1028
|
+
if not sections:
|
|
1029
|
+
warnings.append("Mach-O has no sections; section-based analysis is limited.")
|
|
1030
|
+
if not symbols and not exports:
|
|
1031
|
+
warnings.append(
|
|
1032
|
+
"No symbol table or exports found; function discovery is limited to the entry point."
|
|
1033
|
+
)
|
|
1034
|
+
evidence = None
|
|
1035
|
+
if container is not None:
|
|
1036
|
+
chosen = container.slice
|
|
1037
|
+
evidence = Evidence(
|
|
1038
|
+
source="macho.fat_header",
|
|
1039
|
+
method="fat_arch_table",
|
|
1040
|
+
confidence=Confidence.CERTAIN,
|
|
1041
|
+
detail=(
|
|
1042
|
+
f"selected slice {chosen.index} of {container.slice_count} "
|
|
1043
|
+
f"({chosen.arch_name}) from a {container.magic_name} container: "
|
|
1044
|
+
f"{container.reason}"
|
|
1045
|
+
),
|
|
1046
|
+
extra=container.evidence_extra(),
|
|
1047
|
+
)
|
|
1048
|
+
return BinaryInfo(
|
|
1049
|
+
path=path,
|
|
1050
|
+
format="MachO",
|
|
1051
|
+
architecture=arch,
|
|
1052
|
+
bitness=bitness,
|
|
1053
|
+
endianness=endianness,
|
|
1054
|
+
entry_point=entry_point,
|
|
1055
|
+
image_base=image_base,
|
|
1056
|
+
sha256="",
|
|
1057
|
+
file_size=0,
|
|
1058
|
+
sections=sections,
|
|
1059
|
+
imports=imports,
|
|
1060
|
+
exports=exports,
|
|
1061
|
+
symbols=symbols,
|
|
1062
|
+
is_stripped=not symbols and not exports,
|
|
1063
|
+
warnings=warnings,
|
|
1064
|
+
evidence=evidence,
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
@staticmethod
|
|
1068
|
+
def _macho_arch(cpu: str) -> tuple[str, int]:
|
|
1069
|
+
upper = cpu.upper()
|
|
1070
|
+
if "X86_64" in upper:
|
|
1071
|
+
return "x86_64", 64
|
|
1072
|
+
if "ARM64" in upper or "ARM64E" in upper:
|
|
1073
|
+
return "aarch64", 64
|
|
1074
|
+
if "I386" in upper:
|
|
1075
|
+
return "x86", 32
|
|
1076
|
+
if "ARM" in upper:
|
|
1077
|
+
return "arm", 32
|
|
1078
|
+
if "POWERPC64" in upper:
|
|
1079
|
+
return "ppc64", 64
|
|
1080
|
+
if "POWERPC" in upper:
|
|
1081
|
+
return "ppc", 32
|
|
1082
|
+
return "unknown", 0
|
|
1083
|
+
|
|
1084
|
+
@classmethod
|
|
1085
|
+
def _macho_section(cls, section: Any, fat_offset: int = 0) -> SectionInfo:
|
|
1086
|
+
flags = cls._as_int(getattr(section, "flags", 0))
|
|
1087
|
+
offset = cls._as_int(getattr(section, "offset", 0))
|
|
1088
|
+
size = cls._as_int(getattr(section, "size", 0))
|
|
1089
|
+
virtual_address = cls._as_int(getattr(section, "virtual_address", 0))
|
|
1090
|
+
# A slice-relative offset of 0 means "no file bytes" (S_ZEROFILL and
|
|
1091
|
+
# friends); those stay unmapped even inside a fat container.
|
|
1092
|
+
file_offset = offset + fat_offset if offset > 0 else offset
|
|
1093
|
+
segment = None
|
|
1094
|
+
if bool(getattr(section, "has_segment", False)):
|
|
1095
|
+
try:
|
|
1096
|
+
segment = section.segment
|
|
1097
|
+
except Exception:
|
|
1098
|
+
segment = None
|
|
1099
|
+
init_protection = cls._as_int(getattr(segment, "init_protection", 0))
|
|
1100
|
+
entropy = getattr(section, "entropy", None)
|
|
1101
|
+
name = cls._decode_name(cls._get_attr(section, "fullname", "name"))
|
|
1102
|
+
return SectionInfo(
|
|
1103
|
+
name=name,
|
|
1104
|
+
virtual_address=virtual_address,
|
|
1105
|
+
size=size,
|
|
1106
|
+
offset=file_offset,
|
|
1107
|
+
# Sections without file bytes (S_ZEROFILL and friends) have offset 0.
|
|
1108
|
+
raw_size=size if file_offset > 0 else 0,
|
|
1109
|
+
entropy=float(entropy) if entropy is not None else None,
|
|
1110
|
+
# S_ATTR_PURE_INSTRUCTIONS / S_ATTR_SOME_INSTRUCTIONS.
|
|
1111
|
+
is_executable=bool(flags & 0x80000000) or bool(flags & 0x400),
|
|
1112
|
+
# Write permission lives on the segment; vm_prot_t VM_PROT_WRITE == 2.
|
|
1113
|
+
is_writable=bool(init_protection & 0x2),
|
|
1114
|
+
evidence=Evidence(
|
|
1115
|
+
source="macho.section_table",
|
|
1116
|
+
method="section_header",
|
|
1117
|
+
confidence=Confidence.CERTAIN,
|
|
1118
|
+
address=virtual_address,
|
|
1119
|
+
detail=f"Mach-O section {name!r}",
|
|
1120
|
+
),
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
@classmethod
|
|
1124
|
+
def _macho_imports(cls, binary: Any) -> list[ImportInfo]:
|
|
1125
|
+
imports: list[ImportInfo] = []
|
|
1126
|
+
for symbol in cls._safe(binary, "imported_symbols"):
|
|
1127
|
+
name = cls._decode_name(getattr(symbol, "name", None))
|
|
1128
|
+
if not name:
|
|
1129
|
+
continue
|
|
1130
|
+
library_object = cls._get_attr(symbol, "library")
|
|
1131
|
+
library = cls._decode_name(cls._get_attr(library_object, "name")) if library_object is not None else ""
|
|
1132
|
+
value = cls._as_int(getattr(symbol, "value", 0))
|
|
1133
|
+
imports.append(
|
|
1134
|
+
ImportInfo(
|
|
1135
|
+
name=name,
|
|
1136
|
+
library=library or None,
|
|
1137
|
+
address=value or None,
|
|
1138
|
+
evidence=Evidence(
|
|
1139
|
+
source="macho.undefined_symbols",
|
|
1140
|
+
method="imported_symbol",
|
|
1141
|
+
confidence=Confidence.HIGH,
|
|
1142
|
+
address=value or None,
|
|
1143
|
+
detail="undefined symbol bound by dyld at load time",
|
|
1144
|
+
),
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
return imports
|
|
1148
|
+
|
|
1149
|
+
@classmethod
|
|
1150
|
+
def _macho_exports(cls, binary: Any) -> list[ExportInfo]:
|
|
1151
|
+
exports: list[ExportInfo] = []
|
|
1152
|
+
for symbol in cls._safe(binary, "exported_symbols"):
|
|
1153
|
+
name = cls._decode_name(getattr(symbol, "name", None))
|
|
1154
|
+
if not name:
|
|
1155
|
+
continue
|
|
1156
|
+
value = cls._as_int(getattr(symbol, "value", 0))
|
|
1157
|
+
exports.append(
|
|
1158
|
+
ExportInfo(
|
|
1159
|
+
name=name,
|
|
1160
|
+
address=value or None,
|
|
1161
|
+
evidence=Evidence(
|
|
1162
|
+
source="macho.exported_symbols",
|
|
1163
|
+
method="exported_symbol",
|
|
1164
|
+
confidence=Confidence.HIGH if value else Confidence.MEDIUM,
|
|
1165
|
+
address=value or None,
|
|
1166
|
+
detail="symbol with dyld export information",
|
|
1167
|
+
),
|
|
1168
|
+
)
|
|
1169
|
+
)
|
|
1170
|
+
return exports
|
|
1171
|
+
|
|
1172
|
+
@classmethod
|
|
1173
|
+
def _macho_symbols(cls, binary: Any, sections: list[SectionInfo]) -> list[SymbolInfo]:
|
|
1174
|
+
# Mach-O nlist entries do not encode function vs data. Membership in an
|
|
1175
|
+
# executable section is the strongest structural evidence available, so
|
|
1176
|
+
# it drives is_function and is stated explicitly in the evidence detail.
|
|
1177
|
+
exec_ranges = [
|
|
1178
|
+
(s.virtual_address, s.virtual_address + s.size)
|
|
1179
|
+
for s in sections
|
|
1180
|
+
if s.is_executable and s.size > 0
|
|
1181
|
+
]
|
|
1182
|
+
symbols: list[SymbolInfo] = []
|
|
1183
|
+
seen: set[tuple[str, int]] = set()
|
|
1184
|
+
for symbol in cls._safe(binary, "symbols"):
|
|
1185
|
+
name = cls._decode_name(getattr(symbol, "name", None))
|
|
1186
|
+
if not name:
|
|
1187
|
+
continue
|
|
1188
|
+
value = cls._as_int(getattr(symbol, "value", 0))
|
|
1189
|
+
key = (name, value)
|
|
1190
|
+
if key in seen:
|
|
1191
|
+
continue
|
|
1192
|
+
seen.add(key)
|
|
1193
|
+
size = cls._as_int(getattr(symbol, "size", 0))
|
|
1194
|
+
is_function = bool(value) and any(
|
|
1195
|
+
start <= value < end for start, end in exec_ranges
|
|
1196
|
+
)
|
|
1197
|
+
type_text = str(getattr(symbol, "type", "") or "")
|
|
1198
|
+
symbols.append(
|
|
1199
|
+
SymbolInfo(
|
|
1200
|
+
name=name,
|
|
1201
|
+
address=value,
|
|
1202
|
+
size=size,
|
|
1203
|
+
kind=type_text or "unknown",
|
|
1204
|
+
is_function=is_function,
|
|
1205
|
+
evidence=Evidence(
|
|
1206
|
+
source="macho.symbol_table",
|
|
1207
|
+
method="symtab_entry",
|
|
1208
|
+
confidence=Confidence.HIGH,
|
|
1209
|
+
address=value or None,
|
|
1210
|
+
detail=(
|
|
1211
|
+
"function inferred from executable-section membership"
|
|
1212
|
+
if is_function
|
|
1213
|
+
else "symbol kind is not encoded in the Mach-O symbol table"
|
|
1214
|
+
),
|
|
1215
|
+
),
|
|
1216
|
+
)
|
|
1217
|
+
)
|
|
1218
|
+
return symbols
|
|
1219
|
+
|
|
1220
|
+
# ---- DEX ---------------------------------------------------------------
|
|
1221
|
+
|
|
1222
|
+
@staticmethod
|
|
1223
|
+
def _load_dex(path: Path, raw: bytes) -> BinaryInfo:
|
|
1224
|
+
"""Normalize a DEX bytecode container into :class:`BinaryInfo`.
|
|
1225
|
+
|
|
1226
|
+
DEX has no virtual address space and no sections: method addresses are
|
|
1227
|
+
**file offsets**, stated in a loader warning and in every method's
|
|
1228
|
+
evidence. Defined methods (with bytecode) become function symbols;
|
|
1229
|
+
referenced-but-undefined methods (external APIs like ``println``)
|
|
1230
|
+
become imports with the defining class as their ``library``.
|
|
1231
|
+
"""
|
|
1232
|
+
try:
|
|
1233
|
+
dex = lief.DEX.parse(str(path))
|
|
1234
|
+
except Exception as exc:
|
|
1235
|
+
raise BinaryLoadError(f"LIEF could not parse DEX {path}: {exc}") from exc
|
|
1236
|
+
if dex is None:
|
|
1237
|
+
raise BinaryLoadError(f"Unsupported or unreadable DEX: {path}")
|
|
1238
|
+
|
|
1239
|
+
# The endianness lives in the header's endian_tag at offset 0x28
|
|
1240
|
+
# (0x12345678 = little-endian, byte-swapped = big-endian).
|
|
1241
|
+
endian_tag = int.from_bytes(raw[0x28:0x2C], "little")
|
|
1242
|
+
endianness = "big" if endian_tag == 0x78563412 else "little"
|
|
1243
|
+
|
|
1244
|
+
warnings: list[str] = [
|
|
1245
|
+
"DEX container: no virtual address space; symbol addresses are file offsets",
|
|
1246
|
+
"Dalvik bytecode disassembly is not supported yet",
|
|
1247
|
+
]
|
|
1248
|
+
|
|
1249
|
+
symbols: list[SymbolInfo] = []
|
|
1250
|
+
imports: list[ImportInfo] = []
|
|
1251
|
+
for method in dex.methods:
|
|
1252
|
+
class_name = ""
|
|
1253
|
+
if method.has_class and method.cls is not None:
|
|
1254
|
+
class_name = str(method.cls.fullname)
|
|
1255
|
+
qualified = f"{class_name}->{method.name}" if class_name else str(method.name)
|
|
1256
|
+
code_offset = int(getattr(method, "code_offset", 0) or 0)
|
|
1257
|
+
if code_offset > 0:
|
|
1258
|
+
symbols.append(
|
|
1259
|
+
SymbolInfo(
|
|
1260
|
+
name=qualified,
|
|
1261
|
+
address=code_offset,
|
|
1262
|
+
kind="method",
|
|
1263
|
+
is_function=True,
|
|
1264
|
+
evidence=Evidence(
|
|
1265
|
+
source="dex.method_ids",
|
|
1266
|
+
method="class_data",
|
|
1267
|
+
confidence=Confidence.HIGH,
|
|
1268
|
+
address=code_offset,
|
|
1269
|
+
detail=(
|
|
1270
|
+
f"{qualified}: code at file offset 0x{code_offset:x} "
|
|
1271
|
+
"(DEX addresses are file offsets, not VA)"
|
|
1272
|
+
),
|
|
1273
|
+
),
|
|
1274
|
+
)
|
|
1275
|
+
)
|
|
1276
|
+
else:
|
|
1277
|
+
imports.append(
|
|
1278
|
+
ImportInfo(
|
|
1279
|
+
name=qualified,
|
|
1280
|
+
library=class_name or None,
|
|
1281
|
+
evidence=Evidence(
|
|
1282
|
+
source="dex.method_ids",
|
|
1283
|
+
method="method_reference",
|
|
1284
|
+
confidence=Confidence.HIGH,
|
|
1285
|
+
detail=(
|
|
1286
|
+
f"{qualified}: referenced but not defined in this container"
|
|
1287
|
+
),
|
|
1288
|
+
),
|
|
1289
|
+
)
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
return BinaryInfo(
|
|
1293
|
+
path=path,
|
|
1294
|
+
format="DEX",
|
|
1295
|
+
architecture="dalvik",
|
|
1296
|
+
bitness=0,
|
|
1297
|
+
endianness=endianness,
|
|
1298
|
+
entry_point=None,
|
|
1299
|
+
image_base=0,
|
|
1300
|
+
sha256="",
|
|
1301
|
+
file_size=0,
|
|
1302
|
+
sections=[],
|
|
1303
|
+
imports=imports,
|
|
1304
|
+
exports=[],
|
|
1305
|
+
symbols=symbols,
|
|
1306
|
+
is_stripped=not symbols,
|
|
1307
|
+
warnings=warnings,
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
# ---- helpers ----------------------------------------------------------
|
|
1311
|
+
|
|
1312
|
+
@staticmethod
|
|
1313
|
+
def _decode_name(value: Any) -> str:
|
|
1314
|
+
"""Decode a LIEF name that may arrive as bytes or str; strip padding."""
|
|
1315
|
+
if isinstance(value, (bytes, bytearray)):
|
|
1316
|
+
text = bytes(value).decode("utf-8", "replace")
|
|
1317
|
+
else:
|
|
1318
|
+
text = str(value or "")
|
|
1319
|
+
return text.split("\x00", 1)[0].strip()
|
|
1320
|
+
|
|
1321
|
+
@staticmethod
|
|
1322
|
+
def _get_attr(obj: Any, *names: str, default: Any = None) -> Any:
|
|
1323
|
+
for name in names:
|
|
1324
|
+
if obj is not None and hasattr(obj, name):
|
|
1325
|
+
return getattr(obj, name)
|
|
1326
|
+
return default
|
|
1327
|
+
|
|
1328
|
+
@staticmethod
|
|
1329
|
+
def _as_int(value: Any) -> int:
|
|
1330
|
+
if value is None:
|
|
1331
|
+
return 0
|
|
1332
|
+
try:
|
|
1333
|
+
return int(value)
|
|
1334
|
+
except (TypeError, ValueError):
|
|
1335
|
+
return 0
|
|
1336
|
+
|
|
1337
|
+
@staticmethod
|
|
1338
|
+
def _safe(binary: Any, attribute: str) -> list[Any]:
|
|
1339
|
+
try:
|
|
1340
|
+
value = getattr(binary, attribute)
|
|
1341
|
+
except Exception:
|
|
1342
|
+
return []
|
|
1343
|
+
if value is None:
|
|
1344
|
+
return []
|
|
1345
|
+
try:
|
|
1346
|
+
return list(value)
|
|
1347
|
+
except TypeError:
|
|
1348
|
+
return []
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
|