x64dbgMCP
§ 00 · Source-level Observatory

Tool Registry source deep dive

一个原生 x64dbg 插件,把调试器能力通过 MCP Streamable HTTP 与 SSE 暴露给 AI 客户端。真正值得拆的是:它不是脚本壳,而是把 Win32 socket、JSON-RPC、工具 schema、x64dbg bridge 和菜单配置全部编进同一个 Zig 插件。

★ 956
GitHub Stars
5.2K
LOC
12
Files
2026-08-24
Updated
x64dbg plugin→Win32 HTTP server
MCP client→JSON-RPC tools/list + tools/call
tool handler→bridge wrapper
debugger state→event log + SSE
§ 00 · OVERVIEW

71 个工具不是平铺清单,而是能力边界

工具注册表从 src/mcp/tools.zig:30(证据 src/evidence.md:13) 开始,src/mcp/tools.zig:112(证据 src/evidence.md:14) 后进入 debug-only 区域;server 侧 src/core/mcp_server.zig:621(证据 src/evidence.md:15) 用调试状态过滤,src/core/mcp_server.zig:650(证据 src/evidence.md:16) 调用时再次检查。这个双层结构比单纯隐藏按钮更可靠。

▸注册表形态

每个 ToolDef 把 name、description、category、schema 和 handler 放在一起,schema helpers 从 src/mcp/tools.zig:607(证据 src/evidence.md:26) 开始,参数读取在 src/mcp/tools.zig:761(证据 src/evidence.md:27) 附近统一完成。

§ 01 · REGISTRY

Always-on 工具

▸前置工具

