docs: design spec for Ghidra+MCP native-RE workflow (target u00428010)
Stand up bethington/ghidra-mcp loop; prove it by reversing SCJUMP's native decision->scene resolver. Raw-dump-first, pe-sieve fallback; findings -> docs/engine-re.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
146
docs/superpowers/specs/2026-07-07-ghidra-mcp-native-re-design.md
Normal file
146
docs/superpowers/specs/2026-07-07-ghidra-mcp-native-re-design.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Ghidra + MCP native-RE workflow (proven on `u00428010`) — design
|
||||
|
||||
**Date:** 2026-07-07
|
||||
**Status:** approved (brainstorming) → ready for implementation plan
|
||||
**Slice:** native-engine RE enablement; first target = SCJUMP's decision→scene resolver.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem & goal
|
||||
|
||||
We keep hitting the same wall: logic compiled into `AGE.EXE` that scripts call as native ops —
|
||||
**decision→scene (`u00428010`), call-script dispatch, op 0x60 (the rand-like SCJUMP gate), the gfx
|
||||
command-buffer.** Frida (dynamic observation) has taken us far but stalls where the native logic is a
|
||||
complex state machine that must be *read*, not just watched. The blocker that previously ruled out
|
||||
static analysis (packed exe) is gone — we have the in-place-unpacked engine dumped
|
||||
(`build/engine-dump/`, module @0x400000, validated at the AGF landmark 0x474f1f).
|
||||
|
||||
**Goal:** stand up a repeatable "Claude drives Ghidra over MCP" loop and **prove it end-to-end on one
|
||||
native function — `u00428010`, the SCJUMP decision→scene resolver** — to a concrete result: either
|
||||
recover the `0x62ccf → SCxxxx.BIN` mapping (finishing the SCJUMP slice), or establish precisely what
|
||||
makes it runtime-only. Success de-risks the whole approach and leaves a documented, reusable process
|
||||
for the other native walls.
|
||||
|
||||
## 2. Non-goals (this slice)
|
||||
|
||||
- **Not** reversing call-script dispatch, op 0x60, or the gfx command-buffer — those are follow-on
|
||||
slices that reuse the proven loop. One target only.
|
||||
- **Not** building custom RE tooling — we use an off-the-shelf Ghidra MCP server; we don't write our
|
||||
own bridge.
|
||||
- **No redistribution** of engine binaries — the dump/PE stay local, for analysis only.
|
||||
- Not committing to reimplementing `u00428010` in the VM this slice; the deliverable is the
|
||||
*understanding* (the mapping/table or a clear runtime verdict). Reimplementation is a later step.
|
||||
|
||||
## 3. Environment (confirmed 2026-07-07)
|
||||
|
||||
- **Have:** Ghidra 12.1.2 installed; the game runs locally (JP locale); Frida 17.15.3;
|
||||
`build/engine-dump/` (raw runtime dump: `manifest.json` + `range_<base>.bin`, module base 0x400000).
|
||||
- **Need (this slice sets up):** the bethington/ghidra-mcp server (plugin + bridge) registered in
|
||||
Claude Code; possibly pe-sieve.exe (only if raw-dump analysis is inadequate).
|
||||
- **Target facts:** consumers do `lookup-array(ptr, 0x5f0ed, 0x62ccf)` then `u00428010(ptr)`. Kelebek
|
||||
names the op by its handler VA `0x428010` (may drift in our build → resolve via the dispatch table
|
||||
`[esi+idx*120+0x53d88]` / proximity). The interpreter executes from the **module @0x400000**
|
||||
(stable base) — not the per-run heap (an earlier worry, since corrected).
|
||||
|
||||
## 4. The MCP server — bethington/ghidra-mcp
|
||||
|
||||
Chosen over LaurieWired/GhidraMCP because it explicitly exposes the primitives this analysis needs:
|
||||
`decompile_function`, `get_xrefs_to`/`get_xrefs_from`, `get_function_by_address`, `read_memory`,
|
||||
`list_data_items`, `search_byte_patterns`, struct/enum tools. Actively maintained (v5.14.2, Jun 2026),
|
||||
stdio-MCP transport designed for Claude Code. Requires Ghidra 12.1.2 (matches ours); prereqs Java 21,
|
||||
Maven 3.9+, Python 3.10+. The 256-tool surface is a non-issue for Claude Code — tools are deferred/
|
||||
searchable, so only the handful used are loaded via ToolSearch.
|
||||
|
||||
**Setup (user, one-time):** build + deploy the Ghidra extension (`python -m tools.setup ...`), start
|
||||
the HTTP-serving plugin inside Ghidra (127.0.0.1:8089), run the MCP bridge, and register the server in
|
||||
Claude Code's MCP config. A connectivity check (I call `list_functions` and get a non-empty result)
|
||||
gates progress to analysis.
|
||||
|
||||
**Fallback:** if the bridge proves painful, I can still work from decompiler output the user pastes,
|
||||
or a Ghidra headless export — degraded (no automated xrefs) but not blocked.
|
||||
|
||||
## 5. Analyzable image — raw-dump-first, pe-sieve as escalation
|
||||
|
||||
For reading one function's logic, a perfect IAT is often unnecessary (unnamed API calls are fine when
|
||||
the function is a table lookup). So:
|
||||
|
||||
1. **Stage A — try the existing raw dump** (`build/engine-dump/range_<0x400000>.bin`), imported into
|
||||
Ghidra as a raw 32-bit x86 binary at base 0x400000, auto-analyzed. Zero new setup. If the decompiler
|
||||
renders `u00428010`'s region legibly, prep is done.
|
||||
2. **Stage B — escalate to pe-sieve only if Stage A is inadequate:** with the game running,
|
||||
`pe-sieve.exe /pid <PID> /imp 3` rebuilds the IAT into a clean PE (recorded in
|
||||
`vm-mapping-plan.md` appendix; pe-sieve is a single portable exe, Bash-drivable while the user has
|
||||
the game up). Validate the dump by confirming plaintext `SYS4422`/`.BIN`/`DATA1` strings appear.
|
||||
|
||||
The escalation is triggered by observation (illegible/incomplete decompilation), not assumed up front.
|
||||
|
||||
## 6. Ghidra load parameters
|
||||
|
||||
32-bit x86 (`x86:LE:32:default`), image base `0x400000`. For the raw dump: import as "Raw Binary",
|
||||
set the language + base, run auto-analysis (decompiler, function ID, data references). For a pe-sieve
|
||||
PE: standard PE import. Confirm the AGF landmark (VA 0x474f1f contains the `BM`/0x4D42 check) reads as
|
||||
real code — the recorded dump-sanity anchor.
|
||||
|
||||
## 7. Analysis plan for `u00428010`
|
||||
|
||||
Driven by me over MCP:
|
||||
1. **Locate the real handler.** Try `get_function_by_address(0x428010)`; if it lands mid-function or on
|
||||
non-handler code (Kelebek build drift), find it via the interpreter's op dispatch table
|
||||
(`[esi+idx*120+0x53d88]`) or by xrefs from the operand-fetch path (`call 0x41b940`).
|
||||
2. **Decompile it** and read how it consumes its pointer argument (the `&0x5f0ed[decision]` reference)
|
||||
— what fields it reads, what it returns, what it calls.
|
||||
3. **Chase the data.** Follow xrefs/reads to any static table in `.data`: does the decision index map,
|
||||
directly or via `0x5f0ed`, to a scene id / filename / SYS4INI section? `list_data_items` +
|
||||
`read_memory` on the referenced table; cross-check candidate scene ids against the real corpus
|
||||
(SC#### filenames) and `build/asset-sections.json`.
|
||||
4. **Verdict.** Either (a) recover the `decision → scene` mapping (ideally a static table we can export
|
||||
→ a *static* win finishing SCJUMP), or (b) determine it's computed at runtime (e.g. from live state
|
||||
the table doesn't encode) and document exactly what's needed to resolve it (a Frida hook point).
|
||||
|
||||
**Success criterion:** a concrete, evidence-backed answer to "what scene does decision N load?", or a
|
||||
precise statement of why it can't be answered statically + the identified runtime hook.
|
||||
|
||||
## 8. Findings capture
|
||||
|
||||
- **New canonical doc `docs/engine-re.md`** — "native-engine reverse engineering": the home for
|
||||
decompiled native-op findings. This slice writes the `u00428010` section (handler address, decompiled
|
||||
behavior, the `0x5f0ed`/table structure, the decision→scene verdict). Future native walls (call-script,
|
||||
op 0x60, gfx buffer) get sections here. Includes a short "how to run the Ghidra+MCP loop" runbook
|
||||
(setup + connect) so it's repeatable.
|
||||
- **`vm-map/opcodes.toml`** — if `u00428010` (and any native op identified along the way) gets real
|
||||
semantics, record them there (its existing home); rebuild via `opcodes_build.py --build`.
|
||||
- **`docs/scjump-progression.md`** — update the "native decision→scene boundary" section with the
|
||||
result (mapping recovered, or the precise runtime dependency).
|
||||
- **CLAUDE.md canonical-documents map** — add a row: `Native-engine RE (decompiled AGE.EXE) →
|
||||
docs/engine-re.md`.
|
||||
- If pe-sieve is used, note the dump-prep recipe in `docs/engine-re.md` (and reference the existing
|
||||
`vm-mapping-plan.md` appendix rather than duplicating).
|
||||
|
||||
## 9. Risks & fallbacks
|
||||
|
||||
- **MCP setup friction / version mismatch** → fallback to pasted decompiler output or headless export
|
||||
(degraded, not blocked). A connectivity gate (§4) catches this before analysis.
|
||||
- **Anti-debug / protector** → attach post-title (already works for our Frida dump); pe-sieve is a
|
||||
read-only scan (low risk). Not expected to affect Ghidra static analysis of an already-captured image.
|
||||
- **VA drift (Kelebek build ≠ ours)** → resolve the handler via the dispatch table, not the raw VA.
|
||||
- **The answer is genuinely runtime** → that is still a *successful* outcome for this slice: it
|
||||
validates the workflow and pinpoints the exact Frida hook, converting an open wall into a defined task.
|
||||
|
||||
## 10. Definition of done
|
||||
|
||||
1. bethington/ghidra-mcp connected in Claude Code; I can `list_functions` / `decompile_function` on the
|
||||
loaded engine image.
|
||||
2. `u00428010` located and decompiled; its use of `0x5f0ed` and any static table understood.
|
||||
3. A verdict on decision→scene: recovered mapping (with evidence) **or** a precise runtime-hook
|
||||
statement.
|
||||
4. `docs/engine-re.md` written (findings + repeatable runbook); `scjump-progression.md` and (if
|
||||
applicable) `opcodes.toml` + CLAUDE.md updated; status memory updated.
|
||||
|
||||
## 11. Implementation order (for the plan)
|
||||
|
||||
1. Set up bethington/ghidra-mcp (user) + register in Claude Code; connectivity gate.
|
||||
2. Load the raw engine dump in Ghidra (32-bit, base 0x400000); confirm the AGF landmark; assess decomp
|
||||
quality. Escalate to pe-sieve `/imp 3` only if inadequate.
|
||||
3. Locate + decompile `u00428010`; chase `0x5f0ed` + referenced tables; reach the decision→scene verdict.
|
||||
4. Write `docs/engine-re.md` (findings + runbook); update `scjump-progression.md`, `opcodes.toml` (if
|
||||
applicable), CLAUDE.md map, status memory.
|
||||
Reference in New Issue
Block a user