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,1349 @@
|
|
|
1
|
+
"""Bounded, offline CPU emulation for questions static analysis cannot answer.
|
|
2
|
+
|
|
3
|
+
Three questions motivate this module: *what does this function return for these
|
|
4
|
+
inputs*, *where does this indirect jump land*, and *what does this XOR/ADD
|
|
5
|
+
string loop decode to*. Static decoders answer none of them: they cannot
|
|
6
|
+
propagate values, cannot follow a register-indirect branch, and cannot run a
|
|
7
|
+
decode loop. This module answers them by emulating the target code inside
|
|
8
|
+
`unicorn <https://www.unicorn-engine.org/>`_ -- the bytes are copied into an
|
|
9
|
+
emulator that runs on its own virtual CPU, so **nothing is ever executed on the
|
|
10
|
+
host** and a hard instruction budget bounds the work.
|
|
11
|
+
|
|
12
|
+
Guarantees
|
|
13
|
+
----------
|
|
14
|
+
* **Offline.** Only file bytes are read (through
|
|
15
|
+
:func:`codevanta.core.report.read_code`) and written into emulator memory.
|
|
16
|
+
No host process, syscall, or library is ever invoked, and an instruction that
|
|
17
|
+
would call into an import simply runs off the mapped image (see the stop
|
|
18
|
+
reasons below) instead of importing anything.
|
|
19
|
+
* **Bounded.** ``max_instructions`` (default
|
|
20
|
+
:data:`DEFAULT_MAX_INSTRUCTIONS`, ceiling :data:`MAX_MAX_INSTRUCTIONS`) is
|
|
21
|
+
enforced in the per-instruction hook, and every mapping is capped
|
|
22
|
+
(:data:`MAX_SECTION_BYTES`, :data:`MAX_TOTAL_MAPPED`,
|
|
23
|
+
:data:`MAX_INITIAL_BLOCKS`, :data:`MAX_INITIAL_BLOCK_BYTES`).
|
|
24
|
+
* **Honest.** Every run ends with one of the
|
|
25
|
+
:data:`StopReason` values and a warning that says *why*. A partial answer is
|
|
26
|
+
never dressed up as a complete one: a value read from ``rax`` after a budget
|
|
27
|
+
stop is reported as a register, not as the function's return value.
|
|
28
|
+
|
|
29
|
+
Stop reasons
|
|
30
|
+
------------
|
|
31
|
+
``return``
|
|
32
|
+
Execution reached a ``ret`` whose popped address lies outside the region of
|
|
33
|
+
the section holding ``address``; the return register (``rax``/``eax``) is
|
|
34
|
+
reported as :attr:`EmulationResult.return_value`. A ``ret`` that pops an
|
|
35
|
+
address *inside* the same region is an ordinary intra-region return and
|
|
36
|
+
emulation continues (tail calls, callbacks pushed by the code itself).
|
|
37
|
+
``budget``
|
|
38
|
+
``max_instructions`` instructions completed and the next one was stopped
|
|
39
|
+
before it ran, so :attr:`EmulationResult.instructions` equals the budget.
|
|
40
|
+
``unmapped``
|
|
41
|
+
The emulated code touched an address that is not mapped. The exact faulting
|
|
42
|
+
address comes from unicorn's unmapped-access hook and is written into
|
|
43
|
+
``warnings``. Memory-protection faults are reported here too, because from
|
|
44
|
+
the caller's point of view they are the same class of failure.
|
|
45
|
+
``unsupported``
|
|
46
|
+
The architecture/mode is not one this module emulates (x86 and x86-64 only,
|
|
47
|
+
see :func:`emulator_status`), unicorn cannot decode the instruction, or the
|
|
48
|
+
emulator is unavailable. No emulation happened.
|
|
49
|
+
``error``
|
|
50
|
+
unicorn raised an exception the emulated code caused (for example
|
|
51
|
+
``UC_ERR_EXCEPTION`` from a division by zero) or the emulator failed
|
|
52
|
+
internally. The run is over.
|
|
53
|
+
|
|
54
|
+
Nothing else escapes: the only exception this module raises is
|
|
55
|
+
:class:`~codevanta.core.report.AnalysisError`, and only for caller mistakes
|
|
56
|
+
(address outside an executable section, no file bytes at the address,
|
|
57
|
+
``max_instructions``/``stack_size`` out of range, an unknown register name, or
|
|
58
|
+
an ``initial_memory`` block that breaks the documented limits).
|
|
59
|
+
|
|
60
|
+
ABI defaults
|
|
61
|
+
------------
|
|
62
|
+
Registers are seeded only with what the ABI says a function may assume at
|
|
63
|
+
entry: the instruction pointer (``rip``/``eip``) at ``address`` and the stack
|
|
64
|
+
pointer/base (``rsp``/``rbp``, or ``esp``/``ebp``) near the top of a stack
|
|
65
|
+
region mapped at :data:`STACK_BASE_64` / :data:`STACK_BASE_32` (recorded in the
|
|
66
|
+
evidence ``extra``). Argument registers are *not* guessed -- pass
|
|
67
|
+
``initial_registers``.
|
|
68
|
+
|
|
69
|
+
Architecture support
|
|
70
|
+
--------------------
|
|
71
|
+
x86 (32-bit) and x86-64 are supported; those are the modes whose behaviour was
|
|
72
|
+
verified against the installed unicorn. ARM/AArch64/MIPS binaries get
|
|
73
|
+
``stopped_reason="unsupported"`` and a warning instead of a guess, because the
|
|
74
|
+
return-value, stack and register conventions here are x86-specific.
|
|
75
|
+
|
|
76
|
+
The string-decoding helpers (:func:`xor_bytes`, :func:`decrypt_string_loop`)
|
|
77
|
+
are pure functions over bytes: they reinterpret a key recovered from the
|
|
78
|
+
emulator (or from static analysis) and never execute anything.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
from __future__ import annotations
|
|
82
|
+
|
|
83
|
+
import faulthandler as _faulthandler
|
|
84
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
85
|
+
from dataclasses import dataclass
|
|
86
|
+
from pathlib import Path
|
|
87
|
+
from typing import Any, Literal
|
|
88
|
+
|
|
89
|
+
import capstone
|
|
90
|
+
|
|
91
|
+
from codevanta.core.loader import BinaryInfo, SectionInfo
|
|
92
|
+
from codevanta.core.report import AnalysisError, read_code
|
|
93
|
+
from codevanta.evidence.confidence import Confidence
|
|
94
|
+
from codevanta.evidence.models import Evidence
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"DEFAULT_MAX_INSTRUCTIONS",
|
|
98
|
+
"DEFAULT_STACK_SIZE",
|
|
99
|
+
"EMULATOR_AVAILABLE",
|
|
100
|
+
"MAX_INITIAL_BLOCKS",
|
|
101
|
+
"MAX_INITIAL_BLOCK_BYTES",
|
|
102
|
+
"MAX_MAX_INSTRUCTIONS",
|
|
103
|
+
"MAX_MEMORY_WRITES",
|
|
104
|
+
"MAX_SECTION_BYTES",
|
|
105
|
+
"MAX_STACK_SIZE",
|
|
106
|
+
"MAX_TOTAL_MAPPED",
|
|
107
|
+
"STACK_BASE_32",
|
|
108
|
+
"STACK_BASE_64",
|
|
109
|
+
"EmulationResult",
|
|
110
|
+
"StopReason",
|
|
111
|
+
"DECRYPT_OPS",
|
|
112
|
+
"decrypt_string_loop",
|
|
113
|
+
"emulate_function",
|
|
114
|
+
"emulator_status",
|
|
115
|
+
"resolve_indirect_target",
|
|
116
|
+
"xor_bytes",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---- limits ---------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
#: Instruction budget used when the caller does not pass one.
|
|
123
|
+
DEFAULT_MAX_INSTRUCTIONS = 2000
|
|
124
|
+
|
|
125
|
+
#: Hard ceiling on ``max_instructions`` (bounded work, whatever the caller asks).
|
|
126
|
+
MAX_MAX_INSTRUCTIONS = 100_000
|
|
127
|
+
|
|
128
|
+
#: Stack region size in bytes used when the caller does not pass one.
|
|
129
|
+
DEFAULT_STACK_SIZE = 0x10000
|
|
130
|
+
|
|
131
|
+
#: Hard ceiling on ``stack_size`` (16 MiB of emulator memory).
|
|
132
|
+
MAX_STACK_SIZE = 0x100_0000
|
|
133
|
+
|
|
134
|
+
#: A single ``initial_memory`` block may not be larger than 1 MiB.
|
|
135
|
+
MAX_INITIAL_BLOCK_BYTES = 0x100000
|
|
136
|
+
|
|
137
|
+
#: At most this many ``initial_memory`` blocks are accepted.
|
|
138
|
+
MAX_INITIAL_BLOCKS = 32
|
|
139
|
+
|
|
140
|
+
#: At most this many memory writes are recorded in
|
|
141
|
+
#: :attr:`EmulationResult.memory_writes`; the rest are only counted and
|
|
142
|
+
#: reported through a warning.
|
|
143
|
+
MAX_MEMORY_WRITES = 256
|
|
144
|
+
|
|
145
|
+
#: A section larger than this is not mapped into the emulator (16 MiB). The
|
|
146
|
+
#: code and data a function-level emulation needs are far smaller; an outlier
|
|
147
|
+
#: of this size is a file-layout artefact, not something to copy per call.
|
|
148
|
+
MAX_SECTION_BYTES = 0x100_0000
|
|
149
|
+
|
|
150
|
+
#: Total emulator memory this module will ever map (64 MiB: image + stack +
|
|
151
|
+
#: ``initial_memory``).
|
|
152
|
+
MAX_TOTAL_MAPPED = 0x400_0000
|
|
153
|
+
|
|
154
|
+
#: Emulator page size; unicorn requires page-aligned addresses *and* sizes.
|
|
155
|
+
PAGE_SIZE = 0x1000
|
|
156
|
+
|
|
157
|
+
#: Where the stack region is mapped for 64-bit code (page aligned, chosen far
|
|
158
|
+
#: away from any image VA so a mapping collision is practically impossible).
|
|
159
|
+
STACK_BASE_64 = 0x7FFF_0000_0000
|
|
160
|
+
|
|
161
|
+
#: Where the stack region is mapped for 32-bit code (below the 4 GiB limit and
|
|
162
|
+
#: above the conventional 0x0804_8000 image base).
|
|
163
|
+
STACK_BASE_32 = 0x7E00_0000
|
|
164
|
+
|
|
165
|
+
#: Room left between the initial stack pointer and the top of the stack region
|
|
166
|
+
#: so a prologue can push without immediately leaving the mapping.
|
|
167
|
+
_STACK_HEADROOM = 0x100
|
|
168
|
+
|
|
169
|
+
#: Widest x86 instruction, in bytes.
|
|
170
|
+
_MAX_INSTRUCTION_BYTES = 15
|
|
171
|
+
|
|
172
|
+
#: Byte order used for every register/memory value read back out of the
|
|
173
|
+
#: emulator; x86 has no big-endian mode.
|
|
174
|
+
_ENDIANNESS_LITTLE = "little"
|
|
175
|
+
|
|
176
|
+
# ---- evidence -------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
_EVIDENCE_SOURCE = "emulate.unicorn"
|
|
179
|
+
_EVIDENCE_METHOD = "bounded_cpu_emulation"
|
|
180
|
+
|
|
181
|
+
# ---- stop reasons ---------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
StopReason = Literal["return", "budget", "unmapped", "unsupported", "error"]
|
|
184
|
+
|
|
185
|
+
_STOP_RETURN: StopReason = "return"
|
|
186
|
+
_STOP_BUDGET: StopReason = "budget"
|
|
187
|
+
_STOP_UNMAPPED: StopReason = "unmapped"
|
|
188
|
+
_STOP_UNSUPPORTED: StopReason = "unsupported"
|
|
189
|
+
_STOP_ERROR: StopReason = "error"
|
|
190
|
+
#: Internal only: :func:`resolve_indirect_target` stopped at an indirect
|
|
191
|
+
#: branch. Never surfaces through :class:`EmulationResult`.
|
|
192
|
+
_STOP_INDIRECT = "indirect"
|
|
193
|
+
|
|
194
|
+
#: The reasons :class:`EmulationResult` may report (the public ``StopReason`` set).
|
|
195
|
+
_PUBLIC_REASONS = frozenset(
|
|
196
|
+
{_STOP_RETURN, _STOP_BUDGET, _STOP_UNMAPPED, _STOP_UNSUPPORTED, _STOP_ERROR}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
#: Instruction mnemonics that leave the current function (Capstone's naming).
|
|
200
|
+
_RET_MNEMONICS = frozenset({"ret", "retf", "retn", "retq", "iret", "iretd", "iretq"})
|
|
201
|
+
|
|
202
|
+
#: ``BinaryInfo.architecture`` spellings accepted as 64-bit x86.
|
|
203
|
+
_X86_64_NAMES = frozenset({"x86_64", "x86-64", "amd64", "em_amd64"})
|
|
204
|
+
|
|
205
|
+
#: ``BinaryInfo.architecture`` spellings accepted as 32-bit x86.
|
|
206
|
+
_X86_32_NAMES = frozenset({"x86", "i386", "i686", "em_386"})
|
|
207
|
+
|
|
208
|
+
#: General-purpose registers snapshotted after a run, per bitness.
|
|
209
|
+
_REGISTERS_64 = (
|
|
210
|
+
"rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp",
|
|
211
|
+
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15",
|
|
212
|
+
"rip", "rflags",
|
|
213
|
+
)
|
|
214
|
+
_REGISTERS_32 = (
|
|
215
|
+
"eax", "ebx", "ecx", "edx", "esi", "edi", "ebp", "esp", "eip", "eflags",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---- optional unicorn -----------------------------------------------------
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _probe_unicorn() -> tuple[Any, Any, str]:
|
|
223
|
+
"""Import unicorn lazily and report why it is unavailable.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
``(unicorn_module, x86_const_module, error)``. The modules are ``None``
|
|
227
|
+
when the import failed, and ``error`` then explains why.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
import unicorn as uni
|
|
231
|
+
import unicorn.x86_const as x86_const
|
|
232
|
+
except Exception as exc: # pragma: no cover - depends on the environment
|
|
233
|
+
return None, None, f"unicorn is not importable: {exc}"
|
|
234
|
+
return uni, x86_const, ""
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
_unicorn, _x86_const, _UNICORN_ERROR = _probe_unicorn()
|
|
238
|
+
|
|
239
|
+
#: True when a unicorn build is importable; emulation is refused otherwise.
|
|
240
|
+
EMULATOR_AVAILABLE = _unicorn is not None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def emulator_status() -> str:
|
|
244
|
+
"""Return why emulation is unavailable, or ``""`` when it is available.
|
|
245
|
+
|
|
246
|
+
Callers should check this (or :data:`EMULATOR_AVAILABLE`) before offering
|
|
247
|
+
emulation in a UI. The text is one human-readable sentence naming the
|
|
248
|
+
missing dependency, and it is empty when unicorn imported cleanly.
|
|
249
|
+
"""
|
|
250
|
+
return _UNICORN_ERROR
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---- result ---------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True)
|
|
257
|
+
class EmulationResult:
|
|
258
|
+
"""Everything one bounded emulation run produced.
|
|
259
|
+
|
|
260
|
+
Attributes:
|
|
261
|
+
address: Virtual address the run started at.
|
|
262
|
+
stopped_reason: Why emulation ended (see the module docstring).
|
|
263
|
+
instructions: Instructions that *completed*. The instruction that ended
|
|
264
|
+
the run (the ``ret`` that left the region, or the one stopped by the
|
|
265
|
+
budget) is not counted, so a ``budget`` stop reports exactly
|
|
266
|
+
``max_instructions``.
|
|
267
|
+
return_value: Value of the return register (``rax``/``eax``) when
|
|
268
|
+
``stopped_reason`` is ``"return"``, otherwise ``None`` -- a value
|
|
269
|
+
left in ``rax`` by an interrupted run is not a return value.
|
|
270
|
+
registers: Register snapshot taken after the run (names as used by
|
|
271
|
+
unicorn's x86 constants: ``rax``, ``rip``, ...).
|
|
272
|
+
memory_writes: ``(address, bytes)`` for each recorded store, in the order
|
|
273
|
+
the stores happened. Bounded by :data:`MAX_MEMORY_WRITES`; stores to
|
|
274
|
+
the stack are included.
|
|
275
|
+
indirect_targets: Targets of the indirect jumps/calls that were seen, in
|
|
276
|
+
order. A target is only recorded when the operand could be
|
|
277
|
+
evaluated; unresolvable ones produce a warning instead.
|
|
278
|
+
warnings: Human-readable notes explaining every degradation: the budget,
|
|
279
|
+
the faulting address, unresolved indirect branches, dropped writes,
|
|
280
|
+
skipped sections.
|
|
281
|
+
evidence: Provenance for the run, including the mapped regions and the
|
|
282
|
+
instruction budget in ``extra``.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
address: int
|
|
286
|
+
stopped_reason: StopReason
|
|
287
|
+
instructions: int
|
|
288
|
+
return_value: int | None
|
|
289
|
+
registers: Mapping[str, int]
|
|
290
|
+
memory_writes: tuple[tuple[int, bytes], ...]
|
|
291
|
+
indirect_targets: tuple[int, ...]
|
|
292
|
+
warnings: tuple[str, ...]
|
|
293
|
+
evidence: Evidence
|
|
294
|
+
|
|
295
|
+
def to_dict(self) -> dict[str, Any]:
|
|
296
|
+
"""Serialize to a JSON-safe dict (bytes become hex, ints become hex strings)."""
|
|
297
|
+
return {
|
|
298
|
+
"address": f"0x{self.address:x}",
|
|
299
|
+
"stopped_reason": self.stopped_reason,
|
|
300
|
+
"instructions": self.instructions,
|
|
301
|
+
"return_value": (
|
|
302
|
+
f"0x{self.return_value:x}" if self.return_value is not None else None
|
|
303
|
+
),
|
|
304
|
+
"registers": {
|
|
305
|
+
name: f"0x{value:x}" for name, value in sorted(self.registers.items())
|
|
306
|
+
},
|
|
307
|
+
"memory_writes": [
|
|
308
|
+
{"address": f"0x{address:x}", "size": len(data), "hex": data.hex()}
|
|
309
|
+
for address, data in self.memory_writes
|
|
310
|
+
],
|
|
311
|
+
"indirect_targets": [f"0x{target:x}" for target in self.indirect_targets],
|
|
312
|
+
"warnings": list(self.warnings),
|
|
313
|
+
"evidence": self.evidence.to_dict(),
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---- small helpers --------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _align_up(value: int) -> int:
|
|
321
|
+
"""Round ``value`` up to the next :data:`PAGE_SIZE` boundary."""
|
|
322
|
+
if value <= 0:
|
|
323
|
+
return 0
|
|
324
|
+
return (value + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _byteorder(info: BinaryInfo) -> Literal["little", "big"]:
|
|
328
|
+
"""Return the byte order used to read values back, validating the binary."""
|
|
329
|
+
endianness = (info.endianness or _ENDIANNESS_LITTLE).strip().lower()
|
|
330
|
+
return "big" if endianness == "big" else "little"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _reg_const(name: str) -> int | None:
|
|
334
|
+
"""Map a unicorn register name (``"rax"``, ``"eip"``) to its constant."""
|
|
335
|
+
if _x86_const is None:
|
|
336
|
+
return None
|
|
337
|
+
value = getattr(_x86_const, f"UC_X86_REG_{name.upper()}", None)
|
|
338
|
+
return int(value) if isinstance(value, int) else None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _machine_bits(info: BinaryInfo) -> int | None:
|
|
342
|
+
"""Return 32 or 64 when this module can emulate ``info``, else ``None``.
|
|
343
|
+
|
|
344
|
+
Only x86 modes are accepted: the return-value, stack and register handling
|
|
345
|
+
in this module is x86-specific, so an ARM/MIPS binary gets
|
|
346
|
+
``stopped_reason="unsupported"`` rather than an emulation of unknown
|
|
347
|
+
fidelity.
|
|
348
|
+
"""
|
|
349
|
+
if not EMULATOR_AVAILABLE:
|
|
350
|
+
return None
|
|
351
|
+
name = (info.architecture or "").strip().lower()
|
|
352
|
+
if name in _X86_64_NAMES:
|
|
353
|
+
bits = 64
|
|
354
|
+
elif name in _X86_32_NAMES:
|
|
355
|
+
bits = 32
|
|
356
|
+
else:
|
|
357
|
+
return None
|
|
358
|
+
if int(info.bitness) in (32, 64):
|
|
359
|
+
bits = int(info.bitness)
|
|
360
|
+
if _byteorder(info) == "big": # x86 is little-endian only
|
|
361
|
+
return None
|
|
362
|
+
return bits
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _merge_spans(spans: Iterable[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
366
|
+
"""Merge overlapping/adjacent page spans so each page is mapped once."""
|
|
367
|
+
merged: list[tuple[int, int]] = []
|
|
368
|
+
for start, end in sorted(spans):
|
|
369
|
+
if merged and start <= merged[-1][1]:
|
|
370
|
+
previous_start, previous_end = merged[-1]
|
|
371
|
+
merged[-1] = (previous_start, max(previous_end, end))
|
|
372
|
+
else:
|
|
373
|
+
merged.append((start, end))
|
|
374
|
+
return merged
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ---- instruction decoding -------------------------------------------------
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class _Decoder:
|
|
381
|
+
"""Single-instruction decoder used to recognise ``ret`` and indirect branches.
|
|
382
|
+
|
|
383
|
+
Unicorn tells us *where* control is, not *what* the instruction is, so the
|
|
384
|
+
bytes are also decoded with Capstone (detail enabled) inside the code hook.
|
|
385
|
+
Decoding only ever informs the stop decision; it never decides what executes.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
def __init__(self, mode64: bool) -> None:
|
|
389
|
+
mode = capstone.CS_MODE_64 if mode64 else capstone.CS_MODE_32
|
|
390
|
+
self._cs = capstone.Cs(capstone.CS_ARCH_X86, mode)
|
|
391
|
+
self._cs.detail = True
|
|
392
|
+
|
|
393
|
+
def decode(self, code: bytes, address: int) -> Any:
|
|
394
|
+
"""Decode one instruction from ``code``; ``None`` when it cannot be decoded."""
|
|
395
|
+
if not code:
|
|
396
|
+
return None
|
|
397
|
+
for insn in self._cs.disasm(code, address, 1):
|
|
398
|
+
return insn
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---- input validation -----------------------------------------------------
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _validated_address(address: int) -> int:
|
|
406
|
+
"""Validate a start address (non-negative int) and return it."""
|
|
407
|
+
if isinstance(address, bool) or not isinstance(address, int):
|
|
408
|
+
raise AnalysisError(f"address must be an int, not {type(address).__name__}")
|
|
409
|
+
if address <= 0:
|
|
410
|
+
raise AnalysisError(f"address must be positive, got 0x{address:x}")
|
|
411
|
+
return address
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _validated_budget(max_instructions: int) -> int:
|
|
415
|
+
"""Validate the instruction budget against the documented limits."""
|
|
416
|
+
if isinstance(max_instructions, bool) or not isinstance(max_instructions, int):
|
|
417
|
+
raise AnalysisError("max_instructions must be an int")
|
|
418
|
+
if max_instructions < 1:
|
|
419
|
+
raise AnalysisError("max_instructions must be >= 1")
|
|
420
|
+
if max_instructions > MAX_MAX_INSTRUCTIONS:
|
|
421
|
+
raise AnalysisError(
|
|
422
|
+
f"max_instructions too large ({max_instructions}); limit is {MAX_MAX_INSTRUCTIONS}"
|
|
423
|
+
)
|
|
424
|
+
return max_instructions
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _validated_stack_size(stack_size: int) -> int:
|
|
428
|
+
"""Validate the stack size against the documented limits."""
|
|
429
|
+
if isinstance(stack_size, bool) or not isinstance(stack_size, int):
|
|
430
|
+
raise AnalysisError("stack_size must be an int")
|
|
431
|
+
if stack_size < PAGE_SIZE:
|
|
432
|
+
raise AnalysisError(f"stack_size must be >= {PAGE_SIZE} byte(s)")
|
|
433
|
+
if stack_size > MAX_STACK_SIZE:
|
|
434
|
+
raise AnalysisError(f"stack_size too large ({stack_size}); limit is {MAX_STACK_SIZE}")
|
|
435
|
+
return stack_size
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _validated_registers(
|
|
439
|
+
initial_registers: Mapping[str, int] | None,
|
|
440
|
+
bits: int,
|
|
441
|
+
) -> dict[str, int]:
|
|
442
|
+
"""Normalize and validate caller-supplied register seeds.
|
|
443
|
+
|
|
444
|
+
Raises:
|
|
445
|
+
AnalysisError: On an unknown register name, a non-int value, or a value
|
|
446
|
+
that does not fit the machine word.
|
|
447
|
+
"""
|
|
448
|
+
seeds: dict[str, int] = {}
|
|
449
|
+
if not initial_registers:
|
|
450
|
+
return seeds
|
|
451
|
+
limit = 1 << bits
|
|
452
|
+
for name, value in initial_registers.items():
|
|
453
|
+
key = str(name).strip().lower()
|
|
454
|
+
if _reg_const(key) is None:
|
|
455
|
+
raise AnalysisError(f"unknown {bits}-bit register name {name!r}")
|
|
456
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
457
|
+
raise AnalysisError(f"register {key!r} must be seeded with an int")
|
|
458
|
+
if value < 0 or value >= limit:
|
|
459
|
+
raise AnalysisError(f"value for register {key!r} does not fit in {bits} bits")
|
|
460
|
+
seeds[key] = int(value)
|
|
461
|
+
return seeds
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _validated_memory(initial_memory: Mapping[int, bytes] | None) -> dict[int, bytes]:
|
|
465
|
+
"""Normalize and validate caller-supplied memory blocks.
|
|
466
|
+
|
|
467
|
+
Raises:
|
|
468
|
+
AnalysisError: On too many blocks, an oversized block, a negative
|
|
469
|
+
address, or a non-bytes payload.
|
|
470
|
+
"""
|
|
471
|
+
blocks: dict[int, bytes] = {}
|
|
472
|
+
if not initial_memory:
|
|
473
|
+
return blocks
|
|
474
|
+
if len(initial_memory) > MAX_INITIAL_BLOCKS:
|
|
475
|
+
raise AnalysisError(
|
|
476
|
+
f"initial_memory has {len(initial_memory)} blocks; limit is {MAX_INITIAL_BLOCKS}"
|
|
477
|
+
)
|
|
478
|
+
for address, data in initial_memory.items():
|
|
479
|
+
if isinstance(address, bool) or not isinstance(address, int) or address < 0:
|
|
480
|
+
raise AnalysisError(f"initial_memory address {address!r} is not a non-negative int")
|
|
481
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
482
|
+
raise AnalysisError(
|
|
483
|
+
f"initial_memory block at 0x{address:x} must be bytes"
|
|
484
|
+
)
|
|
485
|
+
payload = bytes(data)
|
|
486
|
+
if len(payload) > MAX_INITIAL_BLOCK_BYTES:
|
|
487
|
+
raise AnalysisError(
|
|
488
|
+
f"initial_memory block at 0x{address:x} is {len(payload)} byte(s); "
|
|
489
|
+
f"limit is {MAX_INITIAL_BLOCK_BYTES}"
|
|
490
|
+
)
|
|
491
|
+
blocks[address] = payload
|
|
492
|
+
return blocks
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ---- the emulation session ------------------------------------------------
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class _Runner:
|
|
500
|
+
"""One bounded emulation session on a private unicorn CPU.
|
|
501
|
+
|
|
502
|
+
The runner owns the emulator, the mapped regions and the per-instruction
|
|
503
|
+
state machine that decides when to stop. It never raises anything other
|
|
504
|
+
than :class:`AnalysisError`; every emulator exception is classified into a
|
|
505
|
+
:data:`StopReason` and a warning.
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
def __init__(
|
|
509
|
+
self,
|
|
510
|
+
path: str | Path,
|
|
511
|
+
info: BinaryInfo,
|
|
512
|
+
address: int,
|
|
513
|
+
*,
|
|
514
|
+
bits: int,
|
|
515
|
+
max_instructions: int,
|
|
516
|
+
stack_size: int,
|
|
517
|
+
stop_at_indirect: bool,
|
|
518
|
+
) -> None:
|
|
519
|
+
self.path = Path(path)
|
|
520
|
+
self.info = info
|
|
521
|
+
self.address = address
|
|
522
|
+
self.bits = bits
|
|
523
|
+
self.max_instructions = max_instructions
|
|
524
|
+
self.stack_size = stack_size
|
|
525
|
+
self.stop_at_indirect = stop_at_indirect
|
|
526
|
+
self.mode64 = bits == 64
|
|
527
|
+
self.word_size = bits // 8
|
|
528
|
+
self.byteorder = _byteorder(info)
|
|
529
|
+
self.uc: Any = None
|
|
530
|
+
self.decoder = _Decoder(self.mode64)
|
|
531
|
+
self.attempted = 0
|
|
532
|
+
self.completed = 0
|
|
533
|
+
self.reason: str | None = None
|
|
534
|
+
self.warnings: list[str] = []
|
|
535
|
+
self.writes: list[tuple[int, bytes]] = []
|
|
536
|
+
self.dropped_writes = 0
|
|
537
|
+
self.indirect: list[int] = []
|
|
538
|
+
self.pending_indirect: int | None = None
|
|
539
|
+
self.fault: tuple[str, int, int] | None = None
|
|
540
|
+
self.mapped_pages: set[int] = set()
|
|
541
|
+
self.regions: list[tuple[str, int, int]] = []
|
|
542
|
+
self.mapped_bytes = 0
|
|
543
|
+
self.stack_base = 0
|
|
544
|
+
self.stack_top = 0
|
|
545
|
+
self._wide_write_warned = False
|
|
546
|
+
section = info.section_for_va(address)
|
|
547
|
+
if section is not None:
|
|
548
|
+
self.region = (section.virtual_address, section.virtual_address + section.size)
|
|
549
|
+
else: # pragma: no cover - the public API rejects this first
|
|
550
|
+
self.region = (address, address + 1)
|
|
551
|
+
|
|
552
|
+
# ---- setup ------------------------------------------------------------
|
|
553
|
+
|
|
554
|
+
def setup(
|
|
555
|
+
self,
|
|
556
|
+
initial_registers: Mapping[str, int],
|
|
557
|
+
initial_memory: Mapping[int, bytes],
|
|
558
|
+
) -> None:
|
|
559
|
+
"""Create the CPU, map the image and the stack, then seed state."""
|
|
560
|
+
uni = _unicorn
|
|
561
|
+
arch = uni.UC_ARCH_X86
|
|
562
|
+
mode = uni.UC_MODE_64 if self.mode64 else uni.UC_MODE_32
|
|
563
|
+
self.uc = uni.Uc(arch, mode)
|
|
564
|
+
self._map_image()
|
|
565
|
+
self._map_stack()
|
|
566
|
+
self._map_initial_memory(initial_memory)
|
|
567
|
+
self._seed_registers(initial_registers)
|
|
568
|
+
self._install_hooks()
|
|
569
|
+
|
|
570
|
+
def _map_image(self) -> None:
|
|
571
|
+
"""Map every section of the image (page aligned, bounded), then copy its bytes."""
|
|
572
|
+
spans: list[tuple[int, int]] = []
|
|
573
|
+
mapped: list[SectionInfo] = []
|
|
574
|
+
for section in self.info.sections:
|
|
575
|
+
span = self._section_span(section)
|
|
576
|
+
if span is not None:
|
|
577
|
+
spans.append(span)
|
|
578
|
+
mapped.append(section)
|
|
579
|
+
for start, end in _merge_spans(spans):
|
|
580
|
+
self._map_pages(start, end, "image")
|
|
581
|
+
for section in mapped:
|
|
582
|
+
data = self._section_bytes(section)
|
|
583
|
+
if not data:
|
|
584
|
+
continue
|
|
585
|
+
try:
|
|
586
|
+
self.uc.mem_write(section.virtual_address, data)
|
|
587
|
+
except Exception as exc: # pragma: no cover - mapped just above
|
|
588
|
+
self.warnings.append(
|
|
589
|
+
f"section {section.name!r} could not be written at "
|
|
590
|
+
f"0x{section.virtual_address:x}: {exc}"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
def _section_span(self, section: SectionInfo) -> tuple[int, int] | None:
|
|
594
|
+
"""Page span to map for ``section``, or ``None`` when it is skipped."""
|
|
595
|
+
if section.size <= 0 or section.virtual_address <= 0:
|
|
596
|
+
return None # not allocated (e.g. .symtab, .shstrtab) or empty
|
|
597
|
+
if section.size > MAX_SECTION_BYTES:
|
|
598
|
+
self.warnings.append(
|
|
599
|
+
f"section {section.name!r} is {section.size} byte(s); sections larger than "
|
|
600
|
+
f"{MAX_SECTION_BYTES} byte(s) are not mapped into the emulator"
|
|
601
|
+
)
|
|
602
|
+
return None
|
|
603
|
+
start = section.virtual_address & ~(PAGE_SIZE - 1)
|
|
604
|
+
end = _align_up(section.virtual_address + section.size)
|
|
605
|
+
if self.mapped_bytes + (end - start) > MAX_TOTAL_MAPPED:
|
|
606
|
+
self.warnings.append(
|
|
607
|
+
f"section {section.name!r} is not mapped: the "
|
|
608
|
+
f"{MAX_TOTAL_MAPPED} byte(s) emulator mapping budget is spent"
|
|
609
|
+
)
|
|
610
|
+
return None
|
|
611
|
+
self.mapped_bytes += end - start
|
|
612
|
+
return start, end
|
|
613
|
+
|
|
614
|
+
def _section_bytes(self, section: SectionInfo) -> bytes:
|
|
615
|
+
"""File bytes backing ``section`` (empty for bss-like sections)."""
|
|
616
|
+
want = min(section.size, MAX_SECTION_BYTES)
|
|
617
|
+
if want <= 0:
|
|
618
|
+
return b""
|
|
619
|
+
try:
|
|
620
|
+
return read_code(self.path, self.info, section.virtual_address, want)
|
|
621
|
+
except (AnalysisError, OSError):
|
|
622
|
+
return b""
|
|
623
|
+
|
|
624
|
+
def _map_stack(self) -> None:
|
|
625
|
+
"""Map the stack region and remember where it lives."""
|
|
626
|
+
base = STACK_BASE_64 if self.mode64 else STACK_BASE_32
|
|
627
|
+
start = base & ~(PAGE_SIZE - 1)
|
|
628
|
+
end = _align_up(base + self.stack_size)
|
|
629
|
+
span = end - start
|
|
630
|
+
if self.mapped_bytes + span > MAX_TOTAL_MAPPED: # pragma: no cover - defensive
|
|
631
|
+
raise AnalysisError("stack does not fit in the emulator mapping budget")
|
|
632
|
+
self.mapped_bytes += span
|
|
633
|
+
self.stack_base, self.stack_top = start, end
|
|
634
|
+
self._map_pages(start, end, "stack")
|
|
635
|
+
|
|
636
|
+
def _map_pages(self, start: int, end: int, kind: str) -> None:
|
|
637
|
+
"""Map the page range ``[start, end)``, skipping pages already mapped."""
|
|
638
|
+
run_start: int | None = None
|
|
639
|
+
page = start
|
|
640
|
+
while page < end:
|
|
641
|
+
if page in self.mapped_pages:
|
|
642
|
+
if run_start is not None:
|
|
643
|
+
self._commit(run_start, page, kind)
|
|
644
|
+
run_start = None
|
|
645
|
+
else:
|
|
646
|
+
self.mapped_pages.add(page)
|
|
647
|
+
if run_start is None:
|
|
648
|
+
run_start = page
|
|
649
|
+
page += PAGE_SIZE
|
|
650
|
+
if run_start is not None:
|
|
651
|
+
self._commit(run_start, end, kind)
|
|
652
|
+
|
|
653
|
+
def _commit(self, start: int, end: int, kind: str) -> None:
|
|
654
|
+
"""Ask unicorn for one contiguous page run and record it."""
|
|
655
|
+
self.uc.mem_map(start, end - start)
|
|
656
|
+
self.regions.append((kind, start, end))
|
|
657
|
+
|
|
658
|
+
def _map_initial_memory(self, initial_memory: Mapping[int, bytes]) -> None:
|
|
659
|
+
"""Map (as needed) and write every caller-supplied memory block."""
|
|
660
|
+
for address, data in initial_memory.items():
|
|
661
|
+
if not data:
|
|
662
|
+
continue
|
|
663
|
+
start = address & ~(PAGE_SIZE - 1)
|
|
664
|
+
end = _align_up(address + len(data))
|
|
665
|
+
span = end - start
|
|
666
|
+
if self.mapped_bytes + span > MAX_TOTAL_MAPPED:
|
|
667
|
+
raise AnalysisError(
|
|
668
|
+
f"initial_memory block at 0x{address:x} does not fit in the "
|
|
669
|
+
f"{MAX_TOTAL_MAPPED} byte(s) emulator mapping budget"
|
|
670
|
+
)
|
|
671
|
+
self.mapped_bytes += span
|
|
672
|
+
self._map_pages(start, end, "initial_memory")
|
|
673
|
+
try:
|
|
674
|
+
self.uc.mem_write(address, data)
|
|
675
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
676
|
+
raise AnalysisError(
|
|
677
|
+
f"initial_memory block at 0x{address:x} could not be written: {exc}"
|
|
678
|
+
) from exc
|
|
679
|
+
|
|
680
|
+
def _seed_registers(self, initial_registers: Mapping[str, int]) -> None:
|
|
681
|
+
"""Seed the ABI defaults (rip/rsp/rbp) plus the caller's registers."""
|
|
682
|
+
sp = self.stack_top - _STACK_HEADROOM
|
|
683
|
+
seeds: dict[str, int] = (
|
|
684
|
+
{"rip": self.address, "rsp": sp, "rbp": sp}
|
|
685
|
+
if self.mode64
|
|
686
|
+
else {"eip": self.address, "esp": sp, "ebp": sp}
|
|
687
|
+
)
|
|
688
|
+
seeds.update(initial_registers)
|
|
689
|
+
for name, value in seeds.items():
|
|
690
|
+
constant = _reg_const(name)
|
|
691
|
+
if constant is None: # pragma: no cover - validated by the public API
|
|
692
|
+
continue
|
|
693
|
+
self.uc.reg_write(constant, value)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# ---- hooks ------------------------------------------------------------
|
|
697
|
+
|
|
698
|
+
def _install_hooks(self) -> None:
|
|
699
|
+
"""Register the instruction, write and unmapped-access hooks."""
|
|
700
|
+
uni = _unicorn
|
|
701
|
+
self.uc.hook_add(uni.UC_HOOK_CODE, self._on_code)
|
|
702
|
+
self.uc.hook_add(uni.UC_HOOK_MEM_WRITE, self._on_write)
|
|
703
|
+
for hook_name in (
|
|
704
|
+
"UC_HOOK_MEM_READ_UNMAPPED",
|
|
705
|
+
"UC_HOOK_MEM_WRITE_UNMAPPED",
|
|
706
|
+
"UC_HOOK_MEM_FETCH_UNMAPPED",
|
|
707
|
+
):
|
|
708
|
+
hook = getattr(uni, hook_name, None)
|
|
709
|
+
if hook is not None:
|
|
710
|
+
self.uc.hook_add(hook, self._on_unmapped)
|
|
711
|
+
|
|
712
|
+
def _on_code(self, uc: Any, address: int, size: int, user_data: Any) -> None:
|
|
713
|
+
"""Count instructions and decide whether to stop *before* each one runs."""
|
|
714
|
+
self.attempted += 1
|
|
715
|
+
self.completed = self.attempted - 1
|
|
716
|
+
if self.attempted > self.max_instructions:
|
|
717
|
+
self.reason = _STOP_BUDGET
|
|
718
|
+
uc.emu_stop()
|
|
719
|
+
return
|
|
720
|
+
insn = self.decoder.decode(self._read_code_bytes(address), address)
|
|
721
|
+
if insn is None:
|
|
722
|
+
return # nothing to classify: unicorn stops us if it cannot run it
|
|
723
|
+
if insn.mnemonic in _RET_MNEMONICS or insn.group(capstone.CS_GRP_RET):
|
|
724
|
+
if self._ret_leaves_region(insn, address):
|
|
725
|
+
self.reason = _STOP_RETURN
|
|
726
|
+
uc.emu_stop()
|
|
727
|
+
return
|
|
728
|
+
if not (insn.group(capstone.CS_GRP_JUMP) or insn.group(capstone.CS_GRP_CALL)):
|
|
729
|
+
return
|
|
730
|
+
if not self._is_indirect(insn):
|
|
731
|
+
return
|
|
732
|
+
target = self._indirect_target(insn)
|
|
733
|
+
if target is None:
|
|
734
|
+
self.warnings.append(
|
|
735
|
+
f"indirect branch at 0x{address:x} ({insn.mnemonic} {insn.op_str}) "
|
|
736
|
+
"could not be resolved; its target is unknown"
|
|
737
|
+
)
|
|
738
|
+
return
|
|
739
|
+
if target not in self.indirect:
|
|
740
|
+
self.indirect.append(target)
|
|
741
|
+
if self.stop_at_indirect:
|
|
742
|
+
self.pending_indirect = target
|
|
743
|
+
self.reason = _STOP_INDIRECT
|
|
744
|
+
uc.emu_stop()
|
|
745
|
+
|
|
746
|
+
def _on_write(
|
|
747
|
+
self,
|
|
748
|
+
uc: Any,
|
|
749
|
+
access: int,
|
|
750
|
+
address: int,
|
|
751
|
+
size: int,
|
|
752
|
+
value: int,
|
|
753
|
+
user_data: Any,
|
|
754
|
+
) -> None:
|
|
755
|
+
"""Record (bounded) the bytes of every store the code performs."""
|
|
756
|
+
if len(self.writes) >= MAX_MEMORY_WRITES:
|
|
757
|
+
self.dropped_writes += 1
|
|
758
|
+
return
|
|
759
|
+
if size <= 0:
|
|
760
|
+
return
|
|
761
|
+
if size > 8:
|
|
762
|
+
if not self._wide_write_warned:
|
|
763
|
+
self._wide_write_warned = True
|
|
764
|
+
self.warnings.append(
|
|
765
|
+
f"store of {size} byte(s) at 0x{address:x} is recorded as its low 8 "
|
|
766
|
+
"byte(s); wide stores are truncated in memory_writes"
|
|
767
|
+
)
|
|
768
|
+
payload = (int(value) & 0xFFFF_FFFF_FFFF_FFFF).to_bytes(8, self.byteorder)
|
|
769
|
+
else:
|
|
770
|
+
mask = (1 << (8 * size)) - 1
|
|
771
|
+
payload = (int(value) & mask).to_bytes(size, self.byteorder)
|
|
772
|
+
self.writes.append((address, payload))
|
|
773
|
+
|
|
774
|
+
def _on_unmapped(
|
|
775
|
+
self,
|
|
776
|
+
uc: Any,
|
|
777
|
+
access: int,
|
|
778
|
+
address: int,
|
|
779
|
+
size: int,
|
|
780
|
+
value: int,
|
|
781
|
+
user_data: Any,
|
|
782
|
+
) -> bool:
|
|
783
|
+
"""Remember the exact faulting address; never map it to paper a bug over."""
|
|
784
|
+
uni = _unicorn
|
|
785
|
+
kinds = {
|
|
786
|
+
getattr(uni, "UC_MEM_READ_UNMAPPED", -1): "read",
|
|
787
|
+
getattr(uni, "UC_MEM_WRITE_UNMAPPED", -2): "write",
|
|
788
|
+
getattr(uni, "UC_MEM_FETCH_UNMAPPED", -3): "fetch",
|
|
789
|
+
}
|
|
790
|
+
self.fault = (kinds.get(access, "memory"), address, size)
|
|
791
|
+
return False
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
# ---- instruction helpers ---------------------------------------------
|
|
795
|
+
|
|
796
|
+
def _read_code_bytes(self, address: int) -> bytes:
|
|
797
|
+
"""Read up to one instruction's bytes, tolerating a page edge."""
|
|
798
|
+
for want in (_MAX_INSTRUCTION_BYTES, 8, 4, 1):
|
|
799
|
+
try:
|
|
800
|
+
return bytes(self.uc.mem_read(address, want))
|
|
801
|
+
except Exception:
|
|
802
|
+
continue
|
|
803
|
+
return b""
|
|
804
|
+
|
|
805
|
+
def _read_register(self, name: str) -> int | None:
|
|
806
|
+
"""Read one register, or ``None`` when this mode has no such register."""
|
|
807
|
+
constant = _reg_const(name)
|
|
808
|
+
if constant is None:
|
|
809
|
+
return None
|
|
810
|
+
try:
|
|
811
|
+
return int(self.uc.reg_read(constant))
|
|
812
|
+
except Exception: # pragma: no cover - defensive
|
|
813
|
+
return None
|
|
814
|
+
|
|
815
|
+
def _read_pointer(self, address: int) -> int | None:
|
|
816
|
+
"""Read one machine word from emulator memory."""
|
|
817
|
+
try:
|
|
818
|
+
raw = bytes(self.uc.mem_read(address, self.word_size))
|
|
819
|
+
except Exception:
|
|
820
|
+
return None
|
|
821
|
+
if len(raw) != self.word_size: # pragma: no cover - unicorn is exact
|
|
822
|
+
return None
|
|
823
|
+
return int.from_bytes(raw, self.byteorder)
|
|
824
|
+
|
|
825
|
+
@staticmethod
|
|
826
|
+
def _is_indirect(insn: Any) -> bool:
|
|
827
|
+
"""True when a branch target is a register or memory operand."""
|
|
828
|
+
operands = insn.operands
|
|
829
|
+
if not operands:
|
|
830
|
+
return False
|
|
831
|
+
return operands[0].type in (capstone.x86.X86_OP_REG, capstone.x86.X86_OP_MEM)
|
|
832
|
+
|
|
833
|
+
def _indirect_target(self, insn: Any) -> int | None:
|
|
834
|
+
"""Evaluate the target of a register/memory indirect branch."""
|
|
835
|
+
operands = insn.operands
|
|
836
|
+
if not operands:
|
|
837
|
+
return None
|
|
838
|
+
operand = operands[0]
|
|
839
|
+
if operand.type == capstone.x86.X86_OP_REG:
|
|
840
|
+
return self._read_register(insn.reg_name(operand.reg).lower())
|
|
841
|
+
if operand.type != capstone.x86.X86_OP_MEM:
|
|
842
|
+
return None
|
|
843
|
+
address = self._memory_address(insn, operand.mem)
|
|
844
|
+
if address is None:
|
|
845
|
+
return None
|
|
846
|
+
return self._read_pointer(address)
|
|
847
|
+
|
|
848
|
+
def _memory_address(self, insn: Any, mem: Any) -> int | None:
|
|
849
|
+
"""Compute the effective address of ``[base + index*scale + disp]``."""
|
|
850
|
+
if mem.index != 0 and mem.scale not in (1, 2, 4, 8):
|
|
851
|
+
return None
|
|
852
|
+
address = int(mem.disp)
|
|
853
|
+
if mem.base != 0:
|
|
854
|
+
name = insn.reg_name(mem.base).lower()
|
|
855
|
+
if name in ("rip", "eip"): # x86: relative to the *next* instruction
|
|
856
|
+
address += insn.address + insn.size
|
|
857
|
+
else:
|
|
858
|
+
base = self._read_register(name)
|
|
859
|
+
if base is None:
|
|
860
|
+
return None
|
|
861
|
+
address += base
|
|
862
|
+
if mem.index != 0:
|
|
863
|
+
index = self._read_register(insn.reg_name(mem.index).lower())
|
|
864
|
+
if index is None:
|
|
865
|
+
return None
|
|
866
|
+
address += index * int(mem.scale or 1)
|
|
867
|
+
if address < 0 or address >= (1 << self.bits):
|
|
868
|
+
return None
|
|
869
|
+
return address
|
|
870
|
+
|
|
871
|
+
def _ret_leaves_region(self, insn: Any, address: int) -> bool:
|
|
872
|
+
"""True when the ``ret`` at ``address`` returns outside the current section."""
|
|
873
|
+
sp_name = "rsp" if self.mode64 else "esp"
|
|
874
|
+
sp = self._read_register(sp_name)
|
|
875
|
+
if sp is None: # pragma: no cover - the register always exists here
|
|
876
|
+
return False
|
|
877
|
+
target = self._read_pointer(sp)
|
|
878
|
+
low, high = self.region
|
|
879
|
+
if target is None:
|
|
880
|
+
self.warnings.append(
|
|
881
|
+
f"return at 0x{address:x} ({insn.mnemonic} {insn.op_str}) could not be "
|
|
882
|
+
f"resolved: the return address at {sp_name}=0x{sp:x} is not readable"
|
|
883
|
+
)
|
|
884
|
+
return False
|
|
885
|
+
if low <= target < high:
|
|
886
|
+
return False # ordinary intra-region return: keep emulating
|
|
887
|
+
return True
|
|
888
|
+
|
|
889
|
+
# ---- run and report ---------------------------------------------------
|
|
890
|
+
|
|
891
|
+
def run(self) -> None:
|
|
892
|
+
"""Emulate until a stop reason is recorded; never re-raises ``UcError``."""
|
|
893
|
+
try:
|
|
894
|
+
# ``until=0`` means "run until something stops us"; the instruction
|
|
895
|
+
# budget is enforced by the code hook, not by unicorn's count, so the
|
|
896
|
+
# reported instruction count and the stop reason can never disagree.
|
|
897
|
+
#
|
|
898
|
+
# Unicorn resolves guest faults (``UC_ERR_READ_UNMAPPED`` and friends)
|
|
899
|
+
# with its own signal/SEH handler. CPython's faulthandler prints a
|
|
900
|
+
# "Windows fatal exception: access violation" line before Unicorn's
|
|
901
|
+
# handler runs, which looks like a crash in the test log even though the
|
|
902
|
+
# emulation is fine. Suppress it for the duration of the run and restore
|
|
903
|
+
# whatever state the caller had.
|
|
904
|
+
was_enabled = _faulthandler.is_enabled()
|
|
905
|
+
if was_enabled:
|
|
906
|
+
_faulthandler.disable()
|
|
907
|
+
try:
|
|
908
|
+
self.uc.emu_start(self.address, 0, 0, 0)
|
|
909
|
+
finally:
|
|
910
|
+
if was_enabled:
|
|
911
|
+
_faulthandler.enable()
|
|
912
|
+
except _unicorn.UcError as exc:
|
|
913
|
+
self._classify_error(exc)
|
|
914
|
+
if self.reason is None: # pragma: no cover - defensive
|
|
915
|
+
self.reason = _STOP_ERROR
|
|
916
|
+
self.warnings.append(
|
|
917
|
+
"emulation stopped without reaching a return, the budget, or a fault"
|
|
918
|
+
)
|
|
919
|
+
if self.reason == _STOP_BUDGET:
|
|
920
|
+
self.warnings.append(
|
|
921
|
+
f"instruction budget exhausted after {self.max_instructions} "
|
|
922
|
+
"instruction(s); the emulation result is truncated"
|
|
923
|
+
)
|
|
924
|
+
if self.dropped_writes:
|
|
925
|
+
self.warnings.append(
|
|
926
|
+
f"{self.dropped_writes} memory write(s) beyond the first "
|
|
927
|
+
f"{MAX_MEMORY_WRITES} were not recorded"
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
def _classify_error(self, exc: Any) -> None:
|
|
931
|
+
"""Turn one unicorn exception into a stop reason plus an exact warning."""
|
|
932
|
+
uni = _unicorn
|
|
933
|
+
errno = getattr(exc, "errno", None)
|
|
934
|
+
pc = self._read_register("rip" if self.mode64 else "eip")
|
|
935
|
+
at = f" at 0x{pc:x}" if pc is not None else ""
|
|
936
|
+
unmapped = {
|
|
937
|
+
getattr(uni, "UC_ERR_READ_UNMAPPED", -6),
|
|
938
|
+
getattr(uni, "UC_ERR_WRITE_UNMAPPED", -7),
|
|
939
|
+
getattr(uni, "UC_ERR_FETCH_UNMAPPED", -8),
|
|
940
|
+
}
|
|
941
|
+
protection = {
|
|
942
|
+
getattr(uni, "UC_ERR_READ_PROT", -9),
|
|
943
|
+
getattr(uni, "UC_ERR_WRITE_PROT", -10),
|
|
944
|
+
getattr(uni, "UC_ERR_FETCH_PROT", -11),
|
|
945
|
+
getattr(uni, "UC_ERR_READ_UNALIGNED", -12),
|
|
946
|
+
getattr(uni, "UC_ERR_WRITE_UNALIGNED", -13),
|
|
947
|
+
getattr(uni, "UC_ERR_FETCH_UNALIGNED", -14),
|
|
948
|
+
}
|
|
949
|
+
if errno in unmapped:
|
|
950
|
+
self.reason = _STOP_UNMAPPED
|
|
951
|
+
self.warnings.append(
|
|
952
|
+
f"invalid {self._fault_text()}{at}; emulation stopped because the "
|
|
953
|
+
"address is not mapped"
|
|
954
|
+
)
|
|
955
|
+
elif errno in protection:
|
|
956
|
+
self.reason = _STOP_UNMAPPED
|
|
957
|
+
self.warnings.append(
|
|
958
|
+
f"memory protection violation ({self._fault_text()}){at}: {exc}"
|
|
959
|
+
)
|
|
960
|
+
elif errno == getattr(uni, "UC_ERR_INSN_INVALID", -15):
|
|
961
|
+
self.reason = _STOP_UNSUPPORTED
|
|
962
|
+
self.warnings.append(
|
|
963
|
+
f"unicorn could not decode the instruction{at}: {exc}; the emulation "
|
|
964
|
+
"result stops there"
|
|
965
|
+
)
|
|
966
|
+
else:
|
|
967
|
+
self.reason = _STOP_ERROR
|
|
968
|
+
self.warnings.append(f"emulation failed{at}: {exc}")
|
|
969
|
+
|
|
970
|
+
def _fault_text(self) -> str:
|
|
971
|
+
"""Describe the recorded unmapped fault (exact address and width)."""
|
|
972
|
+
if self.fault is None:
|
|
973
|
+
return "memory access"
|
|
974
|
+
kind, address, size = self.fault
|
|
975
|
+
return f"{kind} access to unmapped address 0x{address:x} ({size} byte(s))"
|
|
976
|
+
|
|
977
|
+
def _read_registers(self) -> dict[str, int]:
|
|
978
|
+
"""Snapshot the general-purpose registers that this mode exposes."""
|
|
979
|
+
names = _REGISTERS_64 if self.mode64 else _REGISTERS_32
|
|
980
|
+
snapshot: dict[str, int] = {}
|
|
981
|
+
for name in names:
|
|
982
|
+
value = self._read_register(name)
|
|
983
|
+
if value is not None:
|
|
984
|
+
snapshot[name] = value
|
|
985
|
+
return snapshot
|
|
986
|
+
|
|
987
|
+
def result(self) -> EmulationResult:
|
|
988
|
+
"""Build the immutable result for this run."""
|
|
989
|
+
reason: str = self.reason or _STOP_ERROR
|
|
990
|
+
stopped_reason = reason if reason in _PUBLIC_REASONS else _STOP_ERROR
|
|
991
|
+
registers = self._read_registers()
|
|
992
|
+
return_value = None
|
|
993
|
+
if stopped_reason == _STOP_RETURN:
|
|
994
|
+
return_value = registers.get("rax" if self.mode64 else "eax")
|
|
995
|
+
return EmulationResult(
|
|
996
|
+
address=self.address,
|
|
997
|
+
stopped_reason=stopped_reason,
|
|
998
|
+
instructions=self.completed,
|
|
999
|
+
return_value=return_value,
|
|
1000
|
+
registers=registers,
|
|
1001
|
+
memory_writes=tuple(self.writes),
|
|
1002
|
+
indirect_targets=tuple(self.indirect),
|
|
1003
|
+
warnings=tuple(self.warnings),
|
|
1004
|
+
evidence=self._evidence(stopped_reason, registers),
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
def _evidence(self, reason: str, registers: Mapping[str, int]) -> Evidence:
|
|
1008
|
+
"""Describe how this result was produced, including every mapped region."""
|
|
1009
|
+
return Evidence(
|
|
1010
|
+
source=_EVIDENCE_SOURCE,
|
|
1011
|
+
method=_EVIDENCE_METHOD,
|
|
1012
|
+
confidence=Confidence.MEDIUM,
|
|
1013
|
+
address=self.address,
|
|
1014
|
+
detail=(
|
|
1015
|
+
f"{self.completed} instruction(s) emulated on a private "
|
|
1016
|
+
f"{self.bits}-bit unicorn CPU; stopped_reason={reason}"
|
|
1017
|
+
),
|
|
1018
|
+
extra={
|
|
1019
|
+
"stopped_reason": reason,
|
|
1020
|
+
"instructions": self.completed,
|
|
1021
|
+
"max_instructions": self.max_instructions,
|
|
1022
|
+
"instruction_pointer": (
|
|
1023
|
+
f"0x{registers['rip']:x}" if "rip" in registers else (
|
|
1024
|
+
f"0x{registers['eip']:x}" if "eip" in registers else None
|
|
1025
|
+
)
|
|
1026
|
+
),
|
|
1027
|
+
"stack_base": f"0x{self.stack_base:x}",
|
|
1028
|
+
"stack_size": self.stack_top - self.stack_base,
|
|
1029
|
+
"architecture": self.info.architecture,
|
|
1030
|
+
"bitness": self.bits,
|
|
1031
|
+
"regions": [
|
|
1032
|
+
{"kind": kind, "start": f"0x{start:x}", "end": f"0x{end:x}"}
|
|
1033
|
+
for kind, start, end in self.regions
|
|
1034
|
+
],
|
|
1035
|
+
"recorded_memory_writes": len(self.writes),
|
|
1036
|
+
"dropped_memory_writes": self.dropped_writes,
|
|
1037
|
+
"indirect_targets": len(self.indirect),
|
|
1038
|
+
},
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
def close(self) -> None:
|
|
1042
|
+
"""Drop the emulator so unicorn can free its native handle.
|
|
1043
|
+
|
|
1044
|
+
``Uc`` registers a ``weakref.finalize`` that closes the native engine
|
|
1045
|
+
handle when the instance is collected, so dropping the last reference is
|
|
1046
|
+
all this has to do; calling ``Uc.release_handle`` here would risk closing
|
|
1047
|
+
the same handle twice, because the finalizer still refers to it.
|
|
1048
|
+
"""
|
|
1049
|
+
self.uc = None
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
# ---- public API -----------------------------------------------------------
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _start_runner(
|
|
1056
|
+
path: str | Path,
|
|
1057
|
+
info: BinaryInfo,
|
|
1058
|
+
address: int,
|
|
1059
|
+
initial_registers: Mapping[str, int] | None,
|
|
1060
|
+
initial_memory: Mapping[int, bytes] | None,
|
|
1061
|
+
max_instructions: int,
|
|
1062
|
+
stack_size: int,
|
|
1063
|
+
*,
|
|
1064
|
+
stop_at_indirect: bool,
|
|
1065
|
+
) -> _Runner | None:
|
|
1066
|
+
"""Validate the request and return a ready-to-run session.
|
|
1067
|
+
|
|
1068
|
+
Returns:
|
|
1069
|
+
A configured :class:`_Runner`, or ``None`` when this binary or this
|
|
1070
|
+
machine cannot be emulated (reported as ``"unsupported"`` by callers).
|
|
1071
|
+
|
|
1072
|
+
Raises:
|
|
1073
|
+
AnalysisError: When the request itself is invalid (bad address, missing
|
|
1074
|
+
file bytes, out-of-range budget/stack size, bad register or memory
|
|
1075
|
+
seed).
|
|
1076
|
+
"""
|
|
1077
|
+
start = _validated_address(address)
|
|
1078
|
+
budget = _validated_budget(max_instructions)
|
|
1079
|
+
stack = _validated_stack_size(stack_size)
|
|
1080
|
+
bits = _machine_bits(info)
|
|
1081
|
+
if bits is None or not EMULATOR_AVAILABLE:
|
|
1082
|
+
return None
|
|
1083
|
+
seeds = _validated_registers(initial_registers, bits)
|
|
1084
|
+
memory = _validated_memory(initial_memory)
|
|
1085
|
+
section = info.section_for_va(start)
|
|
1086
|
+
if section is None:
|
|
1087
|
+
raise AnalysisError(f"address 0x{start:x} is not mapped by any section")
|
|
1088
|
+
if not section.is_executable:
|
|
1089
|
+
raise AnalysisError(
|
|
1090
|
+
f"address 0x{start:x} is inside section {section.name!r}, which is not executable"
|
|
1091
|
+
)
|
|
1092
|
+
if info.va_to_offset(start) is None:
|
|
1093
|
+
raise AnalysisError(f"address 0x{start:x} has no file bytes to execute")
|
|
1094
|
+
runner = _Runner(
|
|
1095
|
+
path,
|
|
1096
|
+
info,
|
|
1097
|
+
start,
|
|
1098
|
+
bits=bits,
|
|
1099
|
+
max_instructions=budget,
|
|
1100
|
+
stack_size=stack,
|
|
1101
|
+
stop_at_indirect=stop_at_indirect,
|
|
1102
|
+
)
|
|
1103
|
+
try:
|
|
1104
|
+
runner.setup(seeds, memory)
|
|
1105
|
+
except _unicorn.UcError as exc:
|
|
1106
|
+
runner.warnings.append(f"the emulator could not be prepared: {exc}")
|
|
1107
|
+
runner.reason = _STOP_ERROR
|
|
1108
|
+
return runner
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _unsupported_result(
|
|
1112
|
+
address: int,
|
|
1113
|
+
info: BinaryInfo,
|
|
1114
|
+
max_instructions: int,
|
|
1115
|
+
detail: str,
|
|
1116
|
+
) -> EmulationResult:
|
|
1117
|
+
"""Build the ``"unsupported"`` result used when no emulation is possible."""
|
|
1118
|
+
if EMULATOR_AVAILABLE:
|
|
1119
|
+
support = "emulation supports 32-bit and 64-bit x86 only"
|
|
1120
|
+
else:
|
|
1121
|
+
support = emulator_status() or "the emulator is unavailable"
|
|
1122
|
+
warning = f"{detail}; {support}, so no code was run"
|
|
1123
|
+
return EmulationResult(
|
|
1124
|
+
address=address,
|
|
1125
|
+
stopped_reason=_STOP_UNSUPPORTED,
|
|
1126
|
+
instructions=0,
|
|
1127
|
+
return_value=None,
|
|
1128
|
+
registers={},
|
|
1129
|
+
memory_writes=(),
|
|
1130
|
+
indirect_targets=(),
|
|
1131
|
+
warnings=(warning,),
|
|
1132
|
+
evidence=Evidence(
|
|
1133
|
+
source=_EVIDENCE_SOURCE,
|
|
1134
|
+
method=_EVIDENCE_METHOD,
|
|
1135
|
+
confidence=Confidence.UNKNOWN,
|
|
1136
|
+
address=address,
|
|
1137
|
+
detail=detail,
|
|
1138
|
+
extra={
|
|
1139
|
+
"stopped_reason": _STOP_UNSUPPORTED,
|
|
1140
|
+
"instructions": 0,
|
|
1141
|
+
"max_instructions": max_instructions,
|
|
1142
|
+
"architecture": info.architecture,
|
|
1143
|
+
"bitness": info.bitness,
|
|
1144
|
+
"emulator_status": emulator_status(),
|
|
1145
|
+
},
|
|
1146
|
+
),
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def emulate_function(
|
|
1151
|
+
path: str | Path,
|
|
1152
|
+
info: BinaryInfo,
|
|
1153
|
+
address: int,
|
|
1154
|
+
initial_registers: Mapping[str, int] | None = None,
|
|
1155
|
+
initial_memory: Mapping[int, bytes] | None = None,
|
|
1156
|
+
max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
|
|
1157
|
+
stack_size: int = DEFAULT_STACK_SIZE,
|
|
1158
|
+
) -> EmulationResult:
|
|
1159
|
+
"""Emulate the function at ``address`` for the given initial state.
|
|
1160
|
+
|
|
1161
|
+
The image sections are mapped at their virtual addresses, a stack region is
|
|
1162
|
+
mapped at :data:`STACK_BASE_64`/:data:`STACK_BASE_32`, and registers start at
|
|
1163
|
+
the ABI defaults (``rip`` at ``address``, ``rsp``/``rbp`` near the top of the
|
|
1164
|
+
stack) plus ``initial_registers``. Emulation then runs with a private unicorn
|
|
1165
|
+
CPU until one of the :data:`StopReason` conditions is met.
|
|
1166
|
+
|
|
1167
|
+
Args:
|
|
1168
|
+
path: Binary the metadata came from (only its bytes are read).
|
|
1169
|
+
info: Loaded :class:`~codevanta.core.loader.BinaryInfo`.
|
|
1170
|
+
address: Virtual address to start at; must be inside an executable
|
|
1171
|
+
section that has file bytes.
|
|
1172
|
+
initial_registers: Extra register seeds (``{"rdi": 2}``). Names use
|
|
1173
|
+
unicorn's x86 spelling for the binary's bitness; unknown names or
|
|
1174
|
+
values that do not fit the machine word raise
|
|
1175
|
+
:class:`~codevanta.core.report.AnalysisError`.
|
|
1176
|
+
initial_memory: ``{address: bytes}`` blocks to place in emulator memory
|
|
1177
|
+
before the run (at most :data:`MAX_INITIAL_BLOCKS` blocks of at most
|
|
1178
|
+
:data:`MAX_INITIAL_BLOCK_BYTES` bytes each). Blocks that overlap the
|
|
1179
|
+
image are written over it.
|
|
1180
|
+
max_instructions: Instruction budget, at most
|
|
1181
|
+
:data:`MAX_MAX_INSTRUCTIONS`.
|
|
1182
|
+
stack_size: Size of the mapped stack region, at most
|
|
1183
|
+
:data:`MAX_STACK_SIZE`; rounded up to a page.
|
|
1184
|
+
|
|
1185
|
+
Returns:
|
|
1186
|
+
An :class:`EmulationResult`. Limitations are never raised: they are
|
|
1187
|
+
reported through ``stopped_reason`` and ``warnings``.
|
|
1188
|
+
|
|
1189
|
+
Raises:
|
|
1190
|
+
AnalysisError: Only for an invalid request (see :func:`_start_runner`).
|
|
1191
|
+
"""
|
|
1192
|
+
start = _validated_address(address)
|
|
1193
|
+
budget = _validated_budget(max_instructions)
|
|
1194
|
+
runner = _start_runner(
|
|
1195
|
+
path,
|
|
1196
|
+
info,
|
|
1197
|
+
start,
|
|
1198
|
+
initial_registers,
|
|
1199
|
+
initial_memory,
|
|
1200
|
+
budget,
|
|
1201
|
+
stack_size,
|
|
1202
|
+
stop_at_indirect=False,
|
|
1203
|
+
)
|
|
1204
|
+
if runner is None:
|
|
1205
|
+
if not EMULATOR_AVAILABLE:
|
|
1206
|
+
detail = "the emulator is unavailable"
|
|
1207
|
+
else:
|
|
1208
|
+
detail = f"cannot emulate {info.architecture or 'unknown'} ({info.bitness}-bit)"
|
|
1209
|
+
return _unsupported_result(start, info, budget, detail)
|
|
1210
|
+
try:
|
|
1211
|
+
runner.run()
|
|
1212
|
+
return runner.result()
|
|
1213
|
+
finally:
|
|
1214
|
+
runner.close()
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def resolve_indirect_target(
|
|
1218
|
+
path: str | Path,
|
|
1219
|
+
info: BinaryInfo,
|
|
1220
|
+
address: int,
|
|
1221
|
+
initial_registers: Mapping[str, int] | None = None,
|
|
1222
|
+
max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
|
|
1223
|
+
*,
|
|
1224
|
+
initial_memory: Mapping[int, bytes] | None = None,
|
|
1225
|
+
) -> int | None:
|
|
1226
|
+
"""Emulate until the first indirect jump/call and return its target.
|
|
1227
|
+
|
|
1228
|
+
This is the static-analysis bridge: a ``jmp rax`` or ``call qword ptr
|
|
1229
|
+
[rip+0x10]`` cannot be resolved by decoding alone, but seeding the registers
|
|
1230
|
+
it depends on and running up to it can answer it in a few instructions.
|
|
1231
|
+
|
|
1232
|
+
Args:
|
|
1233
|
+
path: Binary the metadata came from.
|
|
1234
|
+
info: Loaded :class:`~codevanta.core.loader.BinaryInfo`.
|
|
1235
|
+
address: Virtual address to start at.
|
|
1236
|
+
initial_registers: Register seeds (see :func:`emulate_function`).
|
|
1237
|
+
max_instructions: Instruction budget, at most
|
|
1238
|
+
:data:`MAX_MAX_INSTRUCTIONS`.
|
|
1239
|
+
initial_memory: Memory blocks to place before the run.
|
|
1240
|
+
|
|
1241
|
+
Returns:
|
|
1242
|
+
The target of the first indirect branch reached, or ``None`` when no
|
|
1243
|
+
indirect branch was reached before the run stopped (budget, fault,
|
|
1244
|
+
return), when the operand could not be evaluated, or when the binary is
|
|
1245
|
+
not emulatable. ``None`` therefore means "not resolved", never "no
|
|
1246
|
+
target".
|
|
1247
|
+
|
|
1248
|
+
Raises:
|
|
1249
|
+
AnalysisError: Only for an invalid request.
|
|
1250
|
+
"""
|
|
1251
|
+
start = _validated_address(address)
|
|
1252
|
+
budget = _validated_budget(max_instructions)
|
|
1253
|
+
runner = _start_runner(
|
|
1254
|
+
path,
|
|
1255
|
+
info,
|
|
1256
|
+
start,
|
|
1257
|
+
initial_registers,
|
|
1258
|
+
initial_memory,
|
|
1259
|
+
budget,
|
|
1260
|
+
DEFAULT_STACK_SIZE,
|
|
1261
|
+
stop_at_indirect=True,
|
|
1262
|
+
)
|
|
1263
|
+
if runner is None:
|
|
1264
|
+
return None
|
|
1265
|
+
if runner.reason == _STOP_ERROR:
|
|
1266
|
+
runner.close()
|
|
1267
|
+
return None
|
|
1268
|
+
try:
|
|
1269
|
+
runner.run()
|
|
1270
|
+
return runner.pending_indirect
|
|
1271
|
+
finally:
|
|
1272
|
+
runner.close()
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
# ---- string-decoding primitives -------------------------------------------
|
|
1276
|
+
|
|
1277
|
+
#: Operations understood by :func:`decrypt_string_loop`, named after what the
|
|
1278
|
+
#: *encoder* did to each byte.
|
|
1279
|
+
DECRYPT_OPS = ("xor", "add", "sub")
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def xor_bytes(data: bytes, key: int) -> bytes:
|
|
1283
|
+
"""XOR every byte of ``data`` with the low 8 bits of ``key``.
|
|
1284
|
+
|
|
1285
|
+
The primitive behind the classic ``xor byte ptr [reg], imm8`` loop. It is a
|
|
1286
|
+
pure function over bytes -- nothing is emulated or executed -- so it can
|
|
1287
|
+
document a decoded blob once the key has been recovered by
|
|
1288
|
+
:func:`emulate_function` (read the key from a register or ``memory_writes``).
|
|
1289
|
+
|
|
1290
|
+
Args:
|
|
1291
|
+
data: Ciphertext bytes.
|
|
1292
|
+
key: XOR key; only the low 8 bits take part in the operation.
|
|
1293
|
+
|
|
1294
|
+
Returns:
|
|
1295
|
+
``data`` with each byte XORed against ``key``. Applying it twice with the
|
|
1296
|
+
same key returns the original bytes.
|
|
1297
|
+
"""
|
|
1298
|
+
mask = int(key) & 0xFF
|
|
1299
|
+
return bytes(byte ^ mask for byte in bytes(data))
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def decrypt_string_loop(
|
|
1303
|
+
data: bytes,
|
|
1304
|
+
key: int,
|
|
1305
|
+
*,
|
|
1306
|
+
key_step: int = 0,
|
|
1307
|
+
op: str = "xor",
|
|
1308
|
+
terminators: Sequence[int] = (0,),
|
|
1309
|
+
) -> bytes:
|
|
1310
|
+
"""Decode a string blob the way an inline decode loop would, without executing it.
|
|
1311
|
+
|
|
1312
|
+
Models the loops compilers and packers emit over a byte buffer: each byte is
|
|
1313
|
+
transformed with the key (optionally advanced per byte) and decoding stops at
|
|
1314
|
+
the first terminator, C-string style.
|
|
1315
|
+
|
|
1316
|
+
Args:
|
|
1317
|
+
data: Ciphertext bytes, already read by the caller (``read_code`` or an
|
|
1318
|
+
``initial_memory`` block).
|
|
1319
|
+
key: Initial key. Only the low 8 bits of each per-byte key are used.
|
|
1320
|
+
key_step: Amount added to the key after every byte (``1`` for the
|
|
1321
|
+
"increment the key each iteration" variant, ``0`` for a fixed key).
|
|
1322
|
+
op: The operation the *encoder* applied: ``"xor"`` (``c = p ^ k``),
|
|
1323
|
+
``"add"`` (``c = p + k``) or ``"sub"`` (``c = p - k``). Decoding
|
|
1324
|
+
inverts it.
|
|
1325
|
+
terminators: Byte values that end the string; the terminator itself is not
|
|
1326
|
+
part of the result. Pass ``()`` to decode the whole blob.
|
|
1327
|
+
|
|
1328
|
+
Returns:
|
|
1329
|
+
The decoded bytes, up to (not including) the first terminator.
|
|
1330
|
+
|
|
1331
|
+
Raises:
|
|
1332
|
+
AnalysisError: When ``op`` is not one of :data:`DECRYPT_OPS`.
|
|
1333
|
+
"""
|
|
1334
|
+
if op not in DECRYPT_OPS:
|
|
1335
|
+
raise AnalysisError(f"op must be one of {DECRYPT_OPS}, not {op!r}")
|
|
1336
|
+
stops = frozenset(int(value) & 0xFF for value in terminators)
|
|
1337
|
+
decoded = bytearray()
|
|
1338
|
+
for index, byte in enumerate(bytes(data)):
|
|
1339
|
+
current = (int(key) + index * int(key_step)) & 0xFF
|
|
1340
|
+
if op == "xor":
|
|
1341
|
+
value = byte ^ current
|
|
1342
|
+
elif op == "add":
|
|
1343
|
+
value = (byte - current) & 0xFF
|
|
1344
|
+
else:
|
|
1345
|
+
value = (byte + current) & 0xFF
|
|
1346
|
+
if value in stops:
|
|
1347
|
+
break
|
|
1348
|
+
decoded.append(value)
|
|
1349
|
+
return bytes(decoded)
|