idare2 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.
- idare2/__init__.py +10 -0
- idare2/__main__.py +6 -0
- idare2/argv.py +342 -0
- idare2/assembler.py +199 -0
- idare2/cli.py +498 -0
- idare2/cmd/__init__.py +306 -0
- idare2/cmd/analysis.py +418 -0
- idare2/cmd/extras.py +347 -0
- idare2/cmd/flag.py +249 -0
- idare2/cmd/ic.py +133 -0
- idare2/cmd/ida.py +153 -0
- idare2/cmd/info.py +674 -0
- idare2/cmd/meta.py +249 -0
- idare2/cmd/meta_info.py +48 -0
- idare2/cmd/misc.py +603 -0
- idare2/cmd/print.py +1604 -0
- idare2/cmd/search.py +237 -0
- idare2/cmd/seek.py +216 -0
- idare2/cmd/types.py +335 -0
- idare2/cmd/write.py +315 -0
- idare2/complete.py +152 -0
- idare2/config.py +222 -0
- idare2/errors.py +159 -0
- idare2/expr.py +349 -0
- idare2/filter.py +956 -0
- idare2/flags.py +87 -0
- idare2/fmt/__init__.py +88 -0
- idare2/fmt/colors.py +75 -0
- idare2/fmt/disasm.py +202 -0
- idare2/fmt/graph.py +141 -0
- idare2/fmt/listing.py +340 -0
- idare2/fmt/memory.py +243 -0
- idare2/fmt/xrefs.py +54 -0
- idare2/nexus.py +322 -0
- idare2/parser.py +278 -0
- idare2/pf.py +444 -0
- idare2/pipe.py +67 -0
- idare2/protocol.py +107 -0
- idare2/remote/__init__.py +11 -0
- idare2/remote/dispatch.py +117 -0
- idare2/remote/elf.py +1243 -0
- idare2/remote/entry.py +36 -0
- idare2/remote/helpers.py +271 -0
- idare2/remote/ops/__init__.py +30 -0
- idare2/remote/ops/analysis.py +657 -0
- idare2/remote/ops/comments.py +138 -0
- idare2/remote/ops/decompile.py +152 -0
- idare2/remote/ops/disasm.py +302 -0
- idare2/remote/ops/info.py +843 -0
- idare2/remote/ops/memory.py +134 -0
- idare2/remote/ops/search.py +270 -0
- idare2/remote/ops/session.py +207 -0
- idare2/remote/ops/strings.py +290 -0
- idare2/remote/ops/types.py +666 -0
- idare2/remote/ops/write.py +161 -0
- idare2/rpc.py +123 -0
- idare2/session.py +742 -0
- idare2-0.1.0.dist-info/METADATA +309 -0
- idare2-0.1.0.dist-info/RECORD +62 -0
- idare2-0.1.0.dist-info/WHEEL +4 -0
- idare2-0.1.0.dist-info/entry_points.txt +3 -0
- idare2-0.1.0.dist-info/licenses/LICENSE +21 -0
idare2/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""idare2 — radare2-style CLI over IDA Pro databases.
|
|
2
|
+
|
|
3
|
+
The package root is intentionally trivial: this module is imported inside the
|
|
4
|
+
IDA process by :mod:`idare2.remote.entry`, so it must not pull in anything that
|
|
5
|
+
is unavailable in IDA's own Python (no ``ida_nexus``, no ``ida_*``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
idare2/__main__.py
ADDED
idare2/argv.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""radare2-flavoured argument parsing.
|
|
2
|
+
|
|
3
|
+
``argparse`` cannot express what r2's CLI actually does: clustered short flags
|
|
4
|
+
(``-qc 'pd 10'``), doubled letters that mean something different from a repeat
|
|
5
|
+
(``-qq`` is "quit before the prompt", ``-AA`` is "deep analysis"), and repeated
|
|
6
|
+
``-c``/``-e``. So this is hand-rolled.
|
|
7
|
+
|
|
8
|
+
The flag set is deliberately a subset. Three tiers:
|
|
9
|
+
|
|
10
|
+
**Implemented** -- kept with r2's meaning.
|
|
11
|
+
**Accepted but ignored** -- harmless in an r2 script, meaningless here; each one
|
|
12
|
+
warns once. Hard-failing on ``-T`` would only break scripts for no gain.
|
|
13
|
+
**Rejected** -- anything that would silently produce a *wrong* result, such as
|
|
14
|
+
``-d`` (debugger), where the user would think they were debugging.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Iterator
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
from idare2.errors import UsageError
|
|
23
|
+
|
|
24
|
+
#: Short flags that take a value.
|
|
25
|
+
VALUE_FLAGS = frozenset("abcBeIikmpst")
|
|
26
|
+
#: Short flags that take no value.
|
|
27
|
+
BOOLEAN_FLAGS = frozenset("0A1E2fhjLnNqQSTuvVw")
|
|
28
|
+
#: Doubled letters with their own meaning, checked before single letters.
|
|
29
|
+
DOUBLED = ("AA", "NN", "qq", "LL", "nn")
|
|
30
|
+
#: Accepted and ignored, with the reason shown in the warning.
|
|
31
|
+
IGNORED = {
|
|
32
|
+
"0": "output is never NUL-separated",
|
|
33
|
+
"1": "use the shell's 2>&1 instead",
|
|
34
|
+
"2": "use the shell's 2>&1 instead",
|
|
35
|
+
"E": "edit ~/.idare2rc directly",
|
|
36
|
+
"f": "use `b $s` to set the block size to the file size",
|
|
37
|
+
"k": "idare2 does not do syscall resolution",
|
|
38
|
+
"M": "use -e asm.demangle=false",
|
|
39
|
+
"nn": "-n already covers this",
|
|
40
|
+
"Q": "the teardown saving is negligible",
|
|
41
|
+
"S": "use -e cfg.sandbox=true",
|
|
42
|
+
"t": "IDA analyses the database itself",
|
|
43
|
+
"T": "IDA's hashes are precomputed, so there is no cost to skip",
|
|
44
|
+
"u": "idare2 does not filter symbol names",
|
|
45
|
+
}
|
|
46
|
+
#: Rejected outright, with the reason.
|
|
47
|
+
REJECTED = {
|
|
48
|
+
"d": "the debugger is not available through idalib",
|
|
49
|
+
"D": "the debugger is not available through idalib",
|
|
50
|
+
"r": "there is no debugger to profile",
|
|
51
|
+
"R": "there is no debugger to profile",
|
|
52
|
+
"x": "load permissions come from IDA's loader",
|
|
53
|
+
"X": "extraction plugins are IDA's business",
|
|
54
|
+
"P": "rapatch would write to the IDB with different semantics",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
LONG_WITH_VALUE = frozenset(
|
|
58
|
+
{
|
|
59
|
+
"attach",
|
|
60
|
+
"idb",
|
|
61
|
+
"backend",
|
|
62
|
+
"linger",
|
|
63
|
+
"idle",
|
|
64
|
+
"timeout",
|
|
65
|
+
"startup-timeout",
|
|
66
|
+
"color",
|
|
67
|
+
"endian",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
LONG_BOOLEAN = frozenset(
|
|
71
|
+
{
|
|
72
|
+
"help",
|
|
73
|
+
"version",
|
|
74
|
+
"no-spawn",
|
|
75
|
+
"save-on-close",
|
|
76
|
+
"no-save-on-close",
|
|
77
|
+
"sessions",
|
|
78
|
+
"json",
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class Options:
|
|
85
|
+
"""Everything the CLI parsed off the command line."""
|
|
86
|
+
|
|
87
|
+
file: str | None = None
|
|
88
|
+
commands: list[str] = field(default_factory=list)
|
|
89
|
+
evals: list[tuple[str, str]] = field(default_factory=list)
|
|
90
|
+
pre_scripts: list[str] = field(default_factory=list)
|
|
91
|
+
post_scripts: list[str] = field(default_factory=list)
|
|
92
|
+
ignored: list[tuple[str, str]] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
analyze: int = 0
|
|
95
|
+
quiet: int = 0
|
|
96
|
+
json: bool = False
|
|
97
|
+
readonly: bool = True
|
|
98
|
+
skip_rc: int = 0
|
|
99
|
+
color: int | None = None
|
|
100
|
+
seek: str | None = None
|
|
101
|
+
project: str | None = None
|
|
102
|
+
|
|
103
|
+
arch: str | None = None
|
|
104
|
+
bits: str | None = None
|
|
105
|
+
base: str | None = None
|
|
106
|
+
#: Raw-binary import hint: ``big`` or ``little``. Spelled ``--endian``
|
|
107
|
+
#: because ``-e`` is r2's config-eval flag (and already is here).
|
|
108
|
+
endian: str | None = None
|
|
109
|
+
no_analysis: bool = False
|
|
110
|
+
|
|
111
|
+
attach: str | None = None
|
|
112
|
+
idb: str | None = None
|
|
113
|
+
spawn: bool = True
|
|
114
|
+
backend: str = "any"
|
|
115
|
+
linger: float | None = None
|
|
116
|
+
idle: float | None = None
|
|
117
|
+
timeout: float | None = None
|
|
118
|
+
startup_timeout: float | None = None
|
|
119
|
+
save_on_close: bool | None = None
|
|
120
|
+
|
|
121
|
+
show_help: bool = False
|
|
122
|
+
show_version: int = 0
|
|
123
|
+
list_sessions: bool = False
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def wants_database(self) -> bool:
|
|
127
|
+
return not (self.show_help or self.show_version or self.list_sessions)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def parse(argv: list[str]) -> Options:
|
|
131
|
+
options = Options()
|
|
132
|
+
index = 0
|
|
133
|
+
only_positional = False
|
|
134
|
+
|
|
135
|
+
while index < len(argv):
|
|
136
|
+
token = argv[index]
|
|
137
|
+
index += 1
|
|
138
|
+
|
|
139
|
+
if only_positional or not token.startswith("-") or token == "-":
|
|
140
|
+
_set_file(options, token)
|
|
141
|
+
continue
|
|
142
|
+
if token == "--":
|
|
143
|
+
only_positional = True
|
|
144
|
+
continue
|
|
145
|
+
if token.startswith("--"):
|
|
146
|
+
index = _long(options, token[2:], argv, index)
|
|
147
|
+
continue
|
|
148
|
+
index = _short(options, token[1:], argv, index)
|
|
149
|
+
|
|
150
|
+
return options
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _set_file(options: Options, token: str) -> None:
|
|
154
|
+
if options.file is not None:
|
|
155
|
+
raise UsageError(f"only one file can be opened, got {options.file!r} and {token!r}")
|
|
156
|
+
options.file = token
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _long(options: Options, body: str, argv: list[str], index: int) -> int:
|
|
160
|
+
name, _, inline = body.partition("=")
|
|
161
|
+
if name in LONG_WITH_VALUE:
|
|
162
|
+
if inline:
|
|
163
|
+
value = inline
|
|
164
|
+
else:
|
|
165
|
+
if index >= len(argv):
|
|
166
|
+
raise UsageError(f"--{name} needs a value")
|
|
167
|
+
value = argv[index]
|
|
168
|
+
index += 1
|
|
169
|
+
_apply_long_value(options, name, value)
|
|
170
|
+
return index
|
|
171
|
+
if name in LONG_BOOLEAN:
|
|
172
|
+
if inline:
|
|
173
|
+
raise UsageError(f"--{name} does not take a value")
|
|
174
|
+
_apply_long_flag(options, name)
|
|
175
|
+
return index
|
|
176
|
+
raise UsageError(f"unknown option --{name}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _apply_long_value(options: Options, name: str, value: str) -> None:
|
|
180
|
+
if name == "attach":
|
|
181
|
+
options.attach = value
|
|
182
|
+
elif name == "idb":
|
|
183
|
+
options.idb = value
|
|
184
|
+
elif name == "backend":
|
|
185
|
+
if value not in ("gui", "idalib", "any"):
|
|
186
|
+
raise UsageError("--backend must be gui, idalib or any")
|
|
187
|
+
options.backend = value
|
|
188
|
+
elif name == "linger":
|
|
189
|
+
options.linger = _float(value, "--linger")
|
|
190
|
+
elif name == "idle":
|
|
191
|
+
options.idle = _float(value, "--idle")
|
|
192
|
+
elif name == "timeout":
|
|
193
|
+
options.timeout = _float(value, "--timeout")
|
|
194
|
+
elif name == "startup-timeout":
|
|
195
|
+
options.startup_timeout = _float(value, "--startup-timeout")
|
|
196
|
+
elif name == "color":
|
|
197
|
+
options.color = _int(value, "--color")
|
|
198
|
+
elif name == "endian":
|
|
199
|
+
if value not in ("big", "little"):
|
|
200
|
+
raise UsageError("--endian must be big or little")
|
|
201
|
+
options.endian = value
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _apply_long_flag(options: Options, name: str) -> None:
|
|
205
|
+
if name == "help":
|
|
206
|
+
options.show_help = True
|
|
207
|
+
elif name == "version":
|
|
208
|
+
options.show_version = 1
|
|
209
|
+
elif name == "no-spawn":
|
|
210
|
+
options.spawn = False
|
|
211
|
+
elif name == "save-on-close":
|
|
212
|
+
options.save_on_close = True
|
|
213
|
+
elif name == "no-save-on-close":
|
|
214
|
+
options.save_on_close = False
|
|
215
|
+
elif name == "sessions":
|
|
216
|
+
options.list_sessions = True
|
|
217
|
+
elif name == "json":
|
|
218
|
+
options.json = True
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _short(options: Options, cluster: str, argv: list[str], index: int) -> int:
|
|
222
|
+
position = 0
|
|
223
|
+
while position < len(cluster):
|
|
224
|
+
doubled = cluster[position : position + 2]
|
|
225
|
+
if doubled in DOUBLED:
|
|
226
|
+
_apply_doubled(options, doubled)
|
|
227
|
+
position += 2
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
char = cluster[position]
|
|
231
|
+
if char in REJECTED:
|
|
232
|
+
raise UsageError(
|
|
233
|
+
f"-{char} is not supported by idare2: {REJECTED[char]}"
|
|
234
|
+
)
|
|
235
|
+
if char in IGNORED:
|
|
236
|
+
options.ignored.append((f"-{char}", IGNORED[char]))
|
|
237
|
+
position += 1
|
|
238
|
+
continue
|
|
239
|
+
if char == "-":
|
|
240
|
+
# A bare `-` means "no file" in r2 (malloc://512); idare2 has no
|
|
241
|
+
# such concept, so treat it as the end of the flag cluster.
|
|
242
|
+
break
|
|
243
|
+
if char in VALUE_FLAGS:
|
|
244
|
+
rest = cluster[position + 1 :]
|
|
245
|
+
if rest:
|
|
246
|
+
value = rest
|
|
247
|
+
else:
|
|
248
|
+
if index >= len(argv):
|
|
249
|
+
raise UsageError(f"-{char} needs a value")
|
|
250
|
+
value = argv[index]
|
|
251
|
+
index += 1
|
|
252
|
+
_apply_short_value(options, char, value)
|
|
253
|
+
return index
|
|
254
|
+
if char in BOOLEAN_FLAGS:
|
|
255
|
+
_apply_short_flag(options, char)
|
|
256
|
+
position += 1
|
|
257
|
+
continue
|
|
258
|
+
raise UsageError(f"unknown option -{char}")
|
|
259
|
+
|
|
260
|
+
return index
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _apply_doubled(options: Options, doubled: str) -> None:
|
|
264
|
+
if doubled == "AA":
|
|
265
|
+
options.analyze = 2
|
|
266
|
+
elif doubled == "NN":
|
|
267
|
+
options.skip_rc = 2
|
|
268
|
+
elif doubled == "qq":
|
|
269
|
+
options.quiet = 2
|
|
270
|
+
elif doubled == "LL":
|
|
271
|
+
options.list_sessions = True
|
|
272
|
+
elif doubled == "nn":
|
|
273
|
+
options.ignored.append(("-nn", IGNORED["nn"]))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _apply_short_value(options: Options, char: str, value: str) -> None:
|
|
277
|
+
if char == "a":
|
|
278
|
+
options.arch = value
|
|
279
|
+
elif char == "b":
|
|
280
|
+
options.bits = value
|
|
281
|
+
elif char == "B" or char == "m":
|
|
282
|
+
options.base = value
|
|
283
|
+
elif char == "c":
|
|
284
|
+
options.commands.append(value)
|
|
285
|
+
elif char == "e":
|
|
286
|
+
key, separator, setting = value.partition("=")
|
|
287
|
+
if not separator:
|
|
288
|
+
raise UsageError(f"-e expects key=value, got {value!r}")
|
|
289
|
+
options.evals.append((key.strip(), setting.strip()))
|
|
290
|
+
elif char == "i":
|
|
291
|
+
options.post_scripts.append(value)
|
|
292
|
+
elif char == "I":
|
|
293
|
+
options.pre_scripts.append(value)
|
|
294
|
+
elif char == "k":
|
|
295
|
+
options.ignored.append(("-k", IGNORED["k"]))
|
|
296
|
+
elif char == "p":
|
|
297
|
+
options.project = value
|
|
298
|
+
elif char == "s":
|
|
299
|
+
options.seek = value
|
|
300
|
+
elif char == "t":
|
|
301
|
+
options.ignored.append(("-t", IGNORED["t"]))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _apply_short_flag(options: Options, char: str) -> None:
|
|
305
|
+
if char == "A":
|
|
306
|
+
options.analyze = max(options.analyze, 1)
|
|
307
|
+
elif char == "h":
|
|
308
|
+
options.show_help = True
|
|
309
|
+
elif char == "j":
|
|
310
|
+
options.json = True
|
|
311
|
+
elif char == "L":
|
|
312
|
+
options.list_sessions = True
|
|
313
|
+
elif char == "n":
|
|
314
|
+
options.no_analysis = True
|
|
315
|
+
elif char == "N":
|
|
316
|
+
options.skip_rc = max(options.skip_rc, 1)
|
|
317
|
+
elif char == "q":
|
|
318
|
+
options.quiet = max(options.quiet, 1)
|
|
319
|
+
elif char == "v":
|
|
320
|
+
options.show_version = 1
|
|
321
|
+
elif char == "V":
|
|
322
|
+
options.show_version = 2
|
|
323
|
+
elif char == "w":
|
|
324
|
+
options.readonly = False
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _float(value: str, what: str) -> float:
|
|
328
|
+
try:
|
|
329
|
+
return float(value)
|
|
330
|
+
except ValueError:
|
|
331
|
+
raise UsageError(f"{what} needs a number, got {value!r}") from None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _int(value: str, what: str) -> int:
|
|
335
|
+
try:
|
|
336
|
+
return int(value, 0)
|
|
337
|
+
except ValueError:
|
|
338
|
+
raise UsageError(f"{what} needs a number, got {value!r}") from None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def iter_ignored(options: Options) -> Iterator[str]:
|
|
342
|
+
return (f"WARN: {flag} ignored: {reason}" for flag, reason in options.ignored)
|
idare2/assembler.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Client-side assembly backend shared by ``pa`` and ``wa``.
|
|
2
|
+
|
|
3
|
+
IDA owns the authoritative disassembler for the current database, and for x86 it
|
|
4
|
+
also provides an assembler through ``idautils.Assemble``. Most other processor
|
|
5
|
+
modules registered in IDA do not provide the reverse operation. radare2 solves
|
|
6
|
+
that with per-architecture assembler plugins; when ``rasm2`` is installed we use
|
|
7
|
+
the same backend as an optional fallback instead of turning ``pa``/``wa`` into
|
|
8
|
+
x86-only commands.
|
|
9
|
+
|
|
10
|
+
The fallback is deliberately optional: IDA remains the primary implementation,
|
|
11
|
+
and a missing ``rasm2`` leaves the original IDA error untouched. This mirrors
|
|
12
|
+
radare2's own split between the generic assembler frontend (``r_asm``) and the
|
|
13
|
+
architecture plugins in ``libr/arch/p``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from idare2.errors import CommandError, UsageError
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from idare2.session import Session
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
#: Enough for assembling a shellcode-sized block without hanging a session.
|
|
30
|
+
RASM2_TIMEOUT = 15.0
|
|
31
|
+
MAX_ASSEMBLY = 1 << 20
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
#: IDA processor names -> radare2 assembler plugin names. IDA reports the
|
|
35
|
+
#: architecture, not the plugin name, so this is the small translation layer
|
|
36
|
+
#: between the two engines.
|
|
37
|
+
_R2_ARCH = {
|
|
38
|
+
"metapc": "x86",
|
|
39
|
+
"x86": "x86",
|
|
40
|
+
"x86_64": "x86",
|
|
41
|
+
"amd64": "x86",
|
|
42
|
+
"arm": "arm",
|
|
43
|
+
"arm64": "arm",
|
|
44
|
+
"arm64e": "arm",
|
|
45
|
+
"aarch64": "arm",
|
|
46
|
+
"mips": "mips",
|
|
47
|
+
"mipsel": "mips",
|
|
48
|
+
"mips64": "mips",
|
|
49
|
+
"ppc": "ppc",
|
|
50
|
+
"powerpc": "ppc",
|
|
51
|
+
"riscv": "riscv",
|
|
52
|
+
"risc-v": "riscv",
|
|
53
|
+
"m68k": "m68k",
|
|
54
|
+
"68000": "m68k",
|
|
55
|
+
"s390": "s390",
|
|
56
|
+
"s390x": "s390",
|
|
57
|
+
"sparc": "sparc",
|
|
58
|
+
"sparc64": "sparc",
|
|
59
|
+
"sh": "sh",
|
|
60
|
+
"sh4": "sh",
|
|
61
|
+
"superh": "sh",
|
|
62
|
+
"msp430": "msp430",
|
|
63
|
+
"avr": "avr",
|
|
64
|
+
"8051": "8051",
|
|
65
|
+
"h8300": "h8300",
|
|
66
|
+
"v850": "v850",
|
|
67
|
+
"tricore": "tricore",
|
|
68
|
+
"bpf": "bpf",
|
|
69
|
+
"ebpf": "bpf",
|
|
70
|
+
"evm": "evm",
|
|
71
|
+
"dalvik": "dalvik",
|
|
72
|
+
"java": "java",
|
|
73
|
+
"cil": "cil",
|
|
74
|
+
"wasm": "wasm",
|
|
75
|
+
"6502": "6502",
|
|
76
|
+
"z80": "z80",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def assemble(session: Session, raw_text: str, *, ea: int | None = None) -> bytes:
|
|
81
|
+
"""Assemble ``raw_text`` at ``ea`` (the seek by default).
|
|
82
|
+
|
|
83
|
+
The IDA operation is read-only: it returns bytes so callers can either print
|
|
84
|
+
them (``pa``) or pass them through the write cache (``wa``).
|
|
85
|
+
"""
|
|
86
|
+
text = instruction_text(raw_text)
|
|
87
|
+
if not text:
|
|
88
|
+
raise UsageError("assembly needs an instruction")
|
|
89
|
+
target = session.seek if ea is None else int(ea)
|
|
90
|
+
# r2 substitutes `$$` with the assembler's program counter before handing
|
|
91
|
+
# the expression to the architecture plugin.
|
|
92
|
+
text = text.replace("$$", f"0x{target:x}")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
payload = session.remote.call("assemble", ea=target, text=text)
|
|
96
|
+
except CommandError as ida_error:
|
|
97
|
+
if "assembl" not in str(ida_error).lower():
|
|
98
|
+
raise
|
|
99
|
+
fallback = _rasm2_assemble(session, text, target, ida_error)
|
|
100
|
+
if fallback is None:
|
|
101
|
+
raise
|
|
102
|
+
return fallback
|
|
103
|
+
|
|
104
|
+
raw = payload.get("data") if isinstance(payload, dict) else None
|
|
105
|
+
if isinstance(raw, (bytes, bytearray)):
|
|
106
|
+
data = bytes(raw)
|
|
107
|
+
elif isinstance(raw, str):
|
|
108
|
+
try:
|
|
109
|
+
data = bytes.fromhex(raw)
|
|
110
|
+
except ValueError:
|
|
111
|
+
raise CommandError(f"assembler returned invalid hexpairs: {raw!r}") from None
|
|
112
|
+
else:
|
|
113
|
+
raise CommandError(
|
|
114
|
+
f"assembler returned {type(raw).__name__} instead of bytes"
|
|
115
|
+
)
|
|
116
|
+
if not data:
|
|
117
|
+
raise CommandError(f"assembler produced nothing for {text!r}")
|
|
118
|
+
if len(data) > MAX_ASSEMBLY:
|
|
119
|
+
raise CommandError(f"refusing to assemble {len(data)} bytes at once")
|
|
120
|
+
return data
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def instruction_text(raw_text: str) -> str:
|
|
124
|
+
"""Strip r2's optional quoting around an assembly expression."""
|
|
125
|
+
text = raw_text.strip()
|
|
126
|
+
if len(text) >= 2 and text[0] == text[-1] and text[0] in "'\"`":
|
|
127
|
+
return text[1:-1].strip()
|
|
128
|
+
return text
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def r2_arch(session: Session) -> str | None:
|
|
132
|
+
"""The radare2 assembler plugin for the attached database, if known."""
|
|
133
|
+
name = str(
|
|
134
|
+
session.info.get("architecture") or session.info.get("arch") or ""
|
|
135
|
+
).strip().lower()
|
|
136
|
+
return _R2_ARCH.get(name)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _rasm2_assemble(
|
|
140
|
+
session: Session, text: str, ea: int, ida_error: CommandError
|
|
141
|
+
) -> bytes | None:
|
|
142
|
+
"""Try the optional ``rasm2`` fallback; ``None`` means it is unavailable."""
|
|
143
|
+
plugin = r2_arch(session)
|
|
144
|
+
if plugin is None:
|
|
145
|
+
return None
|
|
146
|
+
executable = os.environ.get("IDARE2_RASM2") or shutil.which("rasm2")
|
|
147
|
+
if not executable:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# `open_summary` exposes ``bitness``; ``file_info``/the session also expose
|
|
151
|
+
# ``bits``. Use both, otherwise rasm2 silently defaults to 64 bits.
|
|
152
|
+
bits = int(session.info.get("bitness") or session.info.get("bits") or 0)
|
|
153
|
+
argv = [executable, "-q", "-a", plugin, "-s", f"0x{ea:x}"]
|
|
154
|
+
if bits:
|
|
155
|
+
argv.extend(["-b", str(bits)])
|
|
156
|
+
if session.info.get("endian") == "big":
|
|
157
|
+
argv.append("-e")
|
|
158
|
+
argv.extend(["--", text])
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
completed = subprocess.run(
|
|
162
|
+
argv,
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
timeout=RASM2_TIMEOUT,
|
|
166
|
+
check=False,
|
|
167
|
+
)
|
|
168
|
+
except FileNotFoundError:
|
|
169
|
+
return None
|
|
170
|
+
except subprocess.TimeoutExpired:
|
|
171
|
+
raise CommandError(
|
|
172
|
+
f"rasm2 timed out after {RASM2_TIMEOUT:g}s assembling {text!r}"
|
|
173
|
+
) from ida_error
|
|
174
|
+
except OSError as exc:
|
|
175
|
+
raise CommandError(f"cannot run rasm2: {exc}") from ida_error
|
|
176
|
+
|
|
177
|
+
if completed.returncode != 0:
|
|
178
|
+
detail = (completed.stderr or completed.stdout or "").strip()
|
|
179
|
+
raise CommandError(
|
|
180
|
+
f"IDA could not assemble {text!r}; rasm2 ({plugin}) also failed"
|
|
181
|
+
+ (f": {detail}" if detail else "")
|
|
182
|
+
) from ida_error
|
|
183
|
+
|
|
184
|
+
encoded = "".join(completed.stdout.split())
|
|
185
|
+
if not encoded or len(encoded) % 2:
|
|
186
|
+
raise CommandError(
|
|
187
|
+
f"rasm2 returned invalid hexpairs for {text!r}: {completed.stdout!r}"
|
|
188
|
+
) from ida_error
|
|
189
|
+
try:
|
|
190
|
+
raw = bytes.fromhex(encoded)
|
|
191
|
+
except ValueError:
|
|
192
|
+
raise CommandError(
|
|
193
|
+
f"rasm2 returned invalid hexpairs for {text!r}: {completed.stdout!r}"
|
|
194
|
+
) from ida_error
|
|
195
|
+
if not raw:
|
|
196
|
+
raise CommandError(f"assembler produced nothing for {text!r}") from ida_error
|
|
197
|
+
if len(raw) > MAX_ASSEMBLY:
|
|
198
|
+
raise CommandError(f"refusing to assemble {len(raw)} bytes at once") from ida_error
|
|
199
|
+
return raw
|