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,762 @@
|
|
|
1
|
+
"""Definition-use chains, constant propagation and reaching definitions (phase 6).
|
|
2
|
+
|
|
3
|
+
This module turns the phase 2 control-flow graph back into per-instruction data
|
|
4
|
+
flow: which register a definition writes, which constant (if any) it holds, which
|
|
5
|
+
uses read that register, and which definition reached each read. It is the
|
|
6
|
+
foundation for the typed IR step, because a type can only be attached to a value
|
|
7
|
+
once the value's origin is known.
|
|
8
|
+
|
|
9
|
+
The analysis is a *single forward pass*: blocks are visited in ascending address
|
|
10
|
+
order and the instructions of a block in ascending address order. The register
|
|
11
|
+
file is a map from canonical register name to the :class:`Definition` that
|
|
12
|
+
currently holds the value, so a later write *kills* the earlier definition --
|
|
13
|
+
only the most recent definition can be reached by a later read, and the summary
|
|
14
|
+
records at most that one per read in ``reaching_definition``.
|
|
15
|
+
|
|
16
|
+
Folding rules (a folded value is always masked to the width of the written
|
|
17
|
+
register, and constants are computed exactly, never heuristically)::
|
|
18
|
+
|
|
19
|
+
instruction definition kind folded value
|
|
20
|
+
---------------------------------- ---------------- ---------------------
|
|
21
|
+
mov rax, 5 const 5
|
|
22
|
+
mov eax, 5 const 5 (zero-extends)
|
|
23
|
+
movabs rax, 0x1122334455667788 const that immediate
|
|
24
|
+
mov rax, rdi copy rdi's constant, if any
|
|
25
|
+
lea rax, [rip + 0x10] const next instruction + 0x10
|
|
26
|
+
add rax, 3 (rax known) arithmetic rax + 3
|
|
27
|
+
sub rax, 3 (rax known) arithmetic rax - 3
|
|
28
|
+
xor rax, 3 (rax known) arithmetic rax ^ 3
|
|
29
|
+
xor rax, rax / sub rax, rax arithmetic 0
|
|
30
|
+
add rax, rbx (both known) arithmetic rax + rbx
|
|
31
|
+
any other register write unknown none
|
|
32
|
+
|
|
33
|
+
Everything else stays ``unknown`` on purpose. ``movzx``/``movsx``/``movsxd`` are
|
|
34
|
+
never classed as ``copy`` because the extension changes the value; ``and``, ``or``
|
|
35
|
+
and the shifts keep their ``arithmetic`` kind but no value, and a write narrower
|
|
36
|
+
than four bytes (``al``, ``ax``) drops its value while keeping its kind, because a
|
|
37
|
+
partial write cannot leave a whole-register constant. ``lea`` is only folded when
|
|
38
|
+
the address expression is a RIP-relative displacement; an indexed or
|
|
39
|
+
base-relative ``lea`` is an unknown address.
|
|
40
|
+
|
|
41
|
+
Register names are canonicalized before they are reported: on a 64-bit binary
|
|
42
|
+
``eax``/``ax``/``al`` are aliases of ``rax``, on a 32-bit one ``ax``/``al`` are
|
|
43
|
+
aliases of ``eax``. A read of a narrow alias is a read of the canonical register,
|
|
44
|
+
but a constant is only propagated through it when the alias is the whole register,
|
|
45
|
+
because the upper bytes of the canonical register are not visible through it.
|
|
46
|
+
|
|
47
|
+
Two instruction shapes are deliberately *not* definitions: a memory destination
|
|
48
|
+
(``mov [rdi], rax`` writes memory, not a register -- its address registers are
|
|
49
|
+
reads) and any mnemonic that does not write its first operand (``cmp``, ``test``,
|
|
50
|
+
``push``, ``jcc``, ``ret``, ...). A ``call`` additionally invalidates the
|
|
51
|
+
caller-saved registers (``rax``, ``rcx``, ``rdx``, ``rsi``, ``rdi``, ``r8`` ..
|
|
52
|
+
``r11`` and their 32-bit forms), because whatever the callee left there is not
|
|
53
|
+
the constant the caller stored.
|
|
54
|
+
|
|
55
|
+
Every read whose register has no definition in the analysed region is reported
|
|
56
|
+
through ``DataFlowSummary.warnings`` -- bounded and never silent -- and simply has
|
|
57
|
+
no entry in ``reaching_definition``. A truncated CFG is reported the same way,
|
|
58
|
+
because definitions past the instruction or block limit are not modelled at all.
|
|
59
|
+
|
|
60
|
+
Nothing is executed: decoding comes from
|
|
61
|
+
:func:`codevanta.analysis.cfg.build_recursive_cfg`, operand parsing is shared with
|
|
62
|
+
:mod:`codevanta.analysis.type_propagation`, and the result is a pure function of
|
|
63
|
+
the file bytes, the address and the two limits.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
from __future__ import annotations
|
|
67
|
+
|
|
68
|
+
import re
|
|
69
|
+
from collections.abc import Mapping, Sequence
|
|
70
|
+
from dataclasses import dataclass
|
|
71
|
+
from pathlib import Path
|
|
72
|
+
from typing import Any
|
|
73
|
+
|
|
74
|
+
from codevanta.analysis.cfg import build_recursive_cfg
|
|
75
|
+
from codevanta.analysis.type_propagation import (
|
|
76
|
+
_ADDRESS_TERM,
|
|
77
|
+
_ARITHMETIC_MNEMONICS,
|
|
78
|
+
_MOV_MNEMONICS,
|
|
79
|
+
_WORD,
|
|
80
|
+
_instruction_parts,
|
|
81
|
+
_is_plain_register,
|
|
82
|
+
_parse_memory,
|
|
83
|
+
_split_operands,
|
|
84
|
+
_writes_first_operand,
|
|
85
|
+
)
|
|
86
|
+
from codevanta.core.loader import BinaryInfo
|
|
87
|
+
from codevanta.core.report import AnalysisError
|
|
88
|
+
from codevanta.evidence.confidence import Confidence
|
|
89
|
+
from codevanta.evidence.models import Evidence
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"DEFAULT_MAX_BLOCKS",
|
|
93
|
+
"DEFAULT_MAX_INSTRUCTIONS",
|
|
94
|
+
"KIND_ARITHMETIC",
|
|
95
|
+
"KIND_CONST",
|
|
96
|
+
"KIND_COPY",
|
|
97
|
+
"KIND_UNKNOWN",
|
|
98
|
+
"MAX_MAX_BLOCKS",
|
|
99
|
+
"MAX_MAX_INSTRUCTIONS",
|
|
100
|
+
"MAX_UNDEFINED_WARNINGS",
|
|
101
|
+
"MIN_MAX_BLOCKS",
|
|
102
|
+
"MIN_MAX_INSTRUCTIONS",
|
|
103
|
+
"READ",
|
|
104
|
+
"WRITE",
|
|
105
|
+
"DataFlowSummary",
|
|
106
|
+
"Definition",
|
|
107
|
+
"Use",
|
|
108
|
+
"analyze_dataflow",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
#: Default instruction budget of one analysis.
|
|
113
|
+
DEFAULT_MAX_INSTRUCTIONS = 2000
|
|
114
|
+
|
|
115
|
+
#: Default ceiling on the number of blocks one analysis will visit.
|
|
116
|
+
DEFAULT_MAX_BLOCKS = 500
|
|
117
|
+
|
|
118
|
+
#: Smallest accepted ``max_instructions`` / ``max_blocks``.
|
|
119
|
+
MIN_MAX_INSTRUCTIONS = 1
|
|
120
|
+
MIN_MAX_BLOCKS = 1
|
|
121
|
+
|
|
122
|
+
#: Hard ceilings on the budgets (bounded work per call).
|
|
123
|
+
MAX_MAX_INSTRUCTIONS = 8192
|
|
124
|
+
MAX_MAX_BLOCKS = 4096
|
|
125
|
+
|
|
126
|
+
#: ``Definition.kind``: the instruction writes an immediate or a RIP-relative
|
|
127
|
+
#: address, so the value is known exactly.
|
|
128
|
+
KIND_CONST = "const"
|
|
129
|
+
|
|
130
|
+
#: ``Definition.kind``: the instruction copies another register.
|
|
131
|
+
KIND_COPY = "copy"
|
|
132
|
+
|
|
133
|
+
#: ``Definition.kind``: the instruction combines its operand with the current
|
|
134
|
+
#: value of the register (a folded value is present only when both are known).
|
|
135
|
+
KIND_ARITHMETIC = "arithmetic"
|
|
136
|
+
|
|
137
|
+
#: ``Definition.kind``: the write is not modelled, so its value is unknown.
|
|
138
|
+
KIND_UNKNOWN = "unknown"
|
|
139
|
+
|
|
140
|
+
#: ``Use.read_or_write``: the instruction reads the register.
|
|
141
|
+
READ = "read"
|
|
142
|
+
|
|
143
|
+
#: ``Use.read_or_write``: the instruction writes the register.
|
|
144
|
+
WRITE = "write"
|
|
145
|
+
|
|
146
|
+
#: How many "no reaching definition" warnings one summary may carry before the
|
|
147
|
+
#: rest are collapsed into a single count (bounded, deterministic prose).
|
|
148
|
+
MAX_UNDEFINED_WARNINGS = 8
|
|
149
|
+
|
|
150
|
+
#: A write of this many bytes or more replaces the whole canonical register
|
|
151
|
+
#: (``eax`` zero-extends into ``rax``), so it may leave a known constant.
|
|
152
|
+
FULL_CONSTANT_WIDTH = 4
|
|
153
|
+
|
|
154
|
+
#: Mnemonics whose value is folded when both sides are known.
|
|
155
|
+
_FOLDING_MNEMONICS = frozenset({"add", "sub", "xor"})
|
|
156
|
+
|
|
157
|
+
#: ``mov``-family mnemonics that change the value even when copying a register.
|
|
158
|
+
_EXTENDING_MNEMONICS = frozenset({"movsx", "movsxd", "movzx"})
|
|
159
|
+
|
|
160
|
+
#: An immediate as Capstone or a MASM-style printer renders it: ``5``, ``-1``,
|
|
161
|
+
#: ``0x10``, ``0ffh``.
|
|
162
|
+
_IMMEDIATE = re.compile(r"^(?P<sign>[+-]?)(?P<body>0x[0-9a-f]+|[0-9]+)(?P<suffix>h?)$", re.IGNORECASE)
|
|
163
|
+
|
|
164
|
+
#: Canonical 64-bit general-purpose registers -> {alias as printed: width}.
|
|
165
|
+
_ALIAS_WIDTHS: dict[str, dict[str, int]] = {
|
|
166
|
+
"rax": {"rax": 8, "eax": 4, "ax": 2, "al": 1, "ah": 1},
|
|
167
|
+
"rbx": {"rbx": 8, "ebx": 4, "bx": 2, "bl": 1, "bh": 1},
|
|
168
|
+
"rcx": {"rcx": 8, "ecx": 4, "cx": 2, "cl": 1, "ch": 1},
|
|
169
|
+
"rdx": {"rdx": 8, "edx": 4, "dx": 2, "dl": 1, "dh": 1},
|
|
170
|
+
"rsi": {"rsi": 8, "esi": 4, "si": 2, "sil": 1},
|
|
171
|
+
"rdi": {"rdi": 8, "edi": 4, "di": 2, "dil": 1},
|
|
172
|
+
"rbp": {"rbp": 8, "ebp": 4, "bp": 2, "bpl": 1},
|
|
173
|
+
"rsp": {"rsp": 8, "esp": 4, "sp": 2, "spl": 1},
|
|
174
|
+
"r8": {"r8": 8, "r8d": 4, "r8w": 2, "r8b": 1},
|
|
175
|
+
"r9": {"r9": 8, "r9d": 4, "r9w": 2, "r9b": 1},
|
|
176
|
+
"r10": {"r10": 8, "r10d": 4, "r10w": 2, "r10b": 1},
|
|
177
|
+
"r11": {"r11": 8, "r11d": 4, "r11w": 2, "r11b": 1},
|
|
178
|
+
"r12": {"r12": 8, "r12d": 4, "r12w": 2, "r12b": 1},
|
|
179
|
+
"r13": {"r13": 8, "r13d": 4, "r13w": 2, "r13b": 1},
|
|
180
|
+
"r14": {"r14": 8, "r14d": 4, "r14w": 2, "r14b": 1},
|
|
181
|
+
"r15": {"r15": 8, "r15d": 4, "r15w": 2, "r15b": 1},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#: Registers a ``call`` invalidates, spelled as aliases so the set can be
|
|
185
|
+
#: resolved for whichever pointer width the binary uses.
|
|
186
|
+
_CALL_CLOBBER_ALIASES = (
|
|
187
|
+
"rax",
|
|
188
|
+
"rcx",
|
|
189
|
+
"rdx",
|
|
190
|
+
"rsi",
|
|
191
|
+
"rdi",
|
|
192
|
+
"r8",
|
|
193
|
+
"r9",
|
|
194
|
+
"r10",
|
|
195
|
+
"r11",
|
|
196
|
+
"eax",
|
|
197
|
+
"ecx",
|
|
198
|
+
"edx",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
#: Mnemonics that transfer control to a subroutine and therefore clobber the
|
|
202
|
+
#: caller-saved registers.
|
|
203
|
+
_CALL_MNEMONICS = frozenset({"call", "callq"})
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True)
|
|
207
|
+
class Definition:
|
|
208
|
+
"""One register definition, i.e. the value an instruction leaves behind.
|
|
209
|
+
|
|
210
|
+
Attributes:
|
|
211
|
+
address: Virtual address of the defining instruction.
|
|
212
|
+
register: Canonical register name (``rax``, ``rdi``, ``r8``, ...); aliases
|
|
213
|
+
such as ``eax``/``ax``/``al`` are normalized to it.
|
|
214
|
+
kind: Shape of the definition: :data:`KIND_CONST`, :data:`KIND_COPY`,
|
|
215
|
+
:data:`KIND_ARITHMETIC` or :data:`KIND_UNKNOWN`.
|
|
216
|
+
value: The constant this definition provably writes, or ``None`` when the
|
|
217
|
+
value is not known. Present only for full-width writes, so a
|
|
218
|
+
``mov al, 1`` keeps its kind but carries no value.
|
|
219
|
+
source_register: Canonical register a :data:`KIND_COPY` definition copies,
|
|
220
|
+
else ``None``.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
address: int
|
|
224
|
+
register: str
|
|
225
|
+
kind: str
|
|
226
|
+
value: int | None
|
|
227
|
+
source_register: str | None
|
|
228
|
+
|
|
229
|
+
def to_dict(self) -> dict[str, Any]:
|
|
230
|
+
"""Serialize the definition for the JSON report."""
|
|
231
|
+
return {
|
|
232
|
+
"address": f"0x{self.address:x}",
|
|
233
|
+
"register": self.register,
|
|
234
|
+
"kind": self.kind,
|
|
235
|
+
"value": self.value,
|
|
236
|
+
"source_register": self.source_register,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass(frozen=True)
|
|
241
|
+
class Use:
|
|
242
|
+
"""One read or write of a register by a single instruction.
|
|
243
|
+
|
|
244
|
+
Attributes:
|
|
245
|
+
address: Virtual address of the instruction that uses the register.
|
|
246
|
+
register: Canonical register name, aliases normalized as in
|
|
247
|
+
:class:`Definition`.
|
|
248
|
+
read_or_write: :data:`READ` or :data:`WRITE`.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
address: int
|
|
252
|
+
register: str
|
|
253
|
+
read_or_write: str
|
|
254
|
+
|
|
255
|
+
def to_dict(self) -> dict[str, Any]:
|
|
256
|
+
"""Serialize the use for the JSON report."""
|
|
257
|
+
return {
|
|
258
|
+
"address": f"0x{self.address:x}",
|
|
259
|
+
"register": self.register,
|
|
260
|
+
"read_or_write": self.read_or_write,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass(frozen=True)
|
|
265
|
+
class DataFlowSummary:
|
|
266
|
+
"""Definition-use chains, constants and reaching definitions of one function.
|
|
267
|
+
|
|
268
|
+
Attributes:
|
|
269
|
+
address: Function address the analysis started from.
|
|
270
|
+
definitions: Every register definition, in ascending address order.
|
|
271
|
+
uses: Every read and write, in ascending address order; within one
|
|
272
|
+
instruction the reads come before the write it makes.
|
|
273
|
+
constant_registers: Canonical register -> constant still known after the
|
|
274
|
+
forward pass, sorted by register name. Only provable constants appear.
|
|
275
|
+
reaching_definition: ``(canonical register, use address)`` -> address of
|
|
276
|
+
the definition that reaches that read. Reads with no definition in
|
|
277
|
+
the analysed region are absent and reported in ``warnings`` instead.
|
|
278
|
+
warnings: Bounded, human-readable caveats (undefined reads, a truncated
|
|
279
|
+
CFG), each stated once.
|
|
280
|
+
evidence: How the summary as a whole was derived.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
address: int
|
|
284
|
+
definitions: tuple[Definition, ...]
|
|
285
|
+
uses: tuple[Use, ...]
|
|
286
|
+
constant_registers: Mapping[str, int]
|
|
287
|
+
reaching_definition: Mapping[tuple[str, int], int]
|
|
288
|
+
warnings: tuple[str, ...]
|
|
289
|
+
evidence: Evidence
|
|
290
|
+
|
|
291
|
+
def to_dict(self) -> dict[str, Any]:
|
|
292
|
+
"""Serialize the summary for the JSON report (JSON-safe, sorted)."""
|
|
293
|
+
return {
|
|
294
|
+
"address": f"0x{self.address:x}",
|
|
295
|
+
"definition_count": len(self.definitions),
|
|
296
|
+
"use_count": len(self.uses),
|
|
297
|
+
"definitions": [definition.to_dict() for definition in self.definitions],
|
|
298
|
+
"uses": [use.to_dict() for use in self.uses],
|
|
299
|
+
"constant_registers": dict(self.constant_registers),
|
|
300
|
+
"reaching_definition": {
|
|
301
|
+
f"{register}@0x{use:x}": f"0x{definition:x}"
|
|
302
|
+
for (register, use), definition in self.reaching_definition.items()
|
|
303
|
+
},
|
|
304
|
+
"warnings": list(self.warnings),
|
|
305
|
+
"evidence": self.evidence.to_dict(),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# input validation and register name normalization
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _pointer_width(info: BinaryInfo) -> int:
|
|
315
|
+
"""Return the pointer width in bytes implied by ``info.bitness``."""
|
|
316
|
+
bitness = int(getattr(info, "bitness", 0) or 0)
|
|
317
|
+
if bitness == 64:
|
|
318
|
+
return 8
|
|
319
|
+
if bitness == 32:
|
|
320
|
+
return 4
|
|
321
|
+
raise AnalysisError(f"data flow needs a 32-bit or 64-bit binary, got bitness {bitness}")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _require_limit(name: str, value: object, minimum: int, maximum: int) -> int:
|
|
325
|
+
"""Validate an integer budget and return it as ``int``."""
|
|
326
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
327
|
+
raise AnalysisError(f"{name} must be an integer, got {value!r}")
|
|
328
|
+
if value < minimum:
|
|
329
|
+
raise AnalysisError(f"{name} must be >= {minimum}, got {value}")
|
|
330
|
+
if value > maximum:
|
|
331
|
+
raise AnalysisError(f"{name} too large (limit {maximum}), got {value}")
|
|
332
|
+
return value
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _alias_table(pointer_width: int) -> dict[str, tuple[str, int]]:
|
|
336
|
+
"""Map every general-purpose register alias to ``(canonical register, width)``.
|
|
337
|
+
|
|
338
|
+
The table is built per pointer width so the canonical names match the binary:
|
|
339
|
+
``eax`` is canonical on a 32-bit binary and an alias of ``rax`` on a 64-bit
|
|
340
|
+
one, and ``r8`` .. ``r15`` only exist above 32 bits.
|
|
341
|
+
"""
|
|
342
|
+
table: dict[str, tuple[str, int]] = {}
|
|
343
|
+
for canonical, aliases in _ALIAS_WIDTHS.items():
|
|
344
|
+
if pointer_width < 8:
|
|
345
|
+
if canonical[1:].isdigit(): # r8 .. r15 do not exist below 64-bit
|
|
346
|
+
continue
|
|
347
|
+
wide = [alias for alias, width in aliases.items() if width == 4]
|
|
348
|
+
if not wide: # pragma: no cover - every legacy family has a 32-bit name
|
|
349
|
+
continue
|
|
350
|
+
canonical = wide[0]
|
|
351
|
+
aliases = {alias: width for alias, width in aliases.items() if width <= 4}
|
|
352
|
+
for alias, width in aliases.items():
|
|
353
|
+
table.setdefault(alias, (canonical, width))
|
|
354
|
+
return table
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _lookup(
|
|
358
|
+
operand: str, aliases: Mapping[str, tuple[str, int]], pointer_width: int
|
|
359
|
+
) -> tuple[str, int]:
|
|
360
|
+
"""Return ``(canonical register, width)`` of a register operand.
|
|
361
|
+
|
|
362
|
+
Registers outside the general-purpose set (``xmm0``, ``st0``, ...) are still
|
|
363
|
+
tracked, under their own lowercased name at the pointer width, so that a
|
|
364
|
+
write to them kills a previous definition instead of being ignored.
|
|
365
|
+
"""
|
|
366
|
+
name = operand.strip().lower()
|
|
367
|
+
hit = aliases.get(name)
|
|
368
|
+
if hit is not None:
|
|
369
|
+
return hit
|
|
370
|
+
return name, pointer_width
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _full_width(register: str, aliases: Mapping[str, tuple[str, int]], pointer_width: int) -> int:
|
|
374
|
+
"""Return the whole-register width of a canonical register."""
|
|
375
|
+
hit = aliases.get(register)
|
|
376
|
+
return hit[1] if hit is not None else pointer_width
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _mask(value: int, width: int) -> int:
|
|
380
|
+
"""Truncate ``value`` to the ``width``-byte register it is written to."""
|
|
381
|
+
return value & ((1 << (max(width, 1) * 8)) - 1)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _immediate(text: str) -> int | None:
|
|
385
|
+
"""Parse an immediate operand, or return ``None`` when it is not one."""
|
|
386
|
+
match = _IMMEDIATE.match((text or "").strip())
|
|
387
|
+
if match is None:
|
|
388
|
+
return None
|
|
389
|
+
body = match.group("body").lower()
|
|
390
|
+
try:
|
|
391
|
+
value = int(body, 16) if body.startswith("0x") or match.group("suffix") else int(body, 10)
|
|
392
|
+
except ValueError: # pragma: no cover - the pattern already restricts the digits
|
|
393
|
+
return None
|
|
394
|
+
return -value if match.group("sign") == "-" else value
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _rip_displacement(operand: str, pointer_width: int) -> int | None:
|
|
398
|
+
"""Return the displacement of a ``[rip +/- disp]`` operand, else ``None``.
|
|
399
|
+
|
|
400
|
+
Only a bare RIP base with a constant displacement is understood -- the shape
|
|
401
|
+
of ``lea rax, [rip + 0x10]`` and of a RIP-relative load. A base register, an
|
|
402
|
+
index, a symbol or a segment override returns ``None`` rather than a guess.
|
|
403
|
+
"""
|
|
404
|
+
memory = _parse_memory(operand, pointer_width)
|
|
405
|
+
if memory is None or memory.base != "rip" or memory.index is not None:
|
|
406
|
+
return None
|
|
407
|
+
text = operand.strip()
|
|
408
|
+
body = text[text.index("[") + 1 : text.rindex("]")]
|
|
409
|
+
displacement = 0
|
|
410
|
+
for term in _ADDRESS_TERM.finditer(body):
|
|
411
|
+
value = term.group("value")
|
|
412
|
+
if not value[0].isdigit(): # the RIP term itself
|
|
413
|
+
continue
|
|
414
|
+
parsed = _immediate(value)
|
|
415
|
+
if parsed is None:
|
|
416
|
+
return None
|
|
417
|
+
displacement += -parsed if term.group("sign") == "-" else parsed
|
|
418
|
+
return displacement
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
# reading the decoded instructions
|
|
423
|
+
# ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _instruction_address(instruction: Mapping[str, Any]) -> int:
|
|
427
|
+
"""Return the integer address of an ``IRInstruction.to_dict()`` payload."""
|
|
428
|
+
raw = instruction.get("address")
|
|
429
|
+
if isinstance(raw, int) and not isinstance(raw, bool):
|
|
430
|
+
return raw
|
|
431
|
+
token = str(raw or "").strip().lower()
|
|
432
|
+
if not token:
|
|
433
|
+
raise AnalysisError(f"instruction has no address: {instruction!r}")
|
|
434
|
+
try:
|
|
435
|
+
return int(token, 16) if token.startswith("0x") else int(token, 10)
|
|
436
|
+
except ValueError as exc:
|
|
437
|
+
raise AnalysisError(f"instruction has an unreadable address: {instruction!r}") from exc
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _instruction_size(instruction: Mapping[str, Any]) -> int:
|
|
441
|
+
"""Return the encoded size of an instruction, ``0`` when it is not reported."""
|
|
442
|
+
size = instruction.get("size")
|
|
443
|
+
if isinstance(size, int) and not isinstance(size, bool) and size > 0:
|
|
444
|
+
return size
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _read_registers(
|
|
449
|
+
mnemonic: str, parts: Sequence[str], aliases: Mapping[str, tuple[str, int]]
|
|
450
|
+
) -> tuple[str, ...]:
|
|
451
|
+
"""Return the canonical registers one instruction reads, sorted.
|
|
452
|
+
|
|
453
|
+
The first operand is only skipped when the mnemonic writes it *and* it is a
|
|
454
|
+
plain register: ``mov [rdi], rax`` still reads ``rdi``, because that operand
|
|
455
|
+
is an address. Operands are scanned for register tokens, so a bracketed
|
|
456
|
+
expression contributes its address registers and ``qword``/``ptr``/``rip``
|
|
457
|
+
are dropped by the alias table.
|
|
458
|
+
"""
|
|
459
|
+
destination_only = (
|
|
460
|
+
mnemonic in _MOV_MNEMONICS or mnemonic in {"lea", "pop"} or mnemonic.startswith("set")
|
|
461
|
+
)
|
|
462
|
+
found: set[str] = set()
|
|
463
|
+
for position, operand in enumerate(parts):
|
|
464
|
+
if position == 0 and destination_only and _is_plain_register(operand):
|
|
465
|
+
continue
|
|
466
|
+
for token in _WORD.findall(operand):
|
|
467
|
+
hit = aliases.get(token.lower())
|
|
468
|
+
if hit is not None:
|
|
469
|
+
found.add(hit[0])
|
|
470
|
+
return tuple(sorted(found))
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# ---------------------------------------------------------------------------
|
|
474
|
+
# classification of one write
|
|
475
|
+
# ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _copied_value(
|
|
479
|
+
state: Mapping[str, Definition],
|
|
480
|
+
source: str,
|
|
481
|
+
source_width: int,
|
|
482
|
+
width: int,
|
|
483
|
+
aliases: Mapping[str, tuple[str, int]],
|
|
484
|
+
pointer_width: int,
|
|
485
|
+
) -> int | None:
|
|
486
|
+
"""Return the constant a register copy carries, or ``None`` when unknown."""
|
|
487
|
+
if source_width != _full_width(source, aliases, pointer_width):
|
|
488
|
+
return None # a narrow alias does not expose the whole register
|
|
489
|
+
if width < source_width:
|
|
490
|
+
return None # the copy would truncate the source value
|
|
491
|
+
definition = state.get(source)
|
|
492
|
+
if definition is None or definition.value is None:
|
|
493
|
+
return None
|
|
494
|
+
return _mask(definition.value, width)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _folded_value(
|
|
498
|
+
mnemonic: str,
|
|
499
|
+
register: str,
|
|
500
|
+
width: int,
|
|
501
|
+
previous: int | None,
|
|
502
|
+
source: str,
|
|
503
|
+
state: Mapping[str, Definition],
|
|
504
|
+
aliases: Mapping[str, tuple[str, int]],
|
|
505
|
+
pointer_width: int,
|
|
506
|
+
) -> int | None:
|
|
507
|
+
"""Fold ``add``/``sub``/``xor`` of a known operand, or return ``None``."""
|
|
508
|
+
if mnemonic not in _FOLDING_MNEMONICS:
|
|
509
|
+
return None
|
|
510
|
+
if _is_plain_register(source):
|
|
511
|
+
source_register, source_width = _lookup(source, aliases, pointer_width)
|
|
512
|
+
if source_register == register and mnemonic in {"xor", "sub"}:
|
|
513
|
+
return 0 # a register xor/sub itself
|
|
514
|
+
if source_width != _full_width(source_register, aliases, pointer_width):
|
|
515
|
+
return None
|
|
516
|
+
operand: int | None = None
|
|
517
|
+
definition = state.get(source_register)
|
|
518
|
+
if definition is not None:
|
|
519
|
+
operand = definition.value
|
|
520
|
+
else:
|
|
521
|
+
operand = _immediate(source)
|
|
522
|
+
if previous is None or operand is None:
|
|
523
|
+
return None
|
|
524
|
+
if mnemonic == "add":
|
|
525
|
+
return _mask(previous + operand, width)
|
|
526
|
+
if mnemonic == "sub":
|
|
527
|
+
return _mask(previous - operand, width)
|
|
528
|
+
return _mask(previous ^ operand, width)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _classify(
|
|
532
|
+
instruction: Mapping[str, Any],
|
|
533
|
+
mnemonic: str,
|
|
534
|
+
parts: Sequence[str],
|
|
535
|
+
address: int,
|
|
536
|
+
state: Mapping[str, Definition],
|
|
537
|
+
aliases: Mapping[str, tuple[str, int]],
|
|
538
|
+
pointer_width: int,
|
|
539
|
+
) -> Definition | None:
|
|
540
|
+
"""Turn one instruction into a :class:`Definition`, or ``None`` if it writes none."""
|
|
541
|
+
if not parts or not _writes_first_operand(mnemonic):
|
|
542
|
+
return None
|
|
543
|
+
destination = parts[0].strip().lower()
|
|
544
|
+
if not _is_plain_register(destination):
|
|
545
|
+
return None # a memory or implicit destination defines no register
|
|
546
|
+
|
|
547
|
+
register, width = _lookup(destination, aliases, pointer_width)
|
|
548
|
+
source = parts[1].strip().lower() if len(parts) > 1 else ""
|
|
549
|
+
kind = KIND_UNKNOWN
|
|
550
|
+
value: int | None = None
|
|
551
|
+
source_register: str | None = None
|
|
552
|
+
|
|
553
|
+
previous = state.get(register)
|
|
554
|
+
previous_value = previous.value if previous is not None else None
|
|
555
|
+
|
|
556
|
+
if mnemonic in _MOV_MNEMONICS and mnemonic not in _EXTENDING_MNEMONICS and source:
|
|
557
|
+
if _is_plain_register(source):
|
|
558
|
+
source_register, source_width = _lookup(source, aliases, pointer_width)
|
|
559
|
+
kind = KIND_COPY
|
|
560
|
+
value = _copied_value(
|
|
561
|
+
state, source_register, source_width, width, aliases, pointer_width
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
immediate = _immediate(source)
|
|
565
|
+
if immediate is not None:
|
|
566
|
+
kind = KIND_CONST
|
|
567
|
+
value = _mask(immediate, width)
|
|
568
|
+
elif mnemonic == "lea" and source:
|
|
569
|
+
displacement = _rip_displacement(source, pointer_width)
|
|
570
|
+
if displacement is not None:
|
|
571
|
+
kind = KIND_CONST
|
|
572
|
+
value = _mask(address + _instruction_size(instruction) + displacement, width)
|
|
573
|
+
elif mnemonic in _ARITHMETIC_MNEMONICS and source:
|
|
574
|
+
kind = KIND_ARITHMETIC
|
|
575
|
+
value = _folded_value(
|
|
576
|
+
mnemonic, register, width, previous_value, source, state, aliases, pointer_width
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
if width < FULL_CONSTANT_WIDTH:
|
|
580
|
+
value = None # a partial write leaves the upper bytes of the value unknown
|
|
581
|
+
|
|
582
|
+
return Definition(
|
|
583
|
+
address=address,
|
|
584
|
+
register=register,
|
|
585
|
+
kind=kind,
|
|
586
|
+
value=value,
|
|
587
|
+
source_register=source_register,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ---------------------------------------------------------------------------
|
|
592
|
+
# public entry point
|
|
593
|
+
# ---------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def analyze_dataflow(
|
|
597
|
+
path: str | Path,
|
|
598
|
+
info: BinaryInfo,
|
|
599
|
+
address: int,
|
|
600
|
+
max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
|
|
601
|
+
max_blocks: int = DEFAULT_MAX_BLOCKS,
|
|
602
|
+
) -> DataFlowSummary:
|
|
603
|
+
"""Recover definitions, uses, constants and reaching definitions of a function.
|
|
604
|
+
|
|
605
|
+
The CFG is built with :func:`~codevanta.analysis.cfg.build_recursive_cfg` and
|
|
606
|
+
then walked once, forward, block by block in ascending address order and
|
|
607
|
+
instruction by instruction inside a block. Each instruction contributes its
|
|
608
|
+
read uses, which are resolved against the definitions seen so far, and -- when
|
|
609
|
+
it writes a register -- one :class:`Definition` that replaces (kills) whatever
|
|
610
|
+
the register held before. A folded value is masked to the written register's
|
|
611
|
+
width, and only the cases listed in the module docstring are folded.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
path: File the bytes are read from (read-only; never executed).
|
|
615
|
+
info: Loaded metadata used for address mapping, decoding and the pointer
|
|
616
|
+
width; 32-bit and 64-bit binaries are supported.
|
|
617
|
+
address: Virtual address of the first instruction; must be inside an
|
|
618
|
+
executable section.
|
|
619
|
+
max_instructions: Instruction budget
|
|
620
|
+
(``MIN_MAX_INSTRUCTIONS`` .. ``MAX_MAX_INSTRUCTIONS``).
|
|
621
|
+
max_blocks: Block budget (``MIN_MAX_BLOCKS`` .. ``MAX_MAX_BLOCKS``).
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
The :class:`DataFlowSummary` of the region: definitions and uses in
|
|
625
|
+
address order, the constants still known after the pass, one reaching
|
|
626
|
+
definition per resolved read, the bounded warnings and the evidence.
|
|
627
|
+
|
|
628
|
+
Raises:
|
|
629
|
+
AnalysisError: When ``address`` is not a non-negative integer, when a
|
|
630
|
+
budget is out of range or not an integer, when the binary is neither
|
|
631
|
+
32- nor 64-bit, when ``address`` is not inside an executable section,
|
|
632
|
+
or when the CFG cannot be built.
|
|
633
|
+
|
|
634
|
+
Note:
|
|
635
|
+
The pass is a linearization of the CFG, not a fixpoint: state flows into a
|
|
636
|
+
block from the block that precedes it in address order, so a join is
|
|
637
|
+
approximated by whichever predecessor has the highest address. What is
|
|
638
|
+
reported stays sound one definition at a time: a value is only folded
|
|
639
|
+
from instructions the summary itself proves, and only the *last*
|
|
640
|
+
definition of a register survives, so ``constant_registers`` and
|
|
641
|
+
``reaching_definition`` never claim two conflicting origins for one
|
|
642
|
+
value. Two calls with the same arguments always produce the same summary.
|
|
643
|
+
"""
|
|
644
|
+
if isinstance(address, bool) or not isinstance(address, int):
|
|
645
|
+
raise AnalysisError(f"address must be an integer, got {address!r}")
|
|
646
|
+
if address < 0:
|
|
647
|
+
raise AnalysisError(f"address must be non-negative, got {address}")
|
|
648
|
+
instruction_budget = _require_limit(
|
|
649
|
+
"max_instructions", max_instructions, MIN_MAX_INSTRUCTIONS, MAX_MAX_INSTRUCTIONS
|
|
650
|
+
)
|
|
651
|
+
block_budget = _require_limit("max_blocks", max_blocks, MIN_MAX_BLOCKS, MAX_MAX_BLOCKS)
|
|
652
|
+
pointer_width = _pointer_width(info)
|
|
653
|
+
|
|
654
|
+
section = info.section_for_va(address)
|
|
655
|
+
if section is None or not section.is_executable:
|
|
656
|
+
raise AnalysisError(f"address 0x{address:x} is not inside an executable section")
|
|
657
|
+
|
|
658
|
+
cfg = build_recursive_cfg(
|
|
659
|
+
path,
|
|
660
|
+
info,
|
|
661
|
+
address,
|
|
662
|
+
max_instructions=instruction_budget,
|
|
663
|
+
max_blocks=block_budget,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
aliases = _alias_table(pointer_width)
|
|
667
|
+
clobbers = frozenset(
|
|
668
|
+
hit[0] for alias in _CALL_CLOBBER_ALIASES if (hit := aliases.get(alias)) is not None
|
|
669
|
+
)
|
|
670
|
+
state: dict[str, Definition] = {}
|
|
671
|
+
definitions: list[Definition] = []
|
|
672
|
+
uses: list[Use] = []
|
|
673
|
+
reaching: dict[tuple[str, int], int] = {}
|
|
674
|
+
undefined: list[tuple[str, int]] = []
|
|
675
|
+
|
|
676
|
+
for block in sorted(cfg.blocks, key=lambda item: item.start):
|
|
677
|
+
for instruction in sorted(block.instructions, key=_instruction_address):
|
|
678
|
+
mnemonic, operands = _instruction_parts(instruction)
|
|
679
|
+
if not mnemonic: # pragma: no cover - the decoder always names a mnemonic
|
|
680
|
+
continue
|
|
681
|
+
current = _instruction_address(instruction)
|
|
682
|
+
parts = _split_operands(operands)
|
|
683
|
+
|
|
684
|
+
for register in _read_registers(mnemonic, parts, aliases):
|
|
685
|
+
uses.append(Use(current, register, READ))
|
|
686
|
+
known = state.get(register)
|
|
687
|
+
if known is None:
|
|
688
|
+
undefined.append((register, current))
|
|
689
|
+
else:
|
|
690
|
+
reaching[(register, current)] = known.address
|
|
691
|
+
|
|
692
|
+
definition = _classify(
|
|
693
|
+
instruction, mnemonic, parts, current, state, aliases, pointer_width
|
|
694
|
+
)
|
|
695
|
+
if definition is not None:
|
|
696
|
+
definitions.append(definition)
|
|
697
|
+
uses.append(Use(current, definition.register, WRITE))
|
|
698
|
+
state[definition.register] = definition
|
|
699
|
+
|
|
700
|
+
if mnemonic in _CALL_MNEMONICS:
|
|
701
|
+
for register in clobbers:
|
|
702
|
+
state.pop(register, None)
|
|
703
|
+
|
|
704
|
+
warnings = _undefined_warnings(undefined)
|
|
705
|
+
if cfg.truncated:
|
|
706
|
+
warnings.append(
|
|
707
|
+
"the CFG was truncated at the instruction or block budget; "
|
|
708
|
+
"definitions past that point are not modelled"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
constants = {
|
|
712
|
+
register: definition.value
|
|
713
|
+
for register, definition in sorted(state.items(), key=lambda item: item[0])
|
|
714
|
+
if definition.value is not None
|
|
715
|
+
}
|
|
716
|
+
evidence = Evidence(
|
|
717
|
+
source="dataflow.def_use",
|
|
718
|
+
method="forward_propagation",
|
|
719
|
+
confidence=Confidence.MEDIUM,
|
|
720
|
+
address=address,
|
|
721
|
+
detail=(
|
|
722
|
+
f"{len(definitions)} definition(s) and {len(uses)} use(s) over "
|
|
723
|
+
f"{len(cfg.blocks)} block(s); {len(reaching)} read(s) resolved to a "
|
|
724
|
+
f"definition and {len(constants)} register constant(s) survived"
|
|
725
|
+
),
|
|
726
|
+
extra={
|
|
727
|
+
"pointer_width": pointer_width,
|
|
728
|
+
"blocks": len(cfg.blocks),
|
|
729
|
+
"instructions": cfg.instruction_count,
|
|
730
|
+
"definitions": len(definitions),
|
|
731
|
+
"uses": len(uses),
|
|
732
|
+
"constant_registers": len(constants),
|
|
733
|
+
"constant_definitions": sum(1 for item in definitions if item.value is not None),
|
|
734
|
+
"reaching_definitions": len(reaching),
|
|
735
|
+
"undefined_reads": len(undefined),
|
|
736
|
+
"truncated": cfg.truncated,
|
|
737
|
+
},
|
|
738
|
+
)
|
|
739
|
+
return DataFlowSummary(
|
|
740
|
+
address=address,
|
|
741
|
+
definitions=tuple(definitions),
|
|
742
|
+
uses=tuple(uses),
|
|
743
|
+
constant_registers=constants,
|
|
744
|
+
reaching_definition=reaching,
|
|
745
|
+
warnings=tuple(warnings),
|
|
746
|
+
evidence=evidence,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _undefined_warnings(undefined: Sequence[tuple[str, int]]) -> list[str]:
|
|
751
|
+
"""Render the bounded "no reaching definition" warnings of one summary."""
|
|
752
|
+
warnings = [
|
|
753
|
+
f"read of {register} at 0x{address:x} has no reaching definition "
|
|
754
|
+
"in the analysed region"
|
|
755
|
+
for register, address in undefined[:MAX_UNDEFINED_WARNINGS]
|
|
756
|
+
]
|
|
757
|
+
remaining = len(undefined) - MAX_UNDEFINED_WARNINGS
|
|
758
|
+
if remaining > 0:
|
|
759
|
+
warnings.append(
|
|
760
|
+
f"{remaining} further read(s) have no reaching definition in the analysed region"
|
|
761
|
+
)
|
|
762
|
+
return warnings
|