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,434 @@
|
|
|
1
|
+
"""Cross-reference extraction from direct and memory-indirect transfers.
|
|
2
|
+
|
|
3
|
+
The scan stays conservative. Two operand shapes are resolved:
|
|
4
|
+
|
|
5
|
+
1. A direct literal target (``^0x[0-9a-fA-F]+$``) for ``call``/``jmp``/
|
|
6
|
+
conditional jumps.
|
|
7
|
+
2. A ``call`` through an absolute or RIP-relative pointer slot, i.e.
|
|
8
|
+
``(byte|word|dword|qword) ptr [0x...]`` or
|
|
9
|
+
``(byte|word|dword|qword) ptr [rip +/- 0x...]``. The reported
|
|
10
|
+
``to_address`` is the address of that slot, not of the callee: the pointer
|
|
11
|
+
is read from memory at run time, so the callee itself is unknown here. When
|
|
12
|
+
the slot address matches an ``info.imports`` entry the reference is tagged
|
|
13
|
+
``call_import`` and named after the import; otherwise it is ``call_indirect``.
|
|
14
|
+
|
|
15
|
+
Everything else (``call rax``, ``call qword ptr [rax + 8]``, and any indirect
|
|
16
|
+
``jmp``/conditional jump) is skipped rather than guessed. Skipped indirect
|
|
17
|
+
*calls* are counted in :attr:`XrefResult.indirect_skipped` so callers can see
|
|
18
|
+
how much of the call graph was left unresolved.
|
|
19
|
+
|
|
20
|
+
Every step is bounded (``max_bytes``) and the analysed file is only ever read
|
|
21
|
+
from disk; nothing is loaded or executed.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from codevanta.core.disassembler import Disassembler, DisassemblyError
|
|
32
|
+
from codevanta.core.functions import FunctionCandidate, discover_functions
|
|
33
|
+
from codevanta.core.loader import BinaryInfo
|
|
34
|
+
from codevanta.core.report import AnalysisError, read_code
|
|
35
|
+
from codevanta.evidence.confidence import Confidence
|
|
36
|
+
from codevanta.evidence.models import Evidence
|
|
37
|
+
from codevanta.ir.instruction import IRInstruction
|
|
38
|
+
|
|
39
|
+
__all__ = ["DEFAULT_MAX_BYTES", "Xref", "XrefResult", "find_xrefs"]
|
|
40
|
+
|
|
41
|
+
#: Default ceiling on how many executable bytes a single scan will decode.
|
|
42
|
+
DEFAULT_MAX_BYTES = 4_000_000
|
|
43
|
+
|
|
44
|
+
#: Operand text that denotes an absolute, statically known target.
|
|
45
|
+
_DIRECT_OPERAND = re.compile(r"^0x[0-9a-fA-F]+$")
|
|
46
|
+
|
|
47
|
+
#: Operand text of a pointer slot: absolute or RIP-relative, sized explicitly.
|
|
48
|
+
#:
|
|
49
|
+
#: Examples matched: ``qword ptr [0x401000]``, ``qword ptr [rip + 0xa]``,
|
|
50
|
+
#: ``dword ptr [rip - 0xa]``. Anything with a register base or index
|
|
51
|
+
#: (``qword ptr [rax]``, ``qword ptr [rax + rcx*8]``) deliberately misses,
|
|
52
|
+
#: because the slot address is not statically known.
|
|
53
|
+
_MEMORY_POINTER = re.compile(
|
|
54
|
+
r"^(?:byte|word|dword|qword)\s+ptr\s*\[\s*"
|
|
55
|
+
r"(?:(?P<absolute>0x[0-9a-fA-F]+)|rip\s*(?P<sign>[+-])\s*(?P<displacement>0x[0-9a-fA-F]+))"
|
|
56
|
+
r"\s*\]$",
|
|
57
|
+
re.IGNORECASE,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
_BRANCH_MNEMONICS = frozenset(
|
|
61
|
+
{
|
|
62
|
+
"je",
|
|
63
|
+
"jne",
|
|
64
|
+
"jz",
|
|
65
|
+
"jnz",
|
|
66
|
+
"jb",
|
|
67
|
+
"jbe",
|
|
68
|
+
"ja",
|
|
69
|
+
"jae",
|
|
70
|
+
"jl",
|
|
71
|
+
"jle",
|
|
72
|
+
"jg",
|
|
73
|
+
"jge",
|
|
74
|
+
"js",
|
|
75
|
+
"jns",
|
|
76
|
+
"jo",
|
|
77
|
+
"jno",
|
|
78
|
+
"jp",
|
|
79
|
+
"jnp",
|
|
80
|
+
"jcxz",
|
|
81
|
+
"jecxz",
|
|
82
|
+
"jrcxz",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
#: Maps a lowercase mnemonic to the cross-reference kind it produces.
|
|
87
|
+
_KIND_BY_MNEMONIC: dict[str, str] = {
|
|
88
|
+
"call": "call",
|
|
89
|
+
"jmp": "jump",
|
|
90
|
+
**{mnemonic: "branch" for mnemonic in _BRANCH_MNEMONICS},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#: Maps a cross-reference kind to the evidence source that justifies it.
|
|
94
|
+
_SOURCE_BY_KIND: dict[str, str] = {
|
|
95
|
+
"call": "capstone.direct_call",
|
|
96
|
+
"jump": "capstone.direct_jump",
|
|
97
|
+
"branch": "capstone.conditional_branch",
|
|
98
|
+
"call_import": "capstone.import_call",
|
|
99
|
+
"call_indirect": "capstone.indirect_call",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_EVIDENCE_METHOD = "linear_disassembly"
|
|
103
|
+
_RESULT_EVIDENCE_SOURCE = "capstone.xref_scan"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class Xref:
|
|
108
|
+
"""A single directed reference from one instruction to another address.
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
from_address: Virtual address of the referring instruction.
|
|
112
|
+
to_address: Virtual address the instruction transfers control to. For
|
|
113
|
+
``call_import``/``call_indirect`` this is the *pointer slot*, since
|
|
114
|
+
the callee is only known at run time.
|
|
115
|
+
kind: ``"call"``, ``"jump"``, ``"branch"``, ``"call_import"`` or
|
|
116
|
+
``"call_indirect"``.
|
|
117
|
+
instruction: Disassembled instruction text, e.g. ``"call 0x401010"``.
|
|
118
|
+
source: Evidence source label for this specific reference.
|
|
119
|
+
evidence: Full evidence record for the reference.
|
|
120
|
+
function: Name of the function candidate located at ``to_address``.
|
|
121
|
+
target_name: Import name when the call goes through a known import slot,
|
|
122
|
+
otherwise ``None``.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
from_address: int
|
|
126
|
+
to_address: int
|
|
127
|
+
kind: str
|
|
128
|
+
instruction: str
|
|
129
|
+
source: str
|
|
130
|
+
evidence: Evidence
|
|
131
|
+
function: str | None = None
|
|
132
|
+
target_name: str | None = None
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict[str, Any]:
|
|
135
|
+
data: dict[str, Any] = {
|
|
136
|
+
"from_address": f"0x{self.from_address:x}",
|
|
137
|
+
"to_address": f"0x{self.to_address:x}",
|
|
138
|
+
"kind": self.kind,
|
|
139
|
+
"instruction": self.instruction,
|
|
140
|
+
"source": self.source,
|
|
141
|
+
"evidence": self.evidence.to_dict(),
|
|
142
|
+
}
|
|
143
|
+
if self.function:
|
|
144
|
+
data["function"] = self.function
|
|
145
|
+
if self.target_name:
|
|
146
|
+
data["target_name"] = self.target_name
|
|
147
|
+
return data
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class XrefResult:
|
|
152
|
+
"""Outcome of one cross-reference scan.
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
xrefs: References sorted by ``from_address``.
|
|
156
|
+
scanned_sections: Names of the sections that were successfully decoded.
|
|
157
|
+
scanned_bytes: Number of bytes actually decoded into instructions.
|
|
158
|
+
truncated: ``True`` when ``max_bytes`` or a decoder limit cut the scan.
|
|
159
|
+
evidence: Evidence describing the scan as a whole.
|
|
160
|
+
warnings: Non-fatal problems encountered while scanning.
|
|
161
|
+
indirect_skipped: Number of indirect ``call`` instructions whose target
|
|
162
|
+
could not be resolved statically (register operands, indexed memory
|
|
163
|
+
operands, ...). Such calls emit no :class:`Xref`.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
xrefs: list[Xref]
|
|
167
|
+
scanned_sections: list[str]
|
|
168
|
+
scanned_bytes: int
|
|
169
|
+
truncated: bool
|
|
170
|
+
evidence: Evidence
|
|
171
|
+
warnings: tuple[str, ...] = ()
|
|
172
|
+
indirect_skipped: int = 0
|
|
173
|
+
|
|
174
|
+
def to_dict(self) -> dict[str, Any]:
|
|
175
|
+
return {
|
|
176
|
+
"xref_count": len(self.xrefs),
|
|
177
|
+
"xrefs": [xref.to_dict() for xref in self.xrefs],
|
|
178
|
+
"scanned_sections": list(self.scanned_sections),
|
|
179
|
+
"scanned_bytes": self.scanned_bytes,
|
|
180
|
+
"truncated": self.truncated,
|
|
181
|
+
"indirect_skipped": self.indirect_skipped,
|
|
182
|
+
"warnings": list(self.warnings),
|
|
183
|
+
"evidence": self.evidence.to_dict(),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _kind_for(mnemonic: str) -> str | None:
|
|
188
|
+
"""Return the cross-reference kind for ``mnemonic`` if it is a transfer."""
|
|
189
|
+
return _KIND_BY_MNEMONIC.get(mnemonic.strip().lower())
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _direct_target(operands: str) -> int | None:
|
|
193
|
+
"""Parse an absolute target from ``operands``; ignore registers/memory."""
|
|
194
|
+
text = operands.strip()
|
|
195
|
+
if not _DIRECT_OPERAND.match(text):
|
|
196
|
+
return None
|
|
197
|
+
try:
|
|
198
|
+
return int(text, 16)
|
|
199
|
+
except ValueError: # pragma: no cover - the regex guarantees hex digits
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _memory_pointer_target(operands: str, address: int, size: int) -> int | None:
|
|
204
|
+
"""Resolve the pointer-slot address of an indirect memory operand.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
operands: Capstone operand text, e.g. ``"qword ptr [rip + 0xa]"``.
|
|
208
|
+
address: Virtual address of the instruction itself.
|
|
209
|
+
size: Length of the instruction in bytes, used as the RIP bias.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
The absolute address of the pointer slot, or ``None`` when the operand
|
|
213
|
+
is not an absolute/RIP-relative sized memory pointer. Note that the
|
|
214
|
+
value stored in the slot (the real callee) is unknown here.
|
|
215
|
+
"""
|
|
216
|
+
match = _MEMORY_POINTER.match(operands.strip())
|
|
217
|
+
if match is None:
|
|
218
|
+
return None
|
|
219
|
+
absolute = match.group("absolute")
|
|
220
|
+
try:
|
|
221
|
+
if absolute is not None:
|
|
222
|
+
return int(absolute, 16)
|
|
223
|
+
displacement = int(match.group("displacement"), 16)
|
|
224
|
+
except ValueError: # pragma: no cover - the regex guarantees hex digits
|
|
225
|
+
return None
|
|
226
|
+
# RIP-relative operands are encoded relative to the next instruction.
|
|
227
|
+
base = address + size
|
|
228
|
+
return base + displacement if match.group("sign") == "+" else base - displacement
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _build_xref(
|
|
232
|
+
instruction: IRInstruction,
|
|
233
|
+
to_address: int,
|
|
234
|
+
kind: str,
|
|
235
|
+
names_by_address: dict[int, str],
|
|
236
|
+
target_name: str | None = None,
|
|
237
|
+
confidence: Confidence = Confidence.CERTAIN,
|
|
238
|
+
) -> Xref:
|
|
239
|
+
"""Assemble one :class:`Xref` together with its evidence record."""
|
|
240
|
+
text = instruction.text
|
|
241
|
+
source = _SOURCE_BY_KIND[kind]
|
|
242
|
+
return Xref(
|
|
243
|
+
from_address=instruction.address,
|
|
244
|
+
to_address=to_address,
|
|
245
|
+
kind=kind,
|
|
246
|
+
instruction=text,
|
|
247
|
+
source=source,
|
|
248
|
+
evidence=Evidence(
|
|
249
|
+
source=source,
|
|
250
|
+
method=_EVIDENCE_METHOD,
|
|
251
|
+
confidence=confidence,
|
|
252
|
+
address=instruction.address,
|
|
253
|
+
detail=text,
|
|
254
|
+
),
|
|
255
|
+
function=names_by_address.get(to_address),
|
|
256
|
+
target_name=target_name,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _scan_evidence(xref_count: int) -> Evidence:
|
|
261
|
+
"""Build the evidence record for the scan as a whole.
|
|
262
|
+
|
|
263
|
+
The confidence is ``CERTAIN`` because every emitted reference is a read of
|
|
264
|
+
the decoded operand stream; individual indirect references may lower their
|
|
265
|
+
own confidence. Incomplete coverage is reported through
|
|
266
|
+
``XrefResult.warnings``, ``XrefResult.truncated`` and
|
|
267
|
+
``XrefResult.indirect_skipped`` instead.
|
|
268
|
+
"""
|
|
269
|
+
return Evidence(
|
|
270
|
+
source=_RESULT_EVIDENCE_SOURCE,
|
|
271
|
+
method=_EVIDENCE_METHOD,
|
|
272
|
+
confidence=Confidence.CERTAIN,
|
|
273
|
+
detail=f"{xref_count} xrefs",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def find_xrefs(
|
|
278
|
+
path: str | Path,
|
|
279
|
+
info: BinaryInfo,
|
|
280
|
+
functions: list[FunctionCandidate] | None = None,
|
|
281
|
+
max_bytes: int = DEFAULT_MAX_BYTES,
|
|
282
|
+
) -> XrefResult:
|
|
283
|
+
"""Collect cross references from executable sections.
|
|
284
|
+
|
|
285
|
+
Direct transfers (``call``/``jmp``/conditional jumps to a literal address)
|
|
286
|
+
and ``call`` instructions through an absolute or RIP-relative pointer slot
|
|
287
|
+
are recorded. Indirect calls that cannot be resolved statically are skipped
|
|
288
|
+
and counted in :attr:`XrefResult.indirect_skipped`.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
path: File the metadata in ``info`` was loaded from.
|
|
292
|
+
info: Normalized binary metadata. Its ``imports`` list is used to name
|
|
293
|
+
indirect calls whose pointer slot matches an import address.
|
|
294
|
+
functions: Pre-computed candidates used to name targets;
|
|
295
|
+
:func:`~codevanta.core.functions.discover_functions` runs when omitted.
|
|
296
|
+
max_bytes: Hard ceiling on executable bytes read and decoded.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
An :class:`XrefResult` with references sorted by ``from_address``.
|
|
300
|
+
|
|
301
|
+
Raises:
|
|
302
|
+
AnalysisError: If ``max_bytes`` is not positive.
|
|
303
|
+
"""
|
|
304
|
+
if max_bytes < 1:
|
|
305
|
+
raise AnalysisError("max_bytes must be >= 1")
|
|
306
|
+
|
|
307
|
+
candidates = list(functions) if functions is not None else discover_functions(info)
|
|
308
|
+
names_by_address: dict[int, str] = {
|
|
309
|
+
candidate.address: candidate.name for candidate in candidates if candidate.name
|
|
310
|
+
}
|
|
311
|
+
# Built once: absolute pointer-slot address -> import name.
|
|
312
|
+
import_names_by_address: dict[int, str] = {
|
|
313
|
+
imported.address: imported.name
|
|
314
|
+
for imported in info.imports
|
|
315
|
+
if imported.address is not None and imported.name
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
warnings: list[str] = []
|
|
319
|
+
executable_sections = [
|
|
320
|
+
section
|
|
321
|
+
for section in info.sections
|
|
322
|
+
if section.is_executable and (section.raw_size or section.size) > 0
|
|
323
|
+
]
|
|
324
|
+
if not executable_sections:
|
|
325
|
+
warnings.append("no executable sections with file bytes were found")
|
|
326
|
+
return XrefResult(
|
|
327
|
+
xrefs=[],
|
|
328
|
+
scanned_sections=[],
|
|
329
|
+
scanned_bytes=0,
|
|
330
|
+
truncated=False,
|
|
331
|
+
evidence=_scan_evidence(0),
|
|
332
|
+
warnings=tuple(warnings),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
disassembler = Disassembler(info.architecture, info.bitness, info.endianness)
|
|
337
|
+
except DisassemblyError as exc:
|
|
338
|
+
warnings.append(f"disassembly unavailable: {exc}")
|
|
339
|
+
return XrefResult(
|
|
340
|
+
xrefs=[],
|
|
341
|
+
scanned_sections=[],
|
|
342
|
+
scanned_bytes=0,
|
|
343
|
+
truncated=False,
|
|
344
|
+
evidence=_scan_evidence(0),
|
|
345
|
+
warnings=tuple(warnings),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
total_bytes = sum(section.raw_size or section.size for section in executable_sections)
|
|
349
|
+
remaining = max_bytes
|
|
350
|
+
truncated = total_bytes > max_bytes
|
|
351
|
+
scanned_sections: list[str] = []
|
|
352
|
+
scanned_bytes = 0
|
|
353
|
+
indirect_skipped = 0
|
|
354
|
+
xrefs: list[Xref] = []
|
|
355
|
+
|
|
356
|
+
for section in executable_sections:
|
|
357
|
+
span = section.raw_size or section.size
|
|
358
|
+
if remaining <= 0:
|
|
359
|
+
truncated = True
|
|
360
|
+
break
|
|
361
|
+
budget = min(remaining, span)
|
|
362
|
+
if budget < span:
|
|
363
|
+
truncated = True
|
|
364
|
+
try:
|
|
365
|
+
data = read_code(path, info, section.virtual_address, budget)
|
|
366
|
+
except (AnalysisError, OSError) as exc:
|
|
367
|
+
warnings.append(f"section {section.name!r}: cannot read bytes ({exc})")
|
|
368
|
+
continue
|
|
369
|
+
if not data:
|
|
370
|
+
warnings.append(f"section {section.name!r}: no file bytes available")
|
|
371
|
+
continue
|
|
372
|
+
remaining -= len(data)
|
|
373
|
+
try:
|
|
374
|
+
disassembly = disassembler.disassemble(data, base_address=section.virtual_address)
|
|
375
|
+
except DisassemblyError as exc:
|
|
376
|
+
warnings.append(f"section {section.name!r}: disassembly failed ({exc})")
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
scanned_sections.append(section.name)
|
|
380
|
+
scanned_bytes += disassembly.consumed_bytes
|
|
381
|
+
truncated = truncated or disassembly.truncated
|
|
382
|
+
|
|
383
|
+
for instruction in disassembly.instructions:
|
|
384
|
+
kind = _kind_for(instruction.mnemonic)
|
|
385
|
+
if kind is None:
|
|
386
|
+
continue
|
|
387
|
+
to_address = _direct_target(instruction.operands)
|
|
388
|
+
if to_address is not None:
|
|
389
|
+
xrefs.append(_build_xref(instruction, to_address, kind, names_by_address))
|
|
390
|
+
continue
|
|
391
|
+
# From here on only indirect calls are interesting; indirect jumps
|
|
392
|
+
# and conditional branches stay unresolved without being counted.
|
|
393
|
+
if kind != "call":
|
|
394
|
+
continue
|
|
395
|
+
slot_address = _memory_pointer_target(
|
|
396
|
+
instruction.operands, instruction.address, instruction.size
|
|
397
|
+
)
|
|
398
|
+
if slot_address is None:
|
|
399
|
+
indirect_skipped += 1
|
|
400
|
+
continue
|
|
401
|
+
import_name = import_names_by_address.get(slot_address)
|
|
402
|
+
if import_name is not None:
|
|
403
|
+
xrefs.append(
|
|
404
|
+
_build_xref(
|
|
405
|
+
instruction,
|
|
406
|
+
slot_address,
|
|
407
|
+
"call_import",
|
|
408
|
+
names_by_address,
|
|
409
|
+
target_name=import_name,
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
else:
|
|
413
|
+
# The slot address is certain, the callee it holds is not.
|
|
414
|
+
xrefs.append(
|
|
415
|
+
_build_xref(
|
|
416
|
+
instruction,
|
|
417
|
+
slot_address,
|
|
418
|
+
"call_indirect",
|
|
419
|
+
names_by_address,
|
|
420
|
+
confidence=Confidence.HIGH,
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
xrefs.sort(key=lambda xref: (xref.from_address, xref.to_address))
|
|
425
|
+
return XrefResult(
|
|
426
|
+
xrefs=xrefs,
|
|
427
|
+
scanned_sections=scanned_sections,
|
|
428
|
+
scanned_bytes=scanned_bytes,
|
|
429
|
+
truncated=truncated,
|
|
430
|
+
evidence=_scan_evidence(len(xrefs)),
|
|
431
|
+
warnings=tuple(warnings),
|
|
432
|
+
indirect_skipped=indirect_skipped,
|
|
433
|
+
)
|
|
434
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API layer (FastAPI service) — phase 2+."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CODEVANTA command line package."""
|