Files
OpenMaidEngine/docs/superpowers/specs/2026-07-07-ghidra-mcp-native-re-design.md
gamer147 5667a17b64 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>
2026-07-07 10:28:11 -04:00

9.2 KiB

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.