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,1094 @@
|
|
|
1
|
+
"""Heuristic pseudo-C decompilation of one function (phase 3).
|
|
2
|
+
|
|
3
|
+
:func:`decompile_function` renders a single function as readable pseudo-C. It
|
|
4
|
+
never re-implements decoding: the recursive CFG
|
|
5
|
+
(:func:`codevanta.analysis.cfg.build_recursive_cfg`) supplies the instructions
|
|
6
|
+
and the block structure, function discovery supplies symbol names, and this
|
|
7
|
+
module only *shapes* that structure into text.
|
|
8
|
+
|
|
9
|
+
1. **Signature.** The argument count is inferred from the ABI argument registers
|
|
10
|
+
read before the first ``call`` (``rdi``/``rsi``/``rdx``/``rcx``/``r8``/``r9``
|
|
11
|
+
plus their 32/16/8-bit aliases on x86_64, ``ecx``/``edx`` on x86), capped at
|
|
12
|
+
six. A register counts as *read* unless it is only the destination of the
|
|
13
|
+
instruction, so ``mov rax, rdi`` reads ``rdi`` while ``mov rdi, rax`` does
|
|
14
|
+
not. The name comes from the discovered function candidate, else
|
|
15
|
+
``sub_<address>``. Every parameter and the return value then carry an inferred
|
|
16
|
+
C type from :mod:`codevanta.analysis.type_propagation` -- the count rule is
|
|
17
|
+
shared with it, so the signature and the rendered ``a0``..``a5`` operands
|
|
18
|
+
always agree -- and the header repeats one short line per argument. An
|
|
19
|
+
argument nothing can be proven about keeps the ``unknown<bitness>``
|
|
20
|
+
placeholder with a low confidence rather than a guess.
|
|
21
|
+
2. **Operands.** Argument registers become ``a0``..``a5``; every other register
|
|
22
|
+
keeps its architectural name. ``[rbp - 8]``-style operands become frame slots
|
|
23
|
+
(``local_0x8`` below the frame pointer, ``arg_0x8`` above it) and other
|
|
24
|
+
memory operands become ``*(u64*)(a0 + 0x10)`` dereferences with the pointer
|
|
25
|
+
width of the binary (``u32`` for 32-bit files).
|
|
26
|
+
3. **Statements.** Only semantics that the mnemonic alone justifies are
|
|
27
|
+
rendered: ``mov``/``lea``/compound assignments and unary updates. Every other
|
|
28
|
+
instruction keeps its comment but produces no statement, so nothing is
|
|
29
|
+
invented. Every emitted source line carries the raw instruction text as a
|
|
30
|
+
trailing ``/* mov rax, rdi */`` comment.
|
|
31
|
+
4. **Control flow.** A forward conditional branch with two decoded successors
|
|
32
|
+
opens ``if (<condition>) {`` with the *inverted* condition (the fallthrough is
|
|
33
|
+
the then-branch) and closes with ``}``; when the then-branch jumps over an
|
|
34
|
+
exclusive else region the two halves are joined as ``} else {``. A block that
|
|
35
|
+
is the target of a back edge opens ``while (1) {``; the latch renders
|
|
36
|
+
``continue;`` or ``if (<exit condition>) { break; }``. Anything left over --
|
|
37
|
+
irreducible edges, forward branches that cross an enclosing region, targets
|
|
38
|
+
outside the decoded window -- is rendered as ``goto LABEL_<addr>;`` with a
|
|
39
|
+
matching ``LABEL_<addr>:``.
|
|
40
|
+
5. **Conditions.** ``cmp``/``test`` operands feed the following branch:
|
|
41
|
+
``je``/``jne`` give ``==``/``!=``, ``jl``..``jg`` give signed ``< <= > >=``
|
|
42
|
+
and ``jb``..``ja`` give the ``(unsigned)`` forms. When no comparison can be
|
|
43
|
+
justified the condition is ``flag_from_<mnemonic>`` (or
|
|
44
|
+
``!flag_from_<mnemonic>`` for an inverted branch).
|
|
45
|
+
6. **Calls.** ``call_<target>();`` for a direct target, ``<import_name>()`` when
|
|
46
|
+
the target matches an entry of ``BinaryInfo.imports`` and ``(*ptr)();`` for an
|
|
47
|
+
indirect one (recorded as :class:`CallSite` with ``target=None``).
|
|
48
|
+
|
|
49
|
+
Every result carries :class:`Evidence`; the confidence is :attr:`Confidence.LOW`
|
|
50
|
+
because the whole rendering is a mnemonic-level heuristic. The header comment
|
|
51
|
+
block repeats the address, the confidence and the evidence note, and the target
|
|
52
|
+
binary is only ever read, never executed. ``evidence.extra`` carries the
|
|
53
|
+
inferred signature as data (``argument_types``, ``return_type``) so JSON
|
|
54
|
+
consumers do not have to parse the pseudo-C.
|
|
55
|
+
|
|
56
|
+
Work is bounded by ``max_instructions`` and ``max_blocks`` (defaults
|
|
57
|
+
:data:`DEFAULT_MAX_INSTRUCTIONS` / :data:`DEFAULT_MAX_BLOCKS`); a cut-short
|
|
58
|
+
rendering sets :attr:`PseudoFunction.truncated` and adds a ``warning:`` line to
|
|
59
|
+
the header. An address that is not executable code, a budget out of range and a
|
|
60
|
+
region that decodes to nothing all raise :class:`AnalysisError`.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
from __future__ import annotations
|
|
64
|
+
|
|
65
|
+
import re
|
|
66
|
+
from dataclasses import dataclass
|
|
67
|
+
from pathlib import Path
|
|
68
|
+
from typing import Any, Literal
|
|
69
|
+
|
|
70
|
+
from codevanta.analysis.cfg import BasicBlock, FunctionCFG, build_recursive_cfg
|
|
71
|
+
from codevanta.analysis.type_propagation import (
|
|
72
|
+
ArgumentType,
|
|
73
|
+
ReturnType,
|
|
74
|
+
infer_argument_types,
|
|
75
|
+
infer_return_type,
|
|
76
|
+
)
|
|
77
|
+
from codevanta.core.functions import discover_functions
|
|
78
|
+
from codevanta.core.loader import BinaryInfo
|
|
79
|
+
from codevanta.core.report import MAX_DISASSEMBLY_COUNT, AnalysisError
|
|
80
|
+
from codevanta.evidence.confidence import Confidence
|
|
81
|
+
from codevanta.evidence.models import Evidence
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"CallSite",
|
|
85
|
+
"DEFAULT_MAX_BLOCKS",
|
|
86
|
+
"DEFAULT_MAX_INSTRUCTIONS",
|
|
87
|
+
"MAX_MAX_BLOCKS",
|
|
88
|
+
"MAX_MAX_INSTRUCTIONS",
|
|
89
|
+
"PseudoFunction",
|
|
90
|
+
"decompile_function",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
#: Default instruction budget for one rendering.
|
|
94
|
+
DEFAULT_MAX_INSTRUCTIONS = 2_000
|
|
95
|
+
|
|
96
|
+
#: Largest accepted ``max_instructions``; larger requests are rejected.
|
|
97
|
+
MAX_MAX_INSTRUCTIONS = MAX_DISASSEMBLY_COUNT
|
|
98
|
+
|
|
99
|
+
#: Default block budget for one rendering.
|
|
100
|
+
DEFAULT_MAX_BLOCKS = 500
|
|
101
|
+
|
|
102
|
+
#: Largest accepted ``max_blocks``; larger requests are rejected.
|
|
103
|
+
MAX_MAX_BLOCKS = MAX_DISASSEMBLY_COUNT
|
|
104
|
+
|
|
105
|
+
#: One indentation step, in spaces.
|
|
106
|
+
_INDENT = " "
|
|
107
|
+
|
|
108
|
+
#: Evidence source label for a rendered function.
|
|
109
|
+
_SOURCE = "decompile.pseudo_c"
|
|
110
|
+
|
|
111
|
+
#: Evidence method label for a rendered function.
|
|
112
|
+
_METHOD = "mnemonic_heuristic"
|
|
113
|
+
|
|
114
|
+
#: Mnemonics that end a block with a plain return.
|
|
115
|
+
_RET_MNEMONICS = frozenset({"ret", "retn", "retf", "retq", "iret", "iretd", "iretq", "eret"})
|
|
116
|
+
|
|
117
|
+
#: Mnemonics that are an unconditional jump.
|
|
118
|
+
_UNCONDITIONAL_JUMPS = frozenset({"jmp", "ljmp", "b", "br", "bx"})
|
|
119
|
+
|
|
120
|
+
#: Mnemonics that transfer control to a subroutine.
|
|
121
|
+
_CALL_MNEMONICS = frozenset({"call", "callq", "bl", "blx", "blr"})
|
|
122
|
+
|
|
123
|
+
#: ``mov``-style mnemonics: the first operand is only ever written.
|
|
124
|
+
_MOV_MNEMONICS = frozenset({"mov", "movabs", "movbe", "movsx", "movsxd", "movzx", "movq"})
|
|
125
|
+
|
|
126
|
+
#: Read-modify-write mnemonics rendered as ``dst <op>= src``.
|
|
127
|
+
_COMPOUND_ASSIGN: dict[str, str] = {
|
|
128
|
+
"add": "+=",
|
|
129
|
+
"sub": "-=",
|
|
130
|
+
"and": "&=",
|
|
131
|
+
"or": "|=",
|
|
132
|
+
"xor": "^=",
|
|
133
|
+
"imul": "*=",
|
|
134
|
+
"shl": "<<=",
|
|
135
|
+
"sal": "<<=",
|
|
136
|
+
"shr": ">>=",
|
|
137
|
+
"sar": ">>=",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#: Unary mnemonics rendered as ``dst = <prefix>dst``.
|
|
141
|
+
_UNARY_ASSIGN: dict[str, str] = {"neg": "-", "not": "~"}
|
|
142
|
+
|
|
143
|
+
#: Unary increment/decrement mnemonics.
|
|
144
|
+
_STEP_ASSIGN: dict[str, str] = {"inc": "++", "dec": "--"}
|
|
145
|
+
|
|
146
|
+
#: SysV argument registers, in argument order, for x86_64.
|
|
147
|
+
_ARG_REGISTERS_64 = ("rdi", "rsi", "rdx", "rcx", "r8", "r9")
|
|
148
|
+
|
|
149
|
+
#: Argument registers used for 32-bit x86 (``__thiscall``/``__fastcall`` order).
|
|
150
|
+
_ARG_REGISTERS_32 = ("ecx", "edx")
|
|
151
|
+
|
|
152
|
+
#: Every name a canonical argument register can appear under in Capstone text.
|
|
153
|
+
_ARG_ALIASES: dict[str, tuple[str, ...]] = {
|
|
154
|
+
"rdi": ("rdi", "edi", "di", "dil"),
|
|
155
|
+
"rsi": ("rsi", "esi", "si", "sil"),
|
|
156
|
+
"rdx": ("rdx", "edx", "dx", "dl"),
|
|
157
|
+
"rcx": ("rcx", "ecx", "cx", "cl"),
|
|
158
|
+
"r8": ("r8", "r8d", "r8w", "r8b"),
|
|
159
|
+
"r9": ("r9", "r9d", "r9w", "r9b"),
|
|
160
|
+
"ecx": ("ecx", "cx", "cl"),
|
|
161
|
+
"edx": ("edx", "dx", "dl"),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#: Registers whose relative operands denote stack slots.
|
|
165
|
+
_STACK_REGISTERS = frozenset({"rbp", "ebp", "rsp", "esp"})
|
|
166
|
+
|
|
167
|
+
#: Instruction-pointer register names (RIP-relative operands hold a global).
|
|
168
|
+
_PC_REGISTERS = frozenset({"rip", "eip"})
|
|
169
|
+
|
|
170
|
+
#: Capstone size keywords that may prefix a memory operand.
|
|
171
|
+
_SIZE_KEYWORDS = "byte|word|dword|qword|tbyte|xmmword|oword|fword"
|
|
172
|
+
|
|
173
|
+
#: A single address operand, e.g. ``0x401010`` (the only direct form accepted).
|
|
174
|
+
_DIRECT_OPERAND = re.compile(r"^0x[0-9a-fA-F]+$")
|
|
175
|
+
|
|
176
|
+
#: A memory operand: an optional size keyword plus a bracketed expression.
|
|
177
|
+
_MEMORY_OPERAND = re.compile(
|
|
178
|
+
rf"^(?:(?:{_SIZE_KEYWORDS})\s+ptr\s+)?\[\s*(?P<body>[^\]]*?)\s*\]$",
|
|
179
|
+
re.IGNORECASE,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
#: One term of a bracketed address expression.
|
|
183
|
+
_ADDRESS_TERM = re.compile(
|
|
184
|
+
r"(?P<sign>[+-])?\s*(?P<value>0x[0-9a-fA-F]+|\d+|[a-zA-Z][a-zA-Z0-9]*)"
|
|
185
|
+
r"(?:\s*\*\s*(?P<scale>\d+))?"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
#: An operand that is exactly one register or one immediate.
|
|
189
|
+
_SIMPLE_OPERAND = re.compile(r"^(?:[a-z][a-z0-9]*|-?(?:0x[0-9a-f]+|\d+))$")
|
|
190
|
+
|
|
191
|
+
#: Maximum number of arguments rendered in a signature.
|
|
192
|
+
_MAX_ARGUMENTS = 6
|
|
193
|
+
|
|
194
|
+
#: Branch mnemonic -> (operator, unsigned) for a condition built from ``cmp``.
|
|
195
|
+
_COMPARISONS: dict[str, tuple[str, bool]] = {
|
|
196
|
+
"je": ("==", False),
|
|
197
|
+
"jz": ("==", False),
|
|
198
|
+
"jne": ("!=", False),
|
|
199
|
+
"jnz": ("!=", False),
|
|
200
|
+
"jl": ("<", False),
|
|
201
|
+
"jle": ("<=", False),
|
|
202
|
+
"jg": (">", False),
|
|
203
|
+
"jge": (">=", False),
|
|
204
|
+
"jb": ("<", True),
|
|
205
|
+
"jbe": ("<=", True),
|
|
206
|
+
"ja": (">", True),
|
|
207
|
+
"jae": (">=", True),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#: Branch mnemonic -> the mnemonic with the opposite polarity.
|
|
211
|
+
_INVERSES: dict[str, str] = {
|
|
212
|
+
"je": "jne",
|
|
213
|
+
"jne": "je",
|
|
214
|
+
"jz": "jnz",
|
|
215
|
+
"jnz": "jz",
|
|
216
|
+
"jl": "jge",
|
|
217
|
+
"jge": "jl",
|
|
218
|
+
"jle": "jg",
|
|
219
|
+
"jg": "jle",
|
|
220
|
+
"jb": "jae",
|
|
221
|
+
"jae": "jb",
|
|
222
|
+
"jbe": "ja",
|
|
223
|
+
"ja": "jbe",
|
|
224
|
+
"js": "jns",
|
|
225
|
+
"jns": "js",
|
|
226
|
+
"jo": "jno",
|
|
227
|
+
"jno": "jo",
|
|
228
|
+
"jp": "jnp",
|
|
229
|
+
"jnp": "jp",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#: Mnemonics whose only side effect on a condition is a comparison/test.
|
|
233
|
+
_COMPARE_MNEMONICS = frozenset({"cmp", "test", "comiss", "comisd", "ucomiss", "ucomisd"})
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True)
|
|
237
|
+
class CallSite:
|
|
238
|
+
"""One control transfer out of, or into, the rendered function.
|
|
239
|
+
|
|
240
|
+
Attributes:
|
|
241
|
+
address: Virtual address of the transferring instruction.
|
|
242
|
+
target: Absolute destination for a direct transfer, ``None`` when the
|
|
243
|
+
destination is only known at run time (a register or memory
|
|
244
|
+
operand).
|
|
245
|
+
name: Import or function-symbol name known for ``target``, if any.
|
|
246
|
+
kind: ``"call"`` for a subroutine call, ``"jump"`` for a direct jump out
|
|
247
|
+
of the function (a tail call), ``"unknown"`` for an indirect jump
|
|
248
|
+
whose destination could not be resolved.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
address: int
|
|
252
|
+
target: int | None
|
|
253
|
+
name: str | None
|
|
254
|
+
kind: Literal["call", "jump", "unknown"]
|
|
255
|
+
|
|
256
|
+
def to_dict(self) -> dict[str, Any]:
|
|
257
|
+
"""Serialize the call site for the JSON report."""
|
|
258
|
+
return {
|
|
259
|
+
"address": f"0x{self.address:x}",
|
|
260
|
+
"target": None if self.target is None else f"0x{self.target:x}",
|
|
261
|
+
"name": self.name,
|
|
262
|
+
"kind": self.kind,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass(frozen=True)
|
|
267
|
+
class PseudoFunction:
|
|
268
|
+
"""Readable pseudo-C rendering of one function.
|
|
269
|
+
|
|
270
|
+
Attributes:
|
|
271
|
+
address: Virtual address the rendering starts at.
|
|
272
|
+
name: Function name from discovery, or ``None`` when unnamed.
|
|
273
|
+
lines: Rendered body lines in source order; each carries the raw
|
|
274
|
+
instruction text as a trailing comment.
|
|
275
|
+
body: Complete text: header comment block, signature, braces and body.
|
|
276
|
+
call_sites: Control transfers in ascending address order.
|
|
277
|
+
block_count: Blocks reported by the underlying CFG.
|
|
278
|
+
instruction_count: Unique instructions reported by the CFG.
|
|
279
|
+
truncated: ``True`` when a limit cut the rendering short.
|
|
280
|
+
evidence: How the rendering was produced.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
address: int
|
|
284
|
+
name: str | None
|
|
285
|
+
lines: tuple[str, ...]
|
|
286
|
+
body: str
|
|
287
|
+
call_sites: tuple[CallSite, ...]
|
|
288
|
+
block_count: int
|
|
289
|
+
instruction_count: int
|
|
290
|
+
truncated: bool
|
|
291
|
+
evidence: Evidence
|
|
292
|
+
|
|
293
|
+
def to_dict(self) -> dict[str, Any]:
|
|
294
|
+
"""Serialize the rendering for the JSON report."""
|
|
295
|
+
return {
|
|
296
|
+
"address": f"0x{self.address:x}",
|
|
297
|
+
"name": self.name,
|
|
298
|
+
"block_count": self.block_count,
|
|
299
|
+
"instruction_count": self.instruction_count,
|
|
300
|
+
"truncated": self.truncated,
|
|
301
|
+
"lines": list(self.lines),
|
|
302
|
+
"body": self.body,
|
|
303
|
+
"call_sites": [site.to_dict() for site in self.call_sites],
|
|
304
|
+
"evidence": self.evidence.to_dict(),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@dataclass(frozen=True)
|
|
309
|
+
class _Memory:
|
|
310
|
+
"""A parsed bracketed address expression.
|
|
311
|
+
|
|
312
|
+
Attributes:
|
|
313
|
+
base: Base register (already lowercased), or ``None`` for an absolute.
|
|
314
|
+
index: Index register, or ``None``.
|
|
315
|
+
scale: Index scale, ``1`` when absent.
|
|
316
|
+
displacement: Signed displacement added to the base.
|
|
317
|
+
absolute: Literal address for absolute operands, else ``None``.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
base: str | None
|
|
321
|
+
index: str | None
|
|
322
|
+
scale: int
|
|
323
|
+
displacement: int
|
|
324
|
+
absolute: int | None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass
|
|
328
|
+
class _Frame:
|
|
329
|
+
"""One open structured construct while rendering.
|
|
330
|
+
|
|
331
|
+
Attributes:
|
|
332
|
+
kind: ``"if"``, ``"else"`` or ``"while"``.
|
|
333
|
+
stop: Address at which the construct closes (``None`` means "at the end
|
|
334
|
+
of the function", used by a loop whose exit is not known).
|
|
335
|
+
block: Start address of the block that opened the construct.
|
|
336
|
+
merge: For an ``"if"`` frame, the address its else region merges at.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
kind: Literal["if", "else", "while"]
|
|
340
|
+
stop: int | None
|
|
341
|
+
block: int
|
|
342
|
+
merge: int | None = None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _split_operands(text: str) -> list[str]:
|
|
346
|
+
"""Split Capstone operand text on top-level commas."""
|
|
347
|
+
text = (text or "").strip()
|
|
348
|
+
if not text:
|
|
349
|
+
return []
|
|
350
|
+
parts: list[str] = []
|
|
351
|
+
current: list[str] = []
|
|
352
|
+
depth = 0
|
|
353
|
+
for character in text:
|
|
354
|
+
if character == "[":
|
|
355
|
+
depth += 1
|
|
356
|
+
elif character == "]":
|
|
357
|
+
depth = max(0, depth - 1)
|
|
358
|
+
if character == "," and depth == 0:
|
|
359
|
+
parts.append("".join(current).strip())
|
|
360
|
+
current = []
|
|
361
|
+
continue
|
|
362
|
+
current.append(character)
|
|
363
|
+
parts.append("".join(current).strip())
|
|
364
|
+
return [part for part in parts if part]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _to_int(token: str) -> int | None:
|
|
368
|
+
"""Parse a hexadecimal or decimal token, returning ``None`` on junk."""
|
|
369
|
+
try:
|
|
370
|
+
if token[:2].lower() == "0x":
|
|
371
|
+
return int(token, 16)
|
|
372
|
+
return int(token, 10)
|
|
373
|
+
except ValueError: # pragma: no cover - callers only pass matched digits
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _parse_memory(operand: str) -> _Memory | None:
|
|
378
|
+
"""Parse a bracketed address expression, or return ``None`` if unhandled.
|
|
379
|
+
|
|
380
|
+
Only ``[base]``, ``[base +/- disp]``, ``[base + index]`` and
|
|
381
|
+
``[base + index*scale +/- disp]`` shapes are understood. Absolute operands
|
|
382
|
+
keep their literal address in :attr:`_Memory.absolute`; segment prefixes and
|
|
383
|
+
expressions with more than a base and an index are left to the caller.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
operand: Capstone operand text, e.g. ``"qword ptr [rdi + 0x10]"``.
|
|
387
|
+
"""
|
|
388
|
+
match = _MEMORY_OPERAND.match(operand.strip())
|
|
389
|
+
if match is None:
|
|
390
|
+
return None
|
|
391
|
+
body = match.group("body")
|
|
392
|
+
if ":" in body: # a segment override is never guessed at
|
|
393
|
+
return None
|
|
394
|
+
base: str | None = None
|
|
395
|
+
index: str | None = None
|
|
396
|
+
scale = 1
|
|
397
|
+
displacement = 0
|
|
398
|
+
for term in _ADDRESS_TERM.finditer(body):
|
|
399
|
+
value = term.group("value")
|
|
400
|
+
sign = -1 if term.group("sign") == "-" else 1
|
|
401
|
+
if value[0].isdigit():
|
|
402
|
+
number = _to_int(value)
|
|
403
|
+
if number is None: # pragma: no cover - the regex only matches digits
|
|
404
|
+
return None
|
|
405
|
+
displacement += sign * number
|
|
406
|
+
continue
|
|
407
|
+
register = value.lower()
|
|
408
|
+
term_scale = int(term.group("scale")) if term.group("scale") else 1
|
|
409
|
+
if base is None and term_scale == 1:
|
|
410
|
+
base = register
|
|
411
|
+
elif index is None:
|
|
412
|
+
index = register
|
|
413
|
+
scale = term_scale
|
|
414
|
+
else: # more than a base and an index: not modelled
|
|
415
|
+
return None
|
|
416
|
+
if base is None and index is None:
|
|
417
|
+
return _Memory(None, None, 1, 0, displacement)
|
|
418
|
+
return _Memory(base, index, scale, displacement, None)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _instruction_text(instruction: dict[str, Any]) -> str:
|
|
422
|
+
"""Return the raw ``"mnemonic operands"`` text of a decoded instruction."""
|
|
423
|
+
text = str(instruction.get("text") or "").strip()
|
|
424
|
+
if text:
|
|
425
|
+
return text
|
|
426
|
+
mnemonic = str(instruction.get("mnemonic") or "").strip()
|
|
427
|
+
operands = str(instruction.get("operands") or "").strip()
|
|
428
|
+
return f"{mnemonic} {operands}".strip()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _mnemonic(instruction: dict[str, Any]) -> str:
|
|
432
|
+
"""Return the lowercased mnemonic of a decoded instruction."""
|
|
433
|
+
return str(instruction.get("mnemonic") or "").strip().lower()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _operands(instruction: dict[str, Any]) -> str:
|
|
437
|
+
"""Return the operand text of a decoded instruction."""
|
|
438
|
+
return str(instruction.get("operands") or "")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _address(instruction: dict[str, Any]) -> int:
|
|
442
|
+
"""Return the virtual address of a decoded instruction.
|
|
443
|
+
|
|
444
|
+
``BasicBlock.instructions`` holds ``IRInstruction.to_dict()`` payloads, where
|
|
445
|
+
the address is a hexadecimal string, so both shapes are accepted.
|
|
446
|
+
"""
|
|
447
|
+
value = instruction.get("address")
|
|
448
|
+
if isinstance(value, int):
|
|
449
|
+
return value
|
|
450
|
+
if isinstance(value, str):
|
|
451
|
+
return _to_int(value.strip()) or 0
|
|
452
|
+
return 0 # pragma: no cover - defensive
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _size(instruction: dict[str, Any]) -> int:
|
|
456
|
+
"""Return the encoded length of a decoded instruction."""
|
|
457
|
+
return int(instruction.get("size") or 0)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _direct_target(operands: str) -> int | None:
|
|
461
|
+
"""Return the absolute target encoded in ``operands``, if any.
|
|
462
|
+
|
|
463
|
+
Only a straight hexadecimal immediate is accepted (``jmp 0x401006``);
|
|
464
|
+
register and memory operands are indirect and yield ``None``.
|
|
465
|
+
"""
|
|
466
|
+
text = (operands or "").strip()
|
|
467
|
+
if not _DIRECT_OPERAND.match(text):
|
|
468
|
+
return None
|
|
469
|
+
return _to_int(text)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _argument_names(bitness: int) -> dict[str, str]:
|
|
473
|
+
"""Map every alias of an ABI argument register to ``a0``..``a5``."""
|
|
474
|
+
registers = _ARG_REGISTERS_64 if bitness >= 64 else _ARG_REGISTERS_32
|
|
475
|
+
names: dict[str, str] = {}
|
|
476
|
+
for position, register in enumerate(registers[:_MAX_ARGUMENTS]):
|
|
477
|
+
for alias in _ARG_ALIASES.get(register, (register,)):
|
|
478
|
+
names.setdefault(alias, f"a{position}")
|
|
479
|
+
names.setdefault(register, f"a{position}")
|
|
480
|
+
return names
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _identifier(name: str | None, fallback: str) -> str:
|
|
484
|
+
"""Return an ASCII C-ish identifier for a symbol name."""
|
|
485
|
+
if not name:
|
|
486
|
+
return fallback
|
|
487
|
+
clean = re.sub(r"[^0-9A-Za-z_]", "_", name.strip())
|
|
488
|
+
if not clean:
|
|
489
|
+
return fallback
|
|
490
|
+
return f"_{clean}" if clean[0].isdigit() else clean
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _slot(displacement: int) -> str:
|
|
494
|
+
"""Name a frame-relative stack slot."""
|
|
495
|
+
if displacement < 0:
|
|
496
|
+
return f"local_0x{-displacement:x}"
|
|
497
|
+
if displacement == 0:
|
|
498
|
+
return "local_0x0"
|
|
499
|
+
return f"arg_0x{displacement:x}"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _loop_exits(
|
|
503
|
+
blocks: list[BasicBlock], by_address: dict[int, BasicBlock]
|
|
504
|
+
) -> dict[int, int | None]:
|
|
505
|
+
"""Map every loop header to the lowest exit address its latches reveal."""
|
|
506
|
+
candidates: dict[int, list[int]] = {}
|
|
507
|
+
for block in blocks:
|
|
508
|
+
for successor in block.successors:
|
|
509
|
+
if successor > block.start or successor not in by_address:
|
|
510
|
+
continue
|
|
511
|
+
forwards = [
|
|
512
|
+
value for value in block.successors if value > block.start and value in by_address
|
|
513
|
+
]
|
|
514
|
+
candidates.setdefault(successor, []).append(min(forwards) if forwards else -1)
|
|
515
|
+
exits: dict[int, int | None] = {}
|
|
516
|
+
for header, values in candidates.items():
|
|
517
|
+
known = [value for value in values if value >= 0]
|
|
518
|
+
exits[header] = min(known) if known else None
|
|
519
|
+
return exits
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _require_int(name: str, value: int, maximum: int) -> int:
|
|
523
|
+
"""Validate one budget argument against its hard limits."""
|
|
524
|
+
if value < 1:
|
|
525
|
+
raise AnalysisError(f"{name} must be >= 1")
|
|
526
|
+
if value > maximum:
|
|
527
|
+
raise AnalysisError(f"{name} too large (limit {maximum})")
|
|
528
|
+
return value
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class _Renderer:
|
|
532
|
+
"""Shapes one function's blocks into pseudo-C lines.
|
|
533
|
+
|
|
534
|
+
The renderer walks the blocks of a
|
|
535
|
+
:class:`~codevanta.analysis.cfg.FunctionCFG` in ascending address order and
|
|
536
|
+
keeps a stack of open constructs. Blocks are never re-ordered, so every line
|
|
537
|
+
stays in instruction order; a construct closes at the address the branch
|
|
538
|
+
structure predicts, and every edge that cannot be nested inside the current
|
|
539
|
+
region becomes a ``goto``/``LABEL_<addr>`` pair.
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
def __init__(
|
|
543
|
+
self,
|
|
544
|
+
address: int,
|
|
545
|
+
blocks: list[BasicBlock],
|
|
546
|
+
argument_names: dict[str, str],
|
|
547
|
+
pointer_width: int,
|
|
548
|
+
names: dict[int, str],
|
|
549
|
+
imports: dict[int, str],
|
|
550
|
+
) -> None:
|
|
551
|
+
self.address = address
|
|
552
|
+
self.blocks = blocks
|
|
553
|
+
self.by_address = {block.start: block for block in blocks}
|
|
554
|
+
self.argument_names = argument_names
|
|
555
|
+
self.pointer_width = pointer_width
|
|
556
|
+
self.names = names
|
|
557
|
+
self.imports = imports
|
|
558
|
+
self.preds: dict[int, set[int]] = {block.start: set() for block in blocks}
|
|
559
|
+
#: Edges whose control transfer is already expressed by the structure.
|
|
560
|
+
self.consumed: dict[int, set[int]] = {block.start: set() for block in blocks}
|
|
561
|
+
for block in blocks:
|
|
562
|
+
for successor in block.successors:
|
|
563
|
+
if successor not in self.by_address:
|
|
564
|
+
continue
|
|
565
|
+
self.preds[successor].add(block.start)
|
|
566
|
+
if successor == block.end:
|
|
567
|
+
# Falling through to the next block needs no label.
|
|
568
|
+
self.consumed[successor].add(block.start)
|
|
569
|
+
self.loops = _loop_exits(blocks, self.by_address)
|
|
570
|
+
self.lines: list[str] = []
|
|
571
|
+
self.calls: list[CallSite] = []
|
|
572
|
+
self.stack: list[_Frame] = []
|
|
573
|
+
self.indent = 0
|
|
574
|
+
self.opened_loops: set[int] = set()
|
|
575
|
+
self.consumed_jumps: set[int] = set()
|
|
576
|
+
|
|
577
|
+
# -- public ---------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
def render(self) -> tuple[tuple[str, ...], tuple[CallSite, ...]]:
|
|
580
|
+
"""Render every block and return the body lines plus the call sites."""
|
|
581
|
+
for block in self.blocks:
|
|
582
|
+
self._close_frames(block.start)
|
|
583
|
+
opened = self._open_loop(block.start)
|
|
584
|
+
if not opened and self._needs_label(block.start):
|
|
585
|
+
self._emit(f"LABEL_{block.start:x}:")
|
|
586
|
+
self._render_block(block)
|
|
587
|
+
self._close_all()
|
|
588
|
+
return tuple(self.lines), tuple(self.calls)
|
|
589
|
+
|
|
590
|
+
# -- structure bookkeeping ------------------------------------------
|
|
591
|
+
|
|
592
|
+
def _emit(self, text: str) -> None:
|
|
593
|
+
"""Append one already-composed line with the current indentation."""
|
|
594
|
+
self.lines.append(f"{_INDENT * self.indent}{text}")
|
|
595
|
+
|
|
596
|
+
def _line(self, statement: str | None, instruction: dict[str, Any]) -> None:
|
|
597
|
+
"""Append one source line with the raw instruction as a trailing comment."""
|
|
598
|
+
text = _instruction_text(instruction)
|
|
599
|
+
if statement is None:
|
|
600
|
+
self._emit(f"/* {text} */")
|
|
601
|
+
else:
|
|
602
|
+
self._emit(f"{statement} /* {text} */")
|
|
603
|
+
|
|
604
|
+
def _consume(self, target: int, source: int) -> None:
|
|
605
|
+
"""Record that the edge ``source -> target`` is expressed by structure."""
|
|
606
|
+
self.consumed.setdefault(target, set()).add(source)
|
|
607
|
+
|
|
608
|
+
def _needs_label(self, start: int) -> bool:
|
|
609
|
+
"""Whether a block needs ``LABEL_<addr>:`` because something jumps to it."""
|
|
610
|
+
if start == self.address or start in self.opened_loops:
|
|
611
|
+
return False
|
|
612
|
+
return bool(self.preds.get(start, set()) - self.consumed.get(start, set()))
|
|
613
|
+
|
|
614
|
+
def _close_frames(self, address: int) -> None:
|
|
615
|
+
"""Close every construct that ends at or before ``address``."""
|
|
616
|
+
while self.stack:
|
|
617
|
+
frame = self.stack[-1]
|
|
618
|
+
if frame.stop is None or frame.stop > address:
|
|
619
|
+
return
|
|
620
|
+
self.stack.pop()
|
|
621
|
+
self.indent -= 1
|
|
622
|
+
if frame.kind == "if" and frame.merge is not None and frame.merge > address:
|
|
623
|
+
self._emit("} else {")
|
|
624
|
+
self.indent += 1
|
|
625
|
+
self.stack.append(_Frame("else", frame.merge, frame.block))
|
|
626
|
+
else:
|
|
627
|
+
self._emit("}")
|
|
628
|
+
|
|
629
|
+
def _close_all(self) -> None:
|
|
630
|
+
"""Close every construct still open at the end of the function."""
|
|
631
|
+
while self.stack:
|
|
632
|
+
self.stack.pop()
|
|
633
|
+
self.indent -= 1
|
|
634
|
+
self._emit("}")
|
|
635
|
+
|
|
636
|
+
def _open_loop(self, start: int) -> bool:
|
|
637
|
+
"""Open ``while (1) {`` when ``start`` is the target of a back edge."""
|
|
638
|
+
if start not in self.loops or start in self.opened_loops:
|
|
639
|
+
return False
|
|
640
|
+
self.opened_loops.add(start)
|
|
641
|
+
self._emit("while (1) {")
|
|
642
|
+
self.indent += 1
|
|
643
|
+
self.stack.append(_Frame("while", self.loops[start], start))
|
|
644
|
+
for block in self.blocks:
|
|
645
|
+
if start in block.successors and start <= block.start:
|
|
646
|
+
self._consume(start, block.start)
|
|
647
|
+
return True
|
|
648
|
+
|
|
649
|
+
def _limit(self) -> int | None:
|
|
650
|
+
"""Return the address of the innermost enclosing region's end, if any."""
|
|
651
|
+
for frame in reversed(self.stack):
|
|
652
|
+
if frame.stop is not None:
|
|
653
|
+
return frame.stop
|
|
654
|
+
return None
|
|
655
|
+
|
|
656
|
+
def _loop_stop(self) -> int | None:
|
|
657
|
+
"""Return the exit address of the innermost open loop, if any."""
|
|
658
|
+
for frame in reversed(self.stack):
|
|
659
|
+
if frame.kind == "while":
|
|
660
|
+
return frame.stop
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
# -- blocks ---------------------------------------------------------
|
|
664
|
+
|
|
665
|
+
def _render_block(self, block: BasicBlock) -> None:
|
|
666
|
+
"""Render one block's body and its terminator."""
|
|
667
|
+
instructions = block.instructions
|
|
668
|
+
if not instructions:
|
|
669
|
+
return
|
|
670
|
+
body = instructions[:-1] if block.terminator else instructions
|
|
671
|
+
compare: dict[str, Any] | None = None
|
|
672
|
+
for instruction in body:
|
|
673
|
+
if (
|
|
674
|
+
_mnemonic(instruction) in _COMPARE_MNEMONICS
|
|
675
|
+
and len(_split_operands(_operands(instruction))) == 2
|
|
676
|
+
):
|
|
677
|
+
compare = instruction
|
|
678
|
+
self._line(self._statement(instruction), instruction)
|
|
679
|
+
if not block.terminator:
|
|
680
|
+
return
|
|
681
|
+
last = instructions[-1]
|
|
682
|
+
mnemonic = _mnemonic(last)
|
|
683
|
+
if mnemonic in _RET_MNEMONICS:
|
|
684
|
+
self._line("return;", last)
|
|
685
|
+
elif mnemonic in _UNCONDITIONAL_JUMPS:
|
|
686
|
+
self._jump(block, last)
|
|
687
|
+
else:
|
|
688
|
+
self._branch(block, last, compare)
|
|
689
|
+
|
|
690
|
+
def _statement(self, instruction: dict[str, Any]) -> str | None:
|
|
691
|
+
"""Return the C statement for an instruction, or ``None`` when it has none."""
|
|
692
|
+
mnemonic = _mnemonic(instruction)
|
|
693
|
+
operands = _split_operands(_operands(instruction))
|
|
694
|
+
if mnemonic in _CALL_MNEMONICS:
|
|
695
|
+
return self._call(instruction)
|
|
696
|
+
if mnemonic in _MOV_MNEMONICS and len(operands) == 2:
|
|
697
|
+
target = self._operand(operands[0], instruction)
|
|
698
|
+
source = self._operand(operands[1], instruction)
|
|
699
|
+
return f"{target} = {source};"
|
|
700
|
+
if mnemonic == "lea" and len(operands) == 2:
|
|
701
|
+
target = self._operand(operands[0], instruction)
|
|
702
|
+
return f"{target} = {self._address_of(operands[1], instruction)};"
|
|
703
|
+
if mnemonic == "imul" and len(operands) == 3:
|
|
704
|
+
target = self._operand(operands[0], instruction)
|
|
705
|
+
left = self._operand(operands[1], instruction)
|
|
706
|
+
right = self._operand(operands[2], instruction)
|
|
707
|
+
return f"{target} = {left} * {right};"
|
|
708
|
+
if mnemonic in _COMPOUND_ASSIGN and len(operands) == 2:
|
|
709
|
+
target = self._operand(operands[0], instruction)
|
|
710
|
+
source = self._operand(operands[1], instruction)
|
|
711
|
+
return f"{target} {_COMPOUND_ASSIGN[mnemonic]} {source};"
|
|
712
|
+
if mnemonic in _UNARY_ASSIGN and len(operands) == 1:
|
|
713
|
+
target = self._operand(operands[0], instruction)
|
|
714
|
+
return f"{target} = {_UNARY_ASSIGN[mnemonic]}{target};"
|
|
715
|
+
if mnemonic in _STEP_ASSIGN and len(operands) == 1:
|
|
716
|
+
target = self._operand(operands[0], instruction)
|
|
717
|
+
return f"{target}{_STEP_ASSIGN[mnemonic]};"
|
|
718
|
+
return None
|
|
719
|
+
|
|
720
|
+
def _call(self, instruction: dict[str, Any]) -> str:
|
|
721
|
+
"""Render a call and record it as a :class:`CallSite`."""
|
|
722
|
+
address = _address(instruction)
|
|
723
|
+
target = _direct_target(_operands(instruction))
|
|
724
|
+
if target is None:
|
|
725
|
+
self.calls.append(CallSite(address, None, None, "call"))
|
|
726
|
+
return "(*ptr)();"
|
|
727
|
+
imported = self.imports.get(target)
|
|
728
|
+
if imported is not None:
|
|
729
|
+
self.calls.append(CallSite(address, target, imported, "call"))
|
|
730
|
+
return f"{imported}();"
|
|
731
|
+
self.calls.append(CallSite(address, target, self.names.get(target), "call"))
|
|
732
|
+
return f"call_{target:x}();"
|
|
733
|
+
|
|
734
|
+
def _jump(self, block: BasicBlock, instruction: dict[str, Any]) -> None:
|
|
735
|
+
"""Render an unconditional jump as ``continue``, ``goto`` or a tail call."""
|
|
736
|
+
address = _address(instruction)
|
|
737
|
+
target = _direct_target(_operands(instruction))
|
|
738
|
+
if target is None:
|
|
739
|
+
self.calls.append(CallSite(address, None, None, "unknown"))
|
|
740
|
+
self._line(None, instruction)
|
|
741
|
+
return
|
|
742
|
+
if block.start in self.consumed_jumps:
|
|
743
|
+
# The enclosing if/else structure already expresses this transfer.
|
|
744
|
+
self._line(None, instruction)
|
|
745
|
+
return
|
|
746
|
+
if target <= block.start and target in self.opened_loops:
|
|
747
|
+
self._consume(target, block.start)
|
|
748
|
+
self._line("continue;", instruction)
|
|
749
|
+
return
|
|
750
|
+
if target in self.by_address:
|
|
751
|
+
self._line(f"goto LABEL_{target:x};", instruction)
|
|
752
|
+
return
|
|
753
|
+
self.calls.append(CallSite(address, target, self.imports.get(target), "jump"))
|
|
754
|
+
self._line(f"goto 0x{target:x};", instruction)
|
|
755
|
+
|
|
756
|
+
def _branch(
|
|
757
|
+
self,
|
|
758
|
+
block: BasicBlock,
|
|
759
|
+
instruction: dict[str, Any],
|
|
760
|
+
compare: dict[str, Any] | None,
|
|
761
|
+
) -> None:
|
|
762
|
+
"""Render a conditional branch as a loop exit, an ``if`` or a ``goto``."""
|
|
763
|
+
mnemonic = _mnemonic(instruction)
|
|
764
|
+
target = _direct_target(_operands(instruction))
|
|
765
|
+
if target is None:
|
|
766
|
+
self._line(None, instruction)
|
|
767
|
+
return
|
|
768
|
+
loop_stop = self._loop_stop()
|
|
769
|
+
if loop_stop is not None and target == loop_stop:
|
|
770
|
+
self._consume(target, block.start)
|
|
771
|
+
self._break(self._condition(mnemonic, compare, invert=False), instruction)
|
|
772
|
+
return
|
|
773
|
+
if target <= block.start and target in self.opened_loops:
|
|
774
|
+
self._consume(target, block.start)
|
|
775
|
+
self._break(self._condition(mnemonic, compare, invert=True), instruction)
|
|
776
|
+
return
|
|
777
|
+
if self._can_structure(block, target):
|
|
778
|
+
self._structure_if(block, instruction, compare, target)
|
|
779
|
+
return
|
|
780
|
+
condition = self._condition(mnemonic, compare, invert=False)
|
|
781
|
+
self._line(f"if ({condition}) {{", instruction)
|
|
782
|
+
self.indent += 1
|
|
783
|
+
if target in self.by_address:
|
|
784
|
+
self._emit(f"goto LABEL_{target:x};")
|
|
785
|
+
else:
|
|
786
|
+
self._emit(f"goto 0x{target:x};")
|
|
787
|
+
self.indent -= 1
|
|
788
|
+
self._emit("}")
|
|
789
|
+
|
|
790
|
+
def _break(self, condition: str, instruction: dict[str, Any]) -> None:
|
|
791
|
+
"""Emit one loop-exit test followed by ``break;``."""
|
|
792
|
+
self._line(f"if ({condition}) {{", instruction)
|
|
793
|
+
self.indent += 1
|
|
794
|
+
self._emit("break;")
|
|
795
|
+
self.indent -= 1
|
|
796
|
+
self._emit("}")
|
|
797
|
+
|
|
798
|
+
def _can_structure(self, block: BasicBlock, target: int) -> bool:
|
|
799
|
+
"""Whether a forward branch fits inside the current region as an ``if``."""
|
|
800
|
+
if target <= block.start:
|
|
801
|
+
return False
|
|
802
|
+
if target not in self.by_address or block.end not in self.by_address:
|
|
803
|
+
return False
|
|
804
|
+
limit = self._limit()
|
|
805
|
+
return limit is None or target <= limit
|
|
806
|
+
|
|
807
|
+
def _structure_if(
|
|
808
|
+
self,
|
|
809
|
+
block: BasicBlock,
|
|
810
|
+
instruction: dict[str, Any],
|
|
811
|
+
compare: dict[str, Any] | None,
|
|
812
|
+
target: int,
|
|
813
|
+
) -> None:
|
|
814
|
+
"""Open ``if (<inverted condition>) {`` for a forward branch.
|
|
815
|
+
|
|
816
|
+
The condition is inverted because the *fallthrough* path is rendered as
|
|
817
|
+
the then-branch: instruction order then equals source order and the
|
|
818
|
+
branch target stays the else/merge address.
|
|
819
|
+
"""
|
|
820
|
+
condition = self._condition(_mnemonic(instruction), compare, invert=True)
|
|
821
|
+
fallthrough = self.by_address[block.end]
|
|
822
|
+
merge = self._merge_address(block, fallthrough, target)
|
|
823
|
+
self._line(f"if ({condition}) {{", instruction)
|
|
824
|
+
self.indent += 1
|
|
825
|
+
self.stack.append(_Frame("if", target, block.start, merge))
|
|
826
|
+
self._consume(target, block.start)
|
|
827
|
+
if merge is not None:
|
|
828
|
+
self._consume(merge, fallthrough.start)
|
|
829
|
+
self.consumed_jumps.add(fallthrough.start)
|
|
830
|
+
|
|
831
|
+
def _merge_address(self, block: BasicBlock, fallthrough: BasicBlock, target: int) -> int | None:
|
|
832
|
+
"""Return the address an if/else pair merges at, or ``None`` if it cannot.
|
|
833
|
+
|
|
834
|
+
``} else {`` is only honest when the then-branch is a single block that
|
|
835
|
+
jumps over a region only this branch reaches, so the two halves are
|
|
836
|
+
mutually exclusive.
|
|
837
|
+
"""
|
|
838
|
+
if self.preds.get(target, set()) != {block.start}:
|
|
839
|
+
return None
|
|
840
|
+
if not fallthrough.instructions:
|
|
841
|
+
return None
|
|
842
|
+
last = fallthrough.instructions[-1]
|
|
843
|
+
if _mnemonic(last) not in _UNCONDITIONAL_JUMPS:
|
|
844
|
+
return None
|
|
845
|
+
merge = _direct_target(_operands(last))
|
|
846
|
+
if merge is None or merge <= target or merge not in self.by_address:
|
|
847
|
+
return None
|
|
848
|
+
return merge
|
|
849
|
+
|
|
850
|
+
# -- operands -------------------------------------------------------
|
|
851
|
+
|
|
852
|
+
def _register(self, name: str) -> str:
|
|
853
|
+
"""Name a register from the ABI, falling back to its architectural name."""
|
|
854
|
+
lowered = name.lower()
|
|
855
|
+
return self.argument_names.get(lowered, lowered)
|
|
856
|
+
|
|
857
|
+
def _operand(self, operand: str, instruction: dict[str, Any]) -> str:
|
|
858
|
+
"""Render one operand as a pseudo-C expression."""
|
|
859
|
+
text = operand.strip()
|
|
860
|
+
memory = _parse_memory(text)
|
|
861
|
+
if memory is not None:
|
|
862
|
+
expression = self._address_of(text, instruction)
|
|
863
|
+
if memory.base in _STACK_REGISTERS and memory.index is None:
|
|
864
|
+
# A frame slot already *is* the value; only real pointers dereference.
|
|
865
|
+
return expression
|
|
866
|
+
width = self.pointer_width * 8
|
|
867
|
+
return f"*(u{width}*)({expression})"
|
|
868
|
+
lowered = text.lower()
|
|
869
|
+
if not lowered:
|
|
870
|
+
return ""
|
|
871
|
+
if not _SIMPLE_OPERAND.match(lowered):
|
|
872
|
+
# An operand shape the renderer does not model (a segment override, an
|
|
873
|
+
# unsupported expression): keep the raw text instead of guessing.
|
|
874
|
+
return text
|
|
875
|
+
return self._register(lowered)
|
|
876
|
+
|
|
877
|
+
def _address_of(self, operand: str, instruction: dict[str, Any]) -> str:
|
|
878
|
+
"""Render a bracketed operand as a plain address expression (no deref)."""
|
|
879
|
+
memory = _parse_memory(operand)
|
|
880
|
+
if memory is None:
|
|
881
|
+
return self._operand(operand, instruction)
|
|
882
|
+
if memory.base in _PC_REGISTERS:
|
|
883
|
+
return f"0x{_address(instruction) + _size(instruction) + memory.displacement:x}"
|
|
884
|
+
if memory.absolute is not None:
|
|
885
|
+
return f"0x{memory.absolute:x}"
|
|
886
|
+
if memory.base is None and memory.index is None: # pragma: no cover - defensive
|
|
887
|
+
return f"0x{memory.displacement:x}"
|
|
888
|
+
if memory.base in _STACK_REGISTERS and memory.index is None:
|
|
889
|
+
return _slot(memory.displacement)
|
|
890
|
+
parts: list[str] = []
|
|
891
|
+
if memory.base is not None:
|
|
892
|
+
parts.append(self._register(memory.base))
|
|
893
|
+
if memory.index is not None:
|
|
894
|
+
index = self._register(memory.index)
|
|
895
|
+
parts.append(index if memory.scale == 1 else f"{index} * {memory.scale}")
|
|
896
|
+
expression = " + ".join(parts)
|
|
897
|
+
if memory.displacement > 0:
|
|
898
|
+
expression = f"{expression} + 0x{memory.displacement:x}"
|
|
899
|
+
elif memory.displacement < 0:
|
|
900
|
+
expression = f"{expression} - 0x{-memory.displacement:x}"
|
|
901
|
+
return expression
|
|
902
|
+
|
|
903
|
+
def _condition(
|
|
904
|
+
self,
|
|
905
|
+
mnemonic: str,
|
|
906
|
+
compare: dict[str, Any] | None,
|
|
907
|
+
*,
|
|
908
|
+
invert: bool,
|
|
909
|
+
) -> str:
|
|
910
|
+
"""Build a condition from a branch mnemonic and the comparison feeding it."""
|
|
911
|
+
name = mnemonic.lower()
|
|
912
|
+
if invert:
|
|
913
|
+
inverse = _INVERSES.get(name)
|
|
914
|
+
if inverse is None:
|
|
915
|
+
return f"!flag_from_{name}"
|
|
916
|
+
name = inverse
|
|
917
|
+
operators = _COMPARISONS.get(name)
|
|
918
|
+
if operators is not None and compare is not None:
|
|
919
|
+
text = self._comparison(operators, compare)
|
|
920
|
+
if text is not None:
|
|
921
|
+
return text
|
|
922
|
+
return f"flag_from_{name}"
|
|
923
|
+
|
|
924
|
+
def _comparison(self, operators: tuple[str, bool], compare: dict[str, Any]) -> str | None:
|
|
925
|
+
"""Render ``cmp``/``test`` operands as a comparison, or ``None``."""
|
|
926
|
+
operands = _split_operands(_operands(compare))
|
|
927
|
+
if len(operands) != 2:
|
|
928
|
+
return None
|
|
929
|
+
operator, unsigned = operators
|
|
930
|
+
left = self._operand(operands[0], compare)
|
|
931
|
+
right = self._operand(operands[1], compare)
|
|
932
|
+
if _mnemonic(compare) == "test":
|
|
933
|
+
if operator not in ("==", "!="):
|
|
934
|
+
return None
|
|
935
|
+
return f"({left} & {right}) {operator} 0"
|
|
936
|
+
cast = "(unsigned) " if unsigned else ""
|
|
937
|
+
return f"{cast}{left} {operator} {right}"
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _header(
|
|
941
|
+
evidence: Evidence,
|
|
942
|
+
display: str,
|
|
943
|
+
cfg: FunctionCFG,
|
|
944
|
+
truncated: bool,
|
|
945
|
+
argument_types: tuple[ArgumentType, ...] = (),
|
|
946
|
+
return_type: ReturnType | None = None,
|
|
947
|
+
) -> list[str]:
|
|
948
|
+
"""Build the header comment block (ASCII only)."""
|
|
949
|
+
warnings = list(cfg.warnings)
|
|
950
|
+
if truncated and not warnings:
|
|
951
|
+
warnings.append(
|
|
952
|
+
"rendering stopped at the instruction/block budget; the output is incomplete"
|
|
953
|
+
)
|
|
954
|
+
lines = [
|
|
955
|
+
"/*",
|
|
956
|
+
" * CODEVANTA pseudo-C decompilation (heuristic, phase 3)",
|
|
957
|
+
f" * address: 0x{cfg.function_address:x}",
|
|
958
|
+
f" * symbol: {display}",
|
|
959
|
+
f" * confidence: {evidence.confidence.value}",
|
|
960
|
+
f" * evidence: {evidence.source} / {evidence.method} - {evidence.detail}",
|
|
961
|
+
f" * blocks: {len(cfg.blocks)} instructions: {cfg.instruction_count}"
|
|
962
|
+
f" truncated: {str(truncated).lower()}",
|
|
963
|
+
" * caveat: control flow, argument count and types are inferred from mnemonics;"
|
|
964
|
+
" nothing is executed",
|
|
965
|
+
]
|
|
966
|
+
for argument in argument_types:
|
|
967
|
+
lines.append(
|
|
968
|
+
f" * argument {argument.name} ({argument.register}): {argument.c_type} "
|
|
969
|
+
f"[{argument.confidence.value}] - {argument.evidence_note}"
|
|
970
|
+
)
|
|
971
|
+
if return_type is not None:
|
|
972
|
+
lines.append(
|
|
973
|
+
f" * return: {return_type.c_type} [{return_type.confidence.value}] - "
|
|
974
|
+
f"{return_type.evidence_note}"
|
|
975
|
+
)
|
|
976
|
+
for warning in warnings[:6]:
|
|
977
|
+
lines.append(f" * warning: {warning}")
|
|
978
|
+
if len(warnings) > 6:
|
|
979
|
+
lines.append(f" * warning: (+{len(warnings) - 6} more)")
|
|
980
|
+
lines.append(" */")
|
|
981
|
+
return lines
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def decompile_function(
|
|
985
|
+
path: str | Path,
|
|
986
|
+
info: BinaryInfo,
|
|
987
|
+
address: int,
|
|
988
|
+
max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
|
|
989
|
+
max_blocks: int = DEFAULT_MAX_BLOCKS,
|
|
990
|
+
) -> PseudoFunction:
|
|
991
|
+
"""Render one function as pseudo-C.
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
path: File the bytes are read from (read-only; never executed).
|
|
995
|
+
info: Loaded binary metadata used for address mapping, decoding, symbol
|
|
996
|
+
names and imports.
|
|
997
|
+
address: Virtual address of the function; must be inside an executable
|
|
998
|
+
section.
|
|
999
|
+
max_instructions: Instruction budget for the recursive traversal
|
|
1000
|
+
(``1`` .. :data:`MAX_MAX_INSTRUCTIONS`).
|
|
1001
|
+
max_blocks: Block budget for the recursive traversal
|
|
1002
|
+
(``1`` .. :data:`MAX_MAX_BLOCKS`).
|
|
1003
|
+
|
|
1004
|
+
Returns:
|
|
1005
|
+
The rendered :class:`PseudoFunction`.
|
|
1006
|
+
|
|
1007
|
+
Raises:
|
|
1008
|
+
AnalysisError: When ``address`` is not executable code, when a budget is
|
|
1009
|
+
out of range, or when the region decodes to no instruction at all.
|
|
1010
|
+
"""
|
|
1011
|
+
instruction_budget = _require_int("max_instructions", max_instructions, MAX_MAX_INSTRUCTIONS)
|
|
1012
|
+
block_budget = _require_int("max_blocks", max_blocks, MAX_MAX_BLOCKS)
|
|
1013
|
+
cfg = build_recursive_cfg(
|
|
1014
|
+
path,
|
|
1015
|
+
info,
|
|
1016
|
+
address,
|
|
1017
|
+
max_instructions=instruction_budget,
|
|
1018
|
+
max_blocks=block_budget,
|
|
1019
|
+
)
|
|
1020
|
+
if cfg.instruction_count == 0:
|
|
1021
|
+
raise AnalysisError(f"no decodable instruction at 0x{address:x}")
|
|
1022
|
+
|
|
1023
|
+
pointer_width = 8 if info.bitness >= 64 else 4
|
|
1024
|
+
argument_names = _argument_names(info.bitness)
|
|
1025
|
+
names: dict[int, str] = {}
|
|
1026
|
+
for candidate in discover_functions(info):
|
|
1027
|
+
if candidate.name:
|
|
1028
|
+
names[candidate.address] = candidate.name
|
|
1029
|
+
imports: dict[int, str] = {}
|
|
1030
|
+
for item in info.imports:
|
|
1031
|
+
if not item.address:
|
|
1032
|
+
continue
|
|
1033
|
+
printable = _identifier(item.name, "")
|
|
1034
|
+
if printable:
|
|
1035
|
+
imports[int(item.address)] = printable
|
|
1036
|
+
|
|
1037
|
+
blocks = list(cfg.blocks)
|
|
1038
|
+
renderer = _Renderer(
|
|
1039
|
+
address=address,
|
|
1040
|
+
blocks=blocks,
|
|
1041
|
+
argument_names=argument_names,
|
|
1042
|
+
pointer_width=pointer_width,
|
|
1043
|
+
names=names,
|
|
1044
|
+
imports=imports,
|
|
1045
|
+
)
|
|
1046
|
+
lines, call_sites = renderer.render()
|
|
1047
|
+
|
|
1048
|
+
display = _identifier(cfg.function_name, f"sub_{address:x}")
|
|
1049
|
+
instructions = [
|
|
1050
|
+
instruction for block in blocks for instruction in block.instructions
|
|
1051
|
+
]
|
|
1052
|
+
argument_types = infer_argument_types(instructions, pointer_width)
|
|
1053
|
+
return_type = infer_return_type(instructions, pointer_width)
|
|
1054
|
+
parameters = ", ".join(
|
|
1055
|
+
f"{argument.c_type} {argument.name}" for argument in argument_types
|
|
1056
|
+
)
|
|
1057
|
+
if not parameters:
|
|
1058
|
+
parameters = "void"
|
|
1059
|
+
truncated = cfg.truncated
|
|
1060
|
+
evidence = Evidence(
|
|
1061
|
+
source=_SOURCE,
|
|
1062
|
+
method=_METHOD,
|
|
1063
|
+
confidence=Confidence.LOW,
|
|
1064
|
+
address=address,
|
|
1065
|
+
detail=(
|
|
1066
|
+
f"pseudo-C rendered from {cfg.instruction_count} instruction(s) in "
|
|
1067
|
+
f"{len(blocks)} block(s) of a recursive traversal; argument count and "
|
|
1068
|
+
"argument/return types from register use, control flow from branch "
|
|
1069
|
+
"targets, widths from the pointer size; mnemonics only, nothing is "
|
|
1070
|
+
"executed"
|
|
1071
|
+
),
|
|
1072
|
+
extra={
|
|
1073
|
+
"argument_types": [argument.to_dict() for argument in argument_types],
|
|
1074
|
+
"return_type": return_type.to_dict(),
|
|
1075
|
+
},
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
body_lines = _header(evidence, display, cfg, truncated, argument_types, return_type)
|
|
1079
|
+
body_lines.append(f"{return_type.c_type} {display}({parameters})")
|
|
1080
|
+
body_lines.append("{")
|
|
1081
|
+
for line in lines:
|
|
1082
|
+
body_lines.append(f"{_INDENT}{line}" if line else "")
|
|
1083
|
+
body_lines.append("}")
|
|
1084
|
+
return PseudoFunction(
|
|
1085
|
+
address=address,
|
|
1086
|
+
name=cfg.function_name,
|
|
1087
|
+
lines=lines,
|
|
1088
|
+
body="\n".join(body_lines),
|
|
1089
|
+
call_sites=call_sites,
|
|
1090
|
+
block_count=len(blocks),
|
|
1091
|
+
instruction_count=cfg.instruction_count,
|
|
1092
|
+
truncated=truncated,
|
|
1093
|
+
evidence=evidence,
|
|
1094
|
+
)
|