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>
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
u00428010in 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)thenu00428010(ptr). Kelebek names the op by its handler VA0x428010(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:
- 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 rendersu00428010's region legibly, prep is done. - Stage B — escalate to pe-sieve only if Stage A is inadequate: with the game running,
pe-sieve.exe /pid <PID> /imp 3rebuilds the IAT into a clean PE (recorded invm-mapping-plan.mdappendix; pe-sieve is a single portable exe, Bash-drivable while the user has the game up). Validate the dump by confirming plaintextSYS4422/.BIN/DATA1strings 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:
- 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). - Decompile it and read how it consumes its pointer argument (the
&0x5f0ed[decision]reference) — what fields it reads, what it returns, what it calls. - Chase the data. Follow xrefs/reads to any static table in
.data: does the decision index map, directly or via0x5f0ed, to a scene id / filename / SYS4INI section?list_data_items+read_memoryon the referenced table; cross-check candidate scene ids against the real corpus (SC#### filenames) andbuild/asset-sections.json. - Verdict. Either (a) recover the
decision → scenemapping (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 theu00428010section (handler address, decompiled behavior, the0x5f0ed/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— ifu00428010(and any native op identified along the way) gets real semantics, record them there (its existing home); rebuild viaopcodes_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 existingvm-mapping-plan.mdappendix 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
- bethington/ghidra-mcp connected in Claude Code; I can
list_functions/decompile_functionon the loaded engine image. u00428010located and decompiled; its use of0x5f0edand any static table understood.- A verdict on decision→scene: recovered mapping (with evidence) or a precise runtime-hook statement.
docs/engine-re.mdwritten (findings + repeatable runbook);scjump-progression.mdand (if applicable)opcodes.toml+ CLAUDE.md updated; status memory updated.
11. Implementation order (for the plan)
- Set up bethington/ghidra-mcp (user) + register in Claude Code; connectivity gate.
- Load the raw engine dump in Ghidra (32-bit, base 0x400000); confirm the AGF landmark; assess decomp
quality. Escalate to pe-sieve
/imp 3only if inadequate. - Locate + decompile
u00428010; chase0x5f0ed+ referenced tables; reach the decision→scene verdict. - Write
docs/engine-re.md(findings + runbook); updatescjump-progression.md,opcodes.toml(if applicable), CLAUDE.md map, status memory.