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,1809 @@
|
|
|
1
|
+
"""Path exploration for one function, without installing a solver.
|
|
2
|
+
|
|
3
|
+
Two questions motivate this module: *which distinct routes through this function
|
|
4
|
+
exist*, and *what does each route return*. It answers them by **enumerating
|
|
5
|
+
branch decisions**, replaying each decision sequence on a private unicorn CPU
|
|
6
|
+
(the same bounded, offline emulator :mod:`codevanta.analysis.emulate` uses), and
|
|
7
|
+
reporting one :class:`Entry` per path. Nothing is executed on the host, no
|
|
8
|
+
symbolic variable is created, and no constraint is ever solved -- angr/Z3 are
|
|
9
|
+
*optional* here and only used by :func:`explore_with_angr`.
|
|
10
|
+
|
|
11
|
+
Path semantics
|
|
12
|
+
--------------
|
|
13
|
+
A path is a tuple of decisions, one per *conditional* branch met on the way.
|
|
14
|
+
Exploration is breadth first over that decision tree:
|
|
15
|
+
|
|
16
|
+
1. Replay the decision tuple from the entry point on a freshly initialised CPU.
|
|
17
|
+
2. At the ``k``-th conditional branch, if a decision is recorded use it, else
|
|
18
|
+
follow the **observed** direction -- the one the real flags produced -- and
|
|
19
|
+
enqueue ``walked + (not observed,)`` as a child.
|
|
20
|
+
3. A ``ret`` whose popped address lies outside the explored section completes
|
|
21
|
+
the path (``stopped_reason="return"``, ``return_value`` = ``rax``/``eax`` at
|
|
22
|
+
that ``ret``). An intra-section ``ret`` -- the return of a callee this path
|
|
23
|
+
called -- is executed and exploration continues, exactly as in ``emulate``.
|
|
24
|
+
4. Paths stop for one of four reasons: ``return``, ``budget`` (the shared
|
|
25
|
+
``max_steps`` instruction budget ran out), ``blocked`` (this module cannot
|
|
26
|
+
justify a successor) or ``unmapped`` (the emulated code touched an address
|
|
27
|
+
that is not mapped).
|
|
28
|
+
|
|
29
|
+
Explored versus assumed
|
|
30
|
+
-----------------------
|
|
31
|
+
That distinction is the whole point of the module, so every branch carries a
|
|
32
|
+
:data:`Basis`:
|
|
33
|
+
|
|
34
|
+
``observed``
|
|
35
|
+
The direction was produced by real emulation: the flags (or the counter
|
|
36
|
+
register) were computed by the instructions that ran before the branch, and
|
|
37
|
+
the branch instruction itself executed on the CPU. Nothing was forced.
|
|
38
|
+
``seeded``
|
|
39
|
+
The *opposite* direction of a conditional branch. The flags are rewritten to
|
|
40
|
+
the nearest assignment that satisfies that condition (``rflags`` bits are
|
|
41
|
+
searched exhaustively -- 64 combinations -- and the minimum Hamming distance
|
|
42
|
+
from the observed value is chosen), or -- for ``jrcxz``/``jecxz``/``jcxz`` --
|
|
43
|
+
the counter register is written. The branch instruction then executes, and
|
|
44
|
+
the explorer checks that it really landed on the intended successor. **This
|
|
45
|
+
is an assumption about feasibility, not a solved constraint**: no path
|
|
46
|
+
predicate is built, no model is checked, and a ``seeded`` side may well be
|
|
47
|
+
unreachable for the real program.
|
|
48
|
+
|
|
49
|
+
Unconditional ``jmp``/``call`` transfers carry no decision, so they are neither
|
|
50
|
+
``observed`` nor ``seeded``: they are *executed*, and the address the CPU landed
|
|
51
|
+
on is recorded in :attr:`Entry.taken_targets`. An indirect transfer contributes
|
|
52
|
+
only the target its operand held at that point -- other targets it could hold
|
|
53
|
+
are not explored, and the entry says so in a warning.
|
|
54
|
+
|
|
55
|
+
A branch this module cannot decide is **never** guessed: ``loop``/``loope``/
|
|
56
|
+
``loopne`` (they decrement ``rcx`` before testing it), an unknown ``j*``
|
|
57
|
+
mnemonic, an indirect transfer leaving the section, an undecodable instruction,
|
|
58
|
+
or a seed that failed to steer the branch all end the path with
|
|
59
|
+
``stopped_reason="blocked"`` and a note saying exactly what was not resolved.
|
|
60
|
+
If a seed fails to steer the branch, the path is discarded rather than reported
|
|
61
|
+
as if it had reached the intended successor.
|
|
62
|
+
|
|
63
|
+
What is *not* explored: memory state is shared within one replay only, and every
|
|
64
|
+
path starts from the same ABI-seeded state (``initial_registers`` plus the
|
|
65
|
+
``rsp``/``rbp`` defaults from ``emulate``), so a path is not a proof of
|
|
66
|
+
reachability for arbitrary inputs -- it is the set of routes this explorer could
|
|
67
|
+
walk with the given seeds, with the ``seeded`` sides flagged. Two paths that
|
|
68
|
+
rejoin are reported separately: exploration is path-sensitive over decisions,
|
|
69
|
+
not a state merge.
|
|
70
|
+
|
|
71
|
+
Cost and caps
|
|
72
|
+
-------------
|
|
73
|
+
``max_paths`` (default :data:`DEFAULT_MAX_PATHS`) caps emitted paths and
|
|
74
|
+
``max_steps`` (default :data:`DEFAULT_MAX_STEPS`) caps emulator instructions
|
|
75
|
+
*including replays*, so the work is bounded no matter how branchy the function
|
|
76
|
+
is. Because each path is replayed from the entry point, real cost is
|
|
77
|
+
``O(paths x path length)``; the emulator instance and its mappings are created
|
|
78
|
+
once and re-initialised between replays.
|
|
79
|
+
|
|
80
|
+
Supported machines
|
|
81
|
+
------------------
|
|
82
|
+
x86 and x86-64 only, like ``emulate``: the return-value, stack and register
|
|
83
|
+
vocabulary here is x86-specific. Anything else raises
|
|
84
|
+
:class:`~codevanta.core.report.AnalysisError` instead of guessing, as does a
|
|
85
|
+
missing unicorn build, a bad address, or an out-of-range limit.
|
|
86
|
+
|
|
87
|
+
The angr bridge
|
|
88
|
+
---------------
|
|
89
|
+
:func:`explore_with_angr` is **optional** and never imported at module import
|
|
90
|
+
time: :data:`ANGR_AVAILABLE` only records whether a module called ``angr`` is
|
|
91
|
+
findable, and the import happens inside the call. Without angr the function
|
|
92
|
+
raises :class:`~codevanta.core.report.AnalysisError` naming angr and the extra
|
|
93
|
+
that installs it. When angr *is* present the paths come from angr's symbolic
|
|
94
|
+
execution, so the evidence says so and no ``observed``/``seeded`` basis is
|
|
95
|
+
claimed for them -- that distinction belongs to the unicorn explorer.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
from __future__ import annotations
|
|
100
|
+
|
|
101
|
+
import faulthandler as _faulthandler
|
|
102
|
+
import importlib.util as _importlib_util
|
|
103
|
+
from collections import deque
|
|
104
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
105
|
+
from contextlib import contextmanager
|
|
106
|
+
from dataclasses import dataclass
|
|
107
|
+
from pathlib import Path
|
|
108
|
+
from typing import Any, Literal
|
|
109
|
+
|
|
110
|
+
import capstone
|
|
111
|
+
|
|
112
|
+
from codevanta.analysis import emulate
|
|
113
|
+
from codevanta.core.loader import BinaryInfo
|
|
114
|
+
from codevanta.core.report import AnalysisError, read_code
|
|
115
|
+
from codevanta.evidence.confidence import Confidence
|
|
116
|
+
from codevanta.evidence.models import Evidence
|
|
117
|
+
|
|
118
|
+
__all__ = [
|
|
119
|
+
"ANGR_AVAILABLE",
|
|
120
|
+
"DEFAULT_MAX_PATHS",
|
|
121
|
+
"DEFAULT_MAX_STEPS",
|
|
122
|
+
"EMULATOR_AVAILABLE",
|
|
123
|
+
"MAX_BLOCK_INSTRUCTIONS",
|
|
124
|
+
"MAX_MAX_PATHS",
|
|
125
|
+
"MAX_MAX_STEPS",
|
|
126
|
+
"Basis",
|
|
127
|
+
"Branch",
|
|
128
|
+
"Entry",
|
|
129
|
+
"PathExploration",
|
|
130
|
+
"StopReason",
|
|
131
|
+
"angr_available",
|
|
132
|
+
"explore_paths",
|
|
133
|
+
"explore_with_angr",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
_unicorn: Any = None
|
|
137
|
+
_x86_const: Any = None
|
|
138
|
+
|
|
139
|
+
try: # optional: the unicorn explorer needs it, the module must import without it
|
|
140
|
+
import unicorn as _unicorn
|
|
141
|
+
import unicorn.x86_const as _x86_const
|
|
142
|
+
except Exception: # pragma: no cover - depends on the environment
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---- limits ---------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
#: Paths reported when the caller does not pass ``max_paths``.
|
|
149
|
+
DEFAULT_MAX_PATHS = 64
|
|
150
|
+
|
|
151
|
+
#: Hard ceiling on ``max_paths`` (bounded work, whatever the caller asks).
|
|
152
|
+
MAX_MAX_PATHS = 4096
|
|
153
|
+
|
|
154
|
+
#: Instruction budget used when the caller does not pass ``max_steps``.
|
|
155
|
+
DEFAULT_MAX_STEPS = 2000
|
|
156
|
+
|
|
157
|
+
#: Hard ceiling on ``max_steps``.
|
|
158
|
+
MAX_MAX_STEPS = 200_000
|
|
159
|
+
|
|
160
|
+
#: A single basic block is scanned for at most this many instructions before the
|
|
161
|
+
#: explorer re-scans from the next address (a long straight-line tail still runs).
|
|
162
|
+
MAX_BLOCK_INSTRUCTIONS = 64
|
|
163
|
+
|
|
164
|
+
#: Instructions a linear scan reads when collecting a function's ``ret`` sites
|
|
165
|
+
#: for the angr bridge.
|
|
166
|
+
MAX_SCAN_INSTRUCTIONS = 4096
|
|
167
|
+
|
|
168
|
+
#: True when a unicorn build is importable; the unicorn explorer is refused
|
|
169
|
+
#: otherwise. Mirrors :data:`codevanta.analysis.emulate.EMULATOR_AVAILABLE`.
|
|
170
|
+
EMULATOR_AVAILABLE = emulate.EMULATOR_AVAILABLE
|
|
171
|
+
|
|
172
|
+
#: Evidence identity for the unicorn explorer.
|
|
173
|
+
_EVIDENCE_SOURCE = "symbolic.unicorn"
|
|
174
|
+
_EVIDENCE_METHOD = "branch_decision_replay"
|
|
175
|
+
|
|
176
|
+
#: Evidence identity for the angr bridge.
|
|
177
|
+
_ANGR_SOURCE = "symbolic.angr"
|
|
178
|
+
_ANGR_METHOD = "symbolic_execution"
|
|
179
|
+
|
|
180
|
+
#: What a caller needs to install angr.
|
|
181
|
+
_ANGR_HINT = "pip install 'codevanta[decompilers]'"
|
|
182
|
+
|
|
183
|
+
# ---- stop reasons and bases -----------------------------------------------
|
|
184
|
+
|
|
185
|
+
#: Why one path ended (see the module docstring).
|
|
186
|
+
StopReason = Literal["return", "budget", "blocked", "unmapped"]
|
|
187
|
+
|
|
188
|
+
#: How one conditional branch direction was justified (see the module docstring).
|
|
189
|
+
Basis = Literal["observed", "seeded"]
|
|
190
|
+
|
|
191
|
+
_STOP_RETURN: StopReason = "return"
|
|
192
|
+
_STOP_BUDGET: StopReason = "budget"
|
|
193
|
+
_STOP_BLOCKED: StopReason = "blocked"
|
|
194
|
+
_STOP_UNMAPPED: StopReason = "unmapped"
|
|
195
|
+
|
|
196
|
+
_BASIS_OBSERVED: Basis = "observed"
|
|
197
|
+
_BASIS_SEEDED: Basis = "seeded"
|
|
198
|
+
|
|
199
|
+
#: Instruction mnemonics that leave the explored 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
|
+
#: Registers snapshotted per bitness (the same vocabulary as ``emulate``).
|
|
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
|
+
#: Room left between the initial stack pointer and the top of the stack region,
|
|
219
|
+
#: so a prologue can push without leaving the mapping (mirrors ``emulate``).
|
|
220
|
+
_STACK_HEADROOM = 0x100
|
|
221
|
+
|
|
222
|
+
#: Bytes a linear scan may read per block (the widest x86 instruction is 15).
|
|
223
|
+
_MAX_INSTRUCTION_BYTES = 15
|
|
224
|
+
|
|
225
|
+
#: ``loop``/``loope``/``loopne`` decrement the counter before testing it, which
|
|
226
|
+
#: this explorer does not model; they end the path as ``blocked``.
|
|
227
|
+
_LOOP_MNEMONICS = frozenset({"loop", "loope", "loopz", "loopne", "loopnz"})
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---- results --------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True)
|
|
234
|
+
class Branch:
|
|
235
|
+
"""One conditional branch a path met, and how its direction was justified.
|
|
236
|
+
|
|
237
|
+
Attributes:
|
|
238
|
+
address: Address of the branch instruction.
|
|
239
|
+
mnemonic: Capstone mnemonic (``"jz"``, ``"jle"``, ``"jrcxz"``, ...).
|
|
240
|
+
taken: ``True`` when the path took the branch target, ``False`` when it
|
|
241
|
+
fell through.
|
|
242
|
+
target: Address the path actually continued at (the branch target, or
|
|
243
|
+
the fall-through address when ``taken`` is ``False``).
|
|
244
|
+
basis: ``"observed"`` when real emulation produced this direction,
|
|
245
|
+
``"seeded"`` when the flags/counter were rewritten to force it.
|
|
246
|
+
note: Exact description of the seed, empty for an observed direction.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
address: int
|
|
250
|
+
mnemonic: str
|
|
251
|
+
taken: bool
|
|
252
|
+
target: int
|
|
253
|
+
basis: Basis
|
|
254
|
+
note: str = ""
|
|
255
|
+
|
|
256
|
+
def to_dict(self) -> dict[str, Any]:
|
|
257
|
+
"""Serialize to a JSON-safe dict."""
|
|
258
|
+
return {
|
|
259
|
+
"address": f"0x{self.address:x}",
|
|
260
|
+
"mnemonic": self.mnemonic,
|
|
261
|
+
"taken": self.taken,
|
|
262
|
+
"target": f"0x{self.target:x}",
|
|
263
|
+
"basis": self.basis,
|
|
264
|
+
"note": self.note,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass(frozen=True)
|
|
269
|
+
class Entry:
|
|
270
|
+
"""One explored path: where it went, how it ended, what it left behind.
|
|
271
|
+
|
|
272
|
+
Attributes:
|
|
273
|
+
index: Position in :attr:`PathExploration.entries` (deterministic).
|
|
274
|
+
decisions: The conditional branches on this path, in the order met.
|
|
275
|
+
taken_targets: Addresses control transferred to, in order: taken
|
|
276
|
+
conditional targets and every unconditional ``jmp``/``call`` target.
|
|
277
|
+
registers: Register snapshot taken at the end of the path.
|
|
278
|
+
return_value: ``rax``/``eax`` when the path reached a ``ret`` that left
|
|
279
|
+
the section, otherwise ``None`` -- a value left in ``rax`` by a path
|
|
280
|
+
stopped for any other reason is not a return value.
|
|
281
|
+
stopped_reason: Why the path ended (see the module docstring).
|
|
282
|
+
instructions: Instructions this path executed, replayed instructions
|
|
283
|
+
included (the explorer's work unit, not a property of the code).
|
|
284
|
+
warnings: Notes explaining every degradation, including the seed used to
|
|
285
|
+
force a direction and anything that could not be resolved.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
index: int
|
|
289
|
+
decisions: tuple[Branch, ...]
|
|
290
|
+
taken_targets: tuple[int, ...]
|
|
291
|
+
registers: Mapping[str, int]
|
|
292
|
+
return_value: int | None
|
|
293
|
+
stopped_reason: StopReason
|
|
294
|
+
instructions: int
|
|
295
|
+
warnings: tuple[str, ...]
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def seeded(self) -> int:
|
|
299
|
+
"""Number of branches on this path whose direction was forced."""
|
|
300
|
+
return sum(1 for branch in self.decisions if branch.basis == _BASIS_SEEDED)
|
|
301
|
+
|
|
302
|
+
def to_dict(self) -> dict[str, Any]:
|
|
303
|
+
"""Serialize to a JSON-safe dict (addresses and values become hex)."""
|
|
304
|
+
return {
|
|
305
|
+
"index": self.index,
|
|
306
|
+
"decisions": [branch.to_dict() for branch in self.decisions],
|
|
307
|
+
"taken_targets": [f"0x{target:x}" for target in self.taken_targets],
|
|
308
|
+
"registers": {
|
|
309
|
+
name: f"0x{value:x}" for name, value in sorted(self.registers.items())
|
|
310
|
+
},
|
|
311
|
+
"return_value": (
|
|
312
|
+
f"0x{self.return_value:x}" if self.return_value is not None else None
|
|
313
|
+
),
|
|
314
|
+
"stopped_reason": self.stopped_reason,
|
|
315
|
+
"instructions": self.instructions,
|
|
316
|
+
"seeded_branches": self.seeded,
|
|
317
|
+
"warnings": list(self.warnings),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@dataclass(frozen=True)
|
|
322
|
+
class PathExploration:
|
|
323
|
+
"""Every path one exploration produced, plus what it did and did not prove.
|
|
324
|
+
|
|
325
|
+
Attributes:
|
|
326
|
+
address: Virtual address the exploration started at.
|
|
327
|
+
explorer: ``"unicorn"`` or ``"angr"`` -- which engine produced the paths.
|
|
328
|
+
architecture: ``BinaryInfo.architecture`` of the analysed binary.
|
|
329
|
+
bitness: 32 or 64.
|
|
330
|
+
max_paths: Path cap the caller asked for.
|
|
331
|
+
max_steps: Instruction budget the caller asked for (``0`` for the angr
|
|
332
|
+
bridge, which explores until its own state manager stops).
|
|
333
|
+
steps: Emulator instructions actually executed, replays included.
|
|
334
|
+
complete: ``True`` only when nothing was left unexplored *and* every
|
|
335
|
+
reported path reached a ``ret``. ``False`` means something is
|
|
336
|
+
missing -- paths, returns, or both -- and ``warnings`` says what.
|
|
337
|
+
entries: The paths, in deterministic breadth-first order.
|
|
338
|
+
warnings: Exploration-level notes (truncation, limits, engine caveats).
|
|
339
|
+
evidence: Provenance for the whole exploration, including what was
|
|
340
|
+
explored and what was only assumed.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
address: int
|
|
344
|
+
explorer: str
|
|
345
|
+
architecture: str
|
|
346
|
+
bitness: int
|
|
347
|
+
max_paths: int
|
|
348
|
+
max_steps: int
|
|
349
|
+
steps: int
|
|
350
|
+
complete: bool
|
|
351
|
+
entries: tuple[Entry, ...]
|
|
352
|
+
warnings: tuple[str, ...]
|
|
353
|
+
evidence: Evidence
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def paths(self) -> int:
|
|
357
|
+
"""Number of paths reported."""
|
|
358
|
+
return len(self.entries)
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def unresolved(self) -> int:
|
|
362
|
+
"""Paths that ended without a return or a budget stop."""
|
|
363
|
+
return sum(
|
|
364
|
+
1
|
|
365
|
+
for entry in self.entries
|
|
366
|
+
if entry.stopped_reason in (_STOP_BLOCKED, _STOP_UNMAPPED)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def return_values(self) -> tuple[int, ...]:
|
|
371
|
+
"""Return values of the paths that reached a ``ret``, in path order."""
|
|
372
|
+
return tuple(
|
|
373
|
+
entry.return_value
|
|
374
|
+
for entry in self.entries
|
|
375
|
+
if entry.return_value is not None and entry.stopped_reason == _STOP_RETURN
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def to_dict(self) -> dict[str, Any]:
|
|
379
|
+
"""Serialize to a JSON-safe dict (hex addresses, lists, no enums)."""
|
|
380
|
+
return {
|
|
381
|
+
"address": f"0x{self.address:x}",
|
|
382
|
+
"explorer": self.explorer,
|
|
383
|
+
"architecture": self.architecture,
|
|
384
|
+
"bitness": self.bitness,
|
|
385
|
+
"max_paths": self.max_paths,
|
|
386
|
+
"max_steps": self.max_steps,
|
|
387
|
+
"steps": self.steps,
|
|
388
|
+
"complete": self.complete,
|
|
389
|
+
"paths": self.paths,
|
|
390
|
+
"unresolved": self.unresolved,
|
|
391
|
+
"return_values": [f"0x{value:x}" for value in self.return_values],
|
|
392
|
+
"entries": [entry.to_dict() for entry in self.entries],
|
|
393
|
+
"warnings": list(self.warnings),
|
|
394
|
+
"evidence": self.evidence.to_dict(),
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ---- small helpers --------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _align_up(value: int) -> int:
|
|
402
|
+
"""Round ``value`` up to the next page boundary (mirrors ``emulate``)."""
|
|
403
|
+
if value <= 0:
|
|
404
|
+
return 0
|
|
405
|
+
page = emulate.PAGE_SIZE
|
|
406
|
+
return (value + page - 1) & ~(page - 1)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _merge_spans(spans: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
410
|
+
"""Merge overlapping/adjacent page spans so each page is mapped once."""
|
|
411
|
+
merged: list[tuple[int, int]] = []
|
|
412
|
+
for start, end in sorted(spans):
|
|
413
|
+
if merged and start <= merged[-1][1]:
|
|
414
|
+
previous_start, previous_end = merged[-1]
|
|
415
|
+
merged[-1] = (previous_start, max(previous_end, end))
|
|
416
|
+
else:
|
|
417
|
+
merged.append((start, end))
|
|
418
|
+
return merged
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _machine_bits(info: BinaryInfo) -> int | None:
|
|
422
|
+
"""Return 32 or 64 when this module can explore ``info``, else ``None``.
|
|
423
|
+
|
|
424
|
+
Mirrors ``emulate._machine_bits`` (a private helper there, so it is restated
|
|
425
|
+
here rather than imported): only x86 modes are accepted, because the return
|
|
426
|
+
register, stack and register vocabulary of this module is x86-specific.
|
|
427
|
+
"""
|
|
428
|
+
name = (info.architecture or "").strip().lower()
|
|
429
|
+
if name in _X86_64_NAMES:
|
|
430
|
+
bits = 64
|
|
431
|
+
elif name in _X86_32_NAMES:
|
|
432
|
+
bits = 32
|
|
433
|
+
else:
|
|
434
|
+
return None
|
|
435
|
+
if int(info.bitness) in (32, 64):
|
|
436
|
+
bits = int(info.bitness)
|
|
437
|
+
if (info.endianness or "little").strip().lower() == "big":
|
|
438
|
+
return None # x86 is little-endian only
|
|
439
|
+
return bits
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _reg_const(name: str) -> int | None:
|
|
443
|
+
"""Map a unicorn register name (``"rax"``, ``"rflags"``) to its constant."""
|
|
444
|
+
if _x86_const is None:
|
|
445
|
+
return None
|
|
446
|
+
value = getattr(_x86_const, f"UC_X86_REG_{name.upper()}", None)
|
|
447
|
+
return int(value) if isinstance(value, int) else None
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _validated_address(address: int) -> int:
|
|
451
|
+
"""Validate a start address (positive int) and return it."""
|
|
452
|
+
if isinstance(address, bool) or not isinstance(address, int):
|
|
453
|
+
raise AnalysisError(f"address must be an int, not {type(address).__name__}")
|
|
454
|
+
if address <= 0:
|
|
455
|
+
raise AnalysisError(f"address must be positive, got 0x{address:x}")
|
|
456
|
+
return address
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _validated_limit(name: str, value: int, ceiling: int) -> int:
|
|
460
|
+
"""Validate one exploration cap against its ceiling."""
|
|
461
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
462
|
+
raise AnalysisError(f"{name} must be an int")
|
|
463
|
+
if value < 1:
|
|
464
|
+
raise AnalysisError(f"{name} must be >= 1")
|
|
465
|
+
if value > ceiling:
|
|
466
|
+
raise AnalysisError(f"{name} too large ({value}); limit is {ceiling}")
|
|
467
|
+
return value
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _validated_registers(
|
|
471
|
+
initial_registers: Mapping[str, int] | None, bits: int
|
|
472
|
+
) -> dict[str, int]:
|
|
473
|
+
"""Normalize and validate caller-supplied register seeds.
|
|
474
|
+
|
|
475
|
+
Raises:
|
|
476
|
+
AnalysisError: On an unknown register name, a non-int value, or a value
|
|
477
|
+
that does not fit the machine word.
|
|
478
|
+
"""
|
|
479
|
+
seeds: dict[str, int] = {}
|
|
480
|
+
if not initial_registers:
|
|
481
|
+
return seeds
|
|
482
|
+
limit = 1 << bits
|
|
483
|
+
for name, value in initial_registers.items():
|
|
484
|
+
key = str(name).strip().lower()
|
|
485
|
+
if _reg_const(key) is None:
|
|
486
|
+
raise AnalysisError(f"unknown {bits}-bit register name {name!r}")
|
|
487
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
488
|
+
raise AnalysisError(f"register {key!r} must be seeded with an int")
|
|
489
|
+
if value < 0 or value >= limit:
|
|
490
|
+
raise AnalysisError(f"value for register {key!r} does not fit in {bits} bits")
|
|
491
|
+
seeds[key] = int(value)
|
|
492
|
+
return seeds
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# ---- flags and branch conditions ------------------------------------------
|
|
496
|
+
|
|
497
|
+
#: The six x86 condition flags, with their ``rflags`` bit positions. Only these
|
|
498
|
+
#: bits are ever rewritten (``IF``, ``DF`` and the reserved bit are preserved).
|
|
499
|
+
_FLAG_BITS: tuple[tuple[str, int], ...] = (
|
|
500
|
+
("cf", 0x0001),
|
|
501
|
+
("pf", 0x0004),
|
|
502
|
+
("af", 0x0010),
|
|
503
|
+
("zf", 0x0040),
|
|
504
|
+
("sf", 0x0080),
|
|
505
|
+
("of", 0x0800),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
#: Mask covering exactly :data:`_FLAG_BITS`.
|
|
509
|
+
_FLAG_MASK = 0x0001 | 0x0004 | 0x0010 | 0x0040 | 0x0080 | 0x0800
|
|
510
|
+
|
|
511
|
+
#: ``rflags`` value a fresh x86 CPU has (bit 1 is always set).
|
|
512
|
+
_DEFAULT_EFLAGS = 0x2
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@dataclass(frozen=True)
|
|
516
|
+
class _Flags:
|
|
517
|
+
"""The six condition flags, decoded from ``rflags``."""
|
|
518
|
+
|
|
519
|
+
cf: bool
|
|
520
|
+
pf: bool
|
|
521
|
+
af: bool
|
|
522
|
+
zf: bool
|
|
523
|
+
sf: bool
|
|
524
|
+
of: bool
|
|
525
|
+
|
|
526
|
+
@classmethod
|
|
527
|
+
def from_eflags(cls, value: int) -> _Flags:
|
|
528
|
+
"""Decode the condition flags out of an ``rflags`` value."""
|
|
529
|
+
return cls(
|
|
530
|
+
cf=bool(value & 0x0001),
|
|
531
|
+
pf=bool(value & 0x0004),
|
|
532
|
+
af=bool(value & 0x0010),
|
|
533
|
+
zf=bool(value & 0x0040),
|
|
534
|
+
sf=bool(value & 0x0080),
|
|
535
|
+
of=bool(value & 0x0800),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def to_bits(self) -> int:
|
|
539
|
+
"""Encode these flags as ``rflags`` bits."""
|
|
540
|
+
return (
|
|
541
|
+
(0x0001 if self.cf else 0)
|
|
542
|
+
| (0x0004 if self.pf else 0)
|
|
543
|
+
| (0x0010 if self.af else 0)
|
|
544
|
+
| (0x0040 if self.zf else 0)
|
|
545
|
+
| (0x0080 if self.sf else 0)
|
|
546
|
+
| (0x0800 if self.of else 0)
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
def distance(self, other: _Flags) -> int:
|
|
550
|
+
"""Number of condition flags that differ from ``other``."""
|
|
551
|
+
return sum(
|
|
552
|
+
1 for name, _bit in _FLAG_BITS if getattr(self, name) != getattr(other, name)
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
#: Canonical condition predicates, named after the ``jcc`` that tests them.
|
|
557
|
+
#: Aliases are folded onto these by :data:`_CONDITION_ALIASES`.
|
|
558
|
+
_CONDITIONS: dict[str, Callable[[_Flags], bool]] = {
|
|
559
|
+
"jz": lambda f: f.zf,
|
|
560
|
+
"jnz": lambda f: not f.zf,
|
|
561
|
+
"jc": lambda f: f.cf,
|
|
562
|
+
"jnc": lambda f: not f.cf,
|
|
563
|
+
"jbe": lambda f: f.cf or f.zf,
|
|
564
|
+
"ja": lambda f: not f.cf and not f.zf,
|
|
565
|
+
"js": lambda f: f.sf,
|
|
566
|
+
"jns": lambda f: not f.sf,
|
|
567
|
+
"jo": lambda f: f.of,
|
|
568
|
+
"jno": lambda f: not f.of,
|
|
569
|
+
"jp": lambda f: f.pf,
|
|
570
|
+
"jnp": lambda f: not f.pf,
|
|
571
|
+
"jl": lambda f: f.sf != f.of,
|
|
572
|
+
"jge": lambda f: f.sf == f.of,
|
|
573
|
+
"jle": lambda f: f.zf or (f.sf != f.of),
|
|
574
|
+
"jg": lambda f: not f.zf and f.sf == f.of,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#: Every ``jcc`` spelling Capstone produces, folded onto :data:`_CONDITIONS`.
|
|
578
|
+
_CONDITION_ALIASES: dict[str, str] = {
|
|
579
|
+
"jz": "jz", "je": "jz",
|
|
580
|
+
"jnz": "jnz", "jne": "jnz",
|
|
581
|
+
"jc": "jc", "jb": "jc", "jnae": "jc",
|
|
582
|
+
"jnc": "jnc", "jae": "jnc", "jnb": "jnc",
|
|
583
|
+
"jbe": "jbe", "jna": "jbe",
|
|
584
|
+
"ja": "ja", "jnbe": "ja",
|
|
585
|
+
"js": "js", "jns": "jns",
|
|
586
|
+
"jo": "jo", "jno": "jno",
|
|
587
|
+
"jp": "jp", "jpe": "jp",
|
|
588
|
+
"jnp": "jnp", "jpo": "jnp",
|
|
589
|
+
"jl": "jl", "jnge": "jl",
|
|
590
|
+
"jge": "jge", "jnl": "jge",
|
|
591
|
+
"jle": "jle", "jng": "jle",
|
|
592
|
+
"jg": "jg", "jnle": "jg",
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#: ``jcc`` forms that test a counter register instead of a flag. Seeding them
|
|
596
|
+
#: means writing the counter, which is exact for these three: unlike ``loop``
|
|
597
|
+
#: they do not decrement it.
|
|
598
|
+
_COUNTER_JUMPS: dict[str, str] = {"jrcxz": "rcx", "jecxz": "ecx", "jcxz": "cx"}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _condition_holds(mnemonic: str, flags: _Flags) -> bool | None:
|
|
602
|
+
"""Evaluate a ``jcc`` against ``flags``.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
The condition's value, or ``None`` when this module does not know the
|
|
606
|
+
mnemonic (the caller then blocks the path instead of guessing).
|
|
607
|
+
"""
|
|
608
|
+
canonical = _CONDITION_ALIASES.get(mnemonic)
|
|
609
|
+
if canonical is None:
|
|
610
|
+
return None
|
|
611
|
+
return bool(_CONDITIONS[canonical](flags))
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _flags_from_combination(combination: int) -> _Flags:
|
|
615
|
+
"""Build a flag set from the six bits of ``combination`` (:data:`_FLAG_BITS` order)."""
|
|
616
|
+
return _Flags(
|
|
617
|
+
cf=bool(combination & 0b000001),
|
|
618
|
+
pf=bool(combination & 0b000010),
|
|
619
|
+
af=bool(combination & 0b000100),
|
|
620
|
+
zf=bool(combination & 0b001000),
|
|
621
|
+
sf=bool(combination & 0b010000),
|
|
622
|
+
of=bool(combination & 0b100000),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _seeded_flag_bits(canonical: str, desired: bool, observed: int) -> tuple[int, str]:
|
|
627
|
+
"""Pick the ``rflags`` bits that satisfy ``canonical == desired``.
|
|
628
|
+
|
|
629
|
+
The six condition flags are enumerated exhaustively (64 combinations) and the
|
|
630
|
+
assignment closest to the observed value wins, ties broken by the lowest bit
|
|
631
|
+
pattern, so the result is deterministic. This is a *seed*: it says nothing
|
|
632
|
+
about whether real inputs can produce these flags.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
``(rflags_bits, description)``. The bits keep every non-flag bit of
|
|
636
|
+
``observed``; the description names exactly which flags changed.
|
|
637
|
+
"""
|
|
638
|
+
predicate = _CONDITIONS[canonical]
|
|
639
|
+
current = _Flags.from_eflags(observed)
|
|
640
|
+
best: tuple[int, int] | None = None
|
|
641
|
+
for combination in range(1 << len(_FLAG_BITS)):
|
|
642
|
+
candidate = _flags_from_combination(combination)
|
|
643
|
+
if bool(predicate(candidate)) != desired:
|
|
644
|
+
continue
|
|
645
|
+
distance = candidate.distance(current)
|
|
646
|
+
if best is None or distance < best[0]:
|
|
647
|
+
best = (distance, combination)
|
|
648
|
+
if best is None: # pragma: no cover - every predicate here is satisfiable
|
|
649
|
+
return observed, "no flag assignment satisfies the condition"
|
|
650
|
+
chosen = _flags_from_combination(best[1])
|
|
651
|
+
changed = ", ".join(
|
|
652
|
+
f"{name.upper()} {int(getattr(current, name))}->{int(getattr(chosen, name))}"
|
|
653
|
+
for name, _bit in _FLAG_BITS
|
|
654
|
+
if getattr(chosen, name) != getattr(current, name)
|
|
655
|
+
)
|
|
656
|
+
return (observed & ~_FLAG_MASK) | chosen.to_bits(), changed or "no flag changed"
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# ---- decoding -------------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class _Decoder:
|
|
663
|
+
"""One-instruction Capstone decoder for the supported x86 modes."""
|
|
664
|
+
|
|
665
|
+
def __init__(self, mode64: bool) -> None:
|
|
666
|
+
mode = capstone.CS_MODE_64 if mode64 else capstone.CS_MODE_32
|
|
667
|
+
self._cs = capstone.Cs(capstone.CS_ARCH_X86, mode)
|
|
668
|
+
self._cs.detail = True
|
|
669
|
+
|
|
670
|
+
def decode(self, code: bytes, address: int) -> Any:
|
|
671
|
+
"""Decode one instruction from ``code``; ``None`` when it cannot be decoded."""
|
|
672
|
+
if not code:
|
|
673
|
+
return None
|
|
674
|
+
for insn in self._cs.disasm(code, address, 1):
|
|
675
|
+
return insn
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@dataclass(frozen=True)
|
|
680
|
+
class _Scan:
|
|
681
|
+
"""A linearly scanned block: the instructions, and how the block ends.
|
|
682
|
+
|
|
683
|
+
Attributes:
|
|
684
|
+
instructions: Decoded instructions in address order, the terminator
|
|
685
|
+
included when :attr:`terminator` is set.
|
|
686
|
+
terminator: The final instruction when it transfers control, else ``None``.
|
|
687
|
+
error: Why scanning stopped early (undecodable bytes, end of section),
|
|
688
|
+
or ``None`` when the block ended on a terminator or a full scan.
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
instructions: tuple[Any, ...]
|
|
692
|
+
terminator: Any | None
|
|
693
|
+
error: str | None
|
|
694
|
+
|
|
695
|
+
@property
|
|
696
|
+
def body(self) -> tuple[Any, ...]:
|
|
697
|
+
"""Instructions before the terminator (the part that always runs)."""
|
|
698
|
+
return self.instructions[:-1] if self.terminator is not None else self.instructions
|
|
699
|
+
|
|
700
|
+
@property
|
|
701
|
+
def end(self) -> int:
|
|
702
|
+
"""Address right after the last instruction of the scan."""
|
|
703
|
+
if not self.instructions:
|
|
704
|
+
return 0
|
|
705
|
+
last = self.instructions[-1]
|
|
706
|
+
return int(last.address) + int(last.size)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _ends_block(insn: Any) -> bool:
|
|
710
|
+
"""True when ``insn`` transfers control, so the linear scan must stop there."""
|
|
711
|
+
mnemonic = str(insn.mnemonic).lower()
|
|
712
|
+
if mnemonic in _RET_MNEMONICS or mnemonic in _LOOP_MNEMONICS:
|
|
713
|
+
return True
|
|
714
|
+
if insn.group(capstone.CS_GRP_RET) or insn.group(capstone.CS_GRP_IRET):
|
|
715
|
+
return True
|
|
716
|
+
return bool(insn.group(capstone.CS_GRP_JUMP) or insn.group(capstone.CS_GRP_CALL))
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class _Scanner:
|
|
720
|
+
"""Reads a block of code from the file and decodes it up to a control transfer."""
|
|
721
|
+
|
|
722
|
+
def __init__(self, path: Path, info: BinaryInfo, decoder: _Decoder) -> None:
|
|
723
|
+
self.path = path
|
|
724
|
+
self.info = info
|
|
725
|
+
self.decoder = decoder
|
|
726
|
+
|
|
727
|
+
def scan(self, pc: int) -> _Scan:
|
|
728
|
+
"""Scan the block starting at ``pc``; never raises."""
|
|
729
|
+
section = self.info.section_for_va(pc)
|
|
730
|
+
if section is None:
|
|
731
|
+
return _Scan((), None, f"address 0x{pc:x} is not mapped by any section")
|
|
732
|
+
available = (section.virtual_address + section.size) - pc
|
|
733
|
+
if available <= 0:
|
|
734
|
+
return _Scan((), None, f"address 0x{pc:x} is past the end of {section.name!r}")
|
|
735
|
+
if not section.is_executable:
|
|
736
|
+
return _Scan(
|
|
737
|
+
(),
|
|
738
|
+
None,
|
|
739
|
+
f"address 0x{pc:x} is inside {section.name!r}, which is not executable",
|
|
740
|
+
)
|
|
741
|
+
want = min(available, MAX_BLOCK_INSTRUCTIONS * _MAX_INSTRUCTION_BYTES)
|
|
742
|
+
try:
|
|
743
|
+
data = read_code(self.path, self.info, pc, want)
|
|
744
|
+
except AnalysisError as exc:
|
|
745
|
+
return _Scan((), None, f"no code bytes at 0x{pc:x}: {exc}")
|
|
746
|
+
instructions: list[Any] = []
|
|
747
|
+
offset = 0
|
|
748
|
+
while offset < len(data) and len(instructions) < MAX_BLOCK_INSTRUCTIONS:
|
|
749
|
+
insn = self.decoder.decode(bytes(data[offset:]), pc + offset)
|
|
750
|
+
if insn is None:
|
|
751
|
+
break
|
|
752
|
+
instructions.append(insn)
|
|
753
|
+
offset += int(insn.size)
|
|
754
|
+
if _ends_block(insn):
|
|
755
|
+
return _Scan(tuple(instructions), insn, None)
|
|
756
|
+
if not instructions:
|
|
757
|
+
return _Scan((), None, f"address 0x{pc:x} does not decode as an x86 instruction")
|
|
758
|
+
if len(instructions) >= MAX_BLOCK_INSTRUCTIONS:
|
|
759
|
+
# The scan cap, not the code: the caller continues from ``end``.
|
|
760
|
+
return _Scan(tuple(instructions), None, None)
|
|
761
|
+
if offset >= len(data) and offset >= available:
|
|
762
|
+
return _Scan(
|
|
763
|
+
tuple(instructions),
|
|
764
|
+
None,
|
|
765
|
+
f"the block at 0x{pc:x} runs past the readable bytes of {section.name!r}",
|
|
766
|
+
)
|
|
767
|
+
return _Scan(tuple(instructions), None, None)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
# ---- the emulated machine -------------------------------------------------
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@contextmanager
|
|
774
|
+
def _quiet_faulthandler() -> Iterator[None]:
|
|
775
|
+
"""Silence CPython's faulthandler around unicorn's guest-fault machinery.
|
|
776
|
+
|
|
777
|
+
Unicorn resolves guest faults (``UC_ERR_READ_UNMAPPED`` and friends) with its
|
|
778
|
+
own handler. On Windows CPython's ``faulthandler`` prints a first-chance
|
|
779
|
+
"Windows fatal exception" line before that handler runs, which looks like a
|
|
780
|
+
crash in the log even though nothing failed. Mirrors ``emulate._Runner.run``.
|
|
781
|
+
"""
|
|
782
|
+
was_enabled = _faulthandler.is_enabled()
|
|
783
|
+
if was_enabled:
|
|
784
|
+
_faulthandler.disable()
|
|
785
|
+
try:
|
|
786
|
+
yield
|
|
787
|
+
finally:
|
|
788
|
+
if was_enabled:
|
|
789
|
+
_faulthandler.enable()
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
class _Machine:
|
|
793
|
+
"""One unicorn CPU for one function, re-initialised between path replays.
|
|
794
|
+
|
|
795
|
+
The emulator and its mappings are created once (:meth:`open`) and every path
|
|
796
|
+
starts from the same state because :meth:`reset` rewrites the image bytes,
|
|
797
|
+
zeroes the stack and re-seeds the registers. That is what makes a replay
|
|
798
|
+
deterministic and the paths independent of each other.
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
def __init__(
|
|
802
|
+
self,
|
|
803
|
+
path: Path,
|
|
804
|
+
info: BinaryInfo,
|
|
805
|
+
address: int,
|
|
806
|
+
*,
|
|
807
|
+
bits: int,
|
|
808
|
+
stack_size: int,
|
|
809
|
+
) -> None:
|
|
810
|
+
self.path = path
|
|
811
|
+
self.info = info
|
|
812
|
+
self.address = address
|
|
813
|
+
self.bits = bits
|
|
814
|
+
self.stack_size = stack_size
|
|
815
|
+
self.mode64 = bits == 64
|
|
816
|
+
self.uc: Any = None
|
|
817
|
+
self.ip = "rip" if self.mode64 else "eip"
|
|
818
|
+
self.sp = "rsp" if self.mode64 else "esp"
|
|
819
|
+
self.flags = "rflags" if self.mode64 else "eflags"
|
|
820
|
+
self.return_register = "rax" if self.mode64 else "eax"
|
|
821
|
+
self.names = _REGISTERS_64 if self.mode64 else _REGISTERS_32
|
|
822
|
+
self._images: list[tuple[int, bytes]] = []
|
|
823
|
+
self._mapped_pages: set[int] = set()
|
|
824
|
+
self._mapped_bytes = 0
|
|
825
|
+
self.regions: list[tuple[str, int, int]] = []
|
|
826
|
+
self.stack_base = 0
|
|
827
|
+
self.stack_top = 0
|
|
828
|
+
section = info.section_for_va(address)
|
|
829
|
+
if section is not None:
|
|
830
|
+
self.region = (section.virtual_address, section.virtual_address + section.size)
|
|
831
|
+
self.section_name = section.name
|
|
832
|
+
else: # pragma: no cover - the public API rejects this first
|
|
833
|
+
self.region = (address, address + 1)
|
|
834
|
+
self.section_name = "?"
|
|
835
|
+
|
|
836
|
+
# ---- setup ------------------------------------------------------------
|
|
837
|
+
|
|
838
|
+
def open(self) -> None:
|
|
839
|
+
"""Create the CPU and map the image and the stack."""
|
|
840
|
+
uni = _unicorn
|
|
841
|
+
arch = uni.UC_ARCH_X86
|
|
842
|
+
mode = uni.UC_MODE_64 if self.mode64 else uni.UC_MODE_32
|
|
843
|
+
with _quiet_faulthandler():
|
|
844
|
+
self.uc = uni.Uc(arch, mode)
|
|
845
|
+
self._map_image()
|
|
846
|
+
self._map_stack()
|
|
847
|
+
|
|
848
|
+
def _map_image(self) -> None:
|
|
849
|
+
"""Map every allocated section (page aligned, bounded), then copy its bytes."""
|
|
850
|
+
spans: list[tuple[int, int]] = []
|
|
851
|
+
sections: list[Any] = []
|
|
852
|
+
for section in self.info.sections:
|
|
853
|
+
if section.size <= 0 or section.virtual_address <= 0:
|
|
854
|
+
continue
|
|
855
|
+
if section.size > emulate.MAX_SECTION_BYTES:
|
|
856
|
+
continue
|
|
857
|
+
start = section.virtual_address & ~(emulate.PAGE_SIZE - 1)
|
|
858
|
+
end = _align_up(section.virtual_address + section.size)
|
|
859
|
+
if self._mapped_bytes + (end - start) > emulate.MAX_TOTAL_MAPPED:
|
|
860
|
+
break
|
|
861
|
+
self._mapped_bytes += end - start
|
|
862
|
+
spans.append((start, end))
|
|
863
|
+
sections.append(section)
|
|
864
|
+
for start, end in _merge_spans(spans):
|
|
865
|
+
self._map_pages(start, end, "image")
|
|
866
|
+
for section in sections:
|
|
867
|
+
size = section.raw_size or section.size
|
|
868
|
+
try:
|
|
869
|
+
data = read_code(self.path, self.info, section.virtual_address, size)
|
|
870
|
+
except AnalysisError:
|
|
871
|
+
continue
|
|
872
|
+
if not data:
|
|
873
|
+
continue
|
|
874
|
+
self._images.append((section.virtual_address, bytes(data)))
|
|
875
|
+
self.uc.mem_write(section.virtual_address, data)
|
|
876
|
+
|
|
877
|
+
def _map_stack(self) -> None:
|
|
878
|
+
"""Map the stack region where ``emulate`` puts it."""
|
|
879
|
+
base = emulate.STACK_BASE_64 if self.mode64 else emulate.STACK_BASE_32
|
|
880
|
+
start = base & ~(emulate.PAGE_SIZE - 1)
|
|
881
|
+
end = _align_up(base + self.stack_size)
|
|
882
|
+
self._mapped_bytes += end - start
|
|
883
|
+
self.stack_base, self.stack_top = start, end
|
|
884
|
+
self._map_pages(start, end, "stack")
|
|
885
|
+
|
|
886
|
+
def _map_pages(self, start: int, end: int, kind: str) -> None:
|
|
887
|
+
"""Map the page range ``[start, end)``, skipping pages already mapped."""
|
|
888
|
+
run_start: int | None = None
|
|
889
|
+
page = start
|
|
890
|
+
while page < end:
|
|
891
|
+
if page in self._mapped_pages:
|
|
892
|
+
if run_start is not None:
|
|
893
|
+
self._commit(run_start, page, kind)
|
|
894
|
+
run_start = None
|
|
895
|
+
else:
|
|
896
|
+
self._mapped_pages.add(page)
|
|
897
|
+
if run_start is None:
|
|
898
|
+
run_start = page
|
|
899
|
+
page += emulate.PAGE_SIZE
|
|
900
|
+
if run_start is not None:
|
|
901
|
+
self._commit(run_start, end, kind)
|
|
902
|
+
|
|
903
|
+
def _commit(self, start: int, end: int, kind: str) -> None:
|
|
904
|
+
"""Ask unicorn for one contiguous page run and record it."""
|
|
905
|
+
self.uc.mem_map(start, end - start)
|
|
906
|
+
self.regions.append((kind, start, end))
|
|
907
|
+
|
|
908
|
+
def reset(self, seeds: Mapping[str, int]) -> None:
|
|
909
|
+
"""Restore the pristine state: image bytes, zeroed stack, seeded registers."""
|
|
910
|
+
for address, data in self._images:
|
|
911
|
+
self.uc.mem_write(address, data)
|
|
912
|
+
with _quiet_faulthandler():
|
|
913
|
+
self.uc.mem_write(self.stack_base, bytes(self.stack_top - self.stack_base))
|
|
914
|
+
for name in self.names:
|
|
915
|
+
self.write_register(name, 0)
|
|
916
|
+
stack_pointer = self.stack_top - _STACK_HEADROOM
|
|
917
|
+
self.write_register(self.ip, self.address)
|
|
918
|
+
self.write_register(self.sp, stack_pointer)
|
|
919
|
+
self.write_register("rbp" if self.mode64 else "ebp", stack_pointer)
|
|
920
|
+
self.write_register(self.flags, _DEFAULT_EFLAGS)
|
|
921
|
+
for name, value in seeds.items():
|
|
922
|
+
self.write_register(name, value)
|
|
923
|
+
|
|
924
|
+
# ---- state access -----------------------------------------------------
|
|
925
|
+
|
|
926
|
+
def read_register(self, name: str) -> int | None:
|
|
927
|
+
"""Read one register, or ``None`` when the name is unknown here."""
|
|
928
|
+
constant = _reg_const(name)
|
|
929
|
+
if constant is None:
|
|
930
|
+
return None
|
|
931
|
+
try:
|
|
932
|
+
return int(self.uc.reg_read(constant))
|
|
933
|
+
except Exception: # pragma: no cover - a writable register reads back fine
|
|
934
|
+
return None
|
|
935
|
+
|
|
936
|
+
def write_register(self, name: str, value: int) -> None:
|
|
937
|
+
"""Write one register, ignoring names this mode does not expose."""
|
|
938
|
+
constant = _reg_const(name)
|
|
939
|
+
if constant is None:
|
|
940
|
+
return
|
|
941
|
+
self.uc.reg_write(constant, int(value))
|
|
942
|
+
|
|
943
|
+
def snapshot(self) -> dict[str, int]:
|
|
944
|
+
"""Snapshot the general-purpose registers of this mode."""
|
|
945
|
+
state: dict[str, int] = {}
|
|
946
|
+
for name in self.names:
|
|
947
|
+
value = self.read_register(name)
|
|
948
|
+
if value is not None:
|
|
949
|
+
state[name] = value
|
|
950
|
+
return state
|
|
951
|
+
|
|
952
|
+
def read_pointer(self, name: str) -> int | None:
|
|
953
|
+
"""Read the machine word ``name`` points at, or ``None`` when unreadable."""
|
|
954
|
+
address = self.read_register(name)
|
|
955
|
+
if address is None:
|
|
956
|
+
return None
|
|
957
|
+
try:
|
|
958
|
+
raw = bytes(self.uc.mem_read(address, self.bits // 8))
|
|
959
|
+
except Exception:
|
|
960
|
+
return None
|
|
961
|
+
return int.from_bytes(raw, "little")
|
|
962
|
+
|
|
963
|
+
def eflags(self) -> int:
|
|
964
|
+
"""Read ``rflags``/``eflags`` (the default value when it cannot be read)."""
|
|
965
|
+
value = self.read_register(self.flags)
|
|
966
|
+
return _DEFAULT_EFLAGS if value is None else value
|
|
967
|
+
|
|
968
|
+
def in_region(self, address: int) -> bool:
|
|
969
|
+
"""True when ``address`` is inside the section being explored."""
|
|
970
|
+
low, high = self.region
|
|
971
|
+
return low <= address < high
|
|
972
|
+
|
|
973
|
+
# ---- execution --------------------------------------------------------
|
|
974
|
+
|
|
975
|
+
def run_to(self, begin: int, until: int, count: int) -> None:
|
|
976
|
+
"""Emulate ``begin`` until ``until`` (exclusive) or ``count`` instructions."""
|
|
977
|
+
with _quiet_faulthandler():
|
|
978
|
+
self.uc.emu_start(begin, until, 0, count)
|
|
979
|
+
|
|
980
|
+
def step(self, address: int) -> int:
|
|
981
|
+
"""Execute exactly one instruction at ``address`` and return the new ``ip``."""
|
|
982
|
+
with _quiet_faulthandler():
|
|
983
|
+
self.uc.emu_start(address, 0, 0, 1)
|
|
984
|
+
current = self.read_register(self.ip)
|
|
985
|
+
return -1 if current is None else current
|
|
986
|
+
|
|
987
|
+
def close(self) -> None:
|
|
988
|
+
"""Drop the emulator so unicorn can free its native handle.
|
|
989
|
+
|
|
990
|
+
``Uc`` registers a ``weakref.finalize`` that closes the handle when the
|
|
991
|
+
instance is collected, so dropping the reference is all this has to do
|
|
992
|
+
(calling ``release_handle`` could close the same handle twice).
|
|
993
|
+
"""
|
|
994
|
+
self.uc = None
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def _classify_fault(exc: Any, pc: int | None) -> tuple[StopReason, str]:
|
|
998
|
+
"""Turn one unicorn exception into a stop reason plus an exact note."""
|
|
999
|
+
uni = _unicorn
|
|
1000
|
+
errno = getattr(exc, "errno", None)
|
|
1001
|
+
at = f" at 0x{pc:x}" if pc is not None else ""
|
|
1002
|
+
unmapped = {
|
|
1003
|
+
getattr(uni, "UC_ERR_READ_UNMAPPED", -6),
|
|
1004
|
+
getattr(uni, "UC_ERR_WRITE_UNMAPPED", -7),
|
|
1005
|
+
getattr(uni, "UC_ERR_FETCH_UNMAPPED", -8),
|
|
1006
|
+
}
|
|
1007
|
+
if errno in unmapped:
|
|
1008
|
+
return (
|
|
1009
|
+
_STOP_UNMAPPED,
|
|
1010
|
+
f"the path touched an address that is not mapped{at}: emulation stopped "
|
|
1011
|
+
f"there, so this path is incomplete (not a return)",
|
|
1012
|
+
)
|
|
1013
|
+
protection = {
|
|
1014
|
+
getattr(uni, "UC_ERR_READ_PROT", -9),
|
|
1015
|
+
getattr(uni, "UC_ERR_WRITE_PROT", -10),
|
|
1016
|
+
getattr(uni, "UC_ERR_FETCH_PROT", -11),
|
|
1017
|
+
getattr(uni, "UC_ERR_READ_UNALIGNED", -12),
|
|
1018
|
+
getattr(uni, "UC_ERR_WRITE_UNALIGNED", -13),
|
|
1019
|
+
getattr(uni, "UC_ERR_FETCH_UNALIGNED", -14),
|
|
1020
|
+
}
|
|
1021
|
+
if errno in protection:
|
|
1022
|
+
return (_STOP_UNMAPPED, f"memory protection violation{at}: {exc}")
|
|
1023
|
+
if errno == getattr(uni, "UC_ERR_INSN_INVALID", -15):
|
|
1024
|
+
return (
|
|
1025
|
+
_STOP_BLOCKED,
|
|
1026
|
+
f"unicorn could not decode the instruction{at}: {exc}; the path is unresolved",
|
|
1027
|
+
)
|
|
1028
|
+
return (_STOP_BLOCKED, f"emulation failed{at}: {exc}; the path is unresolved")
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _direct_target(insn: Any) -> int | None:
|
|
1032
|
+
"""Return the constant operand of an instruction (its branch target), if any."""
|
|
1033
|
+
for operand in getattr(insn, "operands", ()):
|
|
1034
|
+
value = getattr(operand, "imm", None)
|
|
1035
|
+
if isinstance(value, int):
|
|
1036
|
+
return value
|
|
1037
|
+
return None
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
# ---- the explorer ---------------------------------------------------------
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
class _Explorer:
|
|
1044
|
+
"""Breadth-first enumeration of branch decisions, one replay per path."""
|
|
1045
|
+
|
|
1046
|
+
def __init__(
|
|
1047
|
+
self,
|
|
1048
|
+
machine: _Machine,
|
|
1049
|
+
scanner: _Scanner,
|
|
1050
|
+
*,
|
|
1051
|
+
max_paths: int,
|
|
1052
|
+
max_steps: int,
|
|
1053
|
+
seeds: Mapping[str, int],
|
|
1054
|
+
) -> None:
|
|
1055
|
+
self.machine = machine
|
|
1056
|
+
self.scanner = scanner
|
|
1057
|
+
self.max_paths = max_paths
|
|
1058
|
+
self.max_steps = max_steps
|
|
1059
|
+
self.seeds = seeds
|
|
1060
|
+
self.entries: list[Entry] = []
|
|
1061
|
+
self.steps = 0
|
|
1062
|
+
self.warnings: list[str] = []
|
|
1063
|
+
|
|
1064
|
+
# ---- driver -----------------------------------------------------------
|
|
1065
|
+
|
|
1066
|
+
def run(self) -> None:
|
|
1067
|
+
"""Enumerate paths until the tree, the path cap or the budget ends."""
|
|
1068
|
+
queue: deque[tuple[bool, ...]] = deque([()])
|
|
1069
|
+
truncated = False
|
|
1070
|
+
budget_stop = False
|
|
1071
|
+
while queue:
|
|
1072
|
+
if len(self.entries) >= self.max_paths:
|
|
1073
|
+
truncated = True
|
|
1074
|
+
break
|
|
1075
|
+
if self.steps >= self.max_steps:
|
|
1076
|
+
budget_stop = True
|
|
1077
|
+
break
|
|
1078
|
+
entry, children, used = self._walk(queue.popleft())
|
|
1079
|
+
self.steps += used
|
|
1080
|
+
self.entries.append(entry)
|
|
1081
|
+
queue.extend(children)
|
|
1082
|
+
if truncated:
|
|
1083
|
+
self.warnings.append(
|
|
1084
|
+
f"path limit reached: {self.max_paths} path(s) were reported and the branch "
|
|
1085
|
+
"tree is not exhausted; raise max_paths to explore further"
|
|
1086
|
+
)
|
|
1087
|
+
if budget_stop:
|
|
1088
|
+
self.warnings.append(
|
|
1089
|
+
f"step budget exhausted after {self.steps} emulated instruction(s); the "
|
|
1090
|
+
"remaining paths were not explored"
|
|
1091
|
+
)
|
|
1092
|
+
if truncated or budget_stop:
|
|
1093
|
+
self.warnings.append(f"{len(queue)} path(s) were left unexplored")
|
|
1094
|
+
incomplete = sum(
|
|
1095
|
+
1 for entry in self.entries if entry.stopped_reason != _STOP_RETURN
|
|
1096
|
+
)
|
|
1097
|
+
if incomplete and not truncated and not budget_stop:
|
|
1098
|
+
self.warnings.append(
|
|
1099
|
+
f"{incomplete} path(s) ended without a return (blocked, unmapped or budget); "
|
|
1100
|
+
"the branch tree is not fully resolved"
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
@property
|
|
1104
|
+
def complete(self) -> bool:
|
|
1105
|
+
"""True when nothing was left unexplored and every path reached a ``ret``."""
|
|
1106
|
+
return not self.warnings and all(
|
|
1107
|
+
entry.stopped_reason == _STOP_RETURN for entry in self.entries
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
# ---- one path ---------------------------------------------------------
|
|
1111
|
+
|
|
1112
|
+
def _walk(self, plan: tuple[bool, ...]) -> tuple[Entry, list[tuple[bool, ...]], int]:
|
|
1113
|
+
"""Replay one decision plan and report exactly where it ended.
|
|
1114
|
+
|
|
1115
|
+
Returns:
|
|
1116
|
+
``(entry, children, instructions)``: the path's entry, the decision
|
|
1117
|
+
tuples still to explore, and the instructions this replay executed.
|
|
1118
|
+
"""
|
|
1119
|
+
machine = self.machine
|
|
1120
|
+
machine.reset(self.seeds)
|
|
1121
|
+
pc = machine.address
|
|
1122
|
+
walked: list[bool] = []
|
|
1123
|
+
taken: list[int] = []
|
|
1124
|
+
branches: list[Branch] = []
|
|
1125
|
+
notes: list[str] = []
|
|
1126
|
+
children: list[tuple[bool, ...]] = []
|
|
1127
|
+
used = 0
|
|
1128
|
+
|
|
1129
|
+
def finish(
|
|
1130
|
+
reason: StopReason, value: int | None
|
|
1131
|
+
) -> tuple[Entry, list[tuple[bool, ...]], int]:
|
|
1132
|
+
return (
|
|
1133
|
+
self._entry(branches, taken, reason, value, notes, used),
|
|
1134
|
+
children,
|
|
1135
|
+
used,
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
while True:
|
|
1139
|
+
if self.steps + used >= self.max_steps:
|
|
1140
|
+
notes.append(
|
|
1141
|
+
f"step budget exhausted after {self.max_steps} instruction(s); the path "
|
|
1142
|
+
f"is truncated at 0x{pc:x} and has no return value"
|
|
1143
|
+
)
|
|
1144
|
+
return finish(_STOP_BUDGET, None)
|
|
1145
|
+
scan = self.scanner.scan(pc)
|
|
1146
|
+
if not scan.instructions:
|
|
1147
|
+
notes.append(scan.error or f"nothing to emulate at 0x{pc:x}")
|
|
1148
|
+
return finish(_STOP_BLOCKED, None)
|
|
1149
|
+
body = scan.body
|
|
1150
|
+
terminator = scan.terminator
|
|
1151
|
+
until = int(terminator.address) if terminator is not None else scan.end
|
|
1152
|
+
if body:
|
|
1153
|
+
try:
|
|
1154
|
+
machine.run_to(pc, until, len(body))
|
|
1155
|
+
except Exception as exc: # a unicorn fault, classified by errno
|
|
1156
|
+
return finish(*self._fault(exc, notes))
|
|
1157
|
+
used += len(body)
|
|
1158
|
+
if terminator is None:
|
|
1159
|
+
if scan.error is not None:
|
|
1160
|
+
notes.append(scan.error)
|
|
1161
|
+
return finish(_STOP_BLOCKED, None)
|
|
1162
|
+
pc = scan.end
|
|
1163
|
+
continue
|
|
1164
|
+
address = int(terminator.address)
|
|
1165
|
+
mnemonic = str(terminator.mnemonic).lower()
|
|
1166
|
+
if (
|
|
1167
|
+
mnemonic in _RET_MNEMONICS
|
|
1168
|
+
or terminator.group(capstone.CS_GRP_RET)
|
|
1169
|
+
or terminator.group(capstone.CS_GRP_IRET)
|
|
1170
|
+
):
|
|
1171
|
+
target = machine.read_pointer(machine.sp)
|
|
1172
|
+
if target is not None and machine.in_region(target):
|
|
1173
|
+
try:
|
|
1174
|
+
landed = machine.step(address)
|
|
1175
|
+
except Exception as exc: # pragma: no cover - a mapped stack
|
|
1176
|
+
return finish(*self._fault(exc, notes))
|
|
1177
|
+
used += 1
|
|
1178
|
+
taken.append(landed)
|
|
1179
|
+
pc = landed
|
|
1180
|
+
continue
|
|
1181
|
+
if target is None:
|
|
1182
|
+
notes.append(
|
|
1183
|
+
f"the return address at {machine.sp} could not be read at "
|
|
1184
|
+
f"0x{address:x}; the value in {machine.return_register} is reported "
|
|
1185
|
+
"as this path's return value"
|
|
1186
|
+
)
|
|
1187
|
+
return finish(_STOP_RETURN, machine.read_register(machine.return_register))
|
|
1188
|
+
if mnemonic in _LOOP_MNEMONICS:
|
|
1189
|
+
counter = "rcx" if machine.mode64 else "ecx"
|
|
1190
|
+
notes.append(
|
|
1191
|
+
f"the {mnemonic!r} instruction at 0x{address:x} decrements {counter} "
|
|
1192
|
+
"before testing it, which this explorer does not model; the path is "
|
|
1193
|
+
"unresolved"
|
|
1194
|
+
)
|
|
1195
|
+
return finish(_STOP_BLOCKED, None)
|
|
1196
|
+
canonical = _CONDITION_ALIASES.get(mnemonic)
|
|
1197
|
+
counter_name = _COUNTER_JUMPS.get(mnemonic)
|
|
1198
|
+
if canonical is not None or counter_name is not None:
|
|
1199
|
+
observed: bool | None = None
|
|
1200
|
+
if canonical is not None:
|
|
1201
|
+
observed = _condition_holds(
|
|
1202
|
+
canonical, _Flags.from_eflags(machine.eflags())
|
|
1203
|
+
)
|
|
1204
|
+
else:
|
|
1205
|
+
counter_value = machine.read_register(str(counter_name))
|
|
1206
|
+
if counter_value is not None:
|
|
1207
|
+
observed = counter_value == 0
|
|
1208
|
+
if observed is None:
|
|
1209
|
+
notes.append(
|
|
1210
|
+
f"the {mnemonic!r} instruction at 0x{address:x} cannot be evaluated by "
|
|
1211
|
+
"this explorer; the path is unresolved"
|
|
1212
|
+
)
|
|
1213
|
+
return finish(_STOP_BLOCKED, None)
|
|
1214
|
+
index = len(walked)
|
|
1215
|
+
if index < len(plan):
|
|
1216
|
+
chosen = plan[index]
|
|
1217
|
+
else:
|
|
1218
|
+
chosen = observed
|
|
1219
|
+
children.append(tuple(walked) + (not observed,))
|
|
1220
|
+
basis: Basis = _BASIS_OBSERVED
|
|
1221
|
+
note = ""
|
|
1222
|
+
if chosen != observed:
|
|
1223
|
+
basis = _BASIS_SEEDED
|
|
1224
|
+
note = self._seed_branch(
|
|
1225
|
+
mnemonic, canonical, counter_name, chosen, machine.eflags()
|
|
1226
|
+
)
|
|
1227
|
+
notes.append(f"0x{address:x}: {note}")
|
|
1228
|
+
short = "taken" if chosen else "fall-through"
|
|
1229
|
+
expected = (
|
|
1230
|
+
_direct_target(terminator)
|
|
1231
|
+
if chosen
|
|
1232
|
+
else address + int(terminator.size)
|
|
1233
|
+
)
|
|
1234
|
+
try:
|
|
1235
|
+
landed = machine.step(address)
|
|
1236
|
+
except Exception as exc:
|
|
1237
|
+
return finish(*self._fault(exc, notes))
|
|
1238
|
+
used += 1
|
|
1239
|
+
if expected is not None and landed != expected:
|
|
1240
|
+
notes.append(
|
|
1241
|
+
f"{mnemonic!r} at 0x{address:x} continued at 0x{landed:x} but "
|
|
1242
|
+
f"0x{expected:x} was expected for the {short} side, so the seed did not "
|
|
1243
|
+
"steer it and the path is unresolved"
|
|
1244
|
+
)
|
|
1245
|
+
return finish(_STOP_BLOCKED, None)
|
|
1246
|
+
walked.append(chosen)
|
|
1247
|
+
if chosen:
|
|
1248
|
+
taken.append(landed)
|
|
1249
|
+
branches.append(Branch(address, mnemonic, chosen, landed, basis, note))
|
|
1250
|
+
if not machine.in_region(landed):
|
|
1251
|
+
notes.append(
|
|
1252
|
+
f"control left section {machine.section_name!r} at 0x{address:x} for "
|
|
1253
|
+
f"0x{landed:x}; the path is unresolved"
|
|
1254
|
+
)
|
|
1255
|
+
return finish(_STOP_BLOCKED, None)
|
|
1256
|
+
pc = landed
|
|
1257
|
+
continue
|
|
1258
|
+
# -- unconditional jmp/call: executed by the CPU, never assumed
|
|
1259
|
+
is_call = bool(terminator.group(capstone.CS_GRP_CALL))
|
|
1260
|
+
indirect = _direct_target(terminator) is None
|
|
1261
|
+
try:
|
|
1262
|
+
landed = machine.step(address)
|
|
1263
|
+
except Exception as exc:
|
|
1264
|
+
return finish(*self._fault(exc, notes))
|
|
1265
|
+
used += 1
|
|
1266
|
+
taken.append(landed)
|
|
1267
|
+
if indirect:
|
|
1268
|
+
notes.append(
|
|
1269
|
+
f"the {'call' if is_call else 'jump'} at 0x{address:x} is indirect: only "
|
|
1270
|
+
f"0x{landed:x}, the target of the register/memory value at that point, was "
|
|
1271
|
+
"explored; any other target it could hold was not"
|
|
1272
|
+
)
|
|
1273
|
+
if not machine.in_region(landed):
|
|
1274
|
+
notes.append(
|
|
1275
|
+
f"the {'call' if is_call else 'jump'} at 0x{address:x} left section "
|
|
1276
|
+
f"{machine.section_name!r} for 0x{landed:x} (a tail call or an import "
|
|
1277
|
+
"thunk); the path is unresolved"
|
|
1278
|
+
)
|
|
1279
|
+
return finish(_STOP_BLOCKED, None)
|
|
1280
|
+
pc = landed
|
|
1281
|
+
continue
|
|
1282
|
+
|
|
1283
|
+
# ---- helpers ----------------------------------------------------------
|
|
1284
|
+
|
|
1285
|
+
def _seed_branch(
|
|
1286
|
+
self,
|
|
1287
|
+
mnemonic: str,
|
|
1288
|
+
canonical: str | None,
|
|
1289
|
+
counter_name: str | None,
|
|
1290
|
+
chosen: bool,
|
|
1291
|
+
observed_bits: int,
|
|
1292
|
+
) -> str:
|
|
1293
|
+
"""Force one ``jcc`` direction, and describe the seed in a note."""
|
|
1294
|
+
machine = self.machine
|
|
1295
|
+
short = "taken" if chosen else "falls through"
|
|
1296
|
+
if canonical is not None:
|
|
1297
|
+
seeded, changed = _seeded_flag_bits(canonical, chosen, observed_bits)
|
|
1298
|
+
machine.write_register(machine.flags, seeded)
|
|
1299
|
+
return (
|
|
1300
|
+
f"{machine.flags} 0x{observed_bits:x} -> 0x{seeded:x} ({changed}) so "
|
|
1301
|
+
f"{mnemonic!r} {short}; that direction is assumed feasible, not proven "
|
|
1302
|
+
"(no constraint was solved)"
|
|
1303
|
+
)
|
|
1304
|
+
name = str(counter_name)
|
|
1305
|
+
previous = machine.read_register(name)
|
|
1306
|
+
machine.write_register(name, 0 if chosen else 1)
|
|
1307
|
+
return (
|
|
1308
|
+
f"{name} {previous} -> {0 if chosen else 1} so {mnemonic!r} {short}; that "
|
|
1309
|
+
"direction is assumed feasible, not proven (no constraint was solved)"
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
def _fault(self, exc: Any, notes: list[str]) -> tuple[StopReason, None]:
|
|
1313
|
+
"""Classify a unicorn fault, note it, and make it the path's stop reason."""
|
|
1314
|
+
pc = self.machine.read_register(self.machine.ip)
|
|
1315
|
+
reason, note = _classify_fault(exc, pc)
|
|
1316
|
+
notes.append(note)
|
|
1317
|
+
return reason, None
|
|
1318
|
+
|
|
1319
|
+
def _entry(
|
|
1320
|
+
self,
|
|
1321
|
+
branches: list[Branch],
|
|
1322
|
+
taken: list[int],
|
|
1323
|
+
reason: StopReason,
|
|
1324
|
+
value: int | None,
|
|
1325
|
+
notes: list[str],
|
|
1326
|
+
used: int,
|
|
1327
|
+
) -> Entry:
|
|
1328
|
+
"""Build the immutable entry for the path that just stopped."""
|
|
1329
|
+
return Entry(
|
|
1330
|
+
index=len(self.entries),
|
|
1331
|
+
decisions=tuple(branches),
|
|
1332
|
+
taken_targets=tuple(taken),
|
|
1333
|
+
registers=self.machine.snapshot(),
|
|
1334
|
+
return_value=value if reason == _STOP_RETURN else None,
|
|
1335
|
+
stopped_reason=reason,
|
|
1336
|
+
instructions=used,
|
|
1337
|
+
warnings=tuple(notes),
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
# ---- public API: the unicorn explorer -------------------------------------
|
|
1342
|
+
|
|
1343
|
+
#: What the unicorn explorer explored versus what it merely assumed. Kept in the
|
|
1344
|
+
#: evidence so no caller can read the paths as something stronger.
|
|
1345
|
+
_UNICORN_ASSUMPTIONS: tuple[str, ...] = (
|
|
1346
|
+
"every path is a sequence of branch decisions replayed on a fresh unicorn CPU, "
|
|
1347
|
+
"and each replay starts from the same seeded state",
|
|
1348
|
+
"the observed side of a conditional branch is the real emulation result: no flag "
|
|
1349
|
+
"or register was touched before it ran",
|
|
1350
|
+
"the seeded side is a flag/counter rewrite that satisfies the branch condition; it "
|
|
1351
|
+
"is assumed feasible, never proven -- no path predicate was built, no constraint "
|
|
1352
|
+
"was solved, no symbolic variable exists",
|
|
1353
|
+
"unconditional jumps and calls are executed, so their targets come from the CPU; an "
|
|
1354
|
+
"indirect transfer contributes only the target seen at that point",
|
|
1355
|
+
"register and memory values are concrete, so a path is not a proof of reachability "
|
|
1356
|
+
"for arbitrary inputs",
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
#: What the angr bridge explored versus what the unicorn explorer would report.
|
|
1360
|
+
_ANGR_ASSUMPTIONS: tuple[str, ...] = (
|
|
1361
|
+
"paths come from angr exploring the function until a ret was found; angr owns the "
|
|
1362
|
+
"constraint solver, this module does not",
|
|
1363
|
+
"the observed/seeded distinction of the unicorn explorer does not apply here: every "
|
|
1364
|
+
"path angr reported is feasible under the constraints it solved",
|
|
1365
|
+
"taken_targets lists angr's basic-block destinations; the branch kinds behind them "
|
|
1366
|
+
"are not reconstructed by this bridge",
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def _branch_counts(entries: Sequence[Entry]) -> dict[str, Any]:
|
|
1371
|
+
"""Count stopped reasons and observed/seeded branch decisions."""
|
|
1372
|
+
reasons: dict[str, int] = {}
|
|
1373
|
+
observed = 0
|
|
1374
|
+
seeded = 0
|
|
1375
|
+
for entry in entries:
|
|
1376
|
+
reasons[entry.stopped_reason] = reasons.get(entry.stopped_reason, 0) + 1
|
|
1377
|
+
for branch in entry.decisions:
|
|
1378
|
+
if branch.basis == _BASIS_SEEDED:
|
|
1379
|
+
seeded += 1
|
|
1380
|
+
else:
|
|
1381
|
+
observed += 1
|
|
1382
|
+
return {
|
|
1383
|
+
"stopped_reasons": dict(sorted(reasons.items())),
|
|
1384
|
+
"observed_branches": observed,
|
|
1385
|
+
"seeded_branches": seeded,
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def _exploration_evidence(
|
|
1390
|
+
*,
|
|
1391
|
+
explorer: str,
|
|
1392
|
+
address: int,
|
|
1393
|
+
info: BinaryInfo,
|
|
1394
|
+
bitness: int,
|
|
1395
|
+
entries: Sequence[Entry],
|
|
1396
|
+
steps: int,
|
|
1397
|
+
complete: bool,
|
|
1398
|
+
max_paths: int,
|
|
1399
|
+
max_steps: int,
|
|
1400
|
+
assumptions: Sequence[str],
|
|
1401
|
+
regions: Sequence[tuple[str, int, int]] = (),
|
|
1402
|
+
) -> Evidence:
|
|
1403
|
+
"""Build the provenance record for one exploration."""
|
|
1404
|
+
counts = _branch_counts(entries)
|
|
1405
|
+
unresolved = sum(
|
|
1406
|
+
1 for entry in entries if entry.stopped_reason in (_STOP_BLOCKED, _STOP_UNMAPPED)
|
|
1407
|
+
)
|
|
1408
|
+
trusted = complete and not counts["seeded_branches"] and not unresolved
|
|
1409
|
+
extra: dict[str, Any] = {
|
|
1410
|
+
"explorer": explorer,
|
|
1411
|
+
"paths": len(entries),
|
|
1412
|
+
"complete": complete,
|
|
1413
|
+
"unresolved_paths": unresolved,
|
|
1414
|
+
"steps": steps,
|
|
1415
|
+
"max_paths": max_paths,
|
|
1416
|
+
"max_steps": max_steps,
|
|
1417
|
+
"architecture": info.architecture,
|
|
1418
|
+
"bitness": bitness,
|
|
1419
|
+
"assumptions": list(assumptions),
|
|
1420
|
+
**counts,
|
|
1421
|
+
}
|
|
1422
|
+
if regions:
|
|
1423
|
+
extra["regions"] = [
|
|
1424
|
+
{"kind": kind, "start": f"0x{start:x}", "end": f"0x{end:x}"}
|
|
1425
|
+
for kind, start, end in regions
|
|
1426
|
+
]
|
|
1427
|
+
counted = (
|
|
1428
|
+
f"{steps} emulated instruction(s)"
|
|
1429
|
+
if explorer == "unicorn"
|
|
1430
|
+
else f"{steps} instruction(s) counted from the blocks angr visited"
|
|
1431
|
+
)
|
|
1432
|
+
return Evidence(
|
|
1433
|
+
source=_EVIDENCE_SOURCE if explorer == "unicorn" else _ANGR_SOURCE,
|
|
1434
|
+
method=_EVIDENCE_METHOD if explorer == "unicorn" else _ANGR_METHOD,
|
|
1435
|
+
confidence=Confidence.MEDIUM if trusted else Confidence.LOW,
|
|
1436
|
+
address=address,
|
|
1437
|
+
detail=(
|
|
1438
|
+
f"{len(entries)} path(s) from {counted}; "
|
|
1439
|
+
f"{counts['observed_branches']} observed and {counts['seeded_branches']} "
|
|
1440
|
+
f"seeded branch decision(s)"
|
|
1441
|
+
),
|
|
1442
|
+
extra=extra,
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def explore_paths(
|
|
1447
|
+
path: str | Path,
|
|
1448
|
+
info: BinaryInfo,
|
|
1449
|
+
address: int,
|
|
1450
|
+
max_paths: int = DEFAULT_MAX_PATHS,
|
|
1451
|
+
max_steps: int = DEFAULT_MAX_STEPS,
|
|
1452
|
+
initial_registers: Mapping[str, int] | None = None,
|
|
1453
|
+
*,
|
|
1454
|
+
stack_size: int = emulate.DEFAULT_STACK_SIZE,
|
|
1455
|
+
) -> PathExploration:
|
|
1456
|
+
"""Explore the paths through one function with the unicorn explorer.
|
|
1457
|
+
|
|
1458
|
+
This is the default explorer and needs nothing beyond the optional unicorn
|
|
1459
|
+
build the emulator already uses. Every path is a sequence of branch
|
|
1460
|
+
decisions replayed on a freshly initialised CPU; see the module docstring
|
|
1461
|
+
for the guarantees and for exactly what is *observed* versus *seeded*.
|
|
1462
|
+
|
|
1463
|
+
Args:
|
|
1464
|
+
path: Binary file (only its bytes are read; nothing is executed on the host).
|
|
1465
|
+
info: Loader metadata for that file.
|
|
1466
|
+
address: Start address, inside an executable section with file bytes.
|
|
1467
|
+
max_paths: Maximum number of paths to report.
|
|
1468
|
+
max_steps: Instruction budget for the whole exploration, replays included.
|
|
1469
|
+
initial_registers: Register seeds for every path (``{"edi": 0}``).
|
|
1470
|
+
stack_size: Size of the emulated stack region.
|
|
1471
|
+
|
|
1472
|
+
Returns:
|
|
1473
|
+
A :class:`PathExploration` with up to ``max_paths`` entries in
|
|
1474
|
+
deterministic breadth-first order.
|
|
1475
|
+
|
|
1476
|
+
Raises:
|
|
1477
|
+
AnalysisError: On an unusable address, an out-of-range limit, an unknown
|
|
1478
|
+
register name, a missing unicorn build, or an architecture this
|
|
1479
|
+
module does not emulate (x86 and x86-64 only).
|
|
1480
|
+
"""
|
|
1481
|
+
target = Path(path)
|
|
1482
|
+
start = _validated_address(address)
|
|
1483
|
+
path_limit = _validated_limit("max_paths", max_paths, MAX_MAX_PATHS)
|
|
1484
|
+
step_limit = _validated_limit("max_steps", max_steps, MAX_MAX_STEPS)
|
|
1485
|
+
if isinstance(stack_size, bool) or not isinstance(stack_size, int):
|
|
1486
|
+
raise AnalysisError("stack_size must be an int")
|
|
1487
|
+
if not emulate.PAGE_SIZE <= stack_size <= emulate.MAX_STACK_SIZE:
|
|
1488
|
+
raise AnalysisError(
|
|
1489
|
+
f"stack_size must be between {emulate.PAGE_SIZE} and "
|
|
1490
|
+
f"{emulate.MAX_STACK_SIZE} byte(s)"
|
|
1491
|
+
)
|
|
1492
|
+
if not EMULATOR_AVAILABLE:
|
|
1493
|
+
raise AnalysisError(
|
|
1494
|
+
"path exploration needs the optional unicorn engine: "
|
|
1495
|
+
f"{emulate.emulator_status()}; install it with "
|
|
1496
|
+
"'pip install codevanta[emulation]'"
|
|
1497
|
+
)
|
|
1498
|
+
bits = _machine_bits(info)
|
|
1499
|
+
if bits is None:
|
|
1500
|
+
raise AnalysisError(
|
|
1501
|
+
f"path exploration supports x86 and x86-64 only, not "
|
|
1502
|
+
f"{info.architecture!r} ({info.bitness}-bit, {info.endianness}); no code was run"
|
|
1503
|
+
)
|
|
1504
|
+
seeds = _validated_registers(initial_registers, bits)
|
|
1505
|
+
section = info.section_for_va(start)
|
|
1506
|
+
if section is None:
|
|
1507
|
+
raise AnalysisError(f"address 0x{start:x} is not mapped by any section")
|
|
1508
|
+
if not section.is_executable:
|
|
1509
|
+
raise AnalysisError(
|
|
1510
|
+
f"address 0x{start:x} is inside section {section.name!r}, which is not executable"
|
|
1511
|
+
)
|
|
1512
|
+
if info.va_to_offset(start) is None:
|
|
1513
|
+
raise AnalysisError(f"address 0x{start:x} has no file bytes to execute")
|
|
1514
|
+
machine = _Machine(target, info, start, bits=bits, stack_size=stack_size)
|
|
1515
|
+
try:
|
|
1516
|
+
machine.open()
|
|
1517
|
+
except AnalysisError:
|
|
1518
|
+
machine.close()
|
|
1519
|
+
raise
|
|
1520
|
+
except Exception as exc: # pragma: no cover - depends on the unicorn build
|
|
1521
|
+
machine.close()
|
|
1522
|
+
raise AnalysisError(f"the emulator could not be initialised: {exc}") from exc
|
|
1523
|
+
explorer = _Explorer(
|
|
1524
|
+
machine,
|
|
1525
|
+
_Scanner(target, info, _Decoder(bits == 64)),
|
|
1526
|
+
max_paths=path_limit,
|
|
1527
|
+
max_steps=step_limit,
|
|
1528
|
+
seeds=seeds,
|
|
1529
|
+
)
|
|
1530
|
+
try:
|
|
1531
|
+
explorer.run()
|
|
1532
|
+
entries = tuple(explorer.entries)
|
|
1533
|
+
complete = explorer.complete
|
|
1534
|
+
evidence = _exploration_evidence(
|
|
1535
|
+
explorer="unicorn",
|
|
1536
|
+
address=start,
|
|
1537
|
+
info=info,
|
|
1538
|
+
bitness=bits,
|
|
1539
|
+
entries=entries,
|
|
1540
|
+
steps=explorer.steps,
|
|
1541
|
+
complete=complete,
|
|
1542
|
+
max_paths=path_limit,
|
|
1543
|
+
max_steps=step_limit,
|
|
1544
|
+
assumptions=_UNICORN_ASSUMPTIONS,
|
|
1545
|
+
regions=tuple(machine.regions),
|
|
1546
|
+
)
|
|
1547
|
+
return PathExploration(
|
|
1548
|
+
address=start,
|
|
1549
|
+
explorer="unicorn",
|
|
1550
|
+
architecture=info.architecture,
|
|
1551
|
+
bitness=bits,
|
|
1552
|
+
max_paths=path_limit,
|
|
1553
|
+
max_steps=step_limit,
|
|
1554
|
+
steps=explorer.steps,
|
|
1555
|
+
complete=complete,
|
|
1556
|
+
entries=entries,
|
|
1557
|
+
warnings=tuple(explorer.warnings),
|
|
1558
|
+
evidence=evidence,
|
|
1559
|
+
)
|
|
1560
|
+
finally:
|
|
1561
|
+
machine.close()
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
# ---- optional angr bridge -------------------------------------------------
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def _probe_angr() -> bool:
|
|
1568
|
+
"""True when a module called ``angr`` is findable; never imports angr."""
|
|
1569
|
+
try:
|
|
1570
|
+
return _importlib_util.find_spec("angr") is not None
|
|
1571
|
+
except Exception: # pragma: no cover - depends on the installation
|
|
1572
|
+
return False
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
#: True when a module named ``angr`` is findable. This is a *probe*, not an
|
|
1576
|
+
#: import: angr is imported only inside :func:`explore_with_angr`, so importing
|
|
1577
|
+
#: this module stays cheap and keeps working on a machine without angr.
|
|
1578
|
+
ANGR_AVAILABLE = _probe_angr()
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def angr_available() -> bool:
|
|
1582
|
+
"""Return whether the optional angr bridge can be used on this machine."""
|
|
1583
|
+
return ANGR_AVAILABLE
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
def _import_angr() -> Any:
|
|
1587
|
+
"""Import angr lazily, or raise :class:`AnalysisError` naming it and the extra."""
|
|
1588
|
+
if not ANGR_AVAILABLE:
|
|
1589
|
+
raise AnalysisError(
|
|
1590
|
+
"'angr' is not installed, so explore_with_angr cannot run: the angr bridge is "
|
|
1591
|
+
"optional in CODEVANTA and angr is never imported at module import time; "
|
|
1592
|
+
f"install it with '{_ANGR_HINT}' (angr is large, which is why "
|
|
1593
|
+
"explore_paths needs only unicorn)"
|
|
1594
|
+
)
|
|
1595
|
+
try:
|
|
1596
|
+
import angr
|
|
1597
|
+
except Exception as exc:
|
|
1598
|
+
raise AnalysisError(
|
|
1599
|
+
f"importing 'angr' failed ({type(exc).__name__}: {exc}); reinstall it with "
|
|
1600
|
+
f"'{_ANGR_HINT}', or use explore_paths, which needs only unicorn"
|
|
1601
|
+
) from exc
|
|
1602
|
+
return angr
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
def _ret_addresses(
|
|
1606
|
+
path: Path, info: BinaryInfo, address: int, decoder: _Decoder, section: Any
|
|
1607
|
+
) -> tuple[int, ...]:
|
|
1608
|
+
"""Collect the ``ret`` addresses in ``section`` by a bounded linear sweep.
|
|
1609
|
+
|
|
1610
|
+
The sweep is only used to build angr's ``find`` set, so a byte that cannot be
|
|
1611
|
+
decoded costs one resynchronisation step rather than a wrong target.
|
|
1612
|
+
"""
|
|
1613
|
+
found: list[int] = []
|
|
1614
|
+
end = section.virtual_address + section.size
|
|
1615
|
+
pc = address
|
|
1616
|
+
for _ in range(MAX_SCAN_INSTRUCTIONS):
|
|
1617
|
+
if pc >= end:
|
|
1618
|
+
break
|
|
1619
|
+
try:
|
|
1620
|
+
data = read_code(path, info, pc, _MAX_INSTRUCTION_BYTES)
|
|
1621
|
+
except AnalysisError:
|
|
1622
|
+
break
|
|
1623
|
+
insn = decoder.decode(bytes(data), pc)
|
|
1624
|
+
if insn is None:
|
|
1625
|
+
pc += 1
|
|
1626
|
+
continue
|
|
1627
|
+
if str(insn.mnemonic).lower() in _RET_MNEMONICS:
|
|
1628
|
+
found.append(pc)
|
|
1629
|
+
pc += int(insn.size)
|
|
1630
|
+
return tuple(found)
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
def _count_path_instructions(
|
|
1634
|
+
path: Path, info: BinaryInfo, decoder: _Decoder, blocks: Sequence[int]
|
|
1635
|
+
) -> int:
|
|
1636
|
+
"""Count the instructions of an angr path from the block addresses it visited."""
|
|
1637
|
+
total = 0
|
|
1638
|
+
for block in blocks:
|
|
1639
|
+
pc = block
|
|
1640
|
+
for _ in range(MAX_BLOCK_INSTRUCTIONS):
|
|
1641
|
+
try:
|
|
1642
|
+
data = read_code(path, info, pc, _MAX_INSTRUCTION_BYTES)
|
|
1643
|
+
except AnalysisError:
|
|
1644
|
+
break
|
|
1645
|
+
insn = decoder.decode(bytes(data), pc)
|
|
1646
|
+
if insn is None:
|
|
1647
|
+
break
|
|
1648
|
+
total += 1
|
|
1649
|
+
if _ends_block(insn):
|
|
1650
|
+
break
|
|
1651
|
+
pc += int(insn.size)
|
|
1652
|
+
return total
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def _angr_entry(
|
|
1656
|
+
index: int,
|
|
1657
|
+
state: Any,
|
|
1658
|
+
*,
|
|
1659
|
+
names: Sequence[str],
|
|
1660
|
+
return_register: str,
|
|
1661
|
+
path: Path,
|
|
1662
|
+
info: BinaryInfo,
|
|
1663
|
+
decoder: _Decoder,
|
|
1664
|
+
) -> Entry:
|
|
1665
|
+
"""Build one :class:`Entry` from an angr found state."""
|
|
1666
|
+
registers: dict[str, int] = {}
|
|
1667
|
+
for name in names:
|
|
1668
|
+
try:
|
|
1669
|
+
registers[name] = int(getattr(state.regs, name))
|
|
1670
|
+
except Exception: # pragma: no cover - angr owns the register vocabulary
|
|
1671
|
+
continue
|
|
1672
|
+
blocks = tuple(int(block) for block in getattr(state.history, "bbl_addrs", ()))
|
|
1673
|
+
return Entry(
|
|
1674
|
+
index=index,
|
|
1675
|
+
decisions=(),
|
|
1676
|
+
taken_targets=blocks[1:],
|
|
1677
|
+
registers=registers,
|
|
1678
|
+
return_value=registers.get(return_register),
|
|
1679
|
+
stopped_reason=_STOP_RETURN,
|
|
1680
|
+
instructions=_count_path_instructions(path, info, decoder, blocks),
|
|
1681
|
+
warnings=(
|
|
1682
|
+
"this path comes from angr's symbolic execution; the branch decisions the "
|
|
1683
|
+
"unicorn explorer would record were not reconstructed, so decisions is empty "
|
|
1684
|
+
"and taken_targets lists angr's basic-block destinations",
|
|
1685
|
+
),
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
def explore_with_angr(
|
|
1690
|
+
path: str | Path,
|
|
1691
|
+
info: BinaryInfo,
|
|
1692
|
+
address: int,
|
|
1693
|
+
max_paths: int = DEFAULT_MAX_PATHS,
|
|
1694
|
+
initial_registers: Mapping[str, int] | None = None,
|
|
1695
|
+
) -> PathExploration:
|
|
1696
|
+
"""Explore the same paths with angr, when angr is installed.
|
|
1697
|
+
|
|
1698
|
+
angr is **optional** and imported here, never at module import time. Explore
|
|
1699
|
+
the function symbolically until a ``ret`` (``find=ret``), and report up to
|
|
1700
|
+
``max_paths`` found paths. The ``observed``/``seeded`` distinction of the
|
|
1701
|
+
unicorn explorer does not apply: these paths are whatever angr's simulation
|
|
1702
|
+
manager could solve, and the evidence says so.
|
|
1703
|
+
|
|
1704
|
+
Args:
|
|
1705
|
+
path: Binary file.
|
|
1706
|
+
info: Loader metadata for that file.
|
|
1707
|
+
address: Start address of the function.
|
|
1708
|
+
max_paths: Maximum number of paths to report.
|
|
1709
|
+
initial_registers: Concrete register seeds applied to the entry state.
|
|
1710
|
+
|
|
1711
|
+
Returns:
|
|
1712
|
+
A :class:`PathExploration` whose ``explorer`` is ``"angr"``.
|
|
1713
|
+
|
|
1714
|
+
Raises:
|
|
1715
|
+
AnalysisError: When angr is not installed (the message names angr and the
|
|
1716
|
+
extra that installs it), when angr fails to explore, when the machine
|
|
1717
|
+
is not x86/x86-64, or on a bad address or limit.
|
|
1718
|
+
"""
|
|
1719
|
+
target = Path(path)
|
|
1720
|
+
start = _validated_address(address)
|
|
1721
|
+
path_limit = _validated_limit("max_paths", max_paths, MAX_MAX_PATHS)
|
|
1722
|
+
bits = _machine_bits(info)
|
|
1723
|
+
if bits is None:
|
|
1724
|
+
raise AnalysisError(
|
|
1725
|
+
"the angr bridge reports x86 register names, so it supports x86 and x86-64 "
|
|
1726
|
+
f"only, not {info.architecture!r}; nothing was explored"
|
|
1727
|
+
)
|
|
1728
|
+
seeds = _validated_registers(initial_registers, bits)
|
|
1729
|
+
module = _import_angr()
|
|
1730
|
+
section = info.section_for_va(start)
|
|
1731
|
+
if section is None:
|
|
1732
|
+
raise AnalysisError(f"address 0x{start:x} is not mapped by any section")
|
|
1733
|
+
if not section.is_executable:
|
|
1734
|
+
raise AnalysisError(
|
|
1735
|
+
f"address 0x{start:x} is inside section {section.name!r}, which is not executable"
|
|
1736
|
+
)
|
|
1737
|
+
decoder = _Decoder(bits == 64)
|
|
1738
|
+
rets = _ret_addresses(target, info, start, decoder, section)
|
|
1739
|
+
if not rets:
|
|
1740
|
+
raise AnalysisError(
|
|
1741
|
+
f"no ret instruction was found scanning {section.name!r} from 0x{start:x}, so "
|
|
1742
|
+
"the angr bridge has nothing to use as find; nothing was explored"
|
|
1743
|
+
)
|
|
1744
|
+
try:
|
|
1745
|
+
project = module.Project(str(target), auto_load_libs=False)
|
|
1746
|
+
state = project.factory.entry_state(addr=start)
|
|
1747
|
+
for name, value in seeds.items():
|
|
1748
|
+
setattr(state.regs, name, value)
|
|
1749
|
+
simgr = project.factory.simgr(state)
|
|
1750
|
+
simgr.explore(find=rets, num_find=path_limit)
|
|
1751
|
+
found = list(simgr.found)[:path_limit]
|
|
1752
|
+
remaining = len(simgr.active) + len(simgr.errored)
|
|
1753
|
+
except AnalysisError: # pragma: no cover - nothing above raises this
|
|
1754
|
+
raise
|
|
1755
|
+
except Exception as exc:
|
|
1756
|
+
raise AnalysisError(
|
|
1757
|
+
f"angr exploration of 0x{start:x} in {target.name} failed: "
|
|
1758
|
+
f"{type(exc).__name__}: {exc}"
|
|
1759
|
+
) from exc
|
|
1760
|
+
names = _REGISTERS_64 if bits == 64 else _REGISTERS_32
|
|
1761
|
+
entries = tuple(
|
|
1762
|
+
_angr_entry(
|
|
1763
|
+
index,
|
|
1764
|
+
found_state,
|
|
1765
|
+
names=names,
|
|
1766
|
+
return_register="rax" if bits == 64 else "eax",
|
|
1767
|
+
path=target,
|
|
1768
|
+
info=info,
|
|
1769
|
+
decoder=decoder,
|
|
1770
|
+
)
|
|
1771
|
+
for index, found_state in enumerate(found)
|
|
1772
|
+
)
|
|
1773
|
+
warnings: list[str] = []
|
|
1774
|
+
if remaining:
|
|
1775
|
+
warnings.append(
|
|
1776
|
+
f"{remaining} state(s) were still active or errored when angr stopped; those "
|
|
1777
|
+
"paths are not reported"
|
|
1778
|
+
)
|
|
1779
|
+
if len(found) >= path_limit:
|
|
1780
|
+
warnings.append(
|
|
1781
|
+
f"path limit reached: {path_limit} path(s) were reported, so more may exist"
|
|
1782
|
+
)
|
|
1783
|
+
steps = sum(entry.instructions for entry in entries)
|
|
1784
|
+
complete = not warnings and not remaining
|
|
1785
|
+
evidence = _exploration_evidence(
|
|
1786
|
+
explorer="angr",
|
|
1787
|
+
address=start,
|
|
1788
|
+
info=info,
|
|
1789
|
+
bitness=bits,
|
|
1790
|
+
entries=entries,
|
|
1791
|
+
steps=steps,
|
|
1792
|
+
complete=complete,
|
|
1793
|
+
max_paths=path_limit,
|
|
1794
|
+
max_steps=0,
|
|
1795
|
+
assumptions=_ANGR_ASSUMPTIONS,
|
|
1796
|
+
)
|
|
1797
|
+
return PathExploration(
|
|
1798
|
+
address=start,
|
|
1799
|
+
explorer="angr",
|
|
1800
|
+
architecture=info.architecture,
|
|
1801
|
+
bitness=bits,
|
|
1802
|
+
max_paths=path_limit,
|
|
1803
|
+
max_steps=0,
|
|
1804
|
+
steps=steps,
|
|
1805
|
+
complete=complete,
|
|
1806
|
+
entries=entries,
|
|
1807
|
+
warnings=tuple(warnings),
|
|
1808
|
+
evidence=evidence,
|
|
1809
|
+
)
|