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,546 @@
|
|
|
1
|
+
"""Jump-table resolution for absolute pointer arrays and signed 32-bit offsets.
|
|
2
|
+
|
|
3
|
+
A jump table is a run of fixed-width slots that a dispatcher indexes to reach a
|
|
4
|
+
case target. Two layouts are supported, selected by ``mode``:
|
|
5
|
+
|
|
6
|
+
* ``"absolute"`` (the default) -- each slot is a ``pointer_size``-wide absolute
|
|
7
|
+
virtual address a dispatcher branches to.
|
|
8
|
+
* ``"relative"`` -- each slot is a *signed 32-bit* offset added to a
|
|
9
|
+
caller-supplied ``base_address``, so ``target = base_address + offset``. This
|
|
10
|
+
is the shape compilers emit for position-independent switch tables, where the
|
|
11
|
+
slots stay valid under relocation because they store deltas, not addresses.
|
|
12
|
+
|
|
13
|
+
Both modes share the same guarantees:
|
|
14
|
+
|
|
15
|
+
* Every slot is decoded with :func:`codevanta.core.report.read_code`, so all
|
|
16
|
+
reads are bounded by the containing section's file bytes and nothing is ever
|
|
17
|
+
mapped or executed. The whole window is ``max_entries * pointer_size`` bytes
|
|
18
|
+
and the table address is validated against the binary map before a byte is
|
|
19
|
+
read.
|
|
20
|
+
* The scan stops at the first slot that is *not* a usable target: a value
|
|
21
|
+
outside the caller's ``min_target``/``max_target`` window, a target no section
|
|
22
|
+
maps, or -- when ``executable_only`` is true -- a target inside a
|
|
23
|
+
non-executable section. Every such stop is recorded in
|
|
24
|
+
:attr:`JumpTableResult.warnings`, so a caller can tell a short table from one
|
|
25
|
+
that ran into something unexpected.
|
|
26
|
+
* In absolute mode a zero pointer (the conventional table terminator) also ends
|
|
27
|
+
the scan. In relative mode a zero offset is *not* a terminator: it is a
|
|
28
|
+
legitimate delta that resolves to ``base_address``, so only the rules above
|
|
29
|
+
can end such a table.
|
|
30
|
+
* ``truncated`` is true only when the entry ceiling was reached *and* a further
|
|
31
|
+
slot was still usable: in absolute mode the next slot still holds a non-zero
|
|
32
|
+
pointer, in relative mode the next slot decodes to a target that passes the
|
|
33
|
+
same stop rules.
|
|
34
|
+
|
|
35
|
+
Entries carry :attr:`codevanta.evidence.confidence.Confidence.MEDIUM`: a run of
|
|
36
|
+
same-width slots found by shape is evidence of a dispatch table, not proof of
|
|
37
|
+
one, and nothing here shows that a dispatcher actually indexes the array.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Any, Literal
|
|
45
|
+
|
|
46
|
+
from codevanta.core.loader import BinaryInfo, SectionInfo
|
|
47
|
+
from codevanta.core.report import AnalysisError, read_code
|
|
48
|
+
from codevanta.evidence.confidence import Confidence
|
|
49
|
+
from codevanta.evidence.models import Evidence
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"DEFAULT_MAX_ENTRIES",
|
|
53
|
+
"EVIDENCE_METHOD",
|
|
54
|
+
"EVIDENCE_METHOD_RELATIVE",
|
|
55
|
+
"EVIDENCE_SOURCE",
|
|
56
|
+
"EVIDENCE_SOURCE_RELATIVE",
|
|
57
|
+
"MAX_ENTRIES",
|
|
58
|
+
"MODES",
|
|
59
|
+
"MODE_ABSOLUTE",
|
|
60
|
+
"MODE_RELATIVE",
|
|
61
|
+
"RELATIVE_OFFSET_SIZE",
|
|
62
|
+
"JumpTableEntry",
|
|
63
|
+
"JumpTableResult",
|
|
64
|
+
"resolve_jump_table",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
#: Default number of slots a single resolution will decode.
|
|
68
|
+
DEFAULT_MAX_ENTRIES = 256
|
|
69
|
+
|
|
70
|
+
#: Hard ceiling on ``max_entries`` (bounded work: at most 32 KiB per call).
|
|
71
|
+
MAX_ENTRIES = 4096
|
|
72
|
+
|
|
73
|
+
#: Pointer widths accepted for ``pointer_size`` in absolute mode.
|
|
74
|
+
_ALLOWED_POINTER_SIZES = (4, 8)
|
|
75
|
+
|
|
76
|
+
#: Table layout used when each slot is an absolute virtual address.
|
|
77
|
+
MODE_ABSOLUTE = "absolute"
|
|
78
|
+
|
|
79
|
+
#: Table layout used when each slot is a signed 32-bit offset from a base.
|
|
80
|
+
MODE_RELATIVE = "relative"
|
|
81
|
+
|
|
82
|
+
#: Table layouts accepted for ``mode``.
|
|
83
|
+
MODES = (MODE_ABSOLUTE, MODE_RELATIVE)
|
|
84
|
+
|
|
85
|
+
#: Slot width, in bytes, of a relative table (one signed 32-bit offset).
|
|
86
|
+
RELATIVE_OFFSET_SIZE = 4
|
|
87
|
+
|
|
88
|
+
#: Evidence source label shared by every absolute-pointer record.
|
|
89
|
+
EVIDENCE_SOURCE = "jump_table.pointer_array"
|
|
90
|
+
|
|
91
|
+
#: Evidence method label shared by every absolute-pointer record.
|
|
92
|
+
EVIDENCE_METHOD = "absolute_pointers"
|
|
93
|
+
|
|
94
|
+
#: Evidence source label shared by every relative-offset record.
|
|
95
|
+
EVIDENCE_SOURCE_RELATIVE = "jump_table.relative_offsets"
|
|
96
|
+
|
|
97
|
+
#: Evidence method label shared by every relative-offset record.
|
|
98
|
+
EVIDENCE_METHOD_RELATIVE = "signed_offsets"
|
|
99
|
+
|
|
100
|
+
#: Endianness words that mean big-endian; everything else decodes little-endian.
|
|
101
|
+
_BIG_ENDIAN_MARKERS = ("big", "msb", "be")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class JumpTableEntry:
|
|
106
|
+
"""One decoded slot of a jump table.
|
|
107
|
+
|
|
108
|
+
Attributes:
|
|
109
|
+
index: Zero-based slot index inside the table, relative to
|
|
110
|
+
``table_address``.
|
|
111
|
+
target: Absolute virtual address the slot resolves to. In absolute mode
|
|
112
|
+
that is the value stored in the slot; in relative mode it is
|
|
113
|
+
``base_address + offset``.
|
|
114
|
+
evidence: Evidence record for this slot, pinned to the slot address and
|
|
115
|
+
carrying the decoded index and target.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
index: int
|
|
119
|
+
target: int
|
|
120
|
+
evidence: Evidence
|
|
121
|
+
|
|
122
|
+
def to_dict(self) -> dict[str, Any]:
|
|
123
|
+
return {
|
|
124
|
+
"index": self.index,
|
|
125
|
+
"target": f"0x{self.target:x}",
|
|
126
|
+
"evidence": self.evidence.to_dict(),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class JumpTableResult:
|
|
132
|
+
"""Outcome of one jump-table resolution.
|
|
133
|
+
|
|
134
|
+
Attributes:
|
|
135
|
+
table_address: Absolute virtual address of the first decoded slot.
|
|
136
|
+
entries: Decoded entries in slot order (possibly empty).
|
|
137
|
+
pointer_size: Slot width in bytes used to decode the table (``4`` for a
|
|
138
|
+
relative table).
|
|
139
|
+
truncated: ``True`` when ``max_entries`` cut the table short even though
|
|
140
|
+
a further usable slot was still available.
|
|
141
|
+
evidence: Evidence describing the resolution as a whole.
|
|
142
|
+
warnings: Non-fatal reasons that stopped the scan early, or that made
|
|
143
|
+
the result incomplete.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
table_address: int
|
|
147
|
+
entries: list[JumpTableEntry]
|
|
148
|
+
pointer_size: int
|
|
149
|
+
truncated: bool
|
|
150
|
+
evidence: Evidence
|
|
151
|
+
warnings: tuple[str, ...] = ()
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> dict[str, Any]:
|
|
154
|
+
return {
|
|
155
|
+
"table_address": f"0x{self.table_address:x}",
|
|
156
|
+
"entry_count": len(self.entries),
|
|
157
|
+
"entries": [entry.to_dict() for entry in self.entries],
|
|
158
|
+
"pointer_size": self.pointer_size,
|
|
159
|
+
"truncated": self.truncated,
|
|
160
|
+
"warnings": list(self.warnings),
|
|
161
|
+
"evidence": self.evidence.to_dict(),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _byteorder(info: BinaryInfo) -> Literal["little", "big"]:
|
|
166
|
+
"""Return the ``int.from_bytes`` byte order implied by ``info.endianness``."""
|
|
167
|
+
text = str(getattr(info, "endianness", "") or "").strip().lower()
|
|
168
|
+
return "big" if any(text.startswith(marker) for marker in _BIG_ENDIAN_MARKERS) else "little"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _int_argument(name: str, value: Any) -> int:
|
|
172
|
+
"""Return ``value`` when it is a plain integer, else raise."""
|
|
173
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
174
|
+
raise AnalysisError(f"{name} must be an integer, got {value!r}")
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _target_section(info: BinaryInfo, target: int) -> Any | None:
|
|
179
|
+
"""Return the section that maps ``target`` for jump-table purposes.
|
|
180
|
+
|
|
181
|
+
Sections whose ``virtual_address`` is zero are ignored: non-allocated
|
|
182
|
+
sections (symbol and string tables) are commonly reported at address 0 and
|
|
183
|
+
would otherwise swallow small bogus targets.
|
|
184
|
+
"""
|
|
185
|
+
section = info.section_for_va(target)
|
|
186
|
+
if section is None or section.virtual_address == 0:
|
|
187
|
+
return None
|
|
188
|
+
return section
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _resolve_pointer_size(info: BinaryInfo, pointer_size: int | None) -> int:
|
|
192
|
+
"""Validate ``pointer_size``, defaulting it from the binary's bitness."""
|
|
193
|
+
if pointer_size is None:
|
|
194
|
+
bitness = int(getattr(info, "bitness", 0) or 0)
|
|
195
|
+
if bitness == 64:
|
|
196
|
+
return 8
|
|
197
|
+
if bitness == 32:
|
|
198
|
+
return 4
|
|
199
|
+
raise AnalysisError(
|
|
200
|
+
f"cannot infer pointer size from bitness {bitness}; pass pointer_size explicitly"
|
|
201
|
+
)
|
|
202
|
+
size = _int_argument("pointer_size", pointer_size)
|
|
203
|
+
if size not in _ALLOWED_POINTER_SIZES:
|
|
204
|
+
raise AnalysisError(f"pointer_size must be 4 or 8, got {pointer_size}")
|
|
205
|
+
return size
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _resolve_mode(mode: str) -> str:
|
|
209
|
+
"""Normalise ``mode`` to one of :data:`MODES` or raise :class:`AnalysisError`."""
|
|
210
|
+
if not isinstance(mode, str):
|
|
211
|
+
raise AnalysisError(f"mode must be one of {MODES}, got {mode!r}")
|
|
212
|
+
text = mode.strip().lower()
|
|
213
|
+
if text not in MODES:
|
|
214
|
+
raise AnalysisError(f"mode must be one of {MODES}, got {mode!r}")
|
|
215
|
+
return text
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _resolve_relative_pointer_size(pointer_size: int | None) -> int:
|
|
219
|
+
"""Return :data:`RELATIVE_OFFSET_SIZE`, rejecting any other explicit width."""
|
|
220
|
+
if pointer_size is None:
|
|
221
|
+
return RELATIVE_OFFSET_SIZE
|
|
222
|
+
size = _int_argument("pointer_size", pointer_size)
|
|
223
|
+
if size != RELATIVE_OFFSET_SIZE:
|
|
224
|
+
raise AnalysisError(
|
|
225
|
+
f"pointer_size must be {RELATIVE_OFFSET_SIZE} in relative mode "
|
|
226
|
+
f"(signed 32-bit offsets), got {pointer_size}"
|
|
227
|
+
)
|
|
228
|
+
return size
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _resolve_base_address(base_address: int | None) -> int:
|
|
232
|
+
"""Return the required origin of a relative table or raise :class:`AnalysisError`."""
|
|
233
|
+
if base_address is None:
|
|
234
|
+
raise AnalysisError("relative mode requires base_address")
|
|
235
|
+
base = _int_argument("base_address", base_address)
|
|
236
|
+
if base < 0:
|
|
237
|
+
raise AnalysisError(f"invalid base_address 0x{base:x}")
|
|
238
|
+
return base
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _stop_reason(
|
|
242
|
+
index: int,
|
|
243
|
+
target: int,
|
|
244
|
+
info: BinaryInfo,
|
|
245
|
+
low: int | None,
|
|
246
|
+
high: int | None,
|
|
247
|
+
executable_only: bool,
|
|
248
|
+
) -> str | None:
|
|
249
|
+
"""Return why ``target`` cannot extend the table, or ``None`` when it can.
|
|
250
|
+
|
|
251
|
+
The checks are purely metadata lookups: the target is never read, mapped or
|
|
252
|
+
executed. They are shared by both table layouts, so a relative table stops on
|
|
253
|
+
exactly the same boundaries as an absolute one.
|
|
254
|
+
"""
|
|
255
|
+
if low is not None and target < low:
|
|
256
|
+
return (
|
|
257
|
+
f"index {index}: target 0x{target:x} is below min_target 0x{low:x}; "
|
|
258
|
+
"table scan stopped"
|
|
259
|
+
)
|
|
260
|
+
if high is not None and target > high:
|
|
261
|
+
return (
|
|
262
|
+
f"index {index}: target 0x{target:x} is above max_target 0x{high:x}; "
|
|
263
|
+
"table scan stopped"
|
|
264
|
+
)
|
|
265
|
+
target_section = _target_section(info, target)
|
|
266
|
+
if target_section is None:
|
|
267
|
+
return (
|
|
268
|
+
f"index {index}: target 0x{target:x} is not mapped by any section; "
|
|
269
|
+
"table scan stopped"
|
|
270
|
+
)
|
|
271
|
+
if executable_only and not target_section.is_executable:
|
|
272
|
+
return (
|
|
273
|
+
f"index {index}: target 0x{target:x} is inside non-executable section "
|
|
274
|
+
f"{target_section.name!r}; table scan stopped"
|
|
275
|
+
)
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _validate_table_address(info: BinaryInfo, table_address: int) -> SectionInfo:
|
|
280
|
+
"""Return the section backing ``table_address`` or raise :class:`AnalysisError`."""
|
|
281
|
+
address = _int_argument("table_address", table_address)
|
|
282
|
+
if address < 0:
|
|
283
|
+
raise AnalysisError(f"invalid table_address 0x{address:x}")
|
|
284
|
+
section = info.section_for_va(address)
|
|
285
|
+
if section is None:
|
|
286
|
+
raise AnalysisError(f"table address 0x{address:x} is not inside any section")
|
|
287
|
+
if info.va_to_offset(address) is None:
|
|
288
|
+
raise AnalysisError(f"table address 0x{address:x} is not mapped to file bytes")
|
|
289
|
+
return section
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _entry_evidence(index: int, slot_address: int, target: int) -> Evidence:
|
|
293
|
+
"""Build the evidence record for one decoded pointer slot."""
|
|
294
|
+
return Evidence(
|
|
295
|
+
source=EVIDENCE_SOURCE,
|
|
296
|
+
method=EVIDENCE_METHOD,
|
|
297
|
+
confidence=Confidence.MEDIUM,
|
|
298
|
+
address=slot_address,
|
|
299
|
+
detail=f"pointer #{index} at 0x{slot_address:x} -> 0x{target:x}",
|
|
300
|
+
extra={"index": index, "target": f"0x{target:x}"},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _result_evidence(
|
|
305
|
+
table_address: int, entry_count: int, pointer_size: int, truncated: bool
|
|
306
|
+
) -> Evidence:
|
|
307
|
+
"""Build the evidence record describing the resolution as a whole."""
|
|
308
|
+
return Evidence(
|
|
309
|
+
source=EVIDENCE_SOURCE,
|
|
310
|
+
method=EVIDENCE_METHOD,
|
|
311
|
+
confidence=Confidence.MEDIUM,
|
|
312
|
+
address=table_address,
|
|
313
|
+
detail=(
|
|
314
|
+
f"decoded {entry_count} absolute pointer(s) of {pointer_size} byte(s) "
|
|
315
|
+
f"from 0x{table_address:x}"
|
|
316
|
+
),
|
|
317
|
+
extra={
|
|
318
|
+
"entries": entry_count,
|
|
319
|
+
"pointer_size": pointer_size,
|
|
320
|
+
"truncated": truncated,
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _relative_entry_evidence(
|
|
326
|
+
index: int, slot_address: int, target: int, base_address: int
|
|
327
|
+
) -> Evidence:
|
|
328
|
+
"""Build the evidence record for one signed-offset slot."""
|
|
329
|
+
return Evidence(
|
|
330
|
+
source=EVIDENCE_SOURCE_RELATIVE,
|
|
331
|
+
method=EVIDENCE_METHOD_RELATIVE,
|
|
332
|
+
confidence=Confidence.MEDIUM,
|
|
333
|
+
address=slot_address,
|
|
334
|
+
detail=(
|
|
335
|
+
f"offset #{index} at 0x{slot_address:x} against base 0x{base_address:x} "
|
|
336
|
+
f"-> 0x{target:x}"
|
|
337
|
+
),
|
|
338
|
+
extra={
|
|
339
|
+
"index": index,
|
|
340
|
+
"target": f"0x{target:x}",
|
|
341
|
+
"base": f"0x{base_address:x}",
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _relative_result_evidence(
|
|
347
|
+
table_address: int, entry_count: int, base_address: int, truncated: bool
|
|
348
|
+
) -> Evidence:
|
|
349
|
+
"""Build the evidence record describing a whole relative resolution."""
|
|
350
|
+
return Evidence(
|
|
351
|
+
source=EVIDENCE_SOURCE_RELATIVE,
|
|
352
|
+
method=EVIDENCE_METHOD_RELATIVE,
|
|
353
|
+
confidence=Confidence.MEDIUM,
|
|
354
|
+
address=table_address,
|
|
355
|
+
detail=(
|
|
356
|
+
f"resolved {entry_count} signed 32-bit offset(s) against base "
|
|
357
|
+
f"0x{base_address:x} from 0x{table_address:x}"
|
|
358
|
+
),
|
|
359
|
+
extra={
|
|
360
|
+
"entries": entry_count,
|
|
361
|
+
"pointer_size": RELATIVE_OFFSET_SIZE,
|
|
362
|
+
"truncated": truncated,
|
|
363
|
+
"base": f"0x{base_address:x}",
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def resolve_jump_table(
|
|
369
|
+
path: str | Path,
|
|
370
|
+
info: BinaryInfo,
|
|
371
|
+
table_address: int,
|
|
372
|
+
max_entries: int = DEFAULT_MAX_ENTRIES,
|
|
373
|
+
pointer_size: int | None = None,
|
|
374
|
+
executable_only: bool = True,
|
|
375
|
+
min_target: int | None = None,
|
|
376
|
+
max_target: int | None = None,
|
|
377
|
+
mode: str = MODE_ABSOLUTE,
|
|
378
|
+
base_address: int | None = None,
|
|
379
|
+
) -> JumpTableResult:
|
|
380
|
+
"""Decode a jump table starting at ``table_address``.
|
|
381
|
+
|
|
382
|
+
With ``mode="absolute"`` (the default) the table is read as ``max_entries``
|
|
383
|
+
consecutive ``pointer_size``-wide absolute addresses. With
|
|
384
|
+
``mode="relative"`` it is read as consecutive *signed 32-bit* offsets, each
|
|
385
|
+
resolved against ``base_address``.
|
|
386
|
+
|
|
387
|
+
Values are decoded with ``info.endianness``. In both modes the scan stops at
|
|
388
|
+
the first slot that cannot be a target: a value outside
|
|
389
|
+
``[min_target, max_target]``, a target no section maps, or -- with
|
|
390
|
+
``executable_only`` -- a target inside a non-executable section. Each stop is
|
|
391
|
+
explained in :attr:`JumpTableResult.warnings`. A zero slot additionally
|
|
392
|
+
terminates an absolute table; a zero offset in a relative table is a real
|
|
393
|
+
delta that resolves to ``base_address`` and terminates nothing.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
path: Binary to read.
|
|
397
|
+
info: Metadata for ``path``; its ``bitness``, ``endianness`` and
|
|
398
|
+
``sections`` drive the decode.
|
|
399
|
+
table_address: Absolute virtual address of the first slot.
|
|
400
|
+
max_entries: Maximum number of slots to decode, ``1 .. MAX_ENTRIES``.
|
|
401
|
+
pointer_size: Slot width in bytes. In absolute mode ``4`` or ``8``, with
|
|
402
|
+
``None`` selecting 8 for a 64-bit binary and 4 for a 32-bit one. In
|
|
403
|
+
relative mode only ``4`` is valid, and ``None`` selects it.
|
|
404
|
+
executable_only: When true (default), a target inside a non-executable
|
|
405
|
+
section ends the scan with a warning.
|
|
406
|
+
min_target: Optional inclusive lower bound for accepted targets, in
|
|
407
|
+
absolute address space in both modes.
|
|
408
|
+
max_target: Optional inclusive upper bound for accepted targets.
|
|
409
|
+
mode: ``"absolute"`` (default) or ``"relative"``; case-insensitive.
|
|
410
|
+
base_address: Origin the signed offsets of a relative table are added to.
|
|
411
|
+
Required in relative mode; ignored in absolute mode.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
The decoded table with per-slot and summary evidence. Relative records
|
|
415
|
+
use source ``jump_table.relative_offsets`` and method
|
|
416
|
+
``signed_offsets``, with the base in the detail.
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
AnalysisError: If ``mode`` is unknown, if ``max_entries``,
|
|
420
|
+
``pointer_size``, ``table_address`` or the
|
|
421
|
+
``min_target``/``max_target`` window is invalid, if the table
|
|
422
|
+
address is not backed by file bytes, or if relative mode is
|
|
423
|
+
requested without a usable ``base_address``.
|
|
424
|
+
|
|
425
|
+
Note:
|
|
426
|
+
**Bounded work.** At most ``max_entries * pointer_size`` bytes are read
|
|
427
|
+
for the table itself, plus a single slot-sized peek used to decide
|
|
428
|
+
``truncated``. Nothing is loaded, mapped or executed; the file is only
|
|
429
|
+
read through :func:`codevanta.core.report.read_code`, which never
|
|
430
|
+
returns more bytes than the containing section maps. Relative targets
|
|
431
|
+
are validated by metadata lookup only, so a table cannot make this
|
|
432
|
+
function read code at the addresses it names.
|
|
433
|
+
"""
|
|
434
|
+
relative = _resolve_mode(mode) == MODE_RELATIVE
|
|
435
|
+
limit = _int_argument("max_entries", max_entries)
|
|
436
|
+
if limit < 1:
|
|
437
|
+
raise AnalysisError("max_entries must be >= 1")
|
|
438
|
+
if limit > MAX_ENTRIES:
|
|
439
|
+
raise AnalysisError(f"max_entries too large (limit {MAX_ENTRIES})")
|
|
440
|
+
# ``base`` is not None exactly when the table is relative; that single fact
|
|
441
|
+
# selects signed decoding, offset arithmetic and the evidence labels below.
|
|
442
|
+
base: int | None = None
|
|
443
|
+
if relative:
|
|
444
|
+
size = _resolve_relative_pointer_size(pointer_size)
|
|
445
|
+
base = _resolve_base_address(base_address)
|
|
446
|
+
else:
|
|
447
|
+
size = _resolve_pointer_size(info, pointer_size)
|
|
448
|
+
low = None if min_target is None else _int_argument("min_target", min_target)
|
|
449
|
+
high = None if max_target is None else _int_argument("max_target", max_target)
|
|
450
|
+
if low is not None and high is not None and low > high:
|
|
451
|
+
raise AnalysisError("min_target must be <= max_target")
|
|
452
|
+
section = _validate_table_address(info, table_address)
|
|
453
|
+
table = _int_argument("table_address", table_address)
|
|
454
|
+
byteorder = _byteorder(info)
|
|
455
|
+
|
|
456
|
+
window = limit * size
|
|
457
|
+
data = read_code(path, info, table, window)
|
|
458
|
+
slots = len(data) // size
|
|
459
|
+
if slots < 1:
|
|
460
|
+
raise AnalysisError(
|
|
461
|
+
f"table address 0x{table:x} has fewer than {size} file bytes "
|
|
462
|
+
f"in section {section.name!r}"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
entries: list[JumpTableEntry] = []
|
|
466
|
+
warnings: list[str] = []
|
|
467
|
+
stopped = False
|
|
468
|
+
|
|
469
|
+
for index in range(slots):
|
|
470
|
+
slot_address = table + index * size
|
|
471
|
+
raw = data[index * size : (index + 1) * size]
|
|
472
|
+
value = (
|
|
473
|
+
int.from_bytes(raw, byteorder, signed=True)
|
|
474
|
+
if relative
|
|
475
|
+
else int.from_bytes(raw, byteorder)
|
|
476
|
+
)
|
|
477
|
+
target = value if base is None else base + value
|
|
478
|
+
if not relative and target == 0:
|
|
479
|
+
warnings.append(
|
|
480
|
+
f"zero pointer at index {index} (0x{slot_address:x}) terminates the table"
|
|
481
|
+
)
|
|
482
|
+
stopped = True
|
|
483
|
+
break
|
|
484
|
+
reason = _stop_reason(index, target, info, low, high, executable_only)
|
|
485
|
+
if reason is not None:
|
|
486
|
+
warnings.append(reason)
|
|
487
|
+
stopped = True
|
|
488
|
+
break
|
|
489
|
+
entries.append(
|
|
490
|
+
JumpTableEntry(
|
|
491
|
+
index=index,
|
|
492
|
+
target=target,
|
|
493
|
+
evidence=(
|
|
494
|
+
_entry_evidence(index, slot_address, target)
|
|
495
|
+
if base is None
|
|
496
|
+
else _relative_entry_evidence(index, slot_address, target, base)
|
|
497
|
+
),
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
truncated = False
|
|
502
|
+
if not stopped and slots >= limit:
|
|
503
|
+
# The ceiling ended the scan; only call it truncated when a further slot
|
|
504
|
+
# is actually readable past the requested window and still usable.
|
|
505
|
+
try:
|
|
506
|
+
peek = read_code(path, info, table + limit * size, size)
|
|
507
|
+
except (AnalysisError, OSError):
|
|
508
|
+
peek = b""
|
|
509
|
+
if len(peek) == size:
|
|
510
|
+
if base is None:
|
|
511
|
+
truncated = int.from_bytes(peek, byteorder) != 0
|
|
512
|
+
else:
|
|
513
|
+
# No zero terminator exists here: only a delta that resolves to
|
|
514
|
+
# an acceptable target proves the table continues.
|
|
515
|
+
truncated = (
|
|
516
|
+
_stop_reason(
|
|
517
|
+
limit,
|
|
518
|
+
base + int.from_bytes(peek, byteorder, signed=True),
|
|
519
|
+
info,
|
|
520
|
+
low,
|
|
521
|
+
high,
|
|
522
|
+
executable_only,
|
|
523
|
+
)
|
|
524
|
+
is None
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
if not stopped and len(data) < window:
|
|
528
|
+
# The section ended before the requested window: a short table is
|
|
529
|
+
# indistinguishable from one that is cut off, so say so explicitly.
|
|
530
|
+
warnings.append(
|
|
531
|
+
f"table scan reached the end of section {section.name!r} after {slots} "
|
|
532
|
+
"slot(s); the table may be cut off"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
return JumpTableResult(
|
|
536
|
+
table_address=table,
|
|
537
|
+
entries=entries,
|
|
538
|
+
pointer_size=size,
|
|
539
|
+
truncated=truncated,
|
|
540
|
+
evidence=(
|
|
541
|
+
_result_evidence(table, len(entries), size, truncated)
|
|
542
|
+
if base is None
|
|
543
|
+
else _relative_result_evidence(table, len(entries), base, truncated)
|
|
544
|
+
),
|
|
545
|
+
warnings=tuple(warnings),
|
|
546
|
+
)
|