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
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""Vtable discovery and thunk-chain resolution (indirect xref heuristics).
|
|
2
|
+
|
|
3
|
+
A C++ virtual table is a run of pointer-sized slots in a non-executable data
|
|
4
|
+
section where every slot holds the address of a function. Neither fact alone
|
|
5
|
+
proves a vtable, so the discovery here is an explicitly **medium**-confidence
|
|
6
|
+
heuristic (source ``vtable.candidate`` / ``vtable.slot_ref``), never merged
|
|
7
|
+
into the certain xref stream: a data run of code pointers is also what a
|
|
8
|
+
hand-built callback array looks like.
|
|
9
|
+
|
|
10
|
+
Thunk resolution rides on top. Many slot targets are one-instruction
|
|
11
|
+
tail-call stubs (``jmp``/``b <target>``) that forward to the real override.
|
|
12
|
+
Following the chain recovers the final callee; every hop is a direct operand
|
|
13
|
+
read, but the *judgment* that a one-instruction function is a thunk is
|
|
14
|
+
heuristic, so the resolved target is reported separately (``resolved_target``
|
|
15
|
+
plus ``vtable.thunk_chain`` evidence at medium confidence) instead of
|
|
16
|
+
rewriting the slot target.
|
|
17
|
+
|
|
18
|
+
The analysed file is only ever read from disk; nothing is executed.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Literal
|
|
26
|
+
|
|
27
|
+
from codevanta.core.disassembler import Disassembler, DisassemblyError
|
|
28
|
+
from codevanta.core.loader import BinaryInfo
|
|
29
|
+
from codevanta.core.report import AnalysisError, read_code
|
|
30
|
+
from codevanta.evidence.confidence import Confidence
|
|
31
|
+
from codevanta.evidence.models import Evidence
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"DEFAULT_MAX_SLOTS",
|
|
35
|
+
"DEFAULT_MAX_TABLES",
|
|
36
|
+
"DEFAULT_MIN_SLOTS",
|
|
37
|
+
"MAX_SLOTS_LIMIT",
|
|
38
|
+
"VtableCandidate",
|
|
39
|
+
"VtableScanResult",
|
|
40
|
+
"VtableSlot",
|
|
41
|
+
"find_vtables",
|
|
42
|
+
"resolve_thunk",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
#: Fewest consecutive code pointers for a run to count as a vtable candidate.
|
|
46
|
+
DEFAULT_MIN_SLOTS = 3
|
|
47
|
+
|
|
48
|
+
#: Default cap on slots decoded per candidate table.
|
|
49
|
+
DEFAULT_MAX_SLOTS = 256
|
|
50
|
+
|
|
51
|
+
#: Hard ceiling accepted for ``max_slots`` (bounded-resource rule).
|
|
52
|
+
MAX_SLOTS_LIMIT = 4_096
|
|
53
|
+
|
|
54
|
+
#: Default cap on candidate tables in scan mode.
|
|
55
|
+
DEFAULT_MAX_TABLES = 64
|
|
56
|
+
|
|
57
|
+
#: Default cap on bytes read per scanned section.
|
|
58
|
+
DEFAULT_MAX_SCAN_BYTES = 1_000_000
|
|
59
|
+
|
|
60
|
+
#: Hard ceiling accepted for ``max_scan_bytes`` per section.
|
|
61
|
+
MAX_SCAN_BYTES_LIMIT = 16_000_000
|
|
62
|
+
|
|
63
|
+
#: One-instruction mnemonics that mean "tail-call forwarding" to this layer.
|
|
64
|
+
_THUNK_MNEMONICS = frozenset({"jmp", "b"})
|
|
65
|
+
|
|
66
|
+
_SOURCE = "vtable.candidate"
|
|
67
|
+
_SLOT_SOURCE = "vtable.slot_ref"
|
|
68
|
+
_THUNK_SOURCE = "vtable.thunk_chain"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class VtableSlot:
|
|
73
|
+
"""One decoded slot: a data word that points into executable code."""
|
|
74
|
+
|
|
75
|
+
index: int
|
|
76
|
+
slot_address: int
|
|
77
|
+
target: int
|
|
78
|
+
resolved_target: int | None
|
|
79
|
+
evidence: Evidence
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict[str, Any]:
|
|
82
|
+
data: dict[str, Any] = {
|
|
83
|
+
"index": self.index,
|
|
84
|
+
"slot_address": f"0x{self.slot_address:x}",
|
|
85
|
+
"target": f"0x{self.target:x}",
|
|
86
|
+
"evidence": self.evidence.to_dict(),
|
|
87
|
+
}
|
|
88
|
+
if self.resolved_target is not None:
|
|
89
|
+
data["resolved_target"] = f"0x{self.resolved_target:x}"
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class VtableCandidate:
|
|
95
|
+
"""One discovered run of consecutive code pointers.
|
|
96
|
+
|
|
97
|
+
``header_slots`` records how many leading non-code slots (the Itanium
|
|
98
|
+
layout's offset-to-top and RTTI pointer) were skipped before the run.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
address: int
|
|
102
|
+
slots: list[VtableSlot]
|
|
103
|
+
slot_size: int
|
|
104
|
+
section_name: str
|
|
105
|
+
header_slots: int
|
|
106
|
+
truncated: bool
|
|
107
|
+
evidence: Evidence
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> dict[str, Any]:
|
|
110
|
+
return {
|
|
111
|
+
"address": f"0x{self.address:x}",
|
|
112
|
+
"slot_count": len(self.slots),
|
|
113
|
+
"slot_size": self.slot_size,
|
|
114
|
+
"section": self.section_name,
|
|
115
|
+
"header_slots": self.header_slots,
|
|
116
|
+
"truncated": self.truncated,
|
|
117
|
+
"slots": [slot.to_dict() for slot in self.slots],
|
|
118
|
+
"evidence": self.evidence.to_dict(),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class VtableScanResult:
|
|
124
|
+
"""Outcome of one discovery run (address mode or whole-binary scan)."""
|
|
125
|
+
|
|
126
|
+
tables: list[VtableCandidate]
|
|
127
|
+
scanned_sections: list[str]
|
|
128
|
+
scanned_bytes: int
|
|
129
|
+
truncated: bool
|
|
130
|
+
evidence: Evidence
|
|
131
|
+
warnings: tuple[str, ...] = ()
|
|
132
|
+
|
|
133
|
+
def to_dict(self) -> dict[str, Any]:
|
|
134
|
+
return {
|
|
135
|
+
"table_count": len(self.tables),
|
|
136
|
+
"tables": [table.to_dict() for table in self.tables],
|
|
137
|
+
"scanned_sections": list(self.scanned_sections),
|
|
138
|
+
"scanned_bytes": self.scanned_bytes,
|
|
139
|
+
"truncated": self.truncated,
|
|
140
|
+
"warnings": list(self.warnings),
|
|
141
|
+
"evidence": self.evidence.to_dict(),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _byteorder(info: BinaryInfo) -> Literal["little", "big"]:
|
|
146
|
+
text = str(getattr(info, "endianness", "") or "").strip().lower()
|
|
147
|
+
return "big" if text.startswith(("big", "msb", "be")) else "little"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _relocation_overlay(path: str | Path) -> dict[int, int]:
|
|
151
|
+
"""Map slot VA -> value for position-independent pointer fills.
|
|
152
|
+
|
|
153
|
+
PIE/shared objects keep pointer slots zero in the file and fill them at
|
|
154
|
+
load time through dynamic relocations whose ``addend`` is the target VA.
|
|
155
|
+
LIEF's relocation table gives that mapping directly, so static analysis
|
|
156
|
+
can see the pointers a loader would create. Only symbol-less relocations
|
|
157
|
+
with a non-zero addend are used (``R_*_RELATIVE`` semantics across
|
|
158
|
+
architectures); anything else is not a pointer fill and stays untouched.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
import lief
|
|
162
|
+
|
|
163
|
+
binary = lief.parse(str(path))
|
|
164
|
+
except Exception:
|
|
165
|
+
return {}
|
|
166
|
+
if binary is None:
|
|
167
|
+
return {}
|
|
168
|
+
overlay: dict[int, int] = {}
|
|
169
|
+
for relocation in getattr(binary, "relocations", []) or []:
|
|
170
|
+
symbol = getattr(relocation, "symbol", None)
|
|
171
|
+
if symbol is not None and getattr(symbol, "name", None):
|
|
172
|
+
continue
|
|
173
|
+
addend = int(getattr(relocation, "addend", 0) or 0)
|
|
174
|
+
if addend <= 0:
|
|
175
|
+
continue
|
|
176
|
+
slot_address = int(getattr(relocation, "address", 0) or 0)
|
|
177
|
+
if slot_address > 0:
|
|
178
|
+
overlay[slot_address] = addend
|
|
179
|
+
return overlay
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _pointer_size(info: BinaryInfo) -> int:
|
|
183
|
+
return 8 if info.bitness >= 64 else 4
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _target_in_executable(info: BinaryInfo, target: int) -> bool:
|
|
187
|
+
section = info.section_for_va(target)
|
|
188
|
+
return section is not None and section.is_executable
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _new_disassembler(info: BinaryInfo) -> Disassembler | None:
|
|
192
|
+
try:
|
|
193
|
+
return Disassembler(info.architecture, info.bitness, info.endianness)
|
|
194
|
+
except DisassemblyError:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _read_slots(
|
|
199
|
+
path: str | Path,
|
|
200
|
+
info: BinaryInfo,
|
|
201
|
+
address: int,
|
|
202
|
+
*,
|
|
203
|
+
pointer_size: int,
|
|
204
|
+
byteorder: Literal["little", "big"],
|
|
205
|
+
min_slots: int,
|
|
206
|
+
max_slots: int,
|
|
207
|
+
resolve_thunks: bool,
|
|
208
|
+
max_thunk_depth: int,
|
|
209
|
+
disassembler: Disassembler | None,
|
|
210
|
+
section_name: str,
|
|
211
|
+
overlay: dict[int, int] | None = None,
|
|
212
|
+
skip_header: bool = False,
|
|
213
|
+
) -> tuple[list[VtableSlot], int] | None:
|
|
214
|
+
"""Read consecutive code-pointer slots from ``address``.
|
|
215
|
+
|
|
216
|
+
With ``skip_header`` (address mode), up to two leading non-code slots —
|
|
217
|
+
the Itanium layout's offset-to-top and RTTI pointer — are skipped before
|
|
218
|
+
the run must start. ``overlay`` supplies relocation-filled pointer values
|
|
219
|
+
for PIE files whose slots are zero on disk. Returns ``None`` when fewer
|
|
220
|
+
than ``min_slots`` valid slots were found, else the decoded slots and the
|
|
221
|
+
number of skipped header slots.
|
|
222
|
+
"""
|
|
223
|
+
slots: list[VtableSlot] = []
|
|
224
|
+
cursor = address
|
|
225
|
+
header_slots = 0
|
|
226
|
+
while len(slots) < max_slots:
|
|
227
|
+
try:
|
|
228
|
+
raw = read_code(path, info, cursor, pointer_size)
|
|
229
|
+
except (AnalysisError, OSError):
|
|
230
|
+
break
|
|
231
|
+
if len(raw) < pointer_size:
|
|
232
|
+
break
|
|
233
|
+
value = int.from_bytes(raw, byteorder)
|
|
234
|
+
relocated = bool(overlay and cursor in overlay)
|
|
235
|
+
if relocated:
|
|
236
|
+
value = int(overlay[cursor]) # type: ignore[index]
|
|
237
|
+
is_code = value != 0 and _target_in_executable(info, value)
|
|
238
|
+
if not is_code and skip_header and not slots and header_slots < 2:
|
|
239
|
+
header_slots += 1
|
|
240
|
+
cursor += pointer_size
|
|
241
|
+
continue
|
|
242
|
+
if not is_code:
|
|
243
|
+
break
|
|
244
|
+
resolved: int | None = None
|
|
245
|
+
if resolve_thunks and disassembler is not None:
|
|
246
|
+
resolved = resolve_thunk(
|
|
247
|
+
path,
|
|
248
|
+
info,
|
|
249
|
+
value,
|
|
250
|
+
disassembler=disassembler,
|
|
251
|
+
max_depth=max_thunk_depth,
|
|
252
|
+
)
|
|
253
|
+
if resolved == value:
|
|
254
|
+
resolved = None # no forwarding hop: not a thunk
|
|
255
|
+
slots.append(
|
|
256
|
+
VtableSlot(
|
|
257
|
+
index=len(slots),
|
|
258
|
+
slot_address=cursor,
|
|
259
|
+
target=value,
|
|
260
|
+
resolved_target=resolved,
|
|
261
|
+
evidence=Evidence(
|
|
262
|
+
source=_SLOT_SOURCE,
|
|
263
|
+
method="pointer_array_run",
|
|
264
|
+
confidence=Confidence.MEDIUM,
|
|
265
|
+
address=cursor,
|
|
266
|
+
detail=(
|
|
267
|
+
f"slot {len(slots)} points at code 0x{value:x}"
|
|
268
|
+
+ (" via dynamic relocation" if relocated else "")
|
|
269
|
+
+ (f" via thunk chain to 0x{resolved:x}" if resolved else "")
|
|
270
|
+
+ "; data-pointer run, not proof of a vtable"
|
|
271
|
+
),
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
cursor += pointer_size
|
|
276
|
+
if len(slots) < min_slots:
|
|
277
|
+
return None
|
|
278
|
+
return slots, header_slots
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def resolve_thunk(
|
|
282
|
+
path: str | Path,
|
|
283
|
+
info: BinaryInfo,
|
|
284
|
+
address: int,
|
|
285
|
+
*,
|
|
286
|
+
disassembler: Disassembler | None = None,
|
|
287
|
+
max_depth: int = 4,
|
|
288
|
+
) -> int | None:
|
|
289
|
+
"""Follow a tail-call chain from ``address``.
|
|
290
|
+
|
|
291
|
+
A thunk here means the *first* instruction at ``address`` is a direct
|
|
292
|
+
``jmp``/``b <target>`` — real binaries pack these stubs tightly, so no
|
|
293
|
+
claim is made about what follows. The final non-thunk address is returned,
|
|
294
|
+
or ``None`` when ``address`` does not start with a direct tail-call. The
|
|
295
|
+
heuristic judgment (one branch = thunk) lives in the caller's evidence,
|
|
296
|
+
not in this return value.
|
|
297
|
+
"""
|
|
298
|
+
if max_depth < 1:
|
|
299
|
+
raise AnalysisError("max_depth must be >= 1")
|
|
300
|
+
disassembler = disassembler or _new_disassembler(info)
|
|
301
|
+
if disassembler is None:
|
|
302
|
+
return None
|
|
303
|
+
current = address
|
|
304
|
+
hops = 0
|
|
305
|
+
while hops < max_depth:
|
|
306
|
+
try:
|
|
307
|
+
code = read_code(path, info, current, 16)
|
|
308
|
+
except (AnalysisError, OSError):
|
|
309
|
+
break
|
|
310
|
+
if not code:
|
|
311
|
+
break
|
|
312
|
+
try:
|
|
313
|
+
result = disassembler.disassemble(code, base_address=current, max_instructions=1)
|
|
314
|
+
except DisassemblyError:
|
|
315
|
+
break
|
|
316
|
+
if not result.instructions:
|
|
317
|
+
break
|
|
318
|
+
only = result.instructions[0]
|
|
319
|
+
mnemonic = only.mnemonic.strip().lower()
|
|
320
|
+
operand = only.operands.strip().lstrip("#")
|
|
321
|
+
if (
|
|
322
|
+
mnemonic not in _THUNK_MNEMONICS
|
|
323
|
+
or not operand.startswith("0x")
|
|
324
|
+
or any(ch in operand for ch in "[]+:")
|
|
325
|
+
):
|
|
326
|
+
break # not a forwarding hop: this address is the chain's end
|
|
327
|
+
try:
|
|
328
|
+
target = int(operand, 16)
|
|
329
|
+
except ValueError:
|
|
330
|
+
break
|
|
331
|
+
if not _target_in_executable(info, target) or target == current:
|
|
332
|
+
break
|
|
333
|
+
hops += 1
|
|
334
|
+
current = target
|
|
335
|
+
return current if hops > 0 else None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def find_vtables(
|
|
339
|
+
path: str | Path,
|
|
340
|
+
info: BinaryInfo,
|
|
341
|
+
*,
|
|
342
|
+
address: int | None = None,
|
|
343
|
+
min_slots: int = DEFAULT_MIN_SLOTS,
|
|
344
|
+
max_slots: int = DEFAULT_MAX_SLOTS,
|
|
345
|
+
max_tables: int = DEFAULT_MAX_TABLES,
|
|
346
|
+
max_scan_bytes: int = DEFAULT_MAX_SCAN_BYTES,
|
|
347
|
+
resolve_thunks: bool = True,
|
|
348
|
+
max_thunk_depth: int = 4,
|
|
349
|
+
) -> VtableScanResult:
|
|
350
|
+
"""Discover vtable candidates at ``address`` or across data sections.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
path: File the metadata in ``info`` was loaded from (read-only).
|
|
354
|
+
info: Normalized binary metadata.
|
|
355
|
+
address: Exact vtable address to decode; ``None`` sweeps every
|
|
356
|
+
non-executable section with file bytes for candidate runs.
|
|
357
|
+
min_slots: Fewest consecutive code pointers that count as a table.
|
|
358
|
+
max_slots: Cap on slots decoded per candidate.
|
|
359
|
+
max_tables: Cap on candidates in scan mode.
|
|
360
|
+
max_scan_bytes: Cap on bytes read per scanned section.
|
|
361
|
+
resolve_thunks: Follow one-instruction tail-call chains from slot
|
|
362
|
+
targets and report the final callee as ``resolved_target``.
|
|
363
|
+
max_thunk_depth: Cap on thunk-chain hops.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
A :class:`VtableScanResult` with candidate tables in address order.
|
|
367
|
+
|
|
368
|
+
Raises:
|
|
369
|
+
AnalysisError: If any bound is out of range, or address mode found no
|
|
370
|
+
plausible table at ``address``.
|
|
371
|
+
"""
|
|
372
|
+
if min_slots < 2:
|
|
373
|
+
raise AnalysisError("min_slots must be >= 2")
|
|
374
|
+
if max_slots < min_slots:
|
|
375
|
+
raise AnalysisError("max_slots must be >= min_slots")
|
|
376
|
+
if max_slots > MAX_SLOTS_LIMIT:
|
|
377
|
+
raise AnalysisError(f"max_slots too large (limit {MAX_SLOTS_LIMIT})")
|
|
378
|
+
if max_tables < 1 or max_tables > 1024:
|
|
379
|
+
raise AnalysisError("max_tables must be between 1 and 1024")
|
|
380
|
+
if max_scan_bytes < pointer_size_of(info):
|
|
381
|
+
raise AnalysisError("max_scan_bytes is smaller than one pointer")
|
|
382
|
+
if max_scan_bytes > MAX_SCAN_BYTES_LIMIT:
|
|
383
|
+
raise AnalysisError(f"max_scan_bytes too large (limit {MAX_SCAN_BYTES_LIMIT})")
|
|
384
|
+
if max_thunk_depth < 1 or max_thunk_depth > 16:
|
|
385
|
+
raise AnalysisError("max_thunk_depth must be between 1 and 16")
|
|
386
|
+
|
|
387
|
+
byteorder = _byteorder(info)
|
|
388
|
+
pointer_size = pointer_size_of(info)
|
|
389
|
+
overlay = _relocation_overlay(path)
|
|
390
|
+
disassembler: Disassembler | None = None
|
|
391
|
+
if resolve_thunks:
|
|
392
|
+
try:
|
|
393
|
+
disassembler = Disassembler(info.architecture, info.bitness, info.endianness)
|
|
394
|
+
except DisassemblyError as exc:
|
|
395
|
+
return VtableScanResult(
|
|
396
|
+
tables=[],
|
|
397
|
+
scanned_sections=[],
|
|
398
|
+
scanned_bytes=0,
|
|
399
|
+
truncated=False,
|
|
400
|
+
evidence=Evidence(
|
|
401
|
+
source=_SOURCE,
|
|
402
|
+
method="pointer_array_scan",
|
|
403
|
+
confidence=Confidence.MEDIUM,
|
|
404
|
+
detail=f"vtable scan unavailable: {exc}",
|
|
405
|
+
),
|
|
406
|
+
warnings=(f"disassembly unavailable: {exc}",),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
scanned_sections: list[str] = []
|
|
410
|
+
scanned_bytes = 0
|
|
411
|
+
truncated = False
|
|
412
|
+
warnings: list[str] = []
|
|
413
|
+
tables: list[VtableCandidate] = []
|
|
414
|
+
|
|
415
|
+
if address is not None:
|
|
416
|
+
section = info.section_for_va(address)
|
|
417
|
+
if section is None:
|
|
418
|
+
raise AnalysisError(f"address 0x{address:x} is not mapped")
|
|
419
|
+
read = _read_slots(
|
|
420
|
+
path,
|
|
421
|
+
info,
|
|
422
|
+
address,
|
|
423
|
+
pointer_size=pointer_size,
|
|
424
|
+
byteorder=byteorder,
|
|
425
|
+
min_slots=min_slots,
|
|
426
|
+
max_slots=max_slots,
|
|
427
|
+
resolve_thunks=resolve_thunks,
|
|
428
|
+
max_thunk_depth=max_thunk_depth,
|
|
429
|
+
disassembler=disassembler,
|
|
430
|
+
section_name=section.name,
|
|
431
|
+
overlay=overlay,
|
|
432
|
+
skip_header=True,
|
|
433
|
+
)
|
|
434
|
+
if read is None:
|
|
435
|
+
raise AnalysisError(
|
|
436
|
+
f"address 0x{address:x} does not start a plausible vtable "
|
|
437
|
+
f"(fewer than {min_slots} consecutive code pointers within the "
|
|
438
|
+
"2-slot Itanium header window)"
|
|
439
|
+
)
|
|
440
|
+
slots, header_slots = read
|
|
441
|
+
truncated = len(slots) >= max_slots
|
|
442
|
+
tables.append(
|
|
443
|
+
VtableCandidate(
|
|
444
|
+
address=address,
|
|
445
|
+
slots=slots,
|
|
446
|
+
slot_size=pointer_size,
|
|
447
|
+
section_name=section.name,
|
|
448
|
+
header_slots=header_slots,
|
|
449
|
+
truncated=truncated,
|
|
450
|
+
evidence=Evidence(
|
|
451
|
+
source=_SOURCE,
|
|
452
|
+
method="pointer_array_scan",
|
|
453
|
+
confidence=Confidence.MEDIUM,
|
|
454
|
+
address=address,
|
|
455
|
+
detail=(
|
|
456
|
+
f"{len(slots)} slot(s) at caller-supplied address "
|
|
457
|
+
f"({header_slots} header slot(s) skipped); "
|
|
458
|
+
"heuristic candidate, not proof"
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
scanned_sections.append(section.name)
|
|
464
|
+
scanned_bytes = (len(slots) + header_slots) * pointer_size
|
|
465
|
+
else:
|
|
466
|
+
data_sections = [
|
|
467
|
+
section
|
|
468
|
+
for section in info.sections
|
|
469
|
+
if not section.is_executable
|
|
470
|
+
and section.size > 0
|
|
471
|
+
and (section.raw_size > 0 or info.format == "ELF")
|
|
472
|
+
]
|
|
473
|
+
for section in data_sections:
|
|
474
|
+
if len(tables) >= max_tables:
|
|
475
|
+
truncated = True
|
|
476
|
+
warnings.append(f"table limit {max_tables} reached; scan stopped early")
|
|
477
|
+
break
|
|
478
|
+
budget = min(max_scan_bytes, section.size)
|
|
479
|
+
try:
|
|
480
|
+
data = read_code(path, info, section.virtual_address, budget)
|
|
481
|
+
except (AnalysisError, OSError) as exc:
|
|
482
|
+
warnings.append(
|
|
483
|
+
f"section {section.name!r}: cannot read bytes ({exc})"
|
|
484
|
+
)
|
|
485
|
+
continue
|
|
486
|
+
if not data:
|
|
487
|
+
continue
|
|
488
|
+
scanned_sections.append(section.name)
|
|
489
|
+
scanned_bytes += len(data)
|
|
490
|
+
step = pointer_size
|
|
491
|
+
run_start: int | None = None
|
|
492
|
+
run_length = 0
|
|
493
|
+
for offset in range(0, len(data) - step + 1, step):
|
|
494
|
+
target_va = section.virtual_address + offset
|
|
495
|
+
value = int.from_bytes(data[offset : offset + step], byteorder)
|
|
496
|
+
if target_va in overlay:
|
|
497
|
+
value = overlay[target_va]
|
|
498
|
+
valid = value != 0 and _target_in_executable(info, value)
|
|
499
|
+
if valid:
|
|
500
|
+
if run_start is None:
|
|
501
|
+
run_start = target_va
|
|
502
|
+
run_length += 1
|
|
503
|
+
continue
|
|
504
|
+
if run_start is not None and run_length >= min_slots:
|
|
505
|
+
read = _read_slots(
|
|
506
|
+
path,
|
|
507
|
+
info,
|
|
508
|
+
run_start,
|
|
509
|
+
pointer_size=pointer_size,
|
|
510
|
+
byteorder=byteorder,
|
|
511
|
+
min_slots=min_slots,
|
|
512
|
+
max_slots=max_slots,
|
|
513
|
+
resolve_thunks=resolve_thunks,
|
|
514
|
+
max_thunk_depth=max_thunk_depth,
|
|
515
|
+
disassembler=disassembler,
|
|
516
|
+
section_name=section.name,
|
|
517
|
+
overlay=overlay,
|
|
518
|
+
)
|
|
519
|
+
if read is not None:
|
|
520
|
+
slots, _header = read
|
|
521
|
+
if len(tables) >= max_tables:
|
|
522
|
+
truncated = True
|
|
523
|
+
warnings.append(
|
|
524
|
+
f"table limit {max_tables} reached; scan stopped early"
|
|
525
|
+
)
|
|
526
|
+
break
|
|
527
|
+
truncated = truncated or len(slots) >= max_slots
|
|
528
|
+
tables.append(
|
|
529
|
+
VtableCandidate(
|
|
530
|
+
address=run_start,
|
|
531
|
+
slots=slots,
|
|
532
|
+
slot_size=pointer_size,
|
|
533
|
+
section_name=section.name,
|
|
534
|
+
header_slots=0,
|
|
535
|
+
truncated=truncated,
|
|
536
|
+
evidence=Evidence(
|
|
537
|
+
source=_SOURCE,
|
|
538
|
+
method="pointer_array_scan",
|
|
539
|
+
confidence=Confidence.MEDIUM,
|
|
540
|
+
address=run_start,
|
|
541
|
+
detail=(
|
|
542
|
+
f"{len(slots)} consecutive code pointer(s) in "
|
|
543
|
+
f"{section.name!r}; heuristic candidate, not proof"
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
run_start = None
|
|
549
|
+
run_length = 0
|
|
550
|
+
|
|
551
|
+
tables.sort(key=lambda table: table.address)
|
|
552
|
+
return VtableScanResult(
|
|
553
|
+
tables=tables,
|
|
554
|
+
scanned_sections=scanned_sections,
|
|
555
|
+
scanned_bytes=scanned_bytes,
|
|
556
|
+
truncated=truncated,
|
|
557
|
+
evidence=Evidence(
|
|
558
|
+
source=_SOURCE,
|
|
559
|
+
method="pointer_array_scan",
|
|
560
|
+
confidence=Confidence.MEDIUM,
|
|
561
|
+
detail=(
|
|
562
|
+
f"{len(tables)} vtable candidate(s) across {len(scanned_sections)} "
|
|
563
|
+
"data section(s); runs of code pointers are a heuristic, not proof"
|
|
564
|
+
),
|
|
565
|
+
),
|
|
566
|
+
warnings=tuple(warnings),
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def pointer_size_of(info: BinaryInfo) -> int:
|
|
571
|
+
"""Public alias for the pointer-size helper used by callers and tests."""
|
|
572
|
+
return 8 if info.bitness >= 64 else 4
|