13 KiB
Ghidra + MCP Native-RE Workflow Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to run this plan with review checkpoints. This is a runbook, not code-TDD — steps alternate between [you] (setup / Ghidra GUI / launching the game) and [me] (MCP-driven analysis). Each task ends at a concrete gate. Steps use checkbox (
- [ ]) syntax.
Goal: Stand up a "Claude drives Ghidra over MCP" loop and prove it by reversing u00428010 (SCJUMP's native decision→scene resolver) to a concrete verdict — recover the 0x62ccf → SCxxxx.BIN mapping, or pinpoint exactly why it's runtime-only.
Architecture: Load our existing unpacked engine dump (build/engine-dump/range_00400000.bin) into Ghidra 12.1.2; connect the bethington/ghidra-mcp bridge so I can call decompile_function/get_xrefs_*/read_memory; drive the analysis of u00428010 and the 0x5f0ed array; capture findings in docs/engine-re.md.
Tech Stack: Ghidra 12.1.2 + bethington/ghidra-mcp (Java 21, Maven 3.9+, Python 3.10+, uv); pe-sieve v0.4.1.1 (escalation only); the game (JP locale) for a fresh dump if escalating.
Global Constraints
- Analysis only — no redistribution. The engine dump / any reconstructed PE stay local.
- Substitute your paths where marked:
<GHIDRA_DIR>= your Ghidra 12.1.2 install (README exampleC:\ghidra_12.1.2_PUBLIC);<MCP_DIR>= yourghidra-mcpclone;<PID>= the running game's process id. - Run pe-sieve from PowerShell, not the Bash tool — Git Bash mangles
/pid/imp/dirinto paths. (& .\tools\pe-sieve32.exe /pid <PID> /imp 3 /dir <out>.) - Findings live in
docs/engine-re.md(new canonical doc); resolved op semantics also go tovm-map/opcodes.toml(rebuild withopcodes_build.py --build). One fact, one home. - Git repo root is
age-reimpl/; commit docs from there. Commit trailer:Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>.
Reference facts (verified 2026-07-07):
- Engine dump:
build/engine-dump/range_00400000.bin(2,490,368 bytes) is the full module image at base 0x400000 (memory-laid-out). VA→file offset =VA − 0x400000, sou00428010's handler region is at file offset0x28010, and the AGF landmark VA0x474f1fis at0x74f1f. - Target: consumers run
lookup-array(ptr, 0x5f0ed, 0x62ccf)thenu00428010(ptr). Kelebek names the op by its handler VA0x428010(may drift in our build → resolve via the interpreter op-dispatch table[esi+idx*120+0x53d88]or xrefs from the operand-fetchcall 0x41b940). Interpreter runs from the module @0x400000 (stable base). 0x62ccfdecision values: 847 distinct, range 0..1693 (frombuild/scjump-decisions.json). Scene files areSC0000..SC1690(seepaths.scripts()/build/asset-sections.json).- pe-sieve: v0.4.1.1 (x86) at
tools/pe-sieve32.exe;/imp 3= rebuild ImportTable from scratch.
Task 1: Bring the environment up — image loaded + MCP connected
Deliverable: I can call a bethington MCP tool against the loaded engine image and get a real result.
- Step 1 [you]: Build + deploy the Ghidra MCP plugin
git clone https://github.com/bethington/ghidra-mcp.git <MCP_DIR>
cd <MCP_DIR>
python -m tools.setup ensure-prereqs --ghidra-path "<GHIDRA_DIR>"
python -m tools.setup build
python -m tools.setup deploy --ghidra-path "<GHIDRA_DIR>"
Expected: build succeeds; the extension is deployed into <GHIDRA_DIR>.
- Step 2 [you]: Load the engine image in Ghidra
- Start Ghidra → new project → File > Import File → select
S:\Game Hacking\Eushully\Himegari\age-reimpl\build\engine-dump\range_00400000.bin. - In the import dialog: Format = Raw Binary, Language =
x86:LE:32:default(compiler:windows). - Open it in CodeBrowser. Set the image base: Window > Memory Map → set the block's start to
0x00400000(or setOptions > Image Base = 0x400000at import). - Run Analysis > Auto Analyze (accept defaults; ensure Decompiler + Function ID are on).
Expected: auto-analysis completes; the Listing shows disassembled functions.
- Step 3 [you]: Enable + start the MCP server in Ghidra, then the bridge
- File > Configure > Configure All Plugins > GhidraMCP → check the box.
- Tools > GhidraMCP > Start MCP Server (serves
http://127.0.0.1:8089/). - Verify HTTP:
curl http://127.0.0.1:8089/check_connection→ expect a success/OK response. - Launch the stdio bridge:
cd <MCP_DIR>
uv run bridge-mcp-ghidra
- Step 4 [you]: Register the server in Claude Code
Add to the project MCP config (.mcp.json at the workspace root, or claude mcp add-json ghidra …):
{
"mcpServers": {
"ghidra": {
"command": "uv",
"args": ["run", "--directory", "<MCP_DIR>", "bridge-mcp-ghidra"]
}
}
}
Then reconnect the session so the ghidra MCP tools register (they arrive as deferred tools I fetch via ToolSearch). Tell me when it's connected.
- Step 5 [me]: Connectivity gate
I ToolSearch for the ghidra tools (e.g. select:list_functions,decompile_function,get_function_by_address), then call list_functions (or check_connection).
Gate: a non-empty function list comes back → the loop works, proceed. If it errors → we debug the bridge/Ghidra-server/config before continuing (fallback: you paste decompiler output; degraded but unblocked).
Task 2: Confirm image quality at the target; escalate to pe-sieve only if needed
Deliverable: a confirmed-legible decompilation at the u00428010 region (raw dump, or a pe-sieve-rebuilt PE).
-
Step 1 [me]: Sanity-check the load + assess the target region
-
Validate the dump:
get_function_by_address(0x474f1f)/read_memorynear it — confirm it reads as real code around theBM(0x4D42) check (the recorded AGF-decoder landmark). -
Navigate to
0x428010:get_function_by_address(0x428010)+decompile_function. Assess whether the decompiled output is coherent (recognizable control flow, memory refs) or garbage/truncated.
Gate: coherent → skip to Task 3. Garbage/incomplete (bad section boundaries, unresolved refs blocking reading) → escalate (Step 2).
- Step 2 [you, only if escalating]: pe-sieve a clean, import-rebuilt PE
With the game running (JP locale), from PowerShell:
# NOTE: do not name this $pid — that is a PowerShell automatic variable (the shell's own PID).
$agePid = (Get-Process AGE*).Id # confirm this returns exactly one id
& "S:\Game Hacking\Eushully\Himegari\age-reimpl\tools\pe-sieve32.exe" /pid $agePid /imp 3 /dmode 3 /dir "S:\Game Hacking\Eushully\Himegari\age-reimpl\build\engine-pe"
Expected: an output folder under build/engine-pe\process_<PID>\ containing the rebuilt AGE module PE (named by base, e.g. 400000.AGE.EXE). Validate: it contains plaintext SYS4422 / .BIN / DATA1 strings (recorded dump-sanity check) — e.g. Select-String -Path <dumped.exe> -Pattern 'DATA1' -Encoding ascii.
- Step 3 [you, only if escalating]: Re-import the rebuilt PE in Ghidra
Import the pe-sieve module as Format = Portable Executable (PE) (base 0x400000 is intact), auto-analyze, keep the MCP server pointed at this program. Tell me when ready; I re-run Step 1's assessment.
Gate: coherent decompilation at 0x428010 → proceed to Task 3.
Task 3: Reverse u00428010 → decision→scene verdict
Deliverable: an evidence-backed answer to "what scene does decision N load?", or a precise statement of the runtime dependency + the Frida hook that would resolve it. [me], MCP-driven.
- Step 1: Locate the real handler
get_function_by_address(0x428010). If it lands mid-function or on non-handler code (Kelebek build drift): find the true handler via xrefs — get_xrefs_to around the op-dispatch table [esi+idx*120+0x53d88], or from the operand-fetch path 0x41b940 — and via list_functions near 0x428010.
- Step 2: Decompile + read the handler
decompile_function on the handler. Read: how it consumes its pointer argument (the &0x5f0ed[decision] reference — begin/element), what struct fields it reads, what it returns, and what it calls (esp. anything that looks like a file/scene load or a table index).
- Step 3: Chase the data
Follow references from the handler (and from 0x5f0ed) to any static table in .data: get_xrefs_to(0x5f0ed), list_data_items around it, read_memory on the referenced table(s). Determine whether the decision index maps — directly or via 0x5f0ed — to a scene id / filename / SYS4INI section.
- Step 4: Cross-check against the corpus
For candidate scene ids/indices the table yields, cross-check against real SC####.BIN names (paths.scripts()) and build/asset-sections.json. If a decision value resolves to an existing scene, that's the mapping. Spot-check 2–3 known decisions end-to-end.
- Step 5: Verdict
Write the conclusion:
- (a) Static mapping recovered — the
decision → scenetable/formula, with the cross-checked evidence. (This would finish SCJUMP's open boundary as a static win.) - (b) Runtime-dependent — exactly what live state the resolution needs that the table doesn't encode, and the precise Frida hook (address + what to log) that would capture it.
Gate: one of (a)/(b) with concrete evidence. Report it to you before writing docs.
Task 4: Capture findings + updates
Deliverable: committed docs recording the result and the repeatable workflow.
- Step 1 [me]: Write
docs/engine-re.md
Create the canonical native-RE doc with two parts:
- Runbook — "How to run the Ghidra + MCP loop": the bethington setup (build/deploy, enable plugin, start server:8089, bridge, Claude Code registration), loading
build/engine-dump/range_00400000.bin(raw, base 0x400000,x86:LE:32), the AGF landmark sanity check (VA 0x474f1f), and the pe-sieve escalation recipe (/imp 3 /dmode 3, PowerShell). Cross-linkvm-mapping-plan.mdappendix rather than duplicating the packing detail. - Findings —
u00428010(decision→scene) — handler address (as-found), decompiled behavior, the0x5f0ed+ table structure, and the Task 3 verdict (mapping or runtime hook). Leave a "native walls backlog" list (call-script dispatch, op 0x60, gfx command-buffer) as future sections.
-
Step 2 [me]: Propagate the result
-
docs/scjump-progression.md— update the "native decision→scene boundary" section with the verdict (mapping recovered → link the table; or the identified runtime hook). -
vm-map/opcodes.toml— ifu00428010(or any op identified en route) earned real semantics, add/enrich its entry; runpy -3.11 -X utf8 tools/opcodes_build.py --build. -
CLAUDE.mdcanonical-documents map — add:Native-engine RE (decompiled AGE.EXE) → docs/engine-re.md(workspace root; edit on disk, not in the repo). -
docs/tools-reference.md— addpe-sieve32.exe(dump-prep) if it was used; note its home (tools/or move tobin/per PROJECT-STRUCTURE). -
docs/PROJECT-STRUCTURE.md— adddocs/engine-re.mdand (if kept)pe-sieve32.exe. -
Step 3 [me]: Commit docs + update status memory
git add docs/engine-re.md docs/scjump-progression.md docs/tools-reference.md docs/PROJECT-STRUCTURE.md vm-map/opcodes.toml docs/opcode-reference.md
git commit -m "docs: native-engine RE — Ghidra+MCP workflow + u00428010 decision->scene findings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Update ~/.claude/…/memory/himegari-port-status.md + MEMORY.md: record that the Ghidra+MCP native-RE loop is stood up (bethington/ghidra-mcp on the 0x400000 module dump) and the u00428010 verdict; note it's the reusable path for the remaining native walls. Convert 2026-07-07 to the absolute date. (Memory is outside the repo — no commit.)
Notes for the implementer
- This is a runbook with a hard human/tool dependency (Ghidra GUI + MCP bridge + possibly the running game). Stop at each gate; don't fabricate analysis results — every finding in Task 3/4 must come from an actual MCP call.
- A "runtime-dependent" verdict is still success — it validates the workflow and converts an open wall into a defined Frida task.
- VA drift: never trust the raw Kelebek VA blindly; confirm the handler via the dispatch table / xrefs (Task 3 Step 1).
- If the MCP bridge can't be made to work, fall back to pasted decompiler output for
u00428010— slower (no automated xrefs) but the analysis and docs still land. - The exact bethington commands/config are from the repo README (2026-07-07); if the repo has since changed, follow its current README for build/bridge/registration and keep the rest of this plan.