Tool Registry source deep dive
一个原生 x64dbg 插件,把调试器能力通过 MCP Streamable HTTP 与 SSE 暴露给 AI 客户端。真正值得拆的是:它不是脚本壳,而是把 Win32 socket、JSON-RPC、工具 schema、x64dbg bridge 和菜单配置全部编进同一个 Zig 插件。
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) 附近统一完成。
Always-on 工具
▸前置工具
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 能力。
调试态工具
▸调试能力区
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 状态。
几个关键 handler
▸读写和反汇编
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}
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。
高级逆向辅助
▸扫描、符号、PE
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 分析。它适合受控分析流,不应作为远程自动攻击接口暴露。
工具面治理
▸建议的权限模型
当前 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、只读模式和每次会话授权。