src/mcp/tools.zig · L30-111Zig
30pub const tools = [_]ToolDef{
31    // ── Always available ────────────────────────────────────────
32    .{
33        .name = "GetDebugState",
34        .description = "Returns the current debugger state: whether a binary is loaded, whether it is running or paused, the PID, current instruction pointer, and current module. Always available. Takes no arguments.",
35        .debug_only = false,
36        .read_only = true,
37        .handler = getDebugState,
38        .schema_fn = schemaNoParams,
39    },
40    .{
41        .name = "LoadBinary",
42        .description = "Loads an executable into the debugger for a new debugging session. Equivalent to File > Open in the GUI. If a session is already active it is terminated first. The debuggee pauses at the system breakpoint.",
43        .debug_only = false,
44        .read_only = false,
45        .handler = loadBinary,
46        .schema_fn = schemaLoadBinary,
47    },
48    .{
49        .name = "ExecuteDebuggerCommand",
50        .description = "Executes an arbitrary x64dbg command string. Example: ExecuteDebuggerCommand command=run",
51        .debug_only = false,
52        .read_only = false,
53        .handler = executeDebuggerCommand,
54        .schema_fn = schemaExecuteDebuggerCommand,
55    },
56    .{
57        .name = "ListCommandsByCategory",
58        .description = "Lists the available MCP tools, optionally filtered to a single category.",
59        .debug_only = false,
60        .read_only = true,
61        .handler = listCommandsByCategory,
62        .schema_fn = schemaListCommandsByCategory,
63    },
64    .{
65        .name = "SearchForStrings",
66        .description = "Searches process memory for a specific text string and returns the addresses where it occurs.",
67        .debug_only = false,
68        .read_only = true,
69        .handler = searchForStrings,
70        .schema_fn = schemaSearchForStrings,
71    },
72    .{
73        .name = "GetEventLog",
74        .description = "Returns the last N debugger events (exceptions, breakpoints, DLL loads, thread events, etc.). Keeps the last 64 events in a ring buffer. Use this to see what happened in x64dbg.",
75        .debug_only = false,
76        .read_only = true,
77        .handler = getEventLog,
78        .schema_fn = schemaGetEventLog,
79    },
80    .{
81        .name = "ClearEventLog",
82        .description = "Clears the event log ring buffer.",
83        .debug_only = false,
84        .read_only = false,
85        .handler = clearEventLog,
86        .schema_fn = schemaNoParams,
87    },
88    .{
89        .name = "EvalExpression",
90        .description = "Evaluates an x64dbg expression (address, register, arithmetic, symbol) and returns the result in hex and decimal.",
91        .debug_only = false,
92        .read_only = true,
93        .handler = evalExpression,
94        .schema_fn = schemaEvalExpression,
95    },
96    .{
97        .name = "AttachProcess",
98        .description = "Attaches the debugger to a running process by PID.",
99        .debug_only = false,
100        .read_only = false,
101        .handler = attachProcess,
102        .schema_fn = schemaAttachProcess,
103    },
104    .{
105        .name = "Echo",
106        .description = "Echoes the input back to the client.",
107        .debug_only = false,
108        .read_only = true,
109        .handler = echo,
110        .schema_fn = schemaEcho,
111    },

src/mcp/tools.zig:30(证据 src/evidence.md:13) 的 GetDebugState、LoadBinary、ExecuteDebuggerCommand 和 AttachProcess 是未调试状态也可用的入口;src/mcp/tools.zig:878(证据 src/evidence.md:59) 读状态,src/mcp/tools.zig:925(证据 src/evidence.md:60) 加载二进制,src/mcp/tools.zig:943(证据 src/evidence.md:53) 执行命令。这里最需要治理的是任意 command 能力。

§ 02 · DEBUG TOOLS

调试态工具

▸调试能力区

src/mcp/tools.zig · L112-210Zig
112
113    // ── Debug-only ──────────────────────────────────────────────
114    .{
115        .name = "GetCurrentAddress",
116        .description = "Returns the current instruction pointer (EIP/RIP), the module it belongs to, and its label/comment. Takes no arguments.",
117        .debug_only = true,
118        .read_only = true,
119        .handler = getCurrentAddress,
120        .schema_fn = schemaNoParams,
121    },
122    .{
123        .name = "ReadMemory",
124        .description = "Reads raw bytes from the target process memory and returns a hex dump with ASCII. Example: ReadMemory address=0x7FF600001000, size=128",
125        .debug_only = true,
126        .read_only = true,
127        .handler = readMemory,
128        .schema_fn = schemaReadMemory,
129    },
130    .{
131        .name = "WaitForPause",
132        .description = "Blocks until the debuggee stops running (hits a breakpoint, exception, or exits). Returns the reason for the pause and the current address. Use after 'run' to wait for the target to break.",
133        .debug_only = true,
134        .read_only = true,
135        .handler = waitForPause,
136        .schema_fn = schemaWaitForPause,
137    },
138    .{
139        .name = "run",
140        .description = "Resumes execution of the debugged process (equivalent to F9 / 'run'). Returns whether the process is now running or has paused at a breakpoint. Takes no arguments.",
141        .debug_only = true,
142        .read_only = false,
143        .handler = runTarget,
144        .schema_fn = schemaNoParams,
145    },
146    .{
147        .name = "StepInto",
148        .description = "Executes a single instruction, stepping INTO any call encountered (F7). Takes no arguments.",
149        .debug_only = true,
150        .read_only = false,
151        .handler = stepInto,
152        .schema_fn = schemaNoParams,
153    },
154    .{
155        .name = "StepOver",
156        .description = "Executes a single instruction, stepping OVER any call/subroutine entirely (F8). Takes no arguments.",
157        .debug_only = true,
158        .read_only = false,
159        .handler = stepOver,
160        .schema_fn = schemaNoParams,
161    },
162    .{
163        .name = "StepOut",
164        .description = "Runs until the current function returns to its caller (Ctrl+F9). Takes no arguments.",
165        .debug_only = true,
166        .read_only = false,
167        .handler = stepOut,
168        .schema_fn = schemaNoParams,
169    },
170    .{
171        .name = "PauseDebug",
172        .description = "Pauses (suspends) the running debuggee, equivalent to Debug > Pause (F12). Takes no arguments.",
173        .debug_only = true,
174        .read_only = false,
175        .handler = pauseDebug,
176        .schema_fn = schemaNoParams,
177    },
178    .{
179        .name = "StopDebug",
180        .description = "Terminates the current debugging session and closes the debuggee process. Takes no arguments.",
181        .debug_only = true,
182        .read_only = false,
183        .handler = stopDebug,
184        .schema_fn = schemaNoParams,
185    },
186    .{
187        .name = "RestartDebug",
188        .description = "Restarts the current debugging session from the beginning. Takes no arguments.",
189        .debug_only = true,
190        .read_only = false,
191        .handler = restartDebug,
192        .schema_fn = schemaNoParams,
193    },
194    .{
195        .name = "SetBreakpoint",
196        .description = "Sets a software execution breakpoint (INT3) at an address or API symbol.",
197        .debug_only = true,
198        .read_only = false,
199        .handler = setBreakpoint,
200        .schema_fn = schemaSetBreakpoint,
201    },
202    .{
203        .name = "DeleteBreakpoint",
204        .description = "Removes a breakpoint at an address or API symbol.",
205        .debug_only = true,
206        .read_only = false,
207        .handler = deleteBreakpoint,
208        .schema_fn = schemaDeleteBreakpoint,
209    },
210    .{

src/mcp/tools.zig:112(证据 src/evidence.md:14) 后面的工具需要 active debug session;典型例子是 src/mcp/tools.zig:989(证据 src/evidence.md:34) ReadMemory、src/mcp/tools.zig:1055(证据 src/evidence.md:61) WaitForPause、src/mcp/tools.zig:1221(证据 src/evidence.md:62) SetBreakpoint、src/mcp/tools.zig:1321(证据 src/evidence.md:35) WriteMemToAddress。handler 本身也继续检查 running/paused 状态。

§ 03 · HANDLERS

几个关键 handler

▸读写和反汇编

src/mcp/tools.zig · L989-1052Zig
989fn readMemory(params: ?std.json.Value, out: []u8) ToolResult {
990    if (!bridge.isDebugging()) return errResult(out, "Error: No active debug session.");
991    if (bridge.isRunning()) return errResult(out, "Error: Target is running. Pause first.");
992
993    const addr_str = getParamStr(params, "address") orelse
994        return errResult(out, "Error: address is required.");
995    var size: usize = 64;
996    if (getParamInt(params, "size")) |s| {
997        size = @intCast(@min(@max(s, 1), 4096));
998    }
999
1000    var expr_buf: [256]u8 = undefined;
1001    const expr = std.fmt.bufPrint(&expr_buf, "{s}\x00", .{addr_str}) catch
1002        return errResult(out, "Error: address expression too long.");
1003    const addr = bridge.valFromString(@ptrCast(expr.ptr));
1004    if (addr == 0) return fmtErr(out, "Error: Could not resolve address '{s}'.", .{addr_str});
1005
1006    var mem_buf: [4096]u8 = undefined;
1007    if (!bridge.memRead(addr, mem_buf[0..size])) {
1008        return fmtErr(out, "Error: Failed to read {d} bytes at 0x{X}.", .{ size, addr });
1009    }
1010
1011    var pos: usize = 0;
1012    const header = std.fmt.bufPrint(out[pos..], "[ReadMemory] {d} bytes at 0x{X}:\n", .{ size, addr }) catch return result(out[0..pos], "");
1013    pos += header.len;
1014
1015    var i: usize = 0;
1016    while (i < size) : (i += 16) {
1017        const row_addr = addr + i;
1018        const count = @min(16, size - i);
1019
1020        // address
1021        const addr_s = std.fmt.bufPrint(out[pos..], "  {X:0>8}: ", .{row_addr}) catch break;
1022        pos += addr_s.len;
1023
1024        // hex
1025        for (0..count) |j| {
1026            const h = std.fmt.bufPrint(out[pos..], "{X:0>2} ", .{mem_buf[i + j]}) catch break;
1027            pos += h.len;
1028        }
1029        // pad
1030        for (count..16) |_| {
1031            if (pos + 3 > out.len) break;
1032            out[pos] = ' ';
1033            out[pos + 1] = ' ';
1034            out[pos + 2] = ' ';
1035            pos += 3;
1036        }
1037
1038        // ascii
1039        for (0..count) |j| {
1040            if (pos >= out.len) break;
1041            const b = mem_buf[i + j];
1042            out[pos] = if (b >= 32 and b <= 126) b else '.';
1043            pos += 1;
1044        }
1045        if (pos < out.len) {
1046            out[pos] = '\n';
1047            pos += 1;
1048        }
1049    }
1050
1051    return .{ .text = out[0..pos] };
1052}
src/mcp/tools.zig · L1321-1365Zig
1321fn writeMemToAddress(params: ?std.json.Value, out: []u8) ToolResult {
1322    if (!bridge.isDebugging()) return errResult(out, "Error: No active debug session.");
1323    if (bridge.isRunning()) return errResult(out, "Error: Target is running. Pause first.");
1324
1325    const addr_str = getParamStr(params, "address") orelse
1326        return errResult(out, "Error: address is required.");
1327    const byte_str = getParamStr(params, "byteString") orelse
1328        return errResult(out, "Error: byteString is required.");
1329
1330    var expr_buf: [256]u8 = undefined;
1331    const expr = std.fmt.bufPrint(&expr_buf, "{s}\x00", .{addr_str}) catch
1332        return errResult(out, "Error: address too long.");
1333    const addr = bridge.valFromString(@ptrCast(expr.ptr));
1334    if (addr == 0) return fmtErr(out, "Error: Could not resolve address '{s}'.", .{addr_str});
1335
1336    // Parse hex bytes
1337    var bytes: [512]u8 = undefined;
1338    var byte_count: usize = 0;
1339    var it = std.mem.tokenizeAny(u8, byte_str, " ,");
1340    while (it.next()) |tok| {
1341        if (byte_count >= bytes.len) break;
1342        bytes[byte_count] = std.fmt.parseInt(u8, tok, 16) catch
1343            return fmtErr(out, "Error: Invalid hex byte '{s}'.", .{tok});
1344        byte_count += 1;
1345    }
1346
1347    if (byte_count == 0) return errResult(out, "Error: No bytes to write.");
1348
1349    if (!bridge.memWrite(addr, bytes[0..byte_count])) {
1350        return fmtErr(out, "Error: Failed to write {d} bytes at 0x{X}.", .{ byte_count, addr });
1351    }
1352    return fmtResult(out, "Success: Wrote {d} bytes to 0x{X}.", .{ byte_count, addr });
1353}
1354
1355// ── CommentOrLabelAtAddress ─────────────────────────────────────────
1356fn commentOrLabelAtAddress(params: ?std.json.Value, out: []u8) ToolResult {
1357    if (!bridge.isDebugging()) return errResult(out, "Error: No active debug session.");
1358
1359    const addr_str = getParamStr(params, "address") orelse
1360        return errResult(out, "Error: address is required.");
1361    const value = getParamStr(params, "value") orelse
1362        return errResult(out, "Error: value is required.");
1363    const mode = getParamStr(params, "mode") orelse "Comment";
1364
1365    var expr_buf: [256]u8 = undefined;

src/mcp/tools.zig:989(证据 src/evidence.md:34) 对读取大小做 4096 上限,src/mcp/tools.zig:1321(证据 src/evidence.md:35) 解析十六进制字节再写入,src/mcp/tools.zig:1403(证据 src/evidence.md:36) 用 GUI disassembly 取文本,src/mcp/tools.zig:1489(证据 src/evidence.md:37) 输出内存 map。实现风格是一层薄但明确的 x64dbg command/API adapter。

§ 04 · ADVANCED

高级逆向辅助

▸扫描、符号、PE

src/mcp/tools.zig · L1805-1868Zig
1805    return fmtResult(out, "{s}", .{if (ok) "Patch list displayed in x64dbg Patches window." else "Command failed."});
1806}
1807
1808// ── Helpers (new tools) ───────────────────────────────────────────
1809
1810fn getModSize(base: usize) usize {
1811    var buf4: [4]u8 = undefined;
1812    if (!bridge.memRead(base + 0x3C, &buf4)) return 0;
1813    const pe_off: usize = readU32LE(&buf4);
1814    if (!bridge.memRead(base + pe_off + 80, &buf4)) return 0;
1815    return readU32LE(&buf4);
1816}
1817
1818const CodeSection = struct { start: usize, size: usize };
1819fn getCodeSection(base: usize) CodeSection {
1820    var buf4: [4]u8 = undefined;
1821    var buf2: [2]u8 = undefined;
1822    if (!bridge.memRead(base + 0x3C, &buf4)) return .{ .start = base, .size = 0 };
1823    const pe_off: usize = readU32LE(&buf4);
1824    if (!bridge.memRead(base + pe_off + 6, &buf2)) return .{ .start = base, .size = 0 };
1825    const num_sec = readU16LE(&buf2);
1826    if (!bridge.memRead(base + pe_off + 20, &buf2)) return .{ .start = base, .size = 0 };
1827    const opt_sz: usize = readU16LE(&buf2);
1828    const first_sec = base + pe_off + 24 + opt_sz;
1829    var i: usize = 0;
1830    while (i < num_sec and i < 64) : (i += 1) {
1831        var sec: [40]u8 = undefined;
1832        if (!bridge.memRead(first_sec + i * 40, &sec)) break;
1833        const chars = readU32LE(sec[36..40]);
1834        if (chars & 0x20000000 != 0) {
1835            return .{ .start = base + @as(usize, readU32LE(sec[12..16])), .size = readU32LE(sec[8..12]) };
1836        }
1837    }
1838    return .{ .start = base, .size = 0 };
1839}
1840
1841fn resolveAddr(addr_str: []const u8) usize {
1842    var buf: [256]u8 = undefined;
1843    _ = std.fmt.bufPrint(&buf, "{s}\x00", .{addr_str}) catch return 0;
1844    return bridge.valFromString(@ptrCast(&buf));
1845}
1846
1847// ── FindPattern ───────────────────────────────────────────────────
1848fn findPattern(params: ?std.json.Value, out: []u8) ToolResult {
1849    if (!bridge.isDebugging()) return errResult(out, "Error: No active debug session.");
1850    if (bridge.isRunning()) return errResult(out, "Error: Target is running. Pause first.");
1851    const pattern_str = getParamStr(params, "pattern") orelse
1852        return errResult(out, "Error: pattern is required.");
1853
1854    var pat: [256]u8 = undefined;
1855    var mask: [256]u8 = undefined;
1856    var pat_len: usize = 0;
1857    var it = std.mem.tokenizeAny(u8, pattern_str, " ");
1858    while (it.next()) |tok| {
1859        if (pat_len >= 256) break;
1860        if (tok[0] == '?') {
1861            pat[pat_len] = 0;
1862            mask[pat_len] = 0;
1863        } else {
1864            pat[pat_len] = std.fmt.parseInt(u8, tok, 16) catch
1865                return fmtErr(out, "Error: Invalid hex '{s}'.", .{tok});
1866            mask[pat_len] = 1;
1867        }
1868        pat_len += 1;

高级工具覆盖 src/mcp/tools.zig:1748(证据 src/evidence.md:47) 硬件断点、src/mcp/tools.zig:1805(证据 src/evidence.md:48) pattern scan、src/mcp/tools.zig:1882(证据 src/evidence.md:49) strings、src/mcp/tools.zig:2656(证据 src/evidence.md:63) symbol search、src/mcp/tools.zig:2802(证据 src/evidence.md:64) PE 分析。它适合受控分析流,不应作为远程自动攻击接口暴露。

§ 05 · RISKS

工具面治理

▸建议的权限模型

当前 src/core/mcp_server.zig:650(证据 src/evidence.md:16) 只按 debug-only 与 session 状态做守卫;考虑到 src/mcp/tools.zig:2506(证据 src/evidence.md:54) DumpMemory、src/mcp/tools.zig:2775(证据 src/evidence.md:55) DumpModule、src/mcp/tools.zig:2867(证据 src/evidence.md:56) DetectOEP 等能力,后续可以增加 per-tool allowlist、只读模式和每次会话授权。