ghidra-decomp 0.2.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.
- ghidra_decomp/__init__.py +3 -0
- ghidra_decomp/__main__.py +5 -0
- ghidra_decomp/cli.py +128 -0
- ghidra_decomp/decompiler.py +781 -0
- ghidra_decomp-0.2.0.dist-info/METADATA +124 -0
- ghidra_decomp-0.2.0.dist-info/RECORD +9 -0
- ghidra_decomp-0.2.0.dist-info/WHEEL +4 -0
- ghidra_decomp-0.2.0.dist-info/entry_points.txt +2 -0
- ghidra_decomp-0.2.0.dist-info/licenses/LICENSE +21 -0
ghidra_decomp/cli.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""CLI entry point for ghidra-decomp."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .decompiler import decompile_binary, list_languages
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command()
|
|
10
|
+
@click.argument(
|
|
11
|
+
"binary",
|
|
12
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
13
|
+
required=False,
|
|
14
|
+
)
|
|
15
|
+
@click.option(
|
|
16
|
+
"-o",
|
|
17
|
+
"--output",
|
|
18
|
+
type=click.Path(path_type=Path),
|
|
19
|
+
default=None,
|
|
20
|
+
help="Output directory. Defaults to <binary_name>_decomp/",
|
|
21
|
+
)
|
|
22
|
+
@click.option(
|
|
23
|
+
"--timeout",
|
|
24
|
+
type=int,
|
|
25
|
+
default=60,
|
|
26
|
+
show_default=True,
|
|
27
|
+
help="Decompilation timeout per function (seconds).",
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--ghidra-path",
|
|
31
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
32
|
+
envvar="GHIDRA_INSTALL_DIR",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Path to Ghidra installation. Falls back to GHIDRA_INSTALL_DIR env var.",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--base-addr",
|
|
38
|
+
type=str,
|
|
39
|
+
default=None,
|
|
40
|
+
help="Rebase the binary to this address before analysis (e.g. 0x80000000).",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--entry",
|
|
44
|
+
type=str,
|
|
45
|
+
default=None,
|
|
46
|
+
help="Mark this address as the program entry point and disassemble from it "
|
|
47
|
+
"before analysis (e.g. 0x31000). Useful for raw binaries.",
|
|
48
|
+
)
|
|
49
|
+
@click.option(
|
|
50
|
+
"--language",
|
|
51
|
+
type=str,
|
|
52
|
+
default=None,
|
|
53
|
+
help="Force Ghidra language/processor ID (e.g. ARM:LE:32:v7). "
|
|
54
|
+
"Use when auto-detect fails. See --list-languages.",
|
|
55
|
+
)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--compiler",
|
|
58
|
+
type=str,
|
|
59
|
+
default=None,
|
|
60
|
+
help="Force compiler spec ID (e.g. gcc, default, windows). "
|
|
61
|
+
"Requires --language. See --list-languages.",
|
|
62
|
+
)
|
|
63
|
+
@click.option(
|
|
64
|
+
"--list-languages",
|
|
65
|
+
"list_langs",
|
|
66
|
+
is_flag=True,
|
|
67
|
+
default=False,
|
|
68
|
+
help="List all available Ghidra language IDs and their compiler specs, then exit.",
|
|
69
|
+
)
|
|
70
|
+
def main(
|
|
71
|
+
binary: Path | None,
|
|
72
|
+
output: Path | None,
|
|
73
|
+
timeout: int,
|
|
74
|
+
ghidra_path: Path | None,
|
|
75
|
+
base_addr: str | None,
|
|
76
|
+
entry: str | None,
|
|
77
|
+
language: str | None,
|
|
78
|
+
compiler: str | None,
|
|
79
|
+
list_langs: bool,
|
|
80
|
+
):
|
|
81
|
+
"""Decompile BINARY into a browsable source tree.
|
|
82
|
+
|
|
83
|
+
Produces one .c file per function, plus JSON indexes for strings,
|
|
84
|
+
call graph, imports, and exports.
|
|
85
|
+
"""
|
|
86
|
+
if list_langs:
|
|
87
|
+
list_languages(ghidra_path=ghidra_path)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if binary is None:
|
|
91
|
+
raise click.UsageError("Missing argument 'BINARY'.")
|
|
92
|
+
|
|
93
|
+
if compiler is not None and language is None:
|
|
94
|
+
raise click.UsageError("--compiler requires --language.")
|
|
95
|
+
|
|
96
|
+
binary = binary.resolve()
|
|
97
|
+
if output is None:
|
|
98
|
+
output = Path.cwd() / f"{binary.stem}_decomp"
|
|
99
|
+
|
|
100
|
+
output = output.resolve()
|
|
101
|
+
|
|
102
|
+
click.echo(f"Binary: {binary}")
|
|
103
|
+
click.echo(f"Output: {output}")
|
|
104
|
+
if base_addr is not None:
|
|
105
|
+
click.echo(f"Base: {base_addr}")
|
|
106
|
+
if entry is not None:
|
|
107
|
+
click.echo(f"Entry: {entry}")
|
|
108
|
+
if language is not None:
|
|
109
|
+
click.echo(f"Lang: {language}")
|
|
110
|
+
if compiler is not None:
|
|
111
|
+
click.echo(f"CSpec: {compiler}")
|
|
112
|
+
|
|
113
|
+
decompile_binary(
|
|
114
|
+
binary,
|
|
115
|
+
output,
|
|
116
|
+
timeout=timeout,
|
|
117
|
+
ghidra_path=ghidra_path,
|
|
118
|
+
base_addr=base_addr,
|
|
119
|
+
entry=entry,
|
|
120
|
+
language=language,
|
|
121
|
+
compiler=compiler,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
click.echo(f"\nDone. Output at: {output}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
"""Core decompilation logic using pyghidra."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
from contextlib import ExitStack
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _sanitize_name(name: str) -> str:
|
|
15
|
+
"""Turn a function name into a safe filename component."""
|
|
16
|
+
return re.sub(r"[^\w\-.]", "_", name)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _func_header_comment(func) -> str:
|
|
20
|
+
"""Build a metadata header comment for a decompiled function."""
|
|
21
|
+
addr = func.getEntryPoint().toString()
|
|
22
|
+
name = func.getName()
|
|
23
|
+
body = func.getBody()
|
|
24
|
+
size = body.getNumAddresses()
|
|
25
|
+
convention = func.getCallingConventionName() or "unknown"
|
|
26
|
+
param_count = func.getParameterCount()
|
|
27
|
+
is_thunk = func.isThunk()
|
|
28
|
+
|
|
29
|
+
lines = [
|
|
30
|
+
f"// Function: {name}",
|
|
31
|
+
f"// Address: {addr}",
|
|
32
|
+
f"// Size: {size} bytes",
|
|
33
|
+
f"// Calling: {convention}",
|
|
34
|
+
f"// Params: {param_count}",
|
|
35
|
+
]
|
|
36
|
+
if is_thunk:
|
|
37
|
+
thunked = func.getThunkedFunction(True)
|
|
38
|
+
thunk_name = thunked.getName() if thunked else "unknown"
|
|
39
|
+
lines.append(f"// Thunk: -> {thunk_name}")
|
|
40
|
+
lines.append("")
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _start_pyghidra(ghidra_path: Path | None):
|
|
45
|
+
"""Import and start pyghidra, returning the module."""
|
|
46
|
+
try:
|
|
47
|
+
import pyghidra
|
|
48
|
+
except ImportError:
|
|
49
|
+
raise click.ClickException(
|
|
50
|
+
"pyghidra is not installed. Install it with: pip install pyghidra\n"
|
|
51
|
+
"pyghidra is bundled with Ghidra 11+."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not pyghidra.started():
|
|
55
|
+
install_dir = str(ghidra_path) if ghidra_path else None
|
|
56
|
+
try:
|
|
57
|
+
pyghidra.start(install_dir=install_dir) if install_dir else pyghidra.start()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
hint = ""
|
|
60
|
+
if not ghidra_path:
|
|
61
|
+
hint = (
|
|
62
|
+
"\nSet GHIDRA_INSTALL_DIR or pass --ghidra-path "
|
|
63
|
+
"to your Ghidra installation."
|
|
64
|
+
)
|
|
65
|
+
raise click.ClickException(f"Failed to start Ghidra: {e}{hint}")
|
|
66
|
+
|
|
67
|
+
return pyghidra
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def list_languages(*, ghidra_path: Path | None = None) -> None:
|
|
71
|
+
"""Print all Ghidra language IDs with their available compiler specs."""
|
|
72
|
+
_start_pyghidra(ghidra_path)
|
|
73
|
+
|
|
74
|
+
from ghidra.program.util import DefaultLanguageService
|
|
75
|
+
|
|
76
|
+
svc = DefaultLanguageService.getLanguageService()
|
|
77
|
+
descs = list(svc.getLanguageDescriptions(False))
|
|
78
|
+
descs.sort(key=lambda d: str(d.getLanguageID()))
|
|
79
|
+
|
|
80
|
+
for d in descs:
|
|
81
|
+
lang_id = str(d.getLanguageID())
|
|
82
|
+
processor = str(d.getProcessor())
|
|
83
|
+
endian = str(d.getEndian())
|
|
84
|
+
size = d.getSize()
|
|
85
|
+
cspecs = [
|
|
86
|
+
str(c.getCompilerSpecID())
|
|
87
|
+
for c in d.getCompatibleCompilerSpecDescriptions()
|
|
88
|
+
]
|
|
89
|
+
click.echo(
|
|
90
|
+
f"{lang_id} [{processor} {endian} {size}] cspec: {', '.join(cspecs) or '-'}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
click.echo(f"\n{len(descs)} language(s).")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def decompile_binary(
|
|
97
|
+
binary: Path,
|
|
98
|
+
output: Path,
|
|
99
|
+
*,
|
|
100
|
+
timeout: int = 60,
|
|
101
|
+
ghidra_path: Path | None = None,
|
|
102
|
+
base_addr: str | None = None,
|
|
103
|
+
language: str | None = None,
|
|
104
|
+
compiler: str | None = None,
|
|
105
|
+
entry: str | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Decompile a binary and write structured output."""
|
|
108
|
+
pyghidra = _start_pyghidra(ghidra_path)
|
|
109
|
+
|
|
110
|
+
open_kwargs: dict[str, object] = {"analyze": False}
|
|
111
|
+
if language is not None:
|
|
112
|
+
open_kwargs["language"] = language
|
|
113
|
+
if compiler is not None:
|
|
114
|
+
open_kwargs["compiler"] = compiler
|
|
115
|
+
|
|
116
|
+
stack = ExitStack()
|
|
117
|
+
try:
|
|
118
|
+
flat_api = stack.enter_context(
|
|
119
|
+
pyghidra.open_program(str(binary), **open_kwargs)
|
|
120
|
+
)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
msg = str(e)
|
|
123
|
+
if "No load spec found" in msg:
|
|
124
|
+
hint = (
|
|
125
|
+
f"Ghidra could not identify {binary.name}. Likely a raw or unknown "
|
|
126
|
+
"format.\nPass --language (and optionally --compiler) to force a "
|
|
127
|
+
"spec. Run with --list-languages to discover IDs."
|
|
128
|
+
)
|
|
129
|
+
elif language is not None:
|
|
130
|
+
hint = (
|
|
131
|
+
f"Failed to load with --language {language}"
|
|
132
|
+
+ (f" --compiler {compiler}" if compiler else "")
|
|
133
|
+
+ f": {msg}"
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
hint = f"Failed to open program: {msg}"
|
|
137
|
+
raise click.ClickException(hint)
|
|
138
|
+
|
|
139
|
+
with stack:
|
|
140
|
+
program = flat_api.getCurrentProgram()
|
|
141
|
+
|
|
142
|
+
# Grab the project directory so we can clean it up after closing
|
|
143
|
+
project_loc = program.getDomainFile().getProjectLocator()
|
|
144
|
+
project_dir = Path(str(project_loc.getProjectDir())) if project_loc else None
|
|
145
|
+
|
|
146
|
+
if base_addr is not None:
|
|
147
|
+
_rebase_program(program, base_addr)
|
|
148
|
+
|
|
149
|
+
if entry is not None:
|
|
150
|
+
_mark_entry(program, flat_api, entry)
|
|
151
|
+
|
|
152
|
+
_run_analysis(program, flat_api)
|
|
153
|
+
|
|
154
|
+
_export_all(program, output, timeout)
|
|
155
|
+
|
|
156
|
+
# Clean up Ghidra project cache so runs are always atomic
|
|
157
|
+
if project_dir and project_dir.is_dir():
|
|
158
|
+
try:
|
|
159
|
+
shutil.rmtree(project_dir)
|
|
160
|
+
except OSError as e:
|
|
161
|
+
click.echo(f"Warning: could not clean up Ghidra cache: {e}", err=True)
|
|
162
|
+
# Also clean siblings (.gpr file, or .rep directory)
|
|
163
|
+
if project_dir:
|
|
164
|
+
if project_dir.name.endswith(".rep"):
|
|
165
|
+
# Clean the .gpr project file
|
|
166
|
+
gpr_file = project_dir.parent / (project_dir.name[:-4] + ".gpr")
|
|
167
|
+
if gpr_file.is_file():
|
|
168
|
+
try:
|
|
169
|
+
gpr_file.unlink()
|
|
170
|
+
except OSError:
|
|
171
|
+
pass
|
|
172
|
+
else:
|
|
173
|
+
# Clean the .rep repository directory
|
|
174
|
+
rep_dir = project_dir.parent / (project_dir.name + ".rep")
|
|
175
|
+
if rep_dir.is_dir() and rep_dir != project_dir:
|
|
176
|
+
try:
|
|
177
|
+
shutil.rmtree(rep_dir)
|
|
178
|
+
except OSError:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _rebase_program(program, base_addr: str) -> None:
|
|
183
|
+
"""Rebase the program to a new image base address."""
|
|
184
|
+
try:
|
|
185
|
+
addr_value = int(base_addr, 0) # supports 0x hex, decimal, 0o octal
|
|
186
|
+
except ValueError:
|
|
187
|
+
raise click.BadParameter(
|
|
188
|
+
f"Invalid base address: {base_addr!r}. "
|
|
189
|
+
"Use hex (0x80000000), decimal, or octal (0o...) format.",
|
|
190
|
+
param_hint="'--base-addr'",
|
|
191
|
+
)
|
|
192
|
+
addr_space = program.getAddressFactory().getDefaultAddressSpace()
|
|
193
|
+
new_base = addr_space.getAddress(addr_value)
|
|
194
|
+
|
|
195
|
+
old_base = program.getImageBase()
|
|
196
|
+
click.echo(f"Rebasing: {old_base} -> {new_base}")
|
|
197
|
+
|
|
198
|
+
success = False
|
|
199
|
+
tid = program.startTransaction("Rebase image")
|
|
200
|
+
try:
|
|
201
|
+
program.setImageBase(new_base, True)
|
|
202
|
+
success = True
|
|
203
|
+
finally:
|
|
204
|
+
program.endTransaction(tid, success)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _mark_entry(program, flat_api, entry: str) -> None:
|
|
208
|
+
"""Disassemble from entry, create a function there, mark as program entry."""
|
|
209
|
+
try:
|
|
210
|
+
addr_value = int(entry, 0)
|
|
211
|
+
except ValueError:
|
|
212
|
+
raise click.BadParameter(
|
|
213
|
+
f"Invalid entry address: {entry!r}. "
|
|
214
|
+
"Use hex (0x31000), decimal, or octal (0o...) format.",
|
|
215
|
+
param_hint="'--entry'",
|
|
216
|
+
)
|
|
217
|
+
addr_space = program.getAddressFactory().getDefaultAddressSpace()
|
|
218
|
+
entry_addr = addr_space.getAddress(addr_value)
|
|
219
|
+
click.echo(f"Entry: {entry_addr}")
|
|
220
|
+
|
|
221
|
+
success = False
|
|
222
|
+
tid = program.startTransaction("Mark entry point")
|
|
223
|
+
try:
|
|
224
|
+
program.getSymbolTable().addExternalEntryPoint(entry_addr)
|
|
225
|
+
flat_api.disassemble(entry_addr)
|
|
226
|
+
if flat_api.getFunctionAt(entry_addr) is None:
|
|
227
|
+
flat_api.createFunction(entry_addr, "entry")
|
|
228
|
+
success = True
|
|
229
|
+
finally:
|
|
230
|
+
program.endTransaction(tid, success)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _run_analysis(program, flat_api) -> None:
|
|
234
|
+
"""Run Ghidra's auto-analysis on the program."""
|
|
235
|
+
click.echo("Running auto-analysis...")
|
|
236
|
+
success = False
|
|
237
|
+
tid = program.startTransaction("Auto-analysis")
|
|
238
|
+
try:
|
|
239
|
+
flat_api.analyzeAll(program)
|
|
240
|
+
success = True
|
|
241
|
+
finally:
|
|
242
|
+
program.endTransaction(tid, success)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _export_all(program, output: Path, timeout: int) -> None:
|
|
246
|
+
"""Run all exports against an open Ghidra program."""
|
|
247
|
+
from ghidra.app.decompiler import DecompInterface
|
|
248
|
+
from ghidra.util.task import ConsoleTaskMonitor
|
|
249
|
+
|
|
250
|
+
monitor = ConsoleTaskMonitor()
|
|
251
|
+
|
|
252
|
+
# Set up decompiler
|
|
253
|
+
decomp = DecompInterface()
|
|
254
|
+
decomp.openProgram(program)
|
|
255
|
+
|
|
256
|
+
func_dir = output / "functions"
|
|
257
|
+
if func_dir.exists():
|
|
258
|
+
shutil.rmtree(func_dir)
|
|
259
|
+
func_dir.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
|
|
261
|
+
fm = program.getFunctionManager()
|
|
262
|
+
total = fm.getFunctionCount()
|
|
263
|
+
functions = list(fm.getFunctions(True))
|
|
264
|
+
|
|
265
|
+
click.echo(f"Functions: {total}")
|
|
266
|
+
|
|
267
|
+
# ---- Decompile all functions ----
|
|
268
|
+
callgraph: dict[str, dict] = {}
|
|
269
|
+
failed: list[tuple] = [] # (func, key, filename) for retry
|
|
270
|
+
results_map: dict[
|
|
271
|
+
str, str | None
|
|
272
|
+
] = {} # key -> content, preserves order for combined file
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
with click.progressbar(functions, label="Decompiling", show_pos=True) as bar:
|
|
276
|
+
for func in bar:
|
|
277
|
+
addr = func.getEntryPoint().toString()
|
|
278
|
+
name = func.getName()
|
|
279
|
+
key = f"{addr}_{name}"
|
|
280
|
+
safe_name = _sanitize_name(name)
|
|
281
|
+
filename = f"{addr}_{safe_name}.c"
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
header = _func_header_comment(func)
|
|
285
|
+
|
|
286
|
+
# Decompile
|
|
287
|
+
result = decomp.decompileFunction(func, timeout, monitor)
|
|
288
|
+
if result and result.decompileCompleted():
|
|
289
|
+
decomp_func = result.getDecompiledFunction()
|
|
290
|
+
c_code = decomp_func.getC() if decomp_func else None
|
|
291
|
+
if c_code is not None:
|
|
292
|
+
content = header + c_code
|
|
293
|
+
(func_dir / filename).write_text(content, encoding="utf-8")
|
|
294
|
+
results_map[key] = content
|
|
295
|
+
else:
|
|
296
|
+
failed.append((func, key, filename))
|
|
297
|
+
results_map[key] = None
|
|
298
|
+
else:
|
|
299
|
+
failed.append((func, key, filename))
|
|
300
|
+
results_map[key] = None
|
|
301
|
+
except Exception as e:
|
|
302
|
+
click.echo(
|
|
303
|
+
f"\nWarning: error decompiling {name} at {addr}: {e}", err=True
|
|
304
|
+
)
|
|
305
|
+
results_map[key] = None
|
|
306
|
+
if (func, key, filename) not in failed:
|
|
307
|
+
failed.append((func, key, filename))
|
|
308
|
+
|
|
309
|
+
# Call graph entry (independent of decompilation success)
|
|
310
|
+
try:
|
|
311
|
+
called_funcs = []
|
|
312
|
+
for called in func.getCalledFunctions(None):
|
|
313
|
+
called_funcs.append(
|
|
314
|
+
{
|
|
315
|
+
"address": called.getEntryPoint().toString(),
|
|
316
|
+
"name": called.getName(),
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
calling_funcs = []
|
|
321
|
+
for caller in func.getCallingFunctions(None):
|
|
322
|
+
calling_funcs.append(
|
|
323
|
+
{
|
|
324
|
+
"address": caller.getEntryPoint().toString(),
|
|
325
|
+
"name": caller.getName(),
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
callgraph[addr] = {
|
|
330
|
+
"name": name,
|
|
331
|
+
"address": addr,
|
|
332
|
+
"calls": called_funcs,
|
|
333
|
+
"called_by": calling_funcs,
|
|
334
|
+
}
|
|
335
|
+
except Exception as e:
|
|
336
|
+
click.echo(
|
|
337
|
+
f"\nWarning: error extracting call graph for {name} at {addr}: {e}",
|
|
338
|
+
err=True,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# ---- Retry failed functions with escalated timeout ----
|
|
342
|
+
if failed:
|
|
343
|
+
retry_timeout = timeout * 3
|
|
344
|
+
click.echo(
|
|
345
|
+
f"Retrying {len(failed)} failed functions "
|
|
346
|
+
f"with {retry_timeout}s timeout..."
|
|
347
|
+
)
|
|
348
|
+
still_failed = []
|
|
349
|
+
with click.progressbar(failed, label="Retrying", show_pos=True) as bar:
|
|
350
|
+
for func, key, filename in bar:
|
|
351
|
+
try:
|
|
352
|
+
header = _func_header_comment(func)
|
|
353
|
+
result = decomp.decompileFunction(func, retry_timeout, monitor)
|
|
354
|
+
decomp_func = (
|
|
355
|
+
result.getDecompiledFunction()
|
|
356
|
+
if result and result.decompileCompleted()
|
|
357
|
+
else None
|
|
358
|
+
)
|
|
359
|
+
c_code = decomp_func.getC() if decomp_func else None
|
|
360
|
+
if c_code is not None:
|
|
361
|
+
content = header + c_code
|
|
362
|
+
else:
|
|
363
|
+
still_failed.append(key)
|
|
364
|
+
error_msg = (
|
|
365
|
+
result.getErrorMessage() if result else None
|
|
366
|
+
) or "unknown error"
|
|
367
|
+
asm = _disassemble_function(program, func)
|
|
368
|
+
content = (
|
|
369
|
+
header + f"// DECOMPILATION FAILED: {error_msg}\n" + asm
|
|
370
|
+
)
|
|
371
|
+
(func_dir / filename).write_text(content, encoding="utf-8")
|
|
372
|
+
results_map[key] = content
|
|
373
|
+
except Exception as e:
|
|
374
|
+
click.echo(f"\nWarning: error retrying {key}: {e}", err=True)
|
|
375
|
+
still_failed.append(key)
|
|
376
|
+
# Write a minimal fallback file
|
|
377
|
+
fallback = (
|
|
378
|
+
f"// DECOMPILATION FAILED: exception during retry: {e}\n"
|
|
379
|
+
)
|
|
380
|
+
(func_dir / filename).write_text(fallback, encoding="utf-8")
|
|
381
|
+
results_map[key] = fallback
|
|
382
|
+
failed = still_failed
|
|
383
|
+
finally:
|
|
384
|
+
decomp.dispose()
|
|
385
|
+
|
|
386
|
+
# ---- Combined file ----
|
|
387
|
+
all_chunks = [c for c in results_map.values() if c is not None]
|
|
388
|
+
combined = output / "all_functions.c"
|
|
389
|
+
separator = "\n" + ("/" * 80) + "\n\n"
|
|
390
|
+
combined.write_text(separator.join(all_chunks), encoding="utf-8")
|
|
391
|
+
|
|
392
|
+
if failed:
|
|
393
|
+
click.echo(f"Failed to decompile {len(failed)} functions")
|
|
394
|
+
|
|
395
|
+
# ---- Strings ----
|
|
396
|
+
click.echo("Extracting strings...")
|
|
397
|
+
strings = _extract_strings(program)
|
|
398
|
+
|
|
399
|
+
# ---- Imports / Exports ----
|
|
400
|
+
click.echo("Extracting imports & exports...")
|
|
401
|
+
imports = _extract_imports(program)
|
|
402
|
+
exports = _extract_exports(program)
|
|
403
|
+
|
|
404
|
+
# ---- Symbols (globals, labels, data) ----
|
|
405
|
+
click.echo("Extracting symbols...")
|
|
406
|
+
symbols = _extract_symbols(program)
|
|
407
|
+
|
|
408
|
+
# ---- Types ----
|
|
409
|
+
click.echo("Extracting types...")
|
|
410
|
+
types = _extract_types(program)
|
|
411
|
+
|
|
412
|
+
# ---- Function index ----
|
|
413
|
+
click.echo("Writing function index...")
|
|
414
|
+
func_index = _extract_functions(functions)
|
|
415
|
+
|
|
416
|
+
# ---- Sections ----
|
|
417
|
+
click.echo("Extracting sections...")
|
|
418
|
+
sections = _extract_sections(program)
|
|
419
|
+
|
|
420
|
+
# ---- Metadata ----
|
|
421
|
+
lang = program.getLanguage()
|
|
422
|
+
metadata = {
|
|
423
|
+
"binary": program.getExecutablePath(),
|
|
424
|
+
"format": program.getExecutableFormat(),
|
|
425
|
+
"architecture": lang.getProcessor().toString(),
|
|
426
|
+
"endian": lang.isBigEndian() and "big" or "little",
|
|
427
|
+
"address_size": lang.getDefaultSpace().getSize(),
|
|
428
|
+
"image_base": program.getImageBase().toString(),
|
|
429
|
+
"num_functions": total,
|
|
430
|
+
"num_failed": len(failed),
|
|
431
|
+
"failed_functions": failed,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
# ---- Write JSON files ----
|
|
435
|
+
_write_json(output / "types.json", types)
|
|
436
|
+
_write_json(output / "functions.json", func_index)
|
|
437
|
+
_write_json(output / "callgraph.json", callgraph)
|
|
438
|
+
_write_json(output / "strings.json", strings)
|
|
439
|
+
_write_json(output / "imports.json", imports)
|
|
440
|
+
_write_json(output / "exports.json", exports)
|
|
441
|
+
_write_json(output / "symbols.json", symbols)
|
|
442
|
+
_write_json(output / "sections.json", sections)
|
|
443
|
+
_write_json(output / "metadata.json", metadata)
|
|
444
|
+
|
|
445
|
+
click.echo(
|
|
446
|
+
f"Wrote {total - len(failed)} functions, "
|
|
447
|
+
f"{len(strings)} strings, "
|
|
448
|
+
f"{len(imports)} imports, "
|
|
449
|
+
f"{len(exports)} exports, "
|
|
450
|
+
f"{len(symbols)} symbols, "
|
|
451
|
+
f"{len(sections)} sections"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _disassemble_function(program, func) -> str:
|
|
456
|
+
"""Return the assembly listing for a function as commented C lines."""
|
|
457
|
+
listing = program.getListing()
|
|
458
|
+
lines = ["// Assembly listing:\n"]
|
|
459
|
+
for instr in listing.getInstructions(func.getBody(), True):
|
|
460
|
+
addr = instr.getAddress()
|
|
461
|
+
mnemonic = instr.getMnemonicString()
|
|
462
|
+
num_ops = instr.getNumOperands()
|
|
463
|
+
operands = ", ".join(
|
|
464
|
+
instr.getDefaultOperandRepresentation(i) for i in range(num_ops)
|
|
465
|
+
)
|
|
466
|
+
lines.append(f"// {addr} {mnemonic} {operands}\n")
|
|
467
|
+
return "".join(lines)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _extract_strings(program) -> list[dict]:
|
|
471
|
+
"""Extract all defined strings with their xrefs."""
|
|
472
|
+
from ghidra.program.model.data import StringDataType
|
|
473
|
+
|
|
474
|
+
ref_manager = program.getReferenceManager()
|
|
475
|
+
fm = program.getFunctionManager()
|
|
476
|
+
listing = program.getListing()
|
|
477
|
+
strings = []
|
|
478
|
+
|
|
479
|
+
for data in listing.getDefinedData(True):
|
|
480
|
+
dt = data.getDataType()
|
|
481
|
+
if not isinstance(dt, StringDataType) and "string" not in dt.getName().lower():
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
addr = data.getAddress()
|
|
485
|
+
value = data.getValue()
|
|
486
|
+
if value is None:
|
|
487
|
+
continue
|
|
488
|
+
if isinstance(value, (bytes, bytearray)):
|
|
489
|
+
try:
|
|
490
|
+
value = value.decode("utf-8", errors="replace")
|
|
491
|
+
except Exception:
|
|
492
|
+
value = repr(value)
|
|
493
|
+
else:
|
|
494
|
+
value = str(value)
|
|
495
|
+
|
|
496
|
+
# Find xrefs to this string
|
|
497
|
+
xrefs = []
|
|
498
|
+
for ref in ref_manager.getReferencesTo(addr):
|
|
499
|
+
from_addr = ref.getFromAddress()
|
|
500
|
+
func = fm.getFunctionContaining(from_addr)
|
|
501
|
+
xrefs.append(
|
|
502
|
+
{
|
|
503
|
+
"from_address": from_addr.toString(),
|
|
504
|
+
"function": func.getName() if func else None,
|
|
505
|
+
"function_address": func.getEntryPoint().toString()
|
|
506
|
+
if func
|
|
507
|
+
else None,
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
strings.append(
|
|
512
|
+
{
|
|
513
|
+
"address": addr.toString(),
|
|
514
|
+
"value": value,
|
|
515
|
+
"length": data.getLength(),
|
|
516
|
+
"xrefs": xrefs,
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
return strings
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _extract_imports(program) -> list[dict]:
|
|
524
|
+
"""Extract imported (external) functions."""
|
|
525
|
+
fm = program.getFunctionManager()
|
|
526
|
+
imports = []
|
|
527
|
+
|
|
528
|
+
for func in fm.getExternalFunctions():
|
|
529
|
+
ext_loc = func.getExternalLocation()
|
|
530
|
+
imports.append(
|
|
531
|
+
{
|
|
532
|
+
"name": func.getName(),
|
|
533
|
+
"library": ext_loc.getLibraryName() if ext_loc else None,
|
|
534
|
+
"address": func.getEntryPoint().toString(),
|
|
535
|
+
}
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
return imports
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _extract_exports(program) -> list[dict]:
|
|
542
|
+
"""Extract exported symbols (entry points)."""
|
|
543
|
+
symbol_table = program.getSymbolTable()
|
|
544
|
+
exports = []
|
|
545
|
+
|
|
546
|
+
for symbol in symbol_table.getAllSymbols(True):
|
|
547
|
+
if symbol.isExternalEntryPoint():
|
|
548
|
+
exports.append(
|
|
549
|
+
{
|
|
550
|
+
"name": symbol.getName(),
|
|
551
|
+
"address": symbol.getAddress().toString(),
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
return exports
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _extract_symbols(program) -> list[dict]:
|
|
559
|
+
"""Extract non-function symbols: globals, labels, data."""
|
|
560
|
+
from ghidra.program.model.symbol import SymbolType
|
|
561
|
+
|
|
562
|
+
symbol_table = program.getSymbolTable()
|
|
563
|
+
listing = program.getListing()
|
|
564
|
+
skip_types = {SymbolType.FUNCTION, SymbolType.PARAMETER, SymbolType.LOCAL_VAR}
|
|
565
|
+
symbols = []
|
|
566
|
+
|
|
567
|
+
for symbol in symbol_table.getAllSymbols(True):
|
|
568
|
+
sym_type = symbol.getSymbolType()
|
|
569
|
+
if sym_type in skip_types:
|
|
570
|
+
continue
|
|
571
|
+
if symbol.isExternal():
|
|
572
|
+
continue
|
|
573
|
+
|
|
574
|
+
addr = symbol.getAddress()
|
|
575
|
+
entry = {
|
|
576
|
+
"name": symbol.getName(),
|
|
577
|
+
"address": addr.toString(),
|
|
578
|
+
"type": sym_type.toString(),
|
|
579
|
+
"namespace": symbol.getParentNamespace().getName(True),
|
|
580
|
+
"is_primary": symbol.isPrimary(),
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
# Try to get data type and size for data symbols
|
|
584
|
+
data = listing.getDataAt(addr)
|
|
585
|
+
if data is not None:
|
|
586
|
+
dt = data.getDataType()
|
|
587
|
+
entry["data_type"] = dt.getName() if dt else None
|
|
588
|
+
entry["size"] = data.getLength()
|
|
589
|
+
|
|
590
|
+
symbols.append(entry)
|
|
591
|
+
|
|
592
|
+
return symbols
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _extract_types(program) -> list[dict]:
|
|
596
|
+
"""Extract all recovered types (structs, enums, typedefs, unions, function pointers)."""
|
|
597
|
+
dtm = program.getDataTypeManager()
|
|
598
|
+
|
|
599
|
+
from ghidra.program.model.data import (
|
|
600
|
+
Structure,
|
|
601
|
+
Union,
|
|
602
|
+
Enum,
|
|
603
|
+
TypeDef,
|
|
604
|
+
FunctionDefinition,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
BUILTIN = {
|
|
608
|
+
"byte",
|
|
609
|
+
"word",
|
|
610
|
+
"dword",
|
|
611
|
+
"qword",
|
|
612
|
+
"void",
|
|
613
|
+
"bool",
|
|
614
|
+
"char",
|
|
615
|
+
"short",
|
|
616
|
+
"int",
|
|
617
|
+
"long",
|
|
618
|
+
"float",
|
|
619
|
+
"double",
|
|
620
|
+
"uchar",
|
|
621
|
+
"ushort",
|
|
622
|
+
"uint",
|
|
623
|
+
"ulong",
|
|
624
|
+
"undefined",
|
|
625
|
+
"undefined1",
|
|
626
|
+
"undefined2",
|
|
627
|
+
"undefined4",
|
|
628
|
+
"undefined8",
|
|
629
|
+
"pointer",
|
|
630
|
+
"string",
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
result = []
|
|
634
|
+
|
|
635
|
+
for dt in dtm.getAllDataTypes():
|
|
636
|
+
if dt.getName() in BUILTIN:
|
|
637
|
+
continue
|
|
638
|
+
|
|
639
|
+
if isinstance(dt, Structure):
|
|
640
|
+
fields = []
|
|
641
|
+
for comp in dt.getComponents():
|
|
642
|
+
fields.append(
|
|
643
|
+
{
|
|
644
|
+
"name": comp.getFieldName() or f"field_0x{comp.getOffset():x}",
|
|
645
|
+
"type": comp.getDataType().getName(),
|
|
646
|
+
"offset": comp.getOffset(),
|
|
647
|
+
"size": comp.getLength(),
|
|
648
|
+
}
|
|
649
|
+
)
|
|
650
|
+
result.append(
|
|
651
|
+
{
|
|
652
|
+
"kind": "struct",
|
|
653
|
+
"name": dt.getName(),
|
|
654
|
+
"size": dt.getLength(),
|
|
655
|
+
"fields": fields,
|
|
656
|
+
}
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
elif isinstance(dt, Union):
|
|
660
|
+
fields = []
|
|
661
|
+
for comp in dt.getComponents():
|
|
662
|
+
fields.append(
|
|
663
|
+
{
|
|
664
|
+
"name": comp.getFieldName() or f"field_{comp.getOrdinal()}",
|
|
665
|
+
"type": comp.getDataType().getName(),
|
|
666
|
+
"size": comp.getLength(),
|
|
667
|
+
}
|
|
668
|
+
)
|
|
669
|
+
result.append(
|
|
670
|
+
{
|
|
671
|
+
"kind": "union",
|
|
672
|
+
"name": dt.getName(),
|
|
673
|
+
"size": dt.getLength(),
|
|
674
|
+
"fields": fields,
|
|
675
|
+
}
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
elif isinstance(dt, Enum):
|
|
679
|
+
members = {}
|
|
680
|
+
for name in dt.getNames():
|
|
681
|
+
members[name] = dt.getValue(name)
|
|
682
|
+
result.append(
|
|
683
|
+
{
|
|
684
|
+
"kind": "enum",
|
|
685
|
+
"name": dt.getName(),
|
|
686
|
+
"size": dt.getLength(),
|
|
687
|
+
"members": members,
|
|
688
|
+
}
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
elif isinstance(dt, TypeDef):
|
|
692
|
+
base = dt.getBaseDataType()
|
|
693
|
+
result.append(
|
|
694
|
+
{
|
|
695
|
+
"kind": "typedef",
|
|
696
|
+
"name": dt.getName(),
|
|
697
|
+
"base_type": base.getName() if base else "unknown",
|
|
698
|
+
}
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
elif isinstance(dt, FunctionDefinition):
|
|
702
|
+
params = []
|
|
703
|
+
for arg in dt.getArguments():
|
|
704
|
+
params.append(
|
|
705
|
+
{"type": arg.getDataType().getName(), "name": arg.getName()}
|
|
706
|
+
)
|
|
707
|
+
ret = dt.getReturnType()
|
|
708
|
+
result.append(
|
|
709
|
+
{
|
|
710
|
+
"kind": "function_pointer",
|
|
711
|
+
"name": dt.getName(),
|
|
712
|
+
"return_type": ret.getName() if ret else "void",
|
|
713
|
+
"params": params,
|
|
714
|
+
}
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
return result
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _extract_functions(functions) -> list[dict]:
|
|
721
|
+
"""Extract function index with address ranges and signatures."""
|
|
722
|
+
result = []
|
|
723
|
+
for func in functions:
|
|
724
|
+
entry = func.getEntryPoint()
|
|
725
|
+
body = func.getBody()
|
|
726
|
+
size = body.getNumAddresses()
|
|
727
|
+
ret_type = func.getReturnType().getName() if func.getReturnType() else "void"
|
|
728
|
+
|
|
729
|
+
params = []
|
|
730
|
+
for p in func.getParameters():
|
|
731
|
+
dt = p.getDataType().getName() if p.getDataType() else "undefined"
|
|
732
|
+
pname = p.getName() or f"param_{p.getOrdinal()}"
|
|
733
|
+
params.append({"type": dt, "name": pname})
|
|
734
|
+
|
|
735
|
+
result.append(
|
|
736
|
+
{
|
|
737
|
+
"name": func.getName(),
|
|
738
|
+
"address": entry.toString(),
|
|
739
|
+
"end_address": body.getMaxAddress().toString(),
|
|
740
|
+
"size": size,
|
|
741
|
+
"return_type": ret_type,
|
|
742
|
+
"params": params,
|
|
743
|
+
"calling_convention": func.getCallingConventionName() or "unknown",
|
|
744
|
+
"is_thunk": func.isThunk(),
|
|
745
|
+
}
|
|
746
|
+
)
|
|
747
|
+
return result
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _extract_sections(program) -> list[dict]:
|
|
751
|
+
"""Extract memory sections/segments with permissions."""
|
|
752
|
+
memory = program.getMemory()
|
|
753
|
+
sections = []
|
|
754
|
+
|
|
755
|
+
for block in memory.getBlocks():
|
|
756
|
+
sections.append(
|
|
757
|
+
{
|
|
758
|
+
"name": block.getName(),
|
|
759
|
+
"start": block.getStart().toString(),
|
|
760
|
+
"end": block.getEnd().toString(),
|
|
761
|
+
"size": block.getSize(),
|
|
762
|
+
"readable": block.isRead(),
|
|
763
|
+
"writable": block.isWrite(),
|
|
764
|
+
"executable": block.isExecute(),
|
|
765
|
+
"volatile": block.isVolatile(),
|
|
766
|
+
"initialized": block.isInitialized(),
|
|
767
|
+
"type": block.getType().toString() if block.getType() else "DEFAULT",
|
|
768
|
+
"source": block.getSourceName(),
|
|
769
|
+
}
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
return sections
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _write_json(path: Path, data) -> None:
|
|
776
|
+
"""Write data as formatted JSON."""
|
|
777
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
778
|
+
path.write_text(
|
|
779
|
+
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
780
|
+
encoding="utf-8",
|
|
781
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ghidra-decomp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Bulk-decompile binaries via Ghidra into a browsable source tree
|
|
5
|
+
Project-URL: Homepage, https://github.com/totekuh/ghidra-decomp
|
|
6
|
+
Project-URL: Repository, https://github.com/totekuh/ghidra-decomp
|
|
7
|
+
Project-URL: Issues, https://github.com/totekuh/ghidra-decomp/issues
|
|
8
|
+
Author-email: totekuh <totekuh@protonmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Disassemblers
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: click
|
|
23
|
+
Requires-Dist: pyghidra
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# ghidra-decomp
|
|
27
|
+
|
|
28
|
+
Bulk-decompile binaries into browsable source trees using Ghidra.
|
|
29
|
+
|
|
30
|
+
Takes a binary, runs Ghidra's decompiler on every function, and produces a directory of `.c` files and JSON indexes — ready for grep, code review, or AI-assisted analysis.
|
|
31
|
+
|
|
32
|
+
## Why
|
|
33
|
+
|
|
34
|
+
Reverse engineering through Ghidra's GUI (or MCP) means looking at one function at a time. This tool dumps everything upfront so you can treat the binary like a normal codebase: grep for patterns, read call graphs, search strings — all without waiting for decompilation round-trips.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Requires [Ghidra](https://ghidra-sre.org/) 11.0+ (which bundles pyghidra).
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Set `GHIDRA_INSTALL_DIR` to your Ghidra installation, or pass `--ghidra-path`.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ghidra-decomp ./firmware.bin -o ./firmware_decomp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
|
|
54
|
+
| Flag | Description | Default |
|
|
55
|
+
|------|-------------|---------|
|
|
56
|
+
| `-o, --output` | Output directory | `<binary>_decomp/` |
|
|
57
|
+
| `--timeout` | Per-function decompilation timeout (seconds) | 60 |
|
|
58
|
+
| `--ghidra-path` | Path to Ghidra install | `$GHIDRA_INSTALL_DIR` |
|
|
59
|
+
| `--base-addr` | Rebase binary to this address before analysis (e.g. `0x80000000`) | none |
|
|
60
|
+
| `--entry` | Mark this address as the entry point and disassemble from it before analysis (e.g. `0x31000`). Useful for raw binaries. | none |
|
|
61
|
+
| `--language` | Force Ghidra language ID (e.g. `ARM:LE:32:v7`). Use when auto-detect fails. | auto |
|
|
62
|
+
| `--compiler` | Force compiler spec ID (e.g. `gcc`, `default`, `windows`). Requires `--language`. | auto |
|
|
63
|
+
| `--list-languages` | List all available language IDs and compiler specs, then exit. | |
|
|
64
|
+
|
|
65
|
+
For raw binaries or unknown formats, Ghidra will refuse to auto-load. Discover the right spec then pass it:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
ghidra-decomp --list-languages | grep -i arm
|
|
69
|
+
ghidra-decomp ./firmware.bin --language ARM:LE:32:v7 --compiler default
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Raw binaries typically also need a known image base and an entry point for auto-analysis to reach every function. Both are applied *before* analysis runs, so function boundaries, xrefs, and switch-table recovery all happen at the real addresses:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ghidra-decomp ./dal_ivm.mod \
|
|
76
|
+
--language x86:LE:32:default \
|
|
77
|
+
--compiler gcc \
|
|
78
|
+
--base-addr 0x00031000 \
|
|
79
|
+
--entry 0x00031000
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Output
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
firmware_decomp/
|
|
86
|
+
├── functions/
|
|
87
|
+
│ ├── 00010000_main.c
|
|
88
|
+
│ ├── 00010234_parse_config.c
|
|
89
|
+
│ └── ...
|
|
90
|
+
├── all_functions.c # everything in one file
|
|
91
|
+
├── types.json # recovered structs, enums, unions, typedefs
|
|
92
|
+
├── functions.json # function index with address ranges + signatures
|
|
93
|
+
├── callgraph.json # who calls who
|
|
94
|
+
├── strings.json # strings + xrefs to functions
|
|
95
|
+
├── imports.json # external library functions
|
|
96
|
+
├── exports.json # exported entry points
|
|
97
|
+
├── symbols.json # globals, labels, data
|
|
98
|
+
├── sections.json # memory map with r/w/x permissions
|
|
99
|
+
└── metadata.json # binary info + stats
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each `.c` file includes a metadata header:
|
|
103
|
+
|
|
104
|
+
```c
|
|
105
|
+
// Function: parse_config
|
|
106
|
+
// Address: 00010234
|
|
107
|
+
// Size: 284 bytes
|
|
108
|
+
// Calling: __stdcall
|
|
109
|
+
// Params: 3
|
|
110
|
+
|
|
111
|
+
void parse_config(char *param_1, int param_2, int param_3) {
|
|
112
|
+
...
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Intended workflow
|
|
117
|
+
|
|
118
|
+
1. **Dump** the binary with `ghidra-decomp`
|
|
119
|
+
2. **Analyze** the output like source code — grep, glob, read
|
|
120
|
+
3. **Write back** renames/annotations to Ghidra via MCP (separate tool)
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ghidra_decomp/__init__.py,sha256=GY7s0TTDI0QuEI3FqyixM9dSKWK6fN7fF1W_BwdHNH0,94
|
|
2
|
+
ghidra_decomp/__main__.py,sha256=ORfTZTRWfazNhQAu0w83dgyRfagD-hvz6-W9x87jz8g,79
|
|
3
|
+
ghidra_decomp/cli.py,sha256=pfy_qO78wxIPZwOujTBD3QtrunzE9tWunJVRrXZv1Ak,3237
|
|
4
|
+
ghidra_decomp/decompiler.py,sha256=5wcNupuRq9pDOmMevVYj1PLkYD8E8hzXFmMu_KhVNRI,26321
|
|
5
|
+
ghidra_decomp-0.2.0.dist-info/METADATA,sha256=pt9ElJWB2G18NaOrPsz54-L_U3bBl--_UBUwsUI9-fs,4480
|
|
6
|
+
ghidra_decomp-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
ghidra_decomp-0.2.0.dist-info/entry_points.txt,sha256=AG0OZjiozVjipC0oNLjeW_J-lsnvwq0qbNmVQW8zMVg,57
|
|
8
|
+
ghidra_decomp-0.2.0.dist-info/licenses/LICENSE,sha256=mm2GcMdlOZDcLYghakgPeQWX9Tz7EdvpxAFTOQogt-E,1064
|
|
9
|
+
ghidra_decomp-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 totekuh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|