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,1055 @@
|
|
|
1
|
+
"""Heuristic argument and return type inference from register use (phase 4).
|
|
2
|
+
|
|
3
|
+
A mnemonic-level renderer can never recover real C types, but it *can* report
|
|
4
|
+
how each ABI argument register is used, and the use is a type witness.
|
|
5
|
+
:func:`infer_argument_types` walks the decoded instructions of one function,
|
|
6
|
+
records one observation per use and turns the strongest observation into a C
|
|
7
|
+
type; :func:`infer_return_type` does the same for the value left in the return
|
|
8
|
+
register. Nothing is executed and nothing is invented: every emitted type
|
|
9
|
+
carries the observation that produced it.
|
|
10
|
+
|
|
11
|
+
Rule table for one argument register::
|
|
12
|
+
|
|
13
|
+
observation example C type
|
|
14
|
+
------------------------------------ --------------------------- -----------
|
|
15
|
+
dereferenced at pointer width mov rax, [rdi] uint64_t *
|
|
16
|
+
dereferenced narrower than a pointer mov eax, dword ptr [rdi] void *
|
|
17
|
+
used as a scaled index [rdi + rsi*4] size_t
|
|
18
|
+
only arithmetic on it add rdi, 1 uint64_t
|
|
19
|
+
copied whole, never narrowed mov rax, rdi uint64_t
|
|
20
|
+
compared and branched on test rdi, rdi / je int32_t
|
|
21
|
+
no usable observation never read, or opaque use unknown64
|
|
22
|
+
|
|
23
|
+
The C type of a rule is *width-aware*: the width of the register alias and of
|
|
24
|
+
the memory operand decide the type, so ``add edi, 1`` is ``uint32_t`` and
|
|
25
|
+
``dword ptr [ecx]`` on a 32-bit binary is a ``uint32_t *``. The pointer forms
|
|
26
|
+
and the ``unknownNN`` placeholder are keyed by the binary's pointer width.
|
|
27
|
+
|
|
28
|
+
When more than one rule fires, the most specific one wins -- pointers before
|
|
29
|
+
plain integers, integers before comparisons -- and every other observation is
|
|
30
|
+
kept in the evidence note together with the type it would have produced, so a
|
|
31
|
+
conflict is never resolved silently. The ranking (:data:`RULE_RANKS`) is::
|
|
32
|
+
|
|
33
|
+
pointer_deref > narrow_deref > index > arithmetic > copy > compare
|
|
34
|
+
|
|
35
|
+
Return type inference is deliberately simpler because there is one register to
|
|
36
|
+
watch: scanning stops at the first ``ret``, the last write to the return
|
|
37
|
+
register decides the width (``rax`` -> ``uint64_t``, ``eax`` -> ``uint32_t``,
|
|
38
|
+
``ax`` -> ``uint16_t``, ``al`` -> ``uint8_t``), a ``setcc`` write is called out
|
|
39
|
+
as an ``int32_t`` boolean, and a function that never writes the register before
|
|
40
|
+
a ``ret`` returns ``void``. A ``call`` right before the ``ret`` is mentioned in
|
|
41
|
+
the note, because whatever the callee left in the register is not ours.
|
|
42
|
+
|
|
43
|
+
Accepted instruction shapes are the ones the rest of the engine already uses:
|
|
44
|
+
``IRInstruction.to_dict()`` payloads (``{"mnemonic", "operands", ...}``),
|
|
45
|
+
:class:`~codevanta.ir.instruction.IRInstruction` objects, or a plain
|
|
46
|
+
``"mnemonic operands"`` string.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
import re
|
|
52
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
53
|
+
from dataclasses import dataclass
|
|
54
|
+
from typing import Any
|
|
55
|
+
|
|
56
|
+
from codevanta.core.report import AnalysisError
|
|
57
|
+
from codevanta.evidence.confidence import Confidence
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"ABI_ARGUMENT_REGISTERS",
|
|
61
|
+
"C_TYPE_INT32",
|
|
62
|
+
"C_TYPE_POINTER_32",
|
|
63
|
+
"C_TYPE_POINTER_64",
|
|
64
|
+
"C_TYPE_SIZE_T",
|
|
65
|
+
"C_TYPE_UINT8",
|
|
66
|
+
"C_TYPE_UINT16",
|
|
67
|
+
"C_TYPE_UINT32",
|
|
68
|
+
"C_TYPE_UINT64",
|
|
69
|
+
"C_TYPE_UNKNOWN_32",
|
|
70
|
+
"C_TYPE_UNKNOWN_64",
|
|
71
|
+
"C_TYPE_VOID",
|
|
72
|
+
"C_TYPE_VOID_POINTER",
|
|
73
|
+
"INTEGER_C_TYPES",
|
|
74
|
+
"POINTER_C_TYPES",
|
|
75
|
+
"RULE_RANKS",
|
|
76
|
+
"UNKNOWN_C_TYPES",
|
|
77
|
+
"ArgumentType",
|
|
78
|
+
"ReturnType",
|
|
79
|
+
"infer_argument_types",
|
|
80
|
+
"infer_return_type",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# emitted C type names
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
#: Unsigned integer of exactly one byte.
|
|
88
|
+
C_TYPE_UINT8 = "uint8_t"
|
|
89
|
+
|
|
90
|
+
#: Unsigned integer of exactly two bytes.
|
|
91
|
+
C_TYPE_UINT16 = "uint16_t"
|
|
92
|
+
|
|
93
|
+
#: Unsigned integer of exactly four bytes.
|
|
94
|
+
C_TYPE_UINT32 = "uint32_t"
|
|
95
|
+
|
|
96
|
+
#: Unsigned integer of exactly eight bytes.
|
|
97
|
+
C_TYPE_UINT64 = "uint64_t"
|
|
98
|
+
|
|
99
|
+
#: Signed 32-bit integer, emitted for comparison/boolean evidence.
|
|
100
|
+
C_TYPE_INT32 = "int32_t"
|
|
101
|
+
|
|
102
|
+
#: ``size_t``, emitted for a register used as an address index.
|
|
103
|
+
C_TYPE_SIZE_T = "size_t"
|
|
104
|
+
|
|
105
|
+
#: ``void``, emitted for a function that never writes its return register.
|
|
106
|
+
C_TYPE_VOID = "void"
|
|
107
|
+
|
|
108
|
+
#: A pointer to nothing in particular, emitted for a sub-pointer-width deref.
|
|
109
|
+
C_TYPE_VOID_POINTER = "void *"
|
|
110
|
+
|
|
111
|
+
#: Pointer type of a 64-bit binary.
|
|
112
|
+
C_TYPE_POINTER_64 = "uint64_t *"
|
|
113
|
+
|
|
114
|
+
#: Pointer type of a 32-bit binary.
|
|
115
|
+
C_TYPE_POINTER_32 = "uint32_t *"
|
|
116
|
+
|
|
117
|
+
#: Placeholder emitted when no rule fires on a 64-bit binary.
|
|
118
|
+
C_TYPE_UNKNOWN_64 = "unknown64"
|
|
119
|
+
|
|
120
|
+
#: Placeholder emitted when no rule fires on a 32-bit binary.
|
|
121
|
+
C_TYPE_UNKNOWN_32 = "unknown32"
|
|
122
|
+
|
|
123
|
+
#: Access width in bytes -> the unsigned C integer of that width.
|
|
124
|
+
INTEGER_C_TYPES: dict[int, str] = {
|
|
125
|
+
1: C_TYPE_UINT8,
|
|
126
|
+
2: C_TYPE_UINT16,
|
|
127
|
+
4: C_TYPE_UINT32,
|
|
128
|
+
8: C_TYPE_UINT64,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#: Pointer width in bytes -> the pointer type of a pointer-width dereference.
|
|
132
|
+
POINTER_C_TYPES: dict[int, str] = {
|
|
133
|
+
1: "uint8_t *",
|
|
134
|
+
2: "uint16_t *",
|
|
135
|
+
4: C_TYPE_POINTER_32,
|
|
136
|
+
8: C_TYPE_POINTER_64,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#: Pointer width in bytes -> the placeholder type of an unproven argument.
|
|
140
|
+
UNKNOWN_C_TYPES: dict[int, str] = {
|
|
141
|
+
4: C_TYPE_UNKNOWN_32,
|
|
142
|
+
8: C_TYPE_UNKNOWN_64,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# ABI tables
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
#: Argument registers per ABI, in argument order, for a 64-bit binary.
|
|
150
|
+
ABI_ARGUMENT_REGISTERS: dict[str, tuple[str, ...]] = {
|
|
151
|
+
"sysv": ("rdi", "rsi", "rdx", "rcx", "r8", "r9"),
|
|
152
|
+
"ms64": ("rcx", "rdx", "r8", "r9"),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#: Argument registers used for a 32-bit binary, whatever the ABI is called.
|
|
156
|
+
_ARGUMENTS_32: tuple[str, ...] = ("ecx", "edx")
|
|
157
|
+
|
|
158
|
+
#: Accepted spellings of an ABI name, lowercased and without separators.
|
|
159
|
+
_ABI_SPELLINGS: dict[str, str] = {
|
|
160
|
+
"sysv": "sysv",
|
|
161
|
+
"systemv": "sysv",
|
|
162
|
+
"amd64": "sysv",
|
|
163
|
+
"ms": "ms64",
|
|
164
|
+
"ms64": "ms64",
|
|
165
|
+
"win64": "ms64",
|
|
166
|
+
"microsoft": "ms64",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#: Canonical argument register -> {alias as Capstone prints it: width in bytes}.
|
|
170
|
+
_REGISTER_ALIAS_WIDTHS: dict[str, dict[str, int]] = {
|
|
171
|
+
"rdi": {"rdi": 8, "edi": 4, "di": 2, "dil": 1},
|
|
172
|
+
"rsi": {"rsi": 8, "esi": 4, "si": 2, "sil": 1},
|
|
173
|
+
"rdx": {"rdx": 8, "edx": 4, "dx": 2, "dl": 1, "dh": 1},
|
|
174
|
+
"rcx": {"rcx": 8, "ecx": 4, "cx": 2, "cl": 1, "ch": 1},
|
|
175
|
+
"r8": {"r8": 8, "r8d": 4, "r8w": 2, "r8b": 1},
|
|
176
|
+
"r9": {"r9": 8, "r9d": 4, "r9w": 2, "r9b": 1},
|
|
177
|
+
"ecx": {"ecx": 4, "cx": 2, "cl": 1, "ch": 1},
|
|
178
|
+
"edx": {"edx": 4, "dx": 2, "dl": 1, "dh": 1},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#: Alias -> width in bytes, unioned over every supported register set. Widths
|
|
182
|
+
#: do not conflict between sets (``edx`` is four bytes either way), only the
|
|
183
|
+
#: canonical name does, which is why :func:`_alias_table` is built per ABI.
|
|
184
|
+
_ARGUMENT_ALIAS_WIDTHS: dict[str, int] = {
|
|
185
|
+
alias: width
|
|
186
|
+
for aliases in _REGISTER_ALIAS_WIDTHS.values()
|
|
187
|
+
for alias, width in aliases.items()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#: Return register aliases, keyed by pointer width.
|
|
191
|
+
_RETURN_ALIAS_WIDTHS: dict[int, dict[str, int]] = {
|
|
192
|
+
8: {"rax": 8, "eax": 4, "ax": 2, "al": 1, "ah": 1},
|
|
193
|
+
4: {"eax": 4, "ax": 2, "al": 1, "ah": 1},
|
|
194
|
+
2: {"ax": 2, "al": 1, "ah": 1},
|
|
195
|
+
1: {"al": 1, "ah": 1},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#: Name used in evidence prose for the return register, keyed by pointer width.
|
|
199
|
+
_RETURN_REGISTER: dict[int, str] = {1: "al", 2: "ax", 4: "eax", 8: "rax"}
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# mnemonic tables
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
#: ``mov``-style mnemonics: the first operand is only ever written.
|
|
206
|
+
_MOV_MNEMONICS = frozenset({"mov", "movabs", "movbe", "movsx", "movsxd", "movzx", "movq"})
|
|
207
|
+
|
|
208
|
+
#: Mnemonics that transfer control to a subroutine.
|
|
209
|
+
_CALL_MNEMONICS = frozenset({"call", "callq", "bl", "blx", "blr"})
|
|
210
|
+
|
|
211
|
+
#: Mnemonics that end the scanned region with a plain return.
|
|
212
|
+
_RET_MNEMONICS = frozenset({"ret", "retn", "retf", "retq", "iret", "iretd", "iretq", "eret"})
|
|
213
|
+
|
|
214
|
+
#: Mnemonics that only compare their operands and set flags.
|
|
215
|
+
_COMPARE_MNEMONICS = frozenset({"cmp", "test", "comiss", "comisd", "ucomiss", "ucomisd"})
|
|
216
|
+
|
|
217
|
+
#: Mnemonics that never write their first operand.
|
|
218
|
+
_READ_ONLY_MNEMONICS = frozenset(
|
|
219
|
+
{
|
|
220
|
+
"bt",
|
|
221
|
+
"call",
|
|
222
|
+
"callq",
|
|
223
|
+
"cmp",
|
|
224
|
+
"comisd",
|
|
225
|
+
"comiss",
|
|
226
|
+
"int",
|
|
227
|
+
"int3",
|
|
228
|
+
"jmp",
|
|
229
|
+
"ljmp",
|
|
230
|
+
"nop",
|
|
231
|
+
"push",
|
|
232
|
+
"ret",
|
|
233
|
+
"test",
|
|
234
|
+
"ucomisd",
|
|
235
|
+
"ucomiss",
|
|
236
|
+
"ud2",
|
|
237
|
+
"xlat",
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
#: Unconditional jumps; any other ``j*`` mnemonic branches on the flags.
|
|
242
|
+
_UNCONDITIONAL_JUMPS = frozenset({"jmp", "ljmp", "b", "br", "bx"})
|
|
243
|
+
|
|
244
|
+
#: Integer operations a value can take part in without leaving its width.
|
|
245
|
+
_ARITHMETIC_MNEMONICS = frozenset(
|
|
246
|
+
{
|
|
247
|
+
"adc",
|
|
248
|
+
"add",
|
|
249
|
+
"and",
|
|
250
|
+
"dec",
|
|
251
|
+
"div",
|
|
252
|
+
"idiv",
|
|
253
|
+
"imul",
|
|
254
|
+
"inc",
|
|
255
|
+
"mul",
|
|
256
|
+
"neg",
|
|
257
|
+
"not",
|
|
258
|
+
"or",
|
|
259
|
+
"rol",
|
|
260
|
+
"ror",
|
|
261
|
+
"sal",
|
|
262
|
+
"sar",
|
|
263
|
+
"sbb",
|
|
264
|
+
"shl",
|
|
265
|
+
"shr",
|
|
266
|
+
"sub",
|
|
267
|
+
"xor",
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# rule names and ranking
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
_RULE_POINTER_DEREF = "pointer_deref"
|
|
276
|
+
_RULE_NARROW_DEREF = "narrow_deref"
|
|
277
|
+
_RULE_INDEX = "index"
|
|
278
|
+
_RULE_ARITHMETIC = "arithmetic"
|
|
279
|
+
_RULE_COPY = "copy"
|
|
280
|
+
_RULE_COMPARE = "compare"
|
|
281
|
+
|
|
282
|
+
#: Specificity of every rule: the highest rank wins when several rules fire.
|
|
283
|
+
RULE_RANKS: dict[str, int] = {
|
|
284
|
+
_RULE_POINTER_DEREF: 7,
|
|
285
|
+
_RULE_NARROW_DEREF: 6,
|
|
286
|
+
_RULE_INDEX: 5,
|
|
287
|
+
_RULE_ARITHMETIC: 4,
|
|
288
|
+
_RULE_COPY: 3,
|
|
289
|
+
_RULE_COMPARE: 2,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(frozen=True)
|
|
294
|
+
class ArgumentType:
|
|
295
|
+
"""An inferred C type for one argument of the rendered function.
|
|
296
|
+
|
|
297
|
+
Attributes:
|
|
298
|
+
register: Canonical ABI argument register, e.g. ``"rdi"``.
|
|
299
|
+
name: Parameter name used by the pseudo-C renderer, e.g. ``"a0"``.
|
|
300
|
+
c_type: Emitted C type, e.g. :data:`C_TYPE_POINTER_64`.
|
|
301
|
+
evidence_note: The observation that produced ``c_type``; when several
|
|
302
|
+
rules fired, the losing observations and their types are listed
|
|
303
|
+
too, so a conflict is never hidden.
|
|
304
|
+
confidence: How strongly ``c_type`` is supported. ``MEDIUM`` for a use
|
|
305
|
+
that fixes a width (dereference, index, arithmetic), ``LOW`` for a
|
|
306
|
+
weaker hint (a whole-register copy, a comparison, no evidence).
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
register: str
|
|
310
|
+
name: str
|
|
311
|
+
c_type: str
|
|
312
|
+
evidence_note: str
|
|
313
|
+
confidence: Confidence
|
|
314
|
+
|
|
315
|
+
def to_dict(self) -> dict[str, Any]:
|
|
316
|
+
"""Serialize the inferred argument type for the JSON report."""
|
|
317
|
+
return {
|
|
318
|
+
"register": self.register,
|
|
319
|
+
"name": self.name,
|
|
320
|
+
"c_type": self.c_type,
|
|
321
|
+
"evidence_note": self.evidence_note,
|
|
322
|
+
"confidence": self.confidence.value,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@dataclass(frozen=True)
|
|
327
|
+
class ReturnType:
|
|
328
|
+
"""An inferred C return type for the rendered function.
|
|
329
|
+
|
|
330
|
+
Attributes:
|
|
331
|
+
c_type: Emitted C type: ``void``, a width-matched unsigned integer or
|
|
332
|
+
:data:`C_TYPE_INT32` for a ``setcc`` boolean.
|
|
333
|
+
evidence_note: The observation that produced ``c_type``.
|
|
334
|
+
confidence: ``MEDIUM`` when an instruction writes the return register
|
|
335
|
+
with an observable width, ``LOW`` for ``void`` (absence of a write
|
|
336
|
+
is weaker evidence than a write).
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
c_type: str
|
|
340
|
+
evidence_note: str
|
|
341
|
+
confidence: Confidence
|
|
342
|
+
|
|
343
|
+
def to_dict(self) -> dict[str, Any]:
|
|
344
|
+
"""Serialize the inferred return type for the JSON report."""
|
|
345
|
+
return {
|
|
346
|
+
"c_type": self.c_type,
|
|
347
|
+
"evidence_note": self.evidence_note,
|
|
348
|
+
"confidence": self.confidence.value,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _unsigned_type(width: int) -> str:
|
|
353
|
+
"""Return the unsigned C integer of ``width`` bytes."""
|
|
354
|
+
return INTEGER_C_TYPES.get(width, C_TYPE_UINT64)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _pointer_type(width: int) -> str:
|
|
358
|
+
"""Return the pointer C type of a ``width``-byte pointer."""
|
|
359
|
+
return POINTER_C_TYPES.get(width, f"{_unsigned_type(width)} *")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _unknown_type(width: int) -> str:
|
|
363
|
+
"""Return the placeholder type for a pointer width with no evidence."""
|
|
364
|
+
return UNKNOWN_C_TYPES.get(width, f"unknown{width * 8}")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
# input normalization and operand parsing
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
#: Intel size keyword -> access width in bytes. Keywords that are not listed
|
|
372
|
+
#: (``tbyte``, ``xmmword``, ...) fall back to the pointer width.
|
|
373
|
+
_SIZE_KEYWORD_WIDTHS: dict[str, int] = {
|
|
374
|
+
"byte": 1,
|
|
375
|
+
"word": 2,
|
|
376
|
+
"dword": 4,
|
|
377
|
+
"qword": 8,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#: A memory operand: an optional size keyword plus a bracketed expression.
|
|
381
|
+
_MEMORY_OPERAND = re.compile(
|
|
382
|
+
r"^(?:(?P<size>byte|word|dword|qword|tbyte|xmmword|oword|fword)\s+ptr\s+)?"
|
|
383
|
+
r"\[\s*(?P<body>[^]]*?)\s*\]$",
|
|
384
|
+
re.IGNORECASE,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
#: One term of a bracketed address expression.
|
|
388
|
+
_ADDRESS_TERM = re.compile(
|
|
389
|
+
r"(?P<sign>[+-])?\s*(?P<value>0x[0-9a-fA-F]+|\d+|[a-zA-Z][a-zA-Z0-9]*)"
|
|
390
|
+
r"(?:\s*\*\s*(?P<scale>\d+))?"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
#: A whole identifier-ish token, used to find register mentions.
|
|
394
|
+
_WORD = re.compile(r"[a-zA-Z][a-zA-Z0-9]*")
|
|
395
|
+
|
|
396
|
+
#: Immediates that mean "zero" once Capstone has printed them.
|
|
397
|
+
_ZERO_TOKENS = frozenset({"0", "0x0", "0h", "-0"})
|
|
398
|
+
|
|
399
|
+
#: How many touched instructions one "no evidence" note may remember.
|
|
400
|
+
_MAX_USES = 4
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _instruction_parts(instruction: Any) -> tuple[str, str]:
|
|
404
|
+
"""Return the lowercased mnemonic and the operand text of one instruction.
|
|
405
|
+
|
|
406
|
+
``IRInstruction.to_dict()`` payloads, :class:`~codevanta.ir.instruction.
|
|
407
|
+
IRInstruction` objects and plain ``"mnemonic operands"`` strings are all
|
|
408
|
+
accepted, because that is what the callers in this package already hold.
|
|
409
|
+
"""
|
|
410
|
+
if isinstance(instruction, str):
|
|
411
|
+
head, _, tail = instruction.strip().partition(" ")
|
|
412
|
+
return head.lower(), tail.strip()
|
|
413
|
+
mnemonic: Any = None
|
|
414
|
+
operands: Any = None
|
|
415
|
+
if isinstance(instruction, Mapping):
|
|
416
|
+
mnemonic = instruction.get("mnemonic")
|
|
417
|
+
operands = instruction.get("operands")
|
|
418
|
+
else:
|
|
419
|
+
mnemonic = getattr(instruction, "mnemonic", None)
|
|
420
|
+
operands = getattr(instruction, "operands", None)
|
|
421
|
+
return str(mnemonic or "").strip().lower(), str(operands or "").strip()
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _split_operands(text: str) -> list[str]:
|
|
425
|
+
"""Split Capstone operand text on top-level commas."""
|
|
426
|
+
text = (text or "").strip()
|
|
427
|
+
if not text:
|
|
428
|
+
return []
|
|
429
|
+
parts: list[str] = []
|
|
430
|
+
current: list[str] = []
|
|
431
|
+
depth = 0
|
|
432
|
+
for character in text:
|
|
433
|
+
if character == "[":
|
|
434
|
+
depth += 1
|
|
435
|
+
elif character == "]":
|
|
436
|
+
depth = max(0, depth - 1)
|
|
437
|
+
if character == "," and depth == 0:
|
|
438
|
+
parts.append("".join(current).strip())
|
|
439
|
+
current = []
|
|
440
|
+
continue
|
|
441
|
+
current.append(character)
|
|
442
|
+
parts.append("".join(current).strip())
|
|
443
|
+
return [part for part in parts if part]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _stream(instructions: Iterable[Any]) -> list[tuple[str, str]]:
|
|
447
|
+
"""Materialize the instruction stream as ``(mnemonic, operands)`` pairs."""
|
|
448
|
+
if isinstance(instructions, (str, bytes)):
|
|
449
|
+
raise AnalysisError("instructions must be an iterable of instructions, not a string")
|
|
450
|
+
stream: list[tuple[str, str]] = []
|
|
451
|
+
for instruction in instructions:
|
|
452
|
+
mnemonic, operands = _instruction_parts(instruction)
|
|
453
|
+
if mnemonic:
|
|
454
|
+
stream.append((mnemonic, operands))
|
|
455
|
+
return stream
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _text(mnemonic: str, operands: str) -> str:
|
|
459
|
+
"""Return ``"mnemonic operands"`` of one instruction, for evidence prose."""
|
|
460
|
+
return f"{mnemonic} {operands}".strip()
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@dataclass(frozen=True)
|
|
464
|
+
class _Memory:
|
|
465
|
+
"""A parsed bracketed address expression.
|
|
466
|
+
|
|
467
|
+
Attributes:
|
|
468
|
+
base: Base register, lowercased, or ``None`` for an index-only or
|
|
469
|
+
absolute operand.
|
|
470
|
+
index: Index register, lowercased, or ``None``.
|
|
471
|
+
scale: Index scale, ``1`` when the operand carries none.
|
|
472
|
+
width: Access width in bytes: the size keyword, else the pointer width.
|
|
473
|
+
text: The operand as it was printed, for evidence prose.
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
base: str | None
|
|
477
|
+
index: str | None
|
|
478
|
+
scale: int
|
|
479
|
+
width: int
|
|
480
|
+
text: str
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _parse_memory(operand: str, pointer_width: int) -> _Memory | None:
|
|
484
|
+
"""Parse a bracketed address expression, or return ``None`` if unhandled.
|
|
485
|
+
|
|
486
|
+
``[base]``, ``[base +/- disp]``, ``[index]``, ``[index*scale]`` and
|
|
487
|
+
``[base + index*scale +/- disp]`` are understood. Segment prefixes and
|
|
488
|
+
expressions with more than a base and an index are skipped rather than
|
|
489
|
+
guessed at.
|
|
490
|
+
"""
|
|
491
|
+
match = _MEMORY_OPERAND.match(operand.strip())
|
|
492
|
+
if match is None:
|
|
493
|
+
return None
|
|
494
|
+
body = match.group("body")
|
|
495
|
+
if ":" in body: # a segment override is never guessed at
|
|
496
|
+
return None
|
|
497
|
+
base: str | None = None
|
|
498
|
+
index: str | None = None
|
|
499
|
+
scale = 1
|
|
500
|
+
for term in _ADDRESS_TERM.finditer(body):
|
|
501
|
+
value = term.group("value")
|
|
502
|
+
if value[0].isdigit(): # a displacement, not a register
|
|
503
|
+
continue
|
|
504
|
+
register = value.lower()
|
|
505
|
+
term_scale = int(term.group("scale")) if term.group("scale") else 1
|
|
506
|
+
if base is None and term_scale == 1:
|
|
507
|
+
base = register
|
|
508
|
+
elif index is None:
|
|
509
|
+
index = register
|
|
510
|
+
scale = term_scale
|
|
511
|
+
else: # more than a base and an index: not modelled
|
|
512
|
+
return None
|
|
513
|
+
keyword = match.group("size")
|
|
514
|
+
width = _SIZE_KEYWORD_WIDTHS.get(keyword.lower(), 0) if keyword else 0
|
|
515
|
+
return _Memory(base, index, scale, width or pointer_width, operand.strip())
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
# register aliases and operand scanning
|
|
520
|
+
# ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _alias_table(registers: Sequence[str]) -> dict[str, tuple[str, int]]:
|
|
524
|
+
"""Map every alias of ``registers`` to ``(canonical register, width)``.
|
|
525
|
+
|
|
526
|
+
The table is built per register set, so ``edx`` is *canonical* on a 32-bit
|
|
527
|
+
binary (``ecx``/``edx``) and an alias of ``rdx`` on a 64-bit one.
|
|
528
|
+
"""
|
|
529
|
+
table: dict[str, tuple[str, int]] = {}
|
|
530
|
+
for canonical in registers:
|
|
531
|
+
for alias, width in _REGISTER_ALIAS_WIDTHS.get(canonical, {canonical: 8}).items():
|
|
532
|
+
table.setdefault(alias, (canonical, width))
|
|
533
|
+
return table
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _register_read_tokens(mnemonic: str, parts: Sequence[str]) -> set[str]:
|
|
537
|
+
"""Return the lowercased tokens an instruction *reads*.
|
|
538
|
+
|
|
539
|
+
The first operand is skipped for the mnemonics that only write it
|
|
540
|
+
(``mov``-family, ``lea``, ``pop``, ``setcc``), so ``mov rdi, rax`` does not
|
|
541
|
+
count as a read of ``rdi``. This mirrors the argument counter in
|
|
542
|
+
:mod:`codevanta.analysis.decompile`, so the signature keeps agreeing with
|
|
543
|
+
the rendered body.
|
|
544
|
+
"""
|
|
545
|
+
destination_only = (
|
|
546
|
+
mnemonic in _MOV_MNEMONICS or mnemonic in {"lea", "pop"} or mnemonic.startswith("set")
|
|
547
|
+
)
|
|
548
|
+
tokens: set[str] = set()
|
|
549
|
+
for position, operand in enumerate(parts):
|
|
550
|
+
if position == 0 and destination_only:
|
|
551
|
+
continue
|
|
552
|
+
tokens.update(token.lower() for token in _WORD.findall(operand))
|
|
553
|
+
return tokens
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _plain_operands(parts: Sequence[str], *, skip_first: bool) -> list[tuple[str, int]]:
|
|
557
|
+
"""Return ``(alias, width)`` for every register mentioned outside brackets.
|
|
558
|
+
|
|
559
|
+
Bracket expressions are handled by the dereference and index rules; a token
|
|
560
|
+
inside them is an address component, not the value itself.
|
|
561
|
+
"""
|
|
562
|
+
hits: list[tuple[str, int]] = []
|
|
563
|
+
for position, operand in enumerate(parts):
|
|
564
|
+
if position == 0 and skip_first:
|
|
565
|
+
continue
|
|
566
|
+
if "[" in operand:
|
|
567
|
+
continue
|
|
568
|
+
for token in _WORD.findall(operand):
|
|
569
|
+
name = token.lower()
|
|
570
|
+
width = _ARGUMENT_ALIAS_WIDTHS.get(name)
|
|
571
|
+
if width is not None:
|
|
572
|
+
hits.append((name, width))
|
|
573
|
+
return hits
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _is_plain_register(operand: str) -> bool:
|
|
577
|
+
"""Whether an operand is a bare register name."""
|
|
578
|
+
return bool(_WORD.fullmatch(operand.strip()))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _compare_targets(mnemonic: str, parts: Sequence[str]) -> list[tuple[str, bool]]:
|
|
582
|
+
"""Return ``(alias, compares_against_zero)`` for a comparison's operands."""
|
|
583
|
+
if mnemonic not in _COMPARE_MNEMONICS or len(parts) != 2:
|
|
584
|
+
return []
|
|
585
|
+
left, right = (part.strip().lower() for part in parts)
|
|
586
|
+
left_is_zero = left in _ZERO_TOKENS
|
|
587
|
+
right_is_zero = right in _ZERO_TOKENS
|
|
588
|
+
if mnemonic == "test" and (left == right or (left_is_zero and right_is_zero)):
|
|
589
|
+
return [(left, True)] if _is_plain_register(left) else []
|
|
590
|
+
if left_is_zero and not right_is_zero:
|
|
591
|
+
return [(right, True)] if _is_plain_register(right) else []
|
|
592
|
+
if right_is_zero and not left_is_zero:
|
|
593
|
+
return [(left, True)] if _is_plain_register(left) else []
|
|
594
|
+
targets: list[tuple[str, bool]] = []
|
|
595
|
+
for operand in (left, right):
|
|
596
|
+
if _is_plain_register(operand):
|
|
597
|
+
targets.append((operand, False))
|
|
598
|
+
return targets
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# ---------------------------------------------------------------------------
|
|
602
|
+
# observations
|
|
603
|
+
# ---------------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
@dataclass(frozen=True)
|
|
607
|
+
class _Observation:
|
|
608
|
+
"""One use of an argument register, as a candidate type.
|
|
609
|
+
|
|
610
|
+
Attributes:
|
|
611
|
+
rule: Which inference rule fired, a key of :data:`RULE_RANKS`.
|
|
612
|
+
c_type: The C type this observation alone would emit.
|
|
613
|
+
note: The observation itself, e.g. ``"dereferenced at pointer width ..."``.
|
|
614
|
+
confidence: How strong this kind of evidence is on its own.
|
|
615
|
+
width: Width in bytes the observation was made at; breaks rank ties.
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
rule: str
|
|
619
|
+
c_type: str
|
|
620
|
+
note: str
|
|
621
|
+
confidence: Confidence
|
|
622
|
+
width: int
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def rank(self) -> int:
|
|
626
|
+
"""Specificity of the rule that produced this observation."""
|
|
627
|
+
return RULE_RANKS[self.rule]
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _deref_observation(memory: _Memory, pointer_width: int, text: str) -> _Observation:
|
|
631
|
+
"""Turn a pointer-width or narrow dereference into one observation."""
|
|
632
|
+
if memory.width >= pointer_width:
|
|
633
|
+
return _Observation(
|
|
634
|
+
rule=_RULE_POINTER_DEREF,
|
|
635
|
+
c_type=_pointer_type(pointer_width),
|
|
636
|
+
note=f"dereferenced at pointer width ({memory.text}) in '{text}'",
|
|
637
|
+
confidence=Confidence.MEDIUM,
|
|
638
|
+
width=pointer_width,
|
|
639
|
+
)
|
|
640
|
+
return _Observation(
|
|
641
|
+
rule=_RULE_NARROW_DEREF,
|
|
642
|
+
c_type=C_TYPE_VOID_POINTER,
|
|
643
|
+
note=(
|
|
644
|
+
f"dereferenced at {memory.width} byte(s), narrower than the "
|
|
645
|
+
f"{pointer_width}-byte pointer ({memory.text}) in '{text}'"
|
|
646
|
+
),
|
|
647
|
+
confidence=Confidence.MEDIUM,
|
|
648
|
+
width=memory.width,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _index_observation(memory: _Memory, text: str) -> _Observation:
|
|
653
|
+
"""Turn a register used as an address index into a ``size_t`` observation."""
|
|
654
|
+
scale = f"x{memory.scale}" if memory.scale > 1 else "x1"
|
|
655
|
+
return _Observation(
|
|
656
|
+
rule=_RULE_INDEX,
|
|
657
|
+
c_type=C_TYPE_SIZE_T,
|
|
658
|
+
note=f"used as a scaled address index ({scale}) in '{text}'",
|
|
659
|
+
confidence=Confidence.MEDIUM,
|
|
660
|
+
width=4,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _address_arithmetic(text: str, pointer_width: int) -> _Observation:
|
|
665
|
+
"""Turn a base register of a ``lea`` (no dereference) into an observation."""
|
|
666
|
+
return _Observation(
|
|
667
|
+
rule=_RULE_ARITHMETIC,
|
|
668
|
+
c_type=_unsigned_type(pointer_width),
|
|
669
|
+
note=f"used in address arithmetic only, never dereferenced ({text})",
|
|
670
|
+
confidence=Confidence.MEDIUM,
|
|
671
|
+
width=pointer_width,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _arithmetic_observation(text: str, width: int) -> _Observation:
|
|
676
|
+
"""Turn integer arithmetic on a register into a width-matched observation."""
|
|
677
|
+
return _Observation(
|
|
678
|
+
rule=_RULE_ARITHMETIC,
|
|
679
|
+
c_type=_unsigned_type(width),
|
|
680
|
+
note=f"only integer arithmetic on it at {width * 8} bits ({text})",
|
|
681
|
+
confidence=Confidence.MEDIUM,
|
|
682
|
+
width=width,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _copy_observation(text: str, alias: str, width: int) -> _Observation:
|
|
687
|
+
"""Turn a whole-register copy into a width-matched observation.
|
|
688
|
+
|
|
689
|
+
A copy narrows nothing on its own: ``mov rax, rdi`` moves eight bytes, so the
|
|
690
|
+
value is at least a 64-bit integer. It is weaker evidence than arithmetic,
|
|
691
|
+
hence ``Confidence.LOW``.
|
|
692
|
+
"""
|
|
693
|
+
return _Observation(
|
|
694
|
+
rule=_RULE_COPY,
|
|
695
|
+
c_type=_unsigned_type(width),
|
|
696
|
+
note=f"copied whole through {alias} at {width * 8} bits ({text})",
|
|
697
|
+
confidence=Confidence.LOW,
|
|
698
|
+
width=width,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class _RegisterEvidence:
|
|
703
|
+
"""Everything observed about one argument register."""
|
|
704
|
+
|
|
705
|
+
def __init__(self) -> None:
|
|
706
|
+
self.observations: dict[tuple[str, str], tuple[_Observation, int]] = {}
|
|
707
|
+
self.uses: list[str] = []
|
|
708
|
+
|
|
709
|
+
def record(self, observation: _Observation) -> None:
|
|
710
|
+
"""Add one observation, counting repeats of the same rule and type."""
|
|
711
|
+
key = (observation.rule, observation.c_type)
|
|
712
|
+
existing = self.observations.get(key)
|
|
713
|
+
if existing is None:
|
|
714
|
+
self.observations[key] = (observation, 1)
|
|
715
|
+
else:
|
|
716
|
+
self.observations[key] = (existing[0], existing[1] + 1)
|
|
717
|
+
|
|
718
|
+
def touch(self, text: str) -> None:
|
|
719
|
+
"""Remember an instruction that mentions the register."""
|
|
720
|
+
if text and text not in self.uses and len(self.uses) < _MAX_USES:
|
|
721
|
+
self.uses.append(text)
|
|
722
|
+
|
|
723
|
+
def best(self) -> _Observation | None:
|
|
724
|
+
"""Return the winning observation: highest rank, then widest, then first."""
|
|
725
|
+
winner: _Observation | None = None
|
|
726
|
+
best_key: tuple[int, int, int] | None = None
|
|
727
|
+
for order, (observation, _count) in enumerate(self.observations.values()):
|
|
728
|
+
key = (observation.rank, observation.width, -order)
|
|
729
|
+
if best_key is None or key > best_key:
|
|
730
|
+
best_key = key
|
|
731
|
+
winner = observation
|
|
732
|
+
return winner
|
|
733
|
+
|
|
734
|
+
def describe(self, primary: _Observation) -> str:
|
|
735
|
+
"""Render the primary observation together with every conflicting one."""
|
|
736
|
+
note = ""
|
|
737
|
+
others: list[str] = []
|
|
738
|
+
for observation, count in self.observations.values():
|
|
739
|
+
label = observation.note if count == 1 else f"{observation.note} (x{count})"
|
|
740
|
+
if observation is primary:
|
|
741
|
+
note = label
|
|
742
|
+
else:
|
|
743
|
+
others.append(f"{label} -> {observation.c_type}")
|
|
744
|
+
if not others:
|
|
745
|
+
return note
|
|
746
|
+
listed = "; ".join(others[:2])
|
|
747
|
+
if len(others) > 2:
|
|
748
|
+
listed = f"{listed}; +{len(others) - 2} more"
|
|
749
|
+
return f"{note}; also observed: {listed}"
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
# ---------------------------------------------------------------------------
|
|
753
|
+
# validation
|
|
754
|
+
# ---------------------------------------------------------------------------
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _require_pointer_width(pointer_width: object) -> int:
|
|
758
|
+
"""Validate the pointer width in bytes (1, 2, 4 or 8)."""
|
|
759
|
+
if isinstance(pointer_width, bool) or not isinstance(pointer_width, int):
|
|
760
|
+
raise AnalysisError(f"pointer_width must be an integer, got {pointer_width!r}")
|
|
761
|
+
if pointer_width not in INTEGER_C_TYPES:
|
|
762
|
+
raise AnalysisError(
|
|
763
|
+
f"pointer_width must be one of {sorted(INTEGER_C_TYPES)}, got {pointer_width}"
|
|
764
|
+
)
|
|
765
|
+
return pointer_width
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _argument_registers(abi: object, pointer_width: int) -> tuple[str, ...]:
|
|
769
|
+
"""Return the argument registers of ``abi`` for a binary of that width.
|
|
770
|
+
|
|
771
|
+
A 32-bit binary always uses ``ecx``/``edx``, whatever the ABI is called: the
|
|
772
|
+
register file only has the 32-bit aliases, and those are what the renderer
|
|
773
|
+
names ``a0``/``a1``.
|
|
774
|
+
"""
|
|
775
|
+
if pointer_width < 8:
|
|
776
|
+
return _ARGUMENTS_32
|
|
777
|
+
spelling = str(abi).strip().lower().replace("-", "").replace("_", "").replace(" ", "")
|
|
778
|
+
name = _ABI_SPELLINGS.get(spelling)
|
|
779
|
+
if name is None:
|
|
780
|
+
accepted = ", ".join(sorted(ABI_ARGUMENT_REGISTERS))
|
|
781
|
+
raise AnalysisError(f"unknown ABI {abi!r}; expected one of {accepted}")
|
|
782
|
+
return ABI_ARGUMENT_REGISTERS[name]
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# ---------------------------------------------------------------------------
|
|
786
|
+
# argument inference
|
|
787
|
+
# ---------------------------------------------------------------------------
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _collect(
|
|
791
|
+
stream: Sequence[tuple[str, str]],
|
|
792
|
+
pointer_width: int,
|
|
793
|
+
registers: Sequence[str],
|
|
794
|
+
) -> tuple[dict[str, _RegisterEvidence], int]:
|
|
795
|
+
"""Scan the stream once and return the per-register evidence and the count.
|
|
796
|
+
|
|
797
|
+
The argument count is the highest argument register read *before the first
|
|
798
|
+
``call``, plus one -- exactly the rule the pseudo-C renderer uses -- so the
|
|
799
|
+
signature and the rendered body always name the same parameters. Registers
|
|
800
|
+
below the count that are never read are still reported, as ``unknown64``.
|
|
801
|
+
"""
|
|
802
|
+
evidence = {register: _RegisterEvidence() for register in registers}
|
|
803
|
+
aliases = _alias_table(registers)
|
|
804
|
+
positions = {register: index for index, register in enumerate(registers)}
|
|
805
|
+
count = 0
|
|
806
|
+
call_seen = False
|
|
807
|
+
for position, (mnemonic, operands) in enumerate(stream):
|
|
808
|
+
parts = _split_operands(operands)
|
|
809
|
+
text = _text(mnemonic, operands)
|
|
810
|
+
if mnemonic in _CALL_MNEMONICS:
|
|
811
|
+
call_seen = True
|
|
812
|
+
elif not call_seen:
|
|
813
|
+
for token in _register_read_tokens(mnemonic, parts):
|
|
814
|
+
resolved = aliases.get(token)
|
|
815
|
+
if resolved is not None:
|
|
816
|
+
count = max(count, positions[resolved[0]] + 1)
|
|
817
|
+
|
|
818
|
+
for token in set(_WORD.findall(operands.lower())):
|
|
819
|
+
resolved = aliases.get(token)
|
|
820
|
+
if resolved is not None:
|
|
821
|
+
evidence[resolved[0]].touch(text)
|
|
822
|
+
|
|
823
|
+
for operand in parts:
|
|
824
|
+
memory = _parse_memory(operand, pointer_width)
|
|
825
|
+
if memory is None:
|
|
826
|
+
continue
|
|
827
|
+
if memory.base is not None:
|
|
828
|
+
resolved = aliases.get(memory.base)
|
|
829
|
+
if resolved is not None:
|
|
830
|
+
if mnemonic == "lea":
|
|
831
|
+
evidence[resolved[0]].record(_address_arithmetic(text, pointer_width))
|
|
832
|
+
else:
|
|
833
|
+
evidence[resolved[0]].record(
|
|
834
|
+
_deref_observation(memory, pointer_width, text)
|
|
835
|
+
)
|
|
836
|
+
if memory.index is not None:
|
|
837
|
+
resolved = aliases.get(memory.index)
|
|
838
|
+
if resolved is not None:
|
|
839
|
+
evidence[resolved[0]].record(_index_observation(memory, text))
|
|
840
|
+
|
|
841
|
+
if mnemonic in _ARITHMETIC_MNEMONICS:
|
|
842
|
+
for alias, width in _plain_operands(parts, skip_first=False):
|
|
843
|
+
resolved = aliases.get(alias)
|
|
844
|
+
if resolved is not None:
|
|
845
|
+
evidence[resolved[0]].record(_arithmetic_observation(text, width))
|
|
846
|
+
elif mnemonic in _MOV_MNEMONICS:
|
|
847
|
+
for alias, width in _plain_operands(parts, skip_first=True):
|
|
848
|
+
resolved = aliases.get(alias)
|
|
849
|
+
if resolved is not None:
|
|
850
|
+
evidence[resolved[0]].record(_copy_observation(text, alias, width))
|
|
851
|
+
|
|
852
|
+
if mnemonic in _COMPARE_MNEMONICS:
|
|
853
|
+
following = stream[position + 1][0] if position + 1 < len(stream) else ""
|
|
854
|
+
if following.startswith("j") and following not in _UNCONDITIONAL_JUMPS:
|
|
855
|
+
for alias, zero in _compare_targets(mnemonic, parts):
|
|
856
|
+
resolved = aliases.get(alias)
|
|
857
|
+
if resolved is not None:
|
|
858
|
+
if zero:
|
|
859
|
+
note = (
|
|
860
|
+
f"compared with zero and branched on "
|
|
861
|
+
f"({text} then {following})"
|
|
862
|
+
)
|
|
863
|
+
else:
|
|
864
|
+
note = (
|
|
865
|
+
f"compared with another operand and branched on "
|
|
866
|
+
f"({text} then {following})"
|
|
867
|
+
)
|
|
868
|
+
evidence[resolved[0]].record(
|
|
869
|
+
_Observation(
|
|
870
|
+
rule=_RULE_COMPARE,
|
|
871
|
+
c_type=C_TYPE_INT32,
|
|
872
|
+
note=note,
|
|
873
|
+
confidence=Confidence.LOW,
|
|
874
|
+
width=4,
|
|
875
|
+
)
|
|
876
|
+
)
|
|
877
|
+
return evidence, count
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _argument_type(
|
|
881
|
+
register: str, index: int, evidence: _RegisterEvidence, pointer_width: int
|
|
882
|
+
) -> ArgumentType:
|
|
883
|
+
"""Turn one register's evidence into the reported argument type."""
|
|
884
|
+
best = evidence.best()
|
|
885
|
+
if best is None:
|
|
886
|
+
if evidence.uses:
|
|
887
|
+
note = (
|
|
888
|
+
f"read without a usable type witness (first use: '{evidence.uses[0]}'); "
|
|
889
|
+
"no rule applies"
|
|
890
|
+
)
|
|
891
|
+
else:
|
|
892
|
+
note = f"argument register {register} is never read; nothing to infer from"
|
|
893
|
+
return ArgumentType(
|
|
894
|
+
register=register,
|
|
895
|
+
name=f"a{index}",
|
|
896
|
+
c_type=_unknown_type(pointer_width),
|
|
897
|
+
evidence_note=note,
|
|
898
|
+
confidence=Confidence.LOW,
|
|
899
|
+
)
|
|
900
|
+
return ArgumentType(
|
|
901
|
+
register=register,
|
|
902
|
+
name=f"a{index}",
|
|
903
|
+
c_type=best.c_type,
|
|
904
|
+
evidence_note=evidence.describe(best),
|
|
905
|
+
confidence=best.confidence,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def infer_argument_types(
|
|
910
|
+
instructions: Iterable[Any],
|
|
911
|
+
pointer_width: int,
|
|
912
|
+
*,
|
|
913
|
+
abi: str = "sysv",
|
|
914
|
+
) -> tuple[ArgumentType, ...]:
|
|
915
|
+
"""Infer the C type of every argument of one function.
|
|
916
|
+
|
|
917
|
+
Args:
|
|
918
|
+
instructions: The decoded instructions of the function, in ascending
|
|
919
|
+
address order: ``IRInstruction.to_dict()`` payloads,
|
|
920
|
+
:class:`~codevanta.ir.instruction.IRInstruction` objects or
|
|
921
|
+
``"mnemonic operands"`` strings.
|
|
922
|
+
pointer_width: Pointer width of the binary in bytes (``8`` for x86_64,
|
|
923
|
+
``4`` for 32-bit code). It decides the pointer type and the
|
|
924
|
+
``unknownNN`` placeholder.
|
|
925
|
+
abi: Calling convention naming the argument registers. ``"sysv"`` (the
|
|
926
|
+
default) and its spellings use ``rdi``/``rsi``/``rdx``/``rcx``/
|
|
927
|
+
``r8``/``r9``; ``"ms64"``/``"win64"`` use ``rcx``/``rdx``/``r8``/
|
|
928
|
+
``r9``. A 32-bit binary always uses ``ecx``/``edx``.
|
|
929
|
+
|
|
930
|
+
Returns:
|
|
931
|
+
One :class:`ArgumentType` per argument, in argument order, named
|
|
932
|
+
``a0``..``a5``. The count is the highest argument register read before
|
|
933
|
+
the first ``call`` plus one, i.e. the same rule the pseudo-C renderer
|
|
934
|
+
uses for the signature; a lower register that is never read is still
|
|
935
|
+
reported, as the ``unknownNN`` placeholder with ``Confidence.LOW``.
|
|
936
|
+
|
|
937
|
+
Raises:
|
|
938
|
+
AnalysisError: When ``pointer_width`` is not 1, 2, 4 or 8, when ``abi``
|
|
939
|
+
is not a known calling convention, or when ``instructions`` is a
|
|
940
|
+
string instead of an iterable of instructions.
|
|
941
|
+
|
|
942
|
+
Note:
|
|
943
|
+
The same instructions always give the same result: the scan is linear,
|
|
944
|
+
observations are aggregated per rule and the winner is chosen by
|
|
945
|
+
:data:`RULE_RANKS` with deterministic tie-breaks.
|
|
946
|
+
"""
|
|
947
|
+
width = _require_pointer_width(pointer_width)
|
|
948
|
+
registers = _argument_registers(abi, width)
|
|
949
|
+
stream = _stream(instructions)
|
|
950
|
+
evidence, count = _collect(stream, width, registers)
|
|
951
|
+
return tuple(
|
|
952
|
+
_argument_type(register, index, evidence[register], width)
|
|
953
|
+
for index, register in enumerate(registers[:count])
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _writes_first_operand(mnemonic: str) -> bool:
|
|
958
|
+
"""Whether a mnemonic writes its first operand."""
|
|
959
|
+
if mnemonic in _READ_ONLY_MNEMONICS or mnemonic in _RET_MNEMONICS:
|
|
960
|
+
return False
|
|
961
|
+
return not (mnemonic.startswith("j") or mnemonic.startswith("loop"))
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _last_return_write(
|
|
965
|
+
stream: Sequence[tuple[str, str]], pointer_width: int
|
|
966
|
+
) -> tuple[int, str, str, bool] | None:
|
|
967
|
+
"""Return the last write to the return register before the first ``ret``.
|
|
968
|
+
|
|
969
|
+
Returns ``(width, alias, instruction text, is_setcc)`` or ``None`` when no
|
|
970
|
+
instruction writes the register.
|
|
971
|
+
"""
|
|
972
|
+
aliases = _RETURN_ALIAS_WIDTHS.get(pointer_width, _RETURN_ALIAS_WIDTHS[8])
|
|
973
|
+
last: tuple[int, str, str, bool] | None = None
|
|
974
|
+
for mnemonic, operands in stream:
|
|
975
|
+
if mnemonic in _RET_MNEMONICS:
|
|
976
|
+
break
|
|
977
|
+
if not _writes_first_operand(mnemonic):
|
|
978
|
+
continue
|
|
979
|
+
parts = _split_operands(operands)
|
|
980
|
+
if not parts:
|
|
981
|
+
continue
|
|
982
|
+
destination = parts[0].strip().lower()
|
|
983
|
+
if "[" in destination: # a memory destination only *reads* the register
|
|
984
|
+
continue
|
|
985
|
+
width = aliases.get(destination)
|
|
986
|
+
if width is None:
|
|
987
|
+
continue
|
|
988
|
+
last = (width, destination, _text(mnemonic, operands), mnemonic.startswith("set"))
|
|
989
|
+
return last
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _first_call_before_ret(stream: Sequence[tuple[str, str]]) -> str:
|
|
993
|
+
"""Return the text of the first ``call`` before the first ``ret``, if any."""
|
|
994
|
+
for mnemonic, operands in stream:
|
|
995
|
+
if mnemonic in _RET_MNEMONICS:
|
|
996
|
+
return ""
|
|
997
|
+
if mnemonic in _CALL_MNEMONICS:
|
|
998
|
+
return _text(mnemonic, operands)
|
|
999
|
+
return ""
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def infer_return_type(instructions: Iterable[Any], pointer_width: int) -> ReturnType:
|
|
1003
|
+
"""Infer the C return type of one function.
|
|
1004
|
+
|
|
1005
|
+
The scan stops at the first ``ret`` and the **last write** to the return
|
|
1006
|
+
register decides the type: ``rax`` -> ``uint64_t``, ``eax`` -> ``uint32_t``,
|
|
1007
|
+
``ax`` -> ``uint16_t``, ``al`` -> ``uint8_t``. A ``setcc`` write is reported
|
|
1008
|
+
as :data:`C_TYPE_INT32`, because it leaves a 0/1 boolean that C returns as an
|
|
1009
|
+
``int``. A function that never writes the register returns
|
|
1010
|
+
:data:`C_TYPE_VOID` with ``Confidence.LOW`` -- the absence of a write is
|
|
1011
|
+
weaker evidence than a write -- and a ``call`` before the ``ret`` is named in
|
|
1012
|
+
the note, because the value would then be the callee's.
|
|
1013
|
+
|
|
1014
|
+
Args:
|
|
1015
|
+
instructions: The decoded instructions of the function, in ascending
|
|
1016
|
+
address order (see :func:`infer_argument_types` for the accepted
|
|
1017
|
+
shapes).
|
|
1018
|
+
pointer_width: Pointer width of the binary in bytes; it selects the
|
|
1019
|
+
return register (``rax`` on 64-bit, ``eax`` on 32-bit) and its
|
|
1020
|
+
aliases.
|
|
1021
|
+
|
|
1022
|
+
Returns:
|
|
1023
|
+
The :class:`ReturnType` together with the observation that produced it.
|
|
1024
|
+
|
|
1025
|
+
Raises:
|
|
1026
|
+
AnalysisError: When ``pointer_width`` is not 1, 2, 4 or 8, or when
|
|
1027
|
+
``instructions`` is a string instead of an instruction iterable.
|
|
1028
|
+
"""
|
|
1029
|
+
width = _require_pointer_width(pointer_width)
|
|
1030
|
+
stream = _stream(instructions)
|
|
1031
|
+
register = _RETURN_REGISTER.get(width, "rax")
|
|
1032
|
+
decision = _last_return_write(stream, width)
|
|
1033
|
+
if decision is None:
|
|
1034
|
+
note = f"no instruction writes {register} before the first ret"
|
|
1035
|
+
call = _first_call_before_ret(stream)
|
|
1036
|
+
if call:
|
|
1037
|
+
note = (
|
|
1038
|
+
f"no instruction writes {register} before the first ret; the value "
|
|
1039
|
+
f"would come from '{call}' and is left to the callee"
|
|
1040
|
+
)
|
|
1041
|
+
return ReturnType(c_type=C_TYPE_VOID, evidence_note=note, confidence=Confidence.LOW)
|
|
1042
|
+
write_width, alias, text, boolean = decision
|
|
1043
|
+
if boolean:
|
|
1044
|
+
return ReturnType(
|
|
1045
|
+
c_type=C_TYPE_INT32,
|
|
1046
|
+
evidence_note=f"last write to {register} is the setcc boolean '{text}' ({alias})",
|
|
1047
|
+
confidence=Confidence.MEDIUM,
|
|
1048
|
+
)
|
|
1049
|
+
return ReturnType(
|
|
1050
|
+
c_type=_unsigned_type(write_width),
|
|
1051
|
+
evidence_note=(
|
|
1052
|
+
f"last write to {register} before the ret is {write_width * 8}-bit: '{text}'"
|
|
1053
|
+
),
|
|
1054
|
+
confidence=Confidence.MEDIUM,
|
|
1055
|
+
)
|