Files
OpenMaidEngine/docs/superpowers/plans/2026-07-07-ghidra-mcp-native-re.md
2026-07-07 10:35:55 -04:00

189 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 example `C:\ghidra_12.1.2_PUBLIC`); `<MCP_DIR>` = your `ghidra-mcp` clone; `<PID>` = the running game's process id.
- **Run pe-sieve from PowerShell, not the Bash tool** — Git Bash mangles `/pid` `/imp` `/dir` into 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 to `vm-map/opcodes.toml` (rebuild with `opcodes_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`, so `u00428010`'s handler region is at file offset `0x28010`, and the AGF landmark VA `0x474f1f` is at `0x74f1f`.
- Target: consumers run `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 interpreter op-dispatch table `[esi+idx*120+0x53d88]` or xrefs from the operand-fetch `call 0x41b940`). Interpreter runs from the module @0x400000 (stable base).
- `0x62ccf` decision values: 847 distinct, range 0..1693 (from `build/scjump-decisions.json`). Scene files are `SC0000..SC1690` (see `paths.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**
```bash
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**
1. Start Ghidra → new project → **File > Import File** → select
`S:\Game Hacking\Eushully\Himegari\age-reimpl\build\engine-dump\range_00400000.bin`.
2. In the import dialog: **Format = Raw Binary**, **Language = `x86:LE:32:default`** (compiler: `windows`).
3. Open it in CodeBrowser. Set the image base: **Window > Memory Map** → set the block's start to
`0x00400000` (or set `Options > Image Base = 0x400000` at import).
4. 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**
1. **File > Configure > Configure All Plugins > GhidraMCP** → check the box.
2. **Tools > GhidraMCP > Start MCP Server** (serves `http://127.0.0.1:8089/`).
3. Verify HTTP: `curl http://127.0.0.1:8089/check_connection` → expect a success/OK response.
4. Launch the stdio bridge:
```bash
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 …`):
```json
{
"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_memory` near it — confirm it reads as real code around the `BM` (`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**:
```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 23 known decisions end-to-end.
- [ ] **Step 5: Verdict**
Write the conclusion:
- **(a) Static mapping recovered** — the `decision → scene` table/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:
1. **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-link `vm-mapping-plan.md` appendix rather than duplicating the packing detail.
2. **Findings — `u00428010` (decision→scene)** — handler address (as-found), decompiled behavior, the `0x5f0ed` + 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` — if `u00428010` (or any op identified en route) earned real semantics, add/enrich its entry; run `py -3.11 -X utf8 tools/opcodes_build.py --build`.
- `CLAUDE.md` canonical-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` — add `pe-sieve32.exe` (dump-prep) if it was used; note its home (`tools/` or move to `bin/` per PROJECT-STRUCTURE).
- `docs/PROJECT-STRUCTURE.md` — add `docs/engine-re.md` and (if kept) `pe-sieve32.exe`.
- [ ] **Step 3 [me]: Commit docs + update status memory**
```bash
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.