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,250 @@
|
|
|
1
|
+
"""Pluggable decompiler providers: real decompilers when installed, native always.
|
|
2
|
+
|
|
3
|
+
CODEVANTA can render one function with three different providers, and picks one
|
|
4
|
+
without ever assuming a third-party package is present:
|
|
5
|
+
|
|
6
|
+
=========== ============================================== =====================
|
|
7
|
+
backend runs when output
|
|
8
|
+
=========== ============================================== =====================
|
|
9
|
+
``ghidra`` ``pyghidra`` imports **and** ``GHIDRA_INSTALL_DIR`` Ghidra's C
|
|
10
|
+
points at a Ghidra installation
|
|
11
|
+
``angr`` ``angr`` imports angr's C
|
|
12
|
+
``native`` always heuristic pseudo-C
|
|
13
|
+
=========== ============================================== =====================
|
|
14
|
+
|
|
15
|
+
Only the last one is a CODEVANTA dependency: the optional backends import their
|
|
16
|
+
heavy module lazily inside ``availability()`` / ``decompile()``, so this package
|
|
17
|
+
imports and answers :func:`available_backends` on a machine with neither angr nor
|
|
18
|
+
Ghidra, and never raises ``ImportError`` for a missing decompiler.
|
|
19
|
+
|
|
20
|
+
Public API
|
|
21
|
+
----------
|
|
22
|
+
* :class:`BackendAvailability` -- ``name``, ``available``, a non-empty ``reason``
|
|
23
|
+
and the ``requires`` tuple for each backend, in the order ``ghidra``, ``angr``,
|
|
24
|
+
``native``.
|
|
25
|
+
* :class:`BackendResult` -- ``text``, ``backend``, ``language``, ``warnings`` and
|
|
26
|
+
:class:`Evidence`, with a JSON-safe :meth:`BackendResult.to_dict`.
|
|
27
|
+
* :class:`DecompilerBackend` -- the protocol a provider implements.
|
|
28
|
+
* :func:`available_backends` -- probe all three, in order.
|
|
29
|
+
* :func:`get_backend` -- look one up by name; an unknown name raises
|
|
30
|
+
:class:`AnalysisError` listing the supported set.
|
|
31
|
+
* :func:`decompile_function` -- the one entry point callers need.
|
|
32
|
+
|
|
33
|
+
Selection policy
|
|
34
|
+
----------------
|
|
35
|
+
``backend="native"`` always works. ``backend="angr"`` / ``"ghidra"`` are strict:
|
|
36
|
+
when the backend is unavailable the call raises :class:`AnalysisError` whose
|
|
37
|
+
message carries the probe's reason (install hint included) -- it never leaks an
|
|
38
|
+
``ImportError`` and never silently substitutes another provider.
|
|
39
|
+
``backend="auto"`` (the default) prefers the first available external backend in
|
|
40
|
+
the order ``ghidra``, ``angr`` and falls back to ``native``; whatever it chose,
|
|
41
|
+
the returned :class:`BackendResult` carries one warning per unavailable backend
|
|
42
|
+
naming that backend and its reason, so a fallback is never silent.
|
|
43
|
+
|
|
44
|
+
Budgets are validated once, here (:func:`codevanta.analysis.decompilers.base.
|
|
45
|
+
require_budget`), before any backend is touched: an out-of-range
|
|
46
|
+
``max_instructions`` / ``max_blocks`` raises regardless of the backend.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
from dataclasses import replace
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
from codevanta.analysis.decompile import (
|
|
55
|
+
DEFAULT_MAX_BLOCKS,
|
|
56
|
+
DEFAULT_MAX_INSTRUCTIONS,
|
|
57
|
+
MAX_MAX_BLOCKS,
|
|
58
|
+
MAX_MAX_INSTRUCTIONS,
|
|
59
|
+
)
|
|
60
|
+
from codevanta.core.loader import BinaryInfo
|
|
61
|
+
from codevanta.core.report import AnalysisError
|
|
62
|
+
|
|
63
|
+
from .angr_backend import AngrBackend
|
|
64
|
+
from .base import (
|
|
65
|
+
LANGUAGE,
|
|
66
|
+
BackendAvailability,
|
|
67
|
+
BackendResult,
|
|
68
|
+
DecompilerBackend,
|
|
69
|
+
NativeBackend,
|
|
70
|
+
require_budget,
|
|
71
|
+
)
|
|
72
|
+
from .ghidra_backend import GhidraBackend
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"AUTO",
|
|
76
|
+
"LANGUAGE",
|
|
77
|
+
"AngrBackend",
|
|
78
|
+
"BackendAvailability",
|
|
79
|
+
"BackendResult",
|
|
80
|
+
"DecompilerBackend",
|
|
81
|
+
"GhidraBackend",
|
|
82
|
+
"NativeBackend",
|
|
83
|
+
"available_backends",
|
|
84
|
+
"decompile_function",
|
|
85
|
+
"get_backend",
|
|
86
|
+
"require_budget",
|
|
87
|
+
"supported_backends",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
#: External backends in preference order for ``backend="auto"``.
|
|
91
|
+
_EXTERNAL_ORDER: tuple[str, ...] = ("ghidra", "angr")
|
|
92
|
+
|
|
93
|
+
#: Every supported backend, in report order (external ones first).
|
|
94
|
+
_BACKEND_ORDER: tuple[str, ...] = (*_EXTERNAL_ORDER, "native")
|
|
95
|
+
|
|
96
|
+
#: Backend instances by name; one shared stateless instance each.
|
|
97
|
+
_BACKENDS: dict[str, DecompilerBackend] = {
|
|
98
|
+
backend.name: backend for backend in (GhidraBackend(), AngrBackend(), NativeBackend())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#: Name used by :func:`decompile_function` to mean "pick for me".
|
|
102
|
+
AUTO = "auto"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def supported_backends() -> tuple[str, ...]:
|
|
106
|
+
"""Return the supported backend names, in report order."""
|
|
107
|
+
return _BACKEND_ORDER
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _probe(backend: DecompilerBackend) -> BackendAvailability:
|
|
111
|
+
"""Run one availability probe, turning a crash into an "unavailable" record."""
|
|
112
|
+
try:
|
|
113
|
+
return backend.availability()
|
|
114
|
+
except Exception as exc: # a broken optional dependency must never break discovery
|
|
115
|
+
return BackendAvailability(
|
|
116
|
+
name=backend.name,
|
|
117
|
+
available=False,
|
|
118
|
+
reason=f"the availability probe itself failed ({type(exc).__name__}: {exc})",
|
|
119
|
+
requires=tuple(backend.requires),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def available_backends() -> tuple[BackendAvailability, ...]:
|
|
124
|
+
"""Probe every backend without installing or launching anything.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
One record per supported backend, always in the order ``ghidra``, ``angr``,
|
|
128
|
+
``native``. Unavailable records always carry a non-empty reason.
|
|
129
|
+
"""
|
|
130
|
+
return tuple(_probe(_BACKENDS[name]) for name in _BACKEND_ORDER)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_backend(name: str) -> DecompilerBackend:
|
|
134
|
+
"""Return the backend registered under ``name``.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
name: ``ghidra``, ``angr`` or ``native``; matched case-insensitively and
|
|
138
|
+
ignoring surrounding whitespace.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The shared backend instance. Availability is *not* checked here; call
|
|
142
|
+
:meth:`DecompilerBackend.availability` or use :func:`decompile_function`.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
AnalysisError: When the name is unknown; the message lists the supported set.
|
|
146
|
+
"""
|
|
147
|
+
key = name.strip().lower() if isinstance(name, str) else ""
|
|
148
|
+
backend = _BACKENDS.get(key)
|
|
149
|
+
if backend is None:
|
|
150
|
+
raise AnalysisError(
|
|
151
|
+
f"unknown decompiler backend {name!r}; supported backends: {', '.join(_BACKEND_ORDER)}"
|
|
152
|
+
)
|
|
153
|
+
return backend
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _unavailable_message(availability: BackendAvailability) -> str:
|
|
157
|
+
"""Explain why a strictly requested backend cannot run."""
|
|
158
|
+
return (
|
|
159
|
+
f"decompiler backend {availability.name!r} is unavailable: {availability.reason}; "
|
|
160
|
+
"use backend='auto' to fall back to another provider, or backend='native'"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def decompile_function(
|
|
165
|
+
path: str | Path,
|
|
166
|
+
info: BinaryInfo,
|
|
167
|
+
address: int,
|
|
168
|
+
backend: str = AUTO,
|
|
169
|
+
max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
|
|
170
|
+
max_blocks: int = DEFAULT_MAX_BLOCKS,
|
|
171
|
+
) -> BackendResult:
|
|
172
|
+
"""Decompile one function with the requested backend.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
path: Binary the function lives in (opened read-only; never executed).
|
|
176
|
+
info: Loaded metadata for ``path``.
|
|
177
|
+
address: Virtual address of the function.
|
|
178
|
+
backend: ``"auto"`` (default), ``"native"``, ``"angr"`` or ``"ghidra"``.
|
|
179
|
+
max_instructions: Instruction budget, validated against
|
|
180
|
+
:data:`~codevanta.analysis.decompile.MAX_MAX_INSTRUCTIONS`.
|
|
181
|
+
max_blocks: Block budget, validated against
|
|
182
|
+
:data:`~codevanta.analysis.decompile.MAX_MAX_BLOCKS`.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The :class:`BackendResult` of whichever backend produced the text.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
AnalysisError: On an unknown backend name, on an out-of-range budget, when
|
|
189
|
+
a strictly requested backend is unavailable (the message carries the
|
|
190
|
+
reason), or when the backend itself rejects the request. A missing
|
|
191
|
+
optional dependency never surfaces as ``ImportError``.
|
|
192
|
+
"""
|
|
193
|
+
instruction_budget = require_budget("max_instructions", max_instructions, MAX_MAX_INSTRUCTIONS)
|
|
194
|
+
block_budget = require_budget("max_blocks", max_blocks, MAX_MAX_BLOCKS)
|
|
195
|
+
if not isinstance(backend, str):
|
|
196
|
+
raise AnalysisError(f"backend must be a string, not {type(backend).__name__}")
|
|
197
|
+
key = backend.strip().lower() or AUTO
|
|
198
|
+
if key == AUTO:
|
|
199
|
+
return _decompile_auto(path, info, address, instruction_budget, block_budget)
|
|
200
|
+
chosen = get_backend(key)
|
|
201
|
+
availability = _probe(chosen)
|
|
202
|
+
if not availability.available:
|
|
203
|
+
raise AnalysisError(_unavailable_message(availability))
|
|
204
|
+
return chosen.decompile(
|
|
205
|
+
path,
|
|
206
|
+
info,
|
|
207
|
+
address,
|
|
208
|
+
max_instructions=instruction_budget,
|
|
209
|
+
max_blocks=block_budget,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _decompile_auto(
|
|
214
|
+
path: str | Path,
|
|
215
|
+
info: BinaryInfo,
|
|
216
|
+
address: int,
|
|
217
|
+
max_instructions: int,
|
|
218
|
+
max_blocks: int,
|
|
219
|
+
) -> BackendResult:
|
|
220
|
+
"""Pick the best available provider and record every fallback in ``warnings``."""
|
|
221
|
+
probes = tuple(_probe(_BACKENDS[name]) for name in _EXTERNAL_ORDER)
|
|
222
|
+
notes = [
|
|
223
|
+
f"auto: backend {probe.name!r} was unavailable and skipped: {probe.reason}"
|
|
224
|
+
for probe in probes
|
|
225
|
+
if not probe.available
|
|
226
|
+
]
|
|
227
|
+
for probe in probes:
|
|
228
|
+
if not probe.available:
|
|
229
|
+
continue
|
|
230
|
+
try:
|
|
231
|
+
result = _BACKENDS[probe.name].decompile(
|
|
232
|
+
path,
|
|
233
|
+
info,
|
|
234
|
+
address,
|
|
235
|
+
max_instructions=max_instructions,
|
|
236
|
+
max_blocks=max_blocks,
|
|
237
|
+
)
|
|
238
|
+
except AnalysisError as exc:
|
|
239
|
+
notes.append(f"auto: backend {probe.name!r} failed and was skipped: {exc}")
|
|
240
|
+
continue
|
|
241
|
+
return replace(result, warnings=(*notes, *result.warnings))
|
|
242
|
+
|
|
243
|
+
result = _BACKENDS["native"].decompile(
|
|
244
|
+
path,
|
|
245
|
+
info,
|
|
246
|
+
address,
|
|
247
|
+
max_instructions=max_instructions,
|
|
248
|
+
max_blocks=max_blocks,
|
|
249
|
+
)
|
|
250
|
+
return replace(result, warnings=(*notes, *result.warnings))
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""angr decompiler backend (optional dependency, imported lazily).
|
|
2
|
+
|
|
3
|
+
``angr`` ships a decompiler analysis on top of its CFG recovery, so it is the
|
|
4
|
+
cheapest way to get real pseudo-C without an external process. The package is
|
|
5
|
+
*not* a hard requirement of CODEVANTA: nothing here is imported until
|
|
6
|
+
:meth:`AngrBackend.availability` or :meth:`AngrBackend.decompile` runs, so
|
|
7
|
+
``import codevanta.analysis.decompilers`` never fails on a machine without angr.
|
|
8
|
+
|
|
9
|
+
Pipeline: ``angr.Project(path)`` (no shared libraries, no DWARF) -> ``CFGFast``
|
|
10
|
+
-> ``project.analyses.Decompiler(function, cfg=cfg.model)`` -> ``codegen.text``.
|
|
11
|
+
|
|
12
|
+
Budgeting: angr exposes no instruction or block budget on ``CFGFast`` and none on
|
|
13
|
+
``Decompiler``. The work is bounded *indirectly* -- the CFG starts from the one
|
|
14
|
+
requested address and indirect-jump resolution is disabled -- and the resulting
|
|
15
|
+
block count is checked against ``max_blocks`` afterwards, which can only be
|
|
16
|
+
reported as a warning. ``max_instructions`` cannot be enforced at all; both
|
|
17
|
+
budgets are recorded in the evidence so a reader can see what was asked for.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from codevanta.analysis.decompile import (
|
|
26
|
+
DEFAULT_MAX_BLOCKS,
|
|
27
|
+
DEFAULT_MAX_INSTRUCTIONS,
|
|
28
|
+
MAX_MAX_BLOCKS,
|
|
29
|
+
MAX_MAX_INSTRUCTIONS,
|
|
30
|
+
)
|
|
31
|
+
from codevanta.core.loader import BinaryInfo
|
|
32
|
+
from codevanta.core.report import AnalysisError
|
|
33
|
+
from codevanta.evidence.confidence import Confidence
|
|
34
|
+
from codevanta.evidence.models import Evidence
|
|
35
|
+
|
|
36
|
+
from .base import (
|
|
37
|
+
LANGUAGE,
|
|
38
|
+
BackendAvailability,
|
|
39
|
+
BackendResult,
|
|
40
|
+
require_budget,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = ["AngrBackend", "INSTALL_HINT", "NAME"]
|
|
44
|
+
|
|
45
|
+
#: Backend identifier.
|
|
46
|
+
NAME = "angr"
|
|
47
|
+
|
|
48
|
+
#: What a user has to install for this backend to become available.
|
|
49
|
+
INSTALL_HINT = "pip install angr"
|
|
50
|
+
|
|
51
|
+
#: Evidence source label.
|
|
52
|
+
_SOURCE = "angr.decompiler"
|
|
53
|
+
|
|
54
|
+
#: Evidence method label.
|
|
55
|
+
_METHOD = "analyses.Decompiler"
|
|
56
|
+
|
|
57
|
+
#: Comment emitted when angr's decompiler produced no code.
|
|
58
|
+
_EMPTY_TEXT = "/* angr produced no pseudocode for 0x{address:x} */"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _unavailable(availability: BackendAvailability) -> AnalysisError:
|
|
62
|
+
"""Build the error raised when a caller asked for an unavailable backend."""
|
|
63
|
+
return AnalysisError(
|
|
64
|
+
f"decompiler backend {availability.name!r} is unavailable: {availability.reason}; "
|
|
65
|
+
"use backend='native' or backend='auto' instead"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AngrBackend:
|
|
70
|
+
"""Decompilation through angr's ``analyses.Decompiler``.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
name: ``"angr"``.
|
|
74
|
+
requires: ``("angr",)``.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
name: str = NAME
|
|
78
|
+
requires: tuple[str, ...] = ("angr",)
|
|
79
|
+
|
|
80
|
+
def availability(self) -> BackendAvailability:
|
|
81
|
+
"""Probe ``import angr``; never raises, never installs anything."""
|
|
82
|
+
try:
|
|
83
|
+
import angr
|
|
84
|
+
except ImportError as exc:
|
|
85
|
+
return BackendAvailability(
|
|
86
|
+
name=self.name,
|
|
87
|
+
available=False,
|
|
88
|
+
reason=(
|
|
89
|
+
f"the 'angr' package is not installed ({type(exc).__name__}: {exc}); "
|
|
90
|
+
f"install it with '{INSTALL_HINT}'"
|
|
91
|
+
),
|
|
92
|
+
requires=self.requires,
|
|
93
|
+
)
|
|
94
|
+
except Exception as exc: # a broken install must not break discovery
|
|
95
|
+
return BackendAvailability(
|
|
96
|
+
name=self.name,
|
|
97
|
+
available=False,
|
|
98
|
+
reason=(
|
|
99
|
+
f"importing 'angr' failed ({type(exc).__name__}: {exc}); "
|
|
100
|
+
f"reinstall it with '{INSTALL_HINT}'"
|
|
101
|
+
),
|
|
102
|
+
requires=self.requires,
|
|
103
|
+
)
|
|
104
|
+
version = str(getattr(angr, "__version__", "unknown"))
|
|
105
|
+
return BackendAvailability(
|
|
106
|
+
name=self.name,
|
|
107
|
+
available=True,
|
|
108
|
+
reason=f"angr {version} is importable; CFGFast + analyses.Decompiler can run",
|
|
109
|
+
requires=self.requires,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def decompile(
|
|
113
|
+
self,
|
|
114
|
+
path: str | Path,
|
|
115
|
+
info: BinaryInfo,
|
|
116
|
+
address: int,
|
|
117
|
+
max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
|
|
118
|
+
max_blocks: int = DEFAULT_MAX_BLOCKS,
|
|
119
|
+
) -> BackendResult:
|
|
120
|
+
"""Decompile ``address`` with angr's decompiler analysis.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
path: Binary to analyse (opened read-only by angr; never executed).
|
|
124
|
+
info: Loaded metadata; angr loads the file itself, so only the path
|
|
125
|
+
is used. Kept for a uniform backend signature.
|
|
126
|
+
address: Virtual address of the function to decompile.
|
|
127
|
+
max_instructions: Recorded and validated, but see the module
|
|
128
|
+
docstring: angr cannot enforce an instruction budget.
|
|
129
|
+
max_blocks: Checked against the recovered block count after the fact;
|
|
130
|
+
an overrun becomes a warning, not an error.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The rendering plus any warnings angr could not prevent.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
AnalysisError: When angr is unavailable, a budget is out of range,
|
|
137
|
+
angr cannot load the file, or no function is recovered.
|
|
138
|
+
"""
|
|
139
|
+
instruction_budget = require_budget(
|
|
140
|
+
"max_instructions", max_instructions, MAX_MAX_INSTRUCTIONS
|
|
141
|
+
)
|
|
142
|
+
block_budget = require_budget("max_blocks", max_blocks, MAX_MAX_BLOCKS)
|
|
143
|
+
availability = self.availability()
|
|
144
|
+
if not availability.available:
|
|
145
|
+
raise _unavailable(availability)
|
|
146
|
+
|
|
147
|
+
import angr
|
|
148
|
+
|
|
149
|
+
warnings: list[str] = []
|
|
150
|
+
try:
|
|
151
|
+
project = angr.Project(str(path), auto_load_libs=False, load_debug_info=False)
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
raise AnalysisError(
|
|
154
|
+
f"angr could not load {str(path)!r}: {type(exc).__name__}: {exc}"
|
|
155
|
+
) from exc
|
|
156
|
+
cfg = _recover_cfg(project, address)
|
|
157
|
+
function = _lookup_function(cfg, address)
|
|
158
|
+
try:
|
|
159
|
+
analysis = project.analyses.Decompiler(function, cfg=cfg.model)
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
raise AnalysisError(
|
|
162
|
+
f"angr could not decompile 0x{address:x}: {type(exc).__name__}: {exc}"
|
|
163
|
+
) from exc
|
|
164
|
+
|
|
165
|
+
text = _codegen_text(analysis)
|
|
166
|
+
if not text.strip():
|
|
167
|
+
warnings.append(
|
|
168
|
+
f"angr returned no decompilation for 0x{address:x}: the analysis "
|
|
169
|
+
"produced no codegen text, so the rendering is a placeholder comment"
|
|
170
|
+
)
|
|
171
|
+
text = _EMPTY_TEXT.format(address=address)
|
|
172
|
+
|
|
173
|
+
block_count = len(list(getattr(function, "blocks", ()) or ()))
|
|
174
|
+
if block_count > block_budget:
|
|
175
|
+
warnings.append(
|
|
176
|
+
f"angr decompiled {block_count} block(s), above max_blocks={block_budget}; "
|
|
177
|
+
"angr exposes no block budget, so the text is complete rather than truncated"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
version = _angr_version(angr)
|
|
181
|
+
evidence = Evidence(
|
|
182
|
+
source=_SOURCE,
|
|
183
|
+
method=_METHOD,
|
|
184
|
+
confidence=Confidence.MEDIUM,
|
|
185
|
+
address=address,
|
|
186
|
+
detail=(
|
|
187
|
+
f"angr {version} decompiled the CFGFast function at 0x{address:x} "
|
|
188
|
+
f"({block_count} block(s)); angr exposes no instruction or block budget, so "
|
|
189
|
+
f"max_instructions={instruction_budget} could not be enforced and "
|
|
190
|
+
f"max_blocks={block_budget} is checked afterwards only"
|
|
191
|
+
),
|
|
192
|
+
extra={
|
|
193
|
+
"backend": self.name,
|
|
194
|
+
"angr_version": version,
|
|
195
|
+
"max_instructions": instruction_budget,
|
|
196
|
+
"max_blocks": block_budget,
|
|
197
|
+
"block_count": block_count,
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
return BackendResult(
|
|
201
|
+
text=text,
|
|
202
|
+
backend=self.name,
|
|
203
|
+
language=LANGUAGE,
|
|
204
|
+
warnings=tuple(warnings),
|
|
205
|
+
evidence=evidence,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _angr_version(module: Any) -> str:
|
|
210
|
+
"""Return the installed angr version, or ``"unknown"``."""
|
|
211
|
+
return str(getattr(module, "__version__", "unknown"))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _recover_cfg(project: Any, address: int) -> Any:
|
|
215
|
+
"""Recover a CFG bounded to the one function start at ``address``.
|
|
216
|
+
|
|
217
|
+
Indirect-jump resolution and data-reference discovery are switched off
|
|
218
|
+
because both widen the scan without helping a single function. The keyword set
|
|
219
|
+
differs across angr releases and angr rejects an unknown arch-specific option
|
|
220
|
+
with ``KeyError`` (not ``TypeError``), so both are treated as "try the minimal
|
|
221
|
+
call": ``function_starts`` alone is the option that does the actual bounding.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
return project.analyses.CFGFast(
|
|
225
|
+
function_starts=[address],
|
|
226
|
+
resolve_indirect_jumps=False,
|
|
227
|
+
data_references=False,
|
|
228
|
+
normalize=True,
|
|
229
|
+
)
|
|
230
|
+
except (TypeError, KeyError):
|
|
231
|
+
return project.analyses.CFGFast(function_starts=[address])
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _lookup_function(cfg: Any, address: int) -> Any:
|
|
235
|
+
"""Return the recovered function at ``address`` or raise :class:`AnalysisError`."""
|
|
236
|
+
function = cfg.kb.functions.get(address)
|
|
237
|
+
if function is None:
|
|
238
|
+
raise AnalysisError(
|
|
239
|
+
f"angr recovered no function at 0x{address:x}; the address may not be a "
|
|
240
|
+
"function start or may be unreachable"
|
|
241
|
+
)
|
|
242
|
+
return function
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _codegen_text(analysis: Any) -> str:
|
|
246
|
+
"""Extract the pseudo-C text from a ``Decompiler`` analysis result."""
|
|
247
|
+
codegen = getattr(analysis, "codegen", None)
|
|
248
|
+
if codegen is None:
|
|
249
|
+
return ""
|
|
250
|
+
text = getattr(codegen, "text", "")
|
|
251
|
+
return "" if text is None else str(text)
|