gdare2 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.
- gdare2/__init__.py +4 -0
- gdare2/__main__.py +3 -0
- gdare2/arch_matrix.py +33 -0
- gdare2/argv.py +417 -0
- gdare2/assembler.py +157 -0
- gdare2/cli.py +344 -0
- gdare2/client.py +112 -0
- gdare2/cmd/__init__.py +142 -0
- gdare2/cmd/analysis.py +367 -0
- gdare2/cmd/assemble.py +140 -0
- gdare2/cmd/comments.py +147 -0
- gdare2/cmd/eval.py +401 -0
- gdare2/cmd/extras.py +576 -0
- gdare2/cmd/flags.py +460 -0
- gdare2/cmd/info.py +481 -0
- gdare2/cmd/meta.py +228 -0
- gdare2/cmd/metadata.py +195 -0
- gdare2/cmd/misc.py +54 -0
- gdare2/cmd/print.py +915 -0
- gdare2/cmd/search.py +481 -0
- gdare2/cmd/seek.py +269 -0
- gdare2/cmd/types.py +355 -0
- gdare2/cmd/write.py +288 -0
- gdare2/cmd/xrefs.py +323 -0
- gdare2/completion.py +86 -0
- gdare2/config.py +197 -0
- gdare2/errors.py +103 -0
- gdare2/escape.py +31 -0
- gdare2/expr.py +262 -0
- gdare2/filter.py +243 -0
- gdare2/flags.py +96 -0
- gdare2/fmt/__init__.py +1 -0
- gdare2/fmt/codegen.py +143 -0
- gdare2/fmt/disasm.py +113 -0
- gdare2/fmt/graph.py +93 -0
- gdare2/fmt/listing.py +11 -0
- gdare2/fmt/memory.py +75 -0
- gdare2/fmt/strings.py +93 -0
- gdare2/formats/__init__.py +5 -0
- gdare2/formats/elf.py +888 -0
- gdare2/formats/magic.py +43 -0
- gdare2/jsonutil.py +22 -0
- gdare2/launcher.py +215 -0
- gdare2/parity.py +172 -0
- gdare2/parser.py +225 -0
- gdare2/pf.py +486 -0
- gdare2/pipe.py +59 -0
- gdare2/protocol.py +75 -0
- gdare2/r2pipe_server.py +177 -0
- gdare2/rc.py +74 -0
- gdare2/registry.py +217 -0
- gdare2/session.py +1087 -0
- gdare2-0.1.0.dist-info/METADATA +547 -0
- gdare2-0.1.0.dist-info/RECORD +83 -0
- gdare2-0.1.0.dist-info/WHEEL +4 -0
- gdare2-0.1.0.dist-info/entry_points.txt +3 -0
- gdare2-0.1.0.dist-info/licenses/LICENSE +21 -0
- gdare2d/__init__.py +3 -0
- gdare2d/__main__.py +245 -0
- gdare2d/context.py +29 -0
- gdare2d/dispatch.py +122 -0
- gdare2d/executor.py +68 -0
- gdare2d/import_plan.py +218 -0
- gdare2d/ops/__init__.py +1 -0
- gdare2d/ops/_helpers.py +88 -0
- gdare2d/ops/analysis.py +299 -0
- gdare2d/ops/assemble.py +70 -0
- gdare2d/ops/batch.py +220 -0
- gdare2d/ops/comments.py +151 -0
- gdare2d/ops/decompile.py +84 -0
- gdare2d/ops/disasm.py +221 -0
- gdare2d/ops/escape.py +26 -0
- gdare2d/ops/graph.py +163 -0
- gdare2d/ops/info.py +315 -0
- gdare2d/ops/memory.py +56 -0
- gdare2d/ops/names.py +125 -0
- gdare2d/ops/search.py +324 -0
- gdare2d/ops/session.py +74 -0
- gdare2d/ops/types.py +482 -0
- gdare2d/ops/write.py +246 -0
- gdare2d/ops/xrefs.py +61 -0
- gdare2d/runtime.py +551 -0
- gdare2d/server.py +146 -0
gdare2/__init__.py
ADDED
gdare2/__main__.py
ADDED
gdare2/arch_matrix.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Raw-binary architecture configurations used by the arch matrix."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Keep this list in sync with gdare2d.import_plan.resolve_alias.
|
|
6
|
+
ARCH_MATRIX = (
|
|
7
|
+
{"name": "x86-16", "arch": "x86", "bits": "16", "endian": "little", "language": "x86:LE:16:Real Mode"},
|
|
8
|
+
{"name": "x86-32", "arch": "x86", "bits": "32", "endian": "little", "language": "x86:LE:32:default"},
|
|
9
|
+
{"name": "x86-64", "arch": "x86", "bits": "64", "endian": "little", "language": "x86:LE:64:default"},
|
|
10
|
+
{"name": "arm-le", "arch": "armle", "bits": "32", "endian": "little", "language": "ARM:LE:32:v7"},
|
|
11
|
+
{"name": "arm-be", "arch": "armbe", "bits": "32", "endian": "big", "language": "ARM:BE:32:v7"},
|
|
12
|
+
{"name": "aarch64-le", "arch": "aarch64", "bits": "64", "endian": "little", "language": "AARCH64:LE:64:v8A"},
|
|
13
|
+
{"name": "aarch64-be", "arch": "aarch64be", "bits": "64", "endian": "big", "language": "AARCH64:BE:64:v8A"},
|
|
14
|
+
{"name": "mips-le", "arch": "mipsle", "bits": "32", "endian": "little", "language": "MIPS:LE:32:default"},
|
|
15
|
+
{"name": "mips-be", "arch": "mipsbe", "bits": "32", "endian": "big", "language": "MIPS:BE:32:default"},
|
|
16
|
+
{"name": "mips64-le", "arch": "mips64le", "bits": "64", "endian": "little", "language": "MIPS:LE:64:default"},
|
|
17
|
+
{"name": "mips64-be", "arch": "mips64be", "bits": "64", "endian": "big", "language": "MIPS:BE:64:default"},
|
|
18
|
+
{"name": "ppc-be", "arch": "ppc", "bits": "32", "endian": "big", "language": "PowerPC:BE:32:default"},
|
|
19
|
+
{"name": "ppc-le", "arch": "ppcle", "bits": "32", "endian": "little", "language": "PowerPC:LE:32:default"},
|
|
20
|
+
{"name": "ppc64-be", "arch": "ppc64", "bits": "64", "endian": "big", "language": "PowerPC:BE:64:default"},
|
|
21
|
+
{"name": "ppc64-le", "arch": "ppc64le", "bits": "64", "endian": "little", "language": "PowerPC:LE:64:default"},
|
|
22
|
+
{"name": "riscv32", "arch": "riscv32", "bits": "32", "endian": "little", "language": "RISCV:LE:32:default", "bytes": "13000000"},
|
|
23
|
+
{"name": "riscv64", "arch": "riscv64", "bits": "64", "endian": "little", "language": "RISCV:LE:64:default", "bytes": "13000000"},
|
|
24
|
+
{"name": "m68k", "arch": "m68k", "bits": "32", "endian": "big", "language": "68000:BE:32:default"},
|
|
25
|
+
{"name": "sparc", "arch": "sparc", "bits": "32", "endian": "big", "language": "sparc:BE:32:default"},
|
|
26
|
+
{"name": "sparc64", "arch": "sparc64", "bits": "64", "endian": "big", "language": "sparc:BE:64:default"},
|
|
27
|
+
{"name": "sh2a", "arch": "sh2a", "bits": "32", "endian": "big", "language": "SuperH:BE:32:SH-2A"},
|
|
28
|
+
{"name": "sh2", "arch": "sh2", "bits": "32", "endian": "big", "language": "SuperH:BE:32:SH-2"},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Default decode probe: X86 NOPs plus RET. Entries may override it when the
|
|
32
|
+
# pattern is not a valid instruction for that architecture.
|
|
33
|
+
RAW_BYTES = bytes.fromhex("90909090c3")
|
gdare2/argv.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Hand-rolled radare2-style argument parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from gdare2.errors import UsageError
|
|
8
|
+
|
|
9
|
+
VALUE_FLAGS = frozenset("abcBeIikmpst")
|
|
10
|
+
BOOLEAN_FLAGS = frozenset("0A1E2fhjLnNqQSTuvVw")
|
|
11
|
+
DOUBLED = ("AA", "NN", "qq", "LL", "nn")
|
|
12
|
+
IGNORED = {
|
|
13
|
+
"0": "output is never NUL-separated",
|
|
14
|
+
"1": "use the shell's 2>&1 instead",
|
|
15
|
+
"2": "use the shell's 2>&1 instead",
|
|
16
|
+
"E": "edit the rc file directly",
|
|
17
|
+
"f": "use `b $s` to set the block size",
|
|
18
|
+
"k": "gdare2 does not do syscall resolution",
|
|
19
|
+
"M": "use -e asm.demangle=false",
|
|
20
|
+
"nn": "-n already covers this",
|
|
21
|
+
"Q": "teardown saving is negligible",
|
|
22
|
+
"S": "use -e cfg.sandbox=true",
|
|
23
|
+
"t": "Ghidra analyses the project itself",
|
|
24
|
+
"T": "Ghidra has no IDA-style hash cache",
|
|
25
|
+
"u": "gdare2 does not filter symbol names",
|
|
26
|
+
}
|
|
27
|
+
# Ignored short options that still take a value in radare2.
|
|
28
|
+
IGNORED_VALUE = frozenset("k")
|
|
29
|
+
|
|
30
|
+
REJECTED = {
|
|
31
|
+
"C": "remote HTTP relay (-c+=...) is not implemented",
|
|
32
|
+
"d": "the debugger is not implemented",
|
|
33
|
+
"D": "the debugger is not implemented",
|
|
34
|
+
"F": "bin plugins are chosen by Ghidra's loader",
|
|
35
|
+
"H": "use -v or ?V for build information",
|
|
36
|
+
"r": "there is no debugger to profile",
|
|
37
|
+
"R": "there is no debugger to profile",
|
|
38
|
+
"x": "load permissions come from Ghidra's loader",
|
|
39
|
+
"X": "extraction plugins are not implemented",
|
|
40
|
+
"P": "rapatch would write with different semantics",
|
|
41
|
+
"o": "file-offset mapping is not implemented; use --list-offset for list pagination",
|
|
42
|
+
"z": "string loading is decided by Ghidra's analyser; use iz/izz",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
LONG_VALUE = frozenset(
|
|
46
|
+
{
|
|
47
|
+
"attach",
|
|
48
|
+
"status",
|
|
49
|
+
"kill",
|
|
50
|
+
"project",
|
|
51
|
+
"project-name",
|
|
52
|
+
"program",
|
|
53
|
+
"ghidra-language",
|
|
54
|
+
"compiler",
|
|
55
|
+
"loader",
|
|
56
|
+
"linger",
|
|
57
|
+
"idle",
|
|
58
|
+
"timeout",
|
|
59
|
+
"startup-timeout",
|
|
60
|
+
"analysis-timeout",
|
|
61
|
+
"assembler",
|
|
62
|
+
"listen",
|
|
63
|
+
"color",
|
|
64
|
+
"endian",
|
|
65
|
+
"list-offset",
|
|
66
|
+
"pipe-listen",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
LONG_BOOL = frozenset(
|
|
70
|
+
{
|
|
71
|
+
"help",
|
|
72
|
+
"version",
|
|
73
|
+
"no-spawn",
|
|
74
|
+
"save-on-close",
|
|
75
|
+
"no-save-on-close",
|
|
76
|
+
"sessions",
|
|
77
|
+
"json",
|
|
78
|
+
"force-language",
|
|
79
|
+
"force-base",
|
|
80
|
+
"wait",
|
|
81
|
+
"ephemeral",
|
|
82
|
+
"reimport",
|
|
83
|
+
"allow-exec",
|
|
84
|
+
"daemon-readonly",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Options:
|
|
91
|
+
file: str | None = None
|
|
92
|
+
commands: list[str] = field(default_factory=list)
|
|
93
|
+
evals: list[tuple[str, str]] = field(default_factory=list)
|
|
94
|
+
pre_scripts: list[str] = field(default_factory=list)
|
|
95
|
+
post_scripts: list[str] = field(default_factory=list)
|
|
96
|
+
ignored: list[tuple[str, str]] = field(default_factory=list)
|
|
97
|
+
|
|
98
|
+
analyze: int = 0
|
|
99
|
+
quiet: int = 0
|
|
100
|
+
json: bool = False
|
|
101
|
+
readonly: bool = True
|
|
102
|
+
skip_rc: int = 0
|
|
103
|
+
color: int | None = None
|
|
104
|
+
seek: str | None = None
|
|
105
|
+
|
|
106
|
+
arch: str | None = None
|
|
107
|
+
bits: str | None = None
|
|
108
|
+
base: str | None = None
|
|
109
|
+
endian: str | None = None
|
|
110
|
+
no_analysis: bool = False
|
|
111
|
+
project: str | None = None
|
|
112
|
+
project_name: str | None = None
|
|
113
|
+
program: str | None = None
|
|
114
|
+
ghidra_language: str | None = None
|
|
115
|
+
compiler: str | None = None
|
|
116
|
+
loader: str | None = None
|
|
117
|
+
loader_args: list[tuple[str, str]] = field(default_factory=list)
|
|
118
|
+
force_language: bool = False
|
|
119
|
+
force_base: bool = False
|
|
120
|
+
reimport: bool = False
|
|
121
|
+
|
|
122
|
+
attach: str | None = None
|
|
123
|
+
status: str | None = None
|
|
124
|
+
kill: str | None = None
|
|
125
|
+
spawn: bool = True
|
|
126
|
+
linger: float | None = None
|
|
127
|
+
idle: float | None = None
|
|
128
|
+
timeout: float | None = None
|
|
129
|
+
startup_timeout: float | None = None
|
|
130
|
+
analysis_timeout: float | None = None
|
|
131
|
+
list_offset: int | None = None
|
|
132
|
+
pipe_listen: str | None = None
|
|
133
|
+
save_on_close: bool | None = None
|
|
134
|
+
wait: bool = False
|
|
135
|
+
assembler: str | None = None
|
|
136
|
+
daemon_readonly: bool = False
|
|
137
|
+
ephemeral: bool = False
|
|
138
|
+
allow_exec: bool = False
|
|
139
|
+
listen: str | None = None
|
|
140
|
+
|
|
141
|
+
show_help: bool = False
|
|
142
|
+
show_version: int = 0
|
|
143
|
+
list_sessions: bool = False
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def wants_database(self) -> bool:
|
|
147
|
+
return not (
|
|
148
|
+
self.show_help
|
|
149
|
+
or self.show_version
|
|
150
|
+
or self.list_sessions
|
|
151
|
+
or self.kill
|
|
152
|
+
or self.status
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse(argv: list[str]) -> Options:
|
|
157
|
+
options = Options()
|
|
158
|
+
index = 0
|
|
159
|
+
only_positional = False
|
|
160
|
+
while index < len(argv):
|
|
161
|
+
token = argv[index]
|
|
162
|
+
index += 1
|
|
163
|
+
if only_positional or not token.startswith("-") or token == "-":
|
|
164
|
+
_set_file(options, token)
|
|
165
|
+
continue
|
|
166
|
+
if token == "--":
|
|
167
|
+
only_positional = True
|
|
168
|
+
continue
|
|
169
|
+
if token.startswith("--"):
|
|
170
|
+
index = _long(options, token[2:], argv, index)
|
|
171
|
+
continue
|
|
172
|
+
index = _short(options, token[1:], argv, index)
|
|
173
|
+
return options
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _set_file(options: Options, token: str) -> None:
|
|
177
|
+
if options.file is not None:
|
|
178
|
+
raise UsageError(f"only one file can be opened, got {options.file!r} and {token!r}")
|
|
179
|
+
options.file = token
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _long(options: Options, body: str, argv: list[str], index: int) -> int:
|
|
183
|
+
name, _, inline = body.partition("=")
|
|
184
|
+
if name == "loader-arg":
|
|
185
|
+
key = inline or _next(argv, index, "--loader-arg key")
|
|
186
|
+
index += 0 if inline else 1
|
|
187
|
+
value = _next(argv, index, "--loader-arg value")
|
|
188
|
+
index += 1
|
|
189
|
+
options.loader_args.append((key, value))
|
|
190
|
+
return index
|
|
191
|
+
if name in LONG_VALUE:
|
|
192
|
+
value = inline or _next(argv, index, f"--{name}")
|
|
193
|
+
index += 0 if inline else 1
|
|
194
|
+
_apply_long_value(options, name, value)
|
|
195
|
+
return index
|
|
196
|
+
if name in LONG_BOOL:
|
|
197
|
+
if inline:
|
|
198
|
+
raise UsageError(f"--{name} does not take a value")
|
|
199
|
+
_apply_long_bool(options, name)
|
|
200
|
+
return index
|
|
201
|
+
raise UsageError(f"unknown option --{name}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _apply_long_value(options: Options, name: str, value: str) -> None:
|
|
205
|
+
if name == "attach":
|
|
206
|
+
options.attach = value
|
|
207
|
+
elif name == "status":
|
|
208
|
+
options.status = value
|
|
209
|
+
elif name == "kill":
|
|
210
|
+
options.kill = value
|
|
211
|
+
elif name == "project":
|
|
212
|
+
options.project = value
|
|
213
|
+
elif name == "project-name":
|
|
214
|
+
options.project_name = value
|
|
215
|
+
elif name == "program":
|
|
216
|
+
options.program = value
|
|
217
|
+
elif name == "ghidra-language":
|
|
218
|
+
options.ghidra_language = value
|
|
219
|
+
elif name == "compiler":
|
|
220
|
+
options.compiler = value
|
|
221
|
+
elif name == "loader":
|
|
222
|
+
options.loader = value
|
|
223
|
+
elif name == "linger":
|
|
224
|
+
options.linger = _float(value, "--linger")
|
|
225
|
+
elif name == "idle":
|
|
226
|
+
options.idle = _float(value, "--idle")
|
|
227
|
+
elif name == "timeout":
|
|
228
|
+
options.timeout = _float(value, "--timeout")
|
|
229
|
+
elif name == "startup-timeout":
|
|
230
|
+
options.startup_timeout = _float(value, "--startup-timeout")
|
|
231
|
+
elif name == "analysis-timeout":
|
|
232
|
+
options.analysis_timeout = _float(value, "--analysis-timeout")
|
|
233
|
+
elif name == "assembler":
|
|
234
|
+
if value not in ("ghidra", "rasm2", "auto"):
|
|
235
|
+
raise UsageError("--assembler must be ghidra, rasm2 or auto")
|
|
236
|
+
options.assembler = value
|
|
237
|
+
elif name == "listen":
|
|
238
|
+
options.listen = value
|
|
239
|
+
elif name == "color":
|
|
240
|
+
options.color = _int(value, "--color")
|
|
241
|
+
elif name == "endian":
|
|
242
|
+
if value not in ("big", "little"):
|
|
243
|
+
raise UsageError("--endian must be big or little")
|
|
244
|
+
options.endian = value
|
|
245
|
+
elif name == "list-offset":
|
|
246
|
+
offset = _int(value, "--list-offset")
|
|
247
|
+
if offset < 0:
|
|
248
|
+
raise UsageError("--list-offset must not be negative")
|
|
249
|
+
options.list_offset = offset
|
|
250
|
+
elif name == "pipe-listen":
|
|
251
|
+
options.pipe_listen = _parse_pipe_listen(value)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _apply_long_bool(options: Options, name: str) -> None:
|
|
255
|
+
if name == "help":
|
|
256
|
+
options.show_help = True
|
|
257
|
+
elif name == "version":
|
|
258
|
+
options.show_version = 1
|
|
259
|
+
elif name == "no-spawn":
|
|
260
|
+
options.spawn = False
|
|
261
|
+
elif name == "save-on-close":
|
|
262
|
+
options.save_on_close = True
|
|
263
|
+
elif name == "no-save-on-close":
|
|
264
|
+
options.save_on_close = False
|
|
265
|
+
elif name == "sessions":
|
|
266
|
+
options.list_sessions = True
|
|
267
|
+
elif name == "json":
|
|
268
|
+
options.json = True
|
|
269
|
+
elif name == "force-language":
|
|
270
|
+
options.force_language = True
|
|
271
|
+
elif name == "force-base":
|
|
272
|
+
options.force_base = True
|
|
273
|
+
elif name == "wait":
|
|
274
|
+
options.wait = True
|
|
275
|
+
elif name == "ephemeral":
|
|
276
|
+
options.ephemeral = True
|
|
277
|
+
elif name == "reimport":
|
|
278
|
+
options.reimport = True
|
|
279
|
+
elif name == "allow-exec":
|
|
280
|
+
options.allow_exec = True
|
|
281
|
+
elif name == "daemon-readonly":
|
|
282
|
+
options.daemon_readonly = True
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _short(options: Options, cluster: str, argv: list[str], index: int) -> int:
|
|
286
|
+
position = 0
|
|
287
|
+
while position < len(cluster):
|
|
288
|
+
doubled = cluster[position : position + 2]
|
|
289
|
+
if doubled in DOUBLED:
|
|
290
|
+
_apply_doubled(options, doubled)
|
|
291
|
+
position += 2
|
|
292
|
+
continue
|
|
293
|
+
char = cluster[position]
|
|
294
|
+
if char in REJECTED:
|
|
295
|
+
raise UsageError(f"-{char} is not supported by gdare2: {REJECTED[char]}")
|
|
296
|
+
if char in IGNORED:
|
|
297
|
+
options.ignored.append((f"-{char}", IGNORED[char]))
|
|
298
|
+
if char in IGNORED_VALUE:
|
|
299
|
+
rest = cluster[position + 1 :]
|
|
300
|
+
if rest:
|
|
301
|
+
return index
|
|
302
|
+
index += 1
|
|
303
|
+
_next(argv, index - 1, f"-{char}")
|
|
304
|
+
position += 1
|
|
305
|
+
continue
|
|
306
|
+
if char == "-":
|
|
307
|
+
break
|
|
308
|
+
if char in VALUE_FLAGS:
|
|
309
|
+
rest = cluster[position + 1 :]
|
|
310
|
+
if rest:
|
|
311
|
+
value = rest
|
|
312
|
+
else:
|
|
313
|
+
value = _next(argv, index, f"-{char}")
|
|
314
|
+
index += 1
|
|
315
|
+
_apply_short_value(options, char, value)
|
|
316
|
+
return index
|
|
317
|
+
if char in BOOLEAN_FLAGS:
|
|
318
|
+
_apply_short_flag(options, char)
|
|
319
|
+
position += 1
|
|
320
|
+
continue
|
|
321
|
+
raise UsageError(f"unknown option -{char}")
|
|
322
|
+
return index
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _apply_doubled(options: Options, doubled: str) -> None:
|
|
326
|
+
if doubled == "AA":
|
|
327
|
+
options.analyze = 2
|
|
328
|
+
elif doubled == "NN":
|
|
329
|
+
options.skip_rc = 2
|
|
330
|
+
elif doubled == "qq":
|
|
331
|
+
options.quiet = 2
|
|
332
|
+
elif doubled == "LL":
|
|
333
|
+
options.list_sessions = True
|
|
334
|
+
elif doubled == "nn":
|
|
335
|
+
options.ignored.append(("-nn", IGNORED["nn"]))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _apply_short_value(options: Options, char: str, value: str) -> None:
|
|
339
|
+
if char == "a":
|
|
340
|
+
options.arch = value
|
|
341
|
+
elif char == "b":
|
|
342
|
+
options.bits = value
|
|
343
|
+
elif char in ("B", "m"):
|
|
344
|
+
options.base = value
|
|
345
|
+
elif char == "c":
|
|
346
|
+
options.commands.append(value)
|
|
347
|
+
elif char == "e":
|
|
348
|
+
key, separator, setting = value.partition("=")
|
|
349
|
+
if not separator:
|
|
350
|
+
raise UsageError(f"-e expects key=value, got {value!r}")
|
|
351
|
+
options.evals.append((key.strip(), setting.strip()))
|
|
352
|
+
elif char == "i":
|
|
353
|
+
options.post_scripts.append(value)
|
|
354
|
+
elif char == "I":
|
|
355
|
+
options.pre_scripts.append(value)
|
|
356
|
+
elif char == "k":
|
|
357
|
+
options.ignored.append(("-k", IGNORED["k"]))
|
|
358
|
+
elif char == "p":
|
|
359
|
+
options.project = value
|
|
360
|
+
elif char == "s":
|
|
361
|
+
options.seek = value
|
|
362
|
+
elif char == "t":
|
|
363
|
+
options.ignored.append(("-t", IGNORED["t"]))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _apply_short_flag(options: Options, char: str) -> None:
|
|
367
|
+
if char == "A":
|
|
368
|
+
options.analyze = max(options.analyze, 1)
|
|
369
|
+
elif char == "h":
|
|
370
|
+
options.show_help = True
|
|
371
|
+
elif char == "j":
|
|
372
|
+
options.json = True
|
|
373
|
+
elif char == "L":
|
|
374
|
+
options.list_sessions = True
|
|
375
|
+
elif char == "n":
|
|
376
|
+
options.no_analysis = True
|
|
377
|
+
elif char == "N":
|
|
378
|
+
options.skip_rc = max(options.skip_rc, 1)
|
|
379
|
+
elif char == "q":
|
|
380
|
+
options.quiet = max(options.quiet, 1)
|
|
381
|
+
elif char == "v":
|
|
382
|
+
options.show_version = 1
|
|
383
|
+
elif char == "V":
|
|
384
|
+
options.show_version = 2
|
|
385
|
+
elif char == "w":
|
|
386
|
+
options.readonly = False
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _parse_pipe_listen(value: str) -> str:
|
|
390
|
+
"""Validate ``--pipe-listen [host:]port`` and return it unchanged."""
|
|
391
|
+
host, _, port_text = value.rpartition(":")
|
|
392
|
+
if not port_text.isdigit():
|
|
393
|
+
raise UsageError("--pipe-listen expects [host:]port")
|
|
394
|
+
port = int(port_text)
|
|
395
|
+
if not 0 <= port <= 65535:
|
|
396
|
+
raise UsageError("--pipe-listen port must be 0-65535")
|
|
397
|
+
return value
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _next(argv: list[str], index: int, what: str) -> str:
|
|
401
|
+
if index >= len(argv):
|
|
402
|
+
raise UsageError(f"{what} needs a value")
|
|
403
|
+
return argv[index]
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _float(value: str, what: str) -> float:
|
|
407
|
+
try:
|
|
408
|
+
return float(value)
|
|
409
|
+
except ValueError:
|
|
410
|
+
raise UsageError(f"{what} needs a number, got {value!r}") from None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _int(value: str, what: str) -> int:
|
|
414
|
+
try:
|
|
415
|
+
return int(value, 0)
|
|
416
|
+
except ValueError:
|
|
417
|
+
raise UsageError(f"{what} needs a number, got {value!r}") from None
|
gdare2/assembler.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Assembler backends: Ghidra SLEIGH and an optional rasm2 fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from gdare2.errors import GdarError, UnsupportedError, UsageError
|
|
10
|
+
|
|
11
|
+
MAX_LINES = 1024
|
|
12
|
+
RASM2_TIMEOUT = 15
|
|
13
|
+
|
|
14
|
+
#: gdare2 architecture -> (rasm2 -a name, big endian)
|
|
15
|
+
_RASM2_ARCH = {
|
|
16
|
+
"x86": ("x86", False),
|
|
17
|
+
"armle": ("arm", False),
|
|
18
|
+
"armbe": ("arm", True),
|
|
19
|
+
"aarch64": ("arm", False),
|
|
20
|
+
"aarch64be": ("arm", True),
|
|
21
|
+
"mipsle": ("mips", False),
|
|
22
|
+
"mipsbe": ("mips", True),
|
|
23
|
+
"mips64le": ("mips", False),
|
|
24
|
+
"mips64be": ("mips", True),
|
|
25
|
+
"ppcle": ("ppc", False),
|
|
26
|
+
"ppc": ("ppc", True),
|
|
27
|
+
"ppc64": ("ppc", True),
|
|
28
|
+
"ppc64le": ("ppc", False),
|
|
29
|
+
"riscv32": ("riscv", False),
|
|
30
|
+
"riscv64": ("riscv", False),
|
|
31
|
+
"sparc": ("sparc", True),
|
|
32
|
+
"sparc64": ("sparc", True),
|
|
33
|
+
"m68k": ("m68k", True),
|
|
34
|
+
"sh2": ("sh", True),
|
|
35
|
+
"sh2a": ("sh", True),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#: Ghidra language processors use upper-case names such as ARM/AARCH64; r2
|
|
39
|
+
#: aliases are lower-case. Map the canonical names as well so ELF imports
|
|
40
|
+
#: work with the rasm2 backend without requiring a raw-binary -a alias.
|
|
41
|
+
_RASM2_CANONICAL = {
|
|
42
|
+
"x86": "x86",
|
|
43
|
+
"arm": "arm",
|
|
44
|
+
"aarch64": "arm",
|
|
45
|
+
"mips": "mips",
|
|
46
|
+
"powerpc": "ppc",
|
|
47
|
+
"riscv": "riscv",
|
|
48
|
+
"sparc": "sparc",
|
|
49
|
+
"68000": "m68k",
|
|
50
|
+
"superh": "sh",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_HEXPAIRS = re.compile(r"^[0-9a-fA-F]+$")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def assemble(session, ea: int, text: str) -> dict:
|
|
57
|
+
"""Assemble *text* using the configured backend."""
|
|
58
|
+
mode = session.config.get_str("asm.assembler")
|
|
59
|
+
if mode == "ghidra":
|
|
60
|
+
return session.call_assembler("assemble", ea=int(ea), text=text)
|
|
61
|
+
if mode == "rasm2":
|
|
62
|
+
return rasm2_assemble(session, int(ea), text)
|
|
63
|
+
if mode == "auto":
|
|
64
|
+
try:
|
|
65
|
+
return session.call_assembler("assemble", ea=int(ea), text=text)
|
|
66
|
+
except GdarError as original:
|
|
67
|
+
binary = session.config.get_str("asm.rasm2")
|
|
68
|
+
if shutil.which(binary) is None:
|
|
69
|
+
raise original
|
|
70
|
+
try:
|
|
71
|
+
payload = rasm2_assemble(session, int(ea), text)
|
|
72
|
+
except GdarError:
|
|
73
|
+
raise original from None
|
|
74
|
+
session.stderr("WARN: assembler fallback to rasm2")
|
|
75
|
+
return payload
|
|
76
|
+
raise UsageError("e asm.assembler must be ghidra, rasm2 or auto")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def assemble_write(session, ea: int, text: str) -> dict:
|
|
80
|
+
"""Assemble and write, using one daemon call for the whole SLEIGH path."""
|
|
81
|
+
mode = session.config.get_str("asm.assembler")
|
|
82
|
+
if mode == "ghidra":
|
|
83
|
+
return session.call_assembler("assemble_write", ea=int(ea), text=text)
|
|
84
|
+
if mode == "auto":
|
|
85
|
+
try:
|
|
86
|
+
return session.call_assembler("assemble_write", ea=int(ea), text=text)
|
|
87
|
+
except GdarError as original:
|
|
88
|
+
binary = session.config.get_str("asm.rasm2")
|
|
89
|
+
if shutil.which(binary) is None:
|
|
90
|
+
raise original
|
|
91
|
+
try:
|
|
92
|
+
payload = rasm2_assemble(session, int(ea), text)
|
|
93
|
+
except GdarError:
|
|
94
|
+
raise original from None
|
|
95
|
+
session.stderr("WARN: assembler fallback to rasm2")
|
|
96
|
+
else:
|
|
97
|
+
payload = assemble(session, int(ea), text)
|
|
98
|
+
session.call("write_bytes", ea=int(ea), data=bytes(payload["bytes"]))
|
|
99
|
+
payload["addr"] = int(ea)
|
|
100
|
+
return payload
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def rasm2_assemble(session, ea: int, text: str) -> dict:
|
|
104
|
+
binary = session.config.get_str("asm.rasm2")
|
|
105
|
+
arch = str(session.info.get("arch") or "")
|
|
106
|
+
mapping = _RASM2_ARCH.get(arch.lower())
|
|
107
|
+
if mapping is None:
|
|
108
|
+
canonical = _RASM2_CANONICAL.get(arch.lower())
|
|
109
|
+
if canonical is not None:
|
|
110
|
+
mapping = (canonical, False)
|
|
111
|
+
if mapping is None:
|
|
112
|
+
raise UnsupportedError(f"rasm2 has no architecture mapping for {arch!r}")
|
|
113
|
+
rasm_arch, big_endian = mapping
|
|
114
|
+
big_endian = big_endian or session.info.get("endian") == "big"
|
|
115
|
+
bits = int(session.info.get("bits") or 32)
|
|
116
|
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
117
|
+
if not lines:
|
|
118
|
+
raise UsageError("assembly text is empty")
|
|
119
|
+
if len(lines) > MAX_LINES:
|
|
120
|
+
raise UsageError(f"assembly is limited to {MAX_LINES} lines")
|
|
121
|
+
|
|
122
|
+
address = int(ea)
|
|
123
|
+
items = []
|
|
124
|
+
for line in lines:
|
|
125
|
+
command = [binary, "-a", rasm_arch, "-b", str(bits)]
|
|
126
|
+
if big_endian:
|
|
127
|
+
command.append("-e")
|
|
128
|
+
command.append(line)
|
|
129
|
+
try:
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
command,
|
|
132
|
+
capture_output=True,
|
|
133
|
+
text=True,
|
|
134
|
+
timeout=RASM2_TIMEOUT,
|
|
135
|
+
check=False,
|
|
136
|
+
)
|
|
137
|
+
except FileNotFoundError:
|
|
138
|
+
raise UnsupportedError(f"rasm2 not found: {binary}") from None
|
|
139
|
+
except subprocess.TimeoutExpired:
|
|
140
|
+
raise GdarError(f"rasm2 timed out after {RASM2_TIMEOUT}s") from None
|
|
141
|
+
hexpairs = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else ""
|
|
142
|
+
if result.returncode != 0 or not hexpairs or len(hexpairs) % 2 or not _HEXPAIRS.match(hexpairs):
|
|
143
|
+
detail = (result.stderr or result.stdout or "no output").strip().splitlines()
|
|
144
|
+
raise GdarError(
|
|
145
|
+
f"rasm2 failed for {line!r}: {detail[-1] if detail else 'no output'}"
|
|
146
|
+
)
|
|
147
|
+
data = bytes.fromhex(hexpairs)
|
|
148
|
+
items.append(
|
|
149
|
+
{"addr": address, "text": line, "bytes": data, "size": len(data)}
|
|
150
|
+
)
|
|
151
|
+
address += len(data)
|
|
152
|
+
return {
|
|
153
|
+
"items": items,
|
|
154
|
+
"bytes": b"".join(item["bytes"] for item in items),
|
|
155
|
+
"size": sum(item["size"] for item in items),
|
|
156
|
+
"assembler": "rasm2",
|
|
157
|
+
}
|