phantomscope-mcp
by Absurdity233
README.md
# phantomscope
**DMA-driven reverse engineering.** Static PE analysis, live memory inspection
over MemProcFS, snapshot emulation, and an MCP server that exposes all of it to
an LLM agent.
[](https://www.python.org/downloads/)
[](#license)
[](#tests)
[](#tool-reference)
[](#install)
---
## What this is
DMA hardware (a PCILeech FPGA, or a memory dump) gives you read/write access to
physical memory — and nothing else. No thread suspension, no register context,
no single step, no breakpoints. phantomscope is built around that constraint: it
splits "understand and change a target" into four engines, each honest about
what it can actually do, and exposes the whole thing as **74 MCP tools** so an
agent can drive it.
It also runs with **no hardware at all** — the `mock` backend is a deterministic
synthetic target and the `physical` backend reads raw dumps, so the full pipeline
is testable in CI.
## What this is not
* **Not a debugger.** You cannot break on a live DMA target. The emulator is the
substitute: capture the function and its data, then run it deterministically.
* **Not an injection tool.** It does not inject DLLs, load a driver, or execute
new code in the target. `write` changes bytes that are already there, and
every write is journaled and reversible.
* **Not a kernel-structure parser.** The `physical` backend deliberately assumes
no OS: it finds PEs by header structure, not by walking `PsActiveProcessHead`.
> **Authorized use only.** Reading another machine's memory over DMA is illegal
> in most jurisdictions without written permission. Use phantomscope on systems
> you own or are explicitly engaged to test.
## Contents
- [Architecture](#architecture)
- [Why four engines](#why-four-engines)
- [Install](#install)
- [Quick start (no hardware)](#quick-start-no-hardware)
- [Live workflow](#live-workflow)
- [Sessions](#sessions)
- [Physical layer (no OS)](#physical-layer-no-operating-system)
- [Tool reference](#tool-reference)
- [CLI](#cli)
- [Registering the MCP server](#registering-the-mcp-server)
- [Layout](#layout)
- [Design notes](#design-notes)
- [Limitations](#limitations)
- [Tests](#tests)
- [Development](#development)
- [License](#license)
## Architecture
```
+----------------------- MCP client (agent) -----------------------+
| session_* processes modules read scan_* emu_* ... |
+---------------------------------+--------------------------------+
| stdio / JSON-RPC
+---------------------------------v--------------------------------+
| phantomscope |
| |
| +----------------+ +----------------+ +----------------+ |
| | StaticEngine | | DynamicEngine | | Emulator | |
| | pefile | | scans | | unicorn | |
| | capstone | | patches | | step | |
| | xrefs, structs | | ptr chains | | watchpoints | |
| +-------+--------+ +-------+--------+ +-------+--------+ |
| | | | |
| +--------------------+--------------------+ |
| | |
| +-------v-------+ |
| | MemoryBackend | |
| +-------+-------+ |
| | |
| | | | |
| +--------------------+--------------------+ |
| MemProcFS physical mock |
+------------+--------------------+--------------------+-----------+
| | |
v v v
PCILeech FPGA raw dump / pmem demo target
```
## Why four engines
DMA cannot stop a thread, read a register context or single-step. So a classic
debugger is not available, and phantomscope replaces each of its jobs:
| Need | Engine | What it does |
| --- | --- | --- |
| Understand a module | **Static** | The bytes captured over DMA are a complete PE image; parse, disassemble, index, infer structs locally |
| Find a value / structure | **Dynamic** | Value scans, AOB scans, narrowing, pointer chains, snapshot diffs |
| Change something | **Dynamic** | Journaled writes with one-call restore |
| "Debug" a function | **Emulator** | Snapshot code + data, run it in Unicorn, use watchpoints and API hooks as breakpoints |
| No OS at all | **Physical** | Raw dump: physical addressing, page-table walking, PE discovery by structure |
That last row is the honest substitute for breakpoints: you *cannot* break on a
live DMA target, but you can capture the function, its arguments and its data,
then emulate it deterministically — as many times as you like.
## Install
```bash
git clone https://github.com/Absurdity233/phantomscope
cd phantomscope
python -m venv .venv
```
```powershell
# Windows — the only platform MemProcFS supports
.\.venv\Scripts\python.exe -m pip install -e ".[dev,emulate]"
```
```bash
# Linux / macOS — mock, static and physical layers work; the DMA backend does not
.venv/bin/python -m pip install -e ".[dev,emulate]"
```
`memprocfs` ships the `vmm.dll` runtime and installs on Windows only.
`unicorn` is optional: without it everything except the `emu_*` tools still
works, and those report `unavailable` instead of failing.
## Quick start (no hardware)
```powershell
.\.venv\Scripts\phantomscope.exe demo
```
The demo builds a synthetic target — a mapped PE module, a heap struct with
health/ammo/score, two threads with callstacks, symbols, handles — and walks the
whole pipeline: enumerate, read, scan, patch, restore, capture the module,
disassemble it, and emulate the entry point.
## Live workflow
First, sanity-check the device and time every backend operation:
```powershell
# against a PCILeech FPGA
.\.venv\Scripts\python.exe tools\probe_backend.py fpga
# or an offline dump
.\.venv\Scripts\python.exe tools\probe_backend.py C:\dumps\win10.raw
```
`probe_backend.py` opens the device, then reports the latency of every backend
operation (processes, modules, regions, threads, callstack, a module read,
symbolisation, kernel modules, physical map, handles). Run it before blaming the
toolkit when a tool call feels slow.
Typical numbers on a PCILeech FPGA: the first `Vmm()` initialisation takes
20–40 s while the device is probed and the target's process list is built; after
that `list_processes()` is sub-second. If the target is asleep or DMA reads
stall, MemProcFS **blocks instead of returning an error** — that is a device
state problem, not a phantomscope bug, and `probe_backend.py` shows exactly which
call is stuck.
Then start the server and drive it from an MCP client:
```powershell
phantomscope-mcp
```
```jsonc
// 1. open a session against the FPGA
{"tool": "session_open", "arguments": {"device": "fpga"}}
// 2. find and attach to a process
{"tool": "attach", "arguments": {"process": "target.exe"}}
// 3. look around
{"tool": "modules", "arguments": {}}
{"tool": "regions", "arguments": {}}
{"tool": "threads", "arguments": {}}
{"tool": "callstack", "arguments": {"tid": 1234}}
{"tool": "handles", "arguments": {"filter": "File"}}
// 4. read a struct
{"tool": "read_typed", "arguments": {"va": "0x21A00000120", "value_type": "int32"}}
// 5. find it yourself
{"tool": "scan_value", "arguments": {"value": "100", "value_type": "int32", "label": "health"}}
{"tool": "scan_next", "arguments": {"key": "health", "value": "95"}}
// 6. change it (journaled) and undo
{"tool": "write", "arguments": {"va": "0x21A00000120", "value": "250", "value_type": "int32"}}
{"tool": "patch_restore", "arguments": {"all": true}}
// 7. hand the live module to the static engine
{"tool": "capture_module", "arguments": {"module": "target.exe", "key": "target"}}
{"tool": "functions", "arguments": {"key": "target", "limit": 50}}
{"tool": "disassemble", "arguments": {"key": "target", "va": "0x7FF600001000", "function": true}}
{"tool": "xrefs_to", "arguments": {"key": "target", "va": "0x7FF600001040"}}
{"tool": "struct_layout", "arguments": {"key": "target", "va": "0x7FF600001040"}}
// 8. emulate a function out of the snapshot
{"tool": "emu_create", "arguments": {"key": "target"}}
{"tool": "emu_call", "arguments": {"key": "target", "va": "0x7FF600001040", "args": "0x21A00000120"}}
{"tool": "emu_events", "arguments": {"key": "target"}}
```
## Sessions
Each session owns its backend, static engine and emulators. Open a live FPGA and
an offline dump at the same time and their images, scans and emulators never
collide:
```jsonc
{"tool": "session_open", "arguments": {"name": "live", "device": "fpga"}}
{"tool": "session_open", "arguments": {"name": "disk", "device": "C:/dumps/win10.raw"}}
{"tool": "images", "arguments": {"session": "live"}}
```
Every static and emulation tool accepts an optional `session`. Without one, a
bare image key is resolved against whichever engine owns it, and reusing a key
across sessions is refused rather than silently overwriting. `session_close`
drops that session's images and emulators.
Static-only work (`load_file` with no session) lands in a standalone engine that
outlives any session, so `phantomscope` is usable as a plain PE analyser.
## Physical layer (no operating system)
Some targets have no usable OS view: a dump from a hibernated machine, a
pre-boot capture, a system whose kernel structures are corrupted, or a device
that only gives you physical memory. The `physical` backend assumes nothing:
```powershell
# build a synthetic dump to try it without hardware
python tools\make_demo_dump.py demo_phys.raw
phantomscope physical demo_phys.raw --capture 0x100000
```
```jsonc
{"tool": "session_open", "arguments": {"backend": "physical", "device": "demo_phys.raw"}}
{"tool": "physical_info", "arguments": {}}
{"tool": "physical_read", "arguments": {"pa": "0x100000", "size": 64}}
{"tool": "physical_images", "arguments": {}} // PEs found by structure alone
{"tool": "physical_scan", "arguments": {"pattern": "48 83 ec 28"}}
{"tool": "translate", "arguments": {"va": "0x7ff600001000", "cr3": "0x200000"}}
{"tool": "virtual_read", "arguments": {"va": "0x7ff600001000", "size": 64, "cr3": "0x200000"}}
{"tool": "walk", "arguments": {"va": "0x7ff600000000", "size": 4096, "cr3": "0x200000"}}
{"tool": "capture_image", "arguments": {"pa": "0x100000", "key": "phys"}}
{"tool": "disassemble", "arguments": {"key": "phys", "count": 8}}
```
What the layer provides:
| Piece | File | Notes |
| --- | --- | --- |
| `PhysicalImage` | `physical/image.py` | Sparse segments (`memmap=pa,size;...`), gaps read as zeros and are counted, writes go to the file |
| `X64Pager` | `physical/paging.py` | 4- and 5-level paging, 4 KiB / 2 MiB / 1 GiB pages, present-bit aware |
| `scan_pe_images` | `physical/pescan.py` | Page-aligned `MZ` + validated headers (machine, subsystem, `SizeOfImage`) |
| `PhysicalBackend` | `physical/backend.py` | Implements `MemoryBackend`; `pid` is a physical address when 0, a CR3 otherwise |
Two honest limitations: a physical image has **no process, module, thread or
symbol list**, and `capture_image` bases the image at its PE `ImageBase` rather
than its physical address, because RVAs are relative to the load address.
## Tool reference
74 tools. Every dynamic tool needs a session; every static tool needs a loaded
image key (`images` lists them).
### Session
`session_open`, `session_list`, `session_close`, `attach`, `server_info`
### Process / module / thread
`processes`, `process_info`, `modules`, `regions`, `threads`, `callstack`,
`address_info`
`address_info` resolves a VA to its module, region, protection and symbol.
### Kernel / handles / heaps
`kernel_modules`, `kernel_processes`, `handles`, `heaps`
`handles` filters on type or name. `kernel_processes` currently delegates to the
user-mode list — a hook for a real kernel view, not one yet.
### Memory
`read`, `read_typed`, `write`, `patch_list`, `patch_restore`, `dump_region`
Every `write` is journaled and reversible; `patch_restore(all=true)` undoes the
whole session.
### Discovery
`scan_aob`, `scan_value`, `scan_next`, `scan_results`, `pointer_scan`,
`snapshot`, `diff`, `watch`
`scan_value` → `scan_next` is the narrowing loop: scan once, let the target
change, then keep only the addresses whose value still matches.
### Static
`load_file`, `capture_module`, `images`, `unload_image`, `image_summary`,
`sections`, `imports`, `exports`, `strings`, `functions`, `function_info`,
`disassemble`, `basic_blocks`, `xrefs_to`, `xrefs_from`, `callers`, `callees`,
`scan_pattern`, `rtti`, `static_read`, `static_address_info`,
`struct_layout`, `globals_touched`
`struct_layout` turns every `[rcx+0x10]` in a function into a field hypothesis —
offset, width, read/write, and the instructions that produced it. It reports
what the code *touches*, not what the struct contains, and the base is a *role*
(`arg0`, `arg1`, …) because nothing proves which object a register holds.
### Emulation
`emu_available`, `emu_create`, `emu_regs`, `emu_step`, `emu_run`, `emu_read`,
`emu_write`, `emu_watch`, `emu_events`, `emu_call`
`emu_call` runs a function with the x64 Microsoft calling convention and stops at
a return trap, so you get a `retval` without a debugger.
### Physical (no OS)
`physical_info`, `physical_map`, `physical_read`, `physical_write`,
`translate`, `virtual_read`, `virtual_write`, `walk`, `physical_images`,
`physical_scan`, `capture_image`
## CLI
```
phantomscope demo # hardware-free end-to-end walkthrough
phantomscope static notepad.exe # PE summary
phantomscope functions notepad.exe # discovered functions
phantomscope disasm notepad.exe 0x1400019c0 --count 20
phantomscope strings notepad.exe --contains password
phantomscope scan notepad.exe "48 8B ?? E8"
phantomscope physical dump.raw --capture 0x100000 # raw dump, no OS
phantomscope mcp # MCP server on stdio
```
The CLI builds the same engines the MCP server uses, so anything reachable there
is reachable here.
## Registering the MCP server
A project-scoped [`.mcp.json`](.mcp.json) is included; point its `command` at
your checkout. To add it to your user scope instead:
```powershell
claude mcp add phantomscope --scope user -- <checkout>\.venv\Scripts\phantomscope-mcp.exe
```
Or add it by hand to `mcpServers` in `~/.claude.json`:
```json
"phantomscope": {
"type": "stdio",
"command": "<checkout>\\.venv\\Scripts\\phantomscope-mcp.exe",
"args": [],
"env": {}
}
```
## Layout
```
src/phantomscope/
backend.py MemoryBackend contract + capabilities
backends/
memprocfs_backend.py PCILeech FPGA / dumps / pmem / LeechAgent
mock.py deterministic in-memory target
demo.py synthetic PE + process used by tests and `demo`
factory.py create_backend()
analysis/
pe.py mapped PE image (file or DMA capture)
disasm.py capstone wrapper, normalised instructions
index.py functions, basic blocks, xrefs, call graph
scan.py strings, AOB patterns, RTTI/vtables
structs.py struct layout inference from memory operands
engine.py StaticEngine facade
dynamic/
engine.py DynamicEngine: reads, patches, scans, diffs
emulator.py unicorn snapshot emulator
session.py backend + engines, named sessions
physical/
image.py sparse raw physical image
paging.py x86-64 page-table walking
pescan.py PE discovery by structure alone
backend.py MemoryBackend over physical memory
mcp_server.py MCP tools (74)
cli.py command line front end
tests/ 137 tests, no hardware required
tools/probe_backend.py live device sanity check with per-step timings
tools/make_demo_dump.py synthetic raw dump for the physical layer
```
## Design notes
* **Mapped images everywhere.** A PE from disk and a module captured over DMA
both become an RVA-indexed buffer, so every read, scan and disassembly path is
identical for both. `capture_module` is the bridge between the two engines.
* **Bounded work.** Function discovery is capped by `max_instructions` (1.5M),
scans by `MAX_SCAN_BYTES` (512 MB) and emulation by a step budget, so a tool
call cannot hang a session.
* **Every write is reversible.** `write` records the original bytes;
`patch_list` and `patch_restore` give you a full journal.
* **Honest capability reporting.** `MemoryBackend.caps` and the `Unavailable`
error make it explicit when a backend cannot provide something (for example
callstacks on a raw dump) instead of returning plausible-looking zeros.
* **Per-session state.** Each session owns its static engine and emulators, so
two targets in one server cannot overwrite each other's images or scans.
## Limitations
* **No breakpoints on a live target.** Emulate a snapshot instead.
* **`struct_layout` is a hypothesis, not a type.** Untouched fields are
invisible, and overlapping accesses are reported rather than merged.
* **The physical layer does not parse kernel structures.** It finds PEs by
header and translates VAs given a CR3; it will not hand you a process list.
* **Function discovery is heuristic.** Recursive descent plus a linear sweep and
prologue scanning; heavily virtualised or obfuscated code will be incomplete.
* **DMA reads can block.** If the target sleeps or the link stalls, MemProcFS
blocks instead of erroring — use `probe_backend.py` to find the stuck call.
* **The live FPGA path is not covered by CI.** Tests run against the mock and
synthetic dumps; hardware paths need a device.
## Tests
```powershell
.\.venv\Scripts\python.exe -m pytest tests -q
.\.venv\Scripts\python.exe tests\mcp_smoke.py # real stdio MCP round trip
```
Emulator tests are skipped automatically when `unicorn` is not installed.
## Development
To add a tool: implement it in the relevant engine, wrap it in `mcp_server.py`
with `@server.tool()`, and add the name to
`tests/test_mcp.py::test_tool_registry_is_complete`.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessOutdated data