x64dbg-mcp-server Debugger MCP bridge
一个原生 x64dbg 插件,把调试器能力通过 MCP Streamable HTTP 与 SSE 暴露给 AI 客户端。真正值得拆的是:它不是脚本壳,而是把 Win32 socket、JSON-RPC、工具 schema、x64dbg bridge 和菜单配置全部编进同一个 Zig 插件。
它解决什么,为什么突然火
x64dbg-MCP Server 的核心卖点是把桌面调试器变成 MCP 工具服务:插件入口先在 src/main.zig:59(证据 src/evidence.md:3) 初始化 bridge,随后在 src/main.zig:71(证据 src/evidence.md:4) 与 src/main.zig:75(证据 src/evidence.md:5) 注册启动/停止命令;进入 setup 后,src/main.zig:99(证据 src/evidence.md:6) 开始把调试生命周期事件挂到 x64dbg 回调,src/main.zig:135(证据 src/evidence.md:7) 加载配置,src/main.zig:142(证据 src/evidence.md:8) 决定是否自动开服。这说明它的产品形态不是外部轮询器,而是驻留在调试器内部的控制面。
服务端部分用手写 Win32 socket 和 JSON-RPC:src/core/mcp_server.zig:179(证据 src/evidence.md:9) 创建服务器线程,src/core/mcp_server.zig:264(证据 src/evidence.md:10) 做 socket/bind/listen,src/core/mcp_server.zig:312(证据 src/evidence.md:11) 对每个连接起线程,src/core/mcp_server.zig:358(证据 src/evidence.md:12) 再解析 HTTP 并分流到 MCP。这个路径短,依赖少,但也意味着并发、认证、网络暴露和调试器状态都落在插件自己的边界里。
▸核心数据
src/evidence.md 映射供质量门禁校验。▸架构判断
真正的设计取舍在“能力集中”和“边界集中”:src/mcp/tools.zig:30(证据 src/evidence.md:13) 到 src/mcp/tools.zig:112(证据 src/evidence.md:14) 把 always-on 与 debug-only 工具放在同一数组里,src/core/mcp_server.zig:621(证据 src/evidence.md:15) 按调试状态过滤,src/core/mcp_server.zig:650(证据 src/evidence.md:16) 统一执行和包装错误。这样 MCP 客户端看到的是一个一致工具面,代价是每个 handler 都必须认真做运行态检查。
插件内嵌服务器:不是外置 daemon
启动链路非常直接:src/main.zig:82(证据 src/evidence.md:17) 接收 x64dbg UI 句柄,src/main.zig:330(证据 src/evidence.md:18) 调用 mcp.start(),src/core/mcp_server.zig:179(证据 src/evidence.md:9) 创建服务器线程,src/core/mcp_server.zig:212(证据 src/evidence.md:19) 则负责停止时关掉 listener 和 SSE clients。因为生命周期跟插件同进程,服务挂掉不会只是外部进程重启的问题,而会直接影响调试器扩展体验。
▸系统拓扑
▸插件入口和命令注册
59export fn pluginit(initStruct: *bridge.PLUG_INITSTRUCT) callconv(.c) c_int { 60 bridge.init() catch return 0; 61 62 initStruct.sdkVersion = bridge.PLUG_SDKVERSION; 63 initStruct.pluginVersion = PLUGIN_VERSION; 64 65 const name = PLUGIN_NAME; 66 for (name, 0..) |c, i| { 67 initStruct.pluginName[i] = c; 68 } 69 initStruct.pluginName[name.len] = 0; 70 71 plugin_handle = initStruct.pluginHandle; 72 bridge.logPuts("[x64dbg-MCP Server] Plugin initialized.\x00"); 73 74 _ = bridge._plugin_registercommand(plugin_handle, "StartMCPServer\x00", cmdStartServer, 0); 75 bridge.logPuts("[x64dbg-MCP Server] Command \"StartMCPServer\" registered!\x00"); 76 _ = bridge._plugin_registercommand(plugin_handle, "StopMCPServer\x00", cmdStopServer, 0); 77 bridge.logPuts("[x64dbg-MCP Server] Command \"StopMCPServer\" registered!\x00"); 78 79 return 1; 80}
这段代码把三件事绑在一起:src/main.zig:59(证据 src/evidence.md:3) 先初始化 bridge,src/main.zig:71(证据 src/evidence.md:4) 注册启动命令,src/main.zig:75(证据 src/evidence.md:5) 注册停止命令。失败时直接返回 0,说明插件把 bridge 解析视作硬依赖,没有提供降级模式。
▸HTTP 服务启动
179pub fn start() void { 180 if (server_running) { 181 bridge.logPuts("[x64dbg-MCP Server] Server already running.\x00"); 182 return; 183 } 184 185 server_running = true; 186 187 server_handle = CreateThread(null, 0, serverThreadProc, null, 0, null); 188 if (server_handle == null) { 189 server_running = false; 190 bridge.logPuts("[x64dbg-MCP Server] Failed to create server thread.\x00"); 191 return; 192 } 193 194 { 195 var buf: [192]u8 = undefined; 196 const prefix = "[x64dbg-MCP Server] Starting MCP server on "; 197 @memcpy(buf[0..prefix.len], prefix); 198 var pos: usize = prefix.len; 199 @memcpy(buf[pos .. pos + server_ip_len], server_ip[0..server_ip_len]); 200 pos += server_ip_len; 201 buf[pos] = ':'; 202 pos += 1; 203 pos += fmtPort(server_port, buf[pos..]); 204 const suffix = "..."; 205 @memcpy(buf[pos .. pos + suffix.len], suffix); 206 pos += suffix.len; 207 buf[pos] = 0; 208 bridge.logPuts(@ptrCast(&buf)); 209 } 210}
src/core/mcp_server.zig:179(证据 src/evidence.md:9) 用互斥状态避免重复启动,src/core/mcp_server.zig:190(证据 src/evidence.md:20) 创建 Win32 thread,src/core/mcp_server.zig:198(证据 src/evidence.md:21) 在失败时恢复 running 标记。实现朴素但足够清晰,关键风险是后续所有连接和 tool call 都共享调试器进程里的地址空间。
MCP 分发与工具注册表
▸HTTP 路由核心
src/core/mcp_server.zig:358(证据 src/evidence.md:12) 是请求入口,src/core/mcp_server.zig:374(证据 src/evidence.md:22) 解析 Authorization,src/core/mcp_server.zig:388(证据 src/evidence.md:23) 拒绝错误 token,src/core/mcp_server.zig:393(证据 src/evidence.md:24) 处理 GET,src/core/mcp_server.zig:409(证据 src/evidence.md:25) 处理 POST。它没有引入 web 框架,因此每个 header、method 和路径分支都在同一个函数里显式展开。
358fn handleClient(sock: SOCKET) void { 359 const recv_result = wsRecv(sock); 360 if (recv_result.len == 0) { 361 _ = ws2.closesocket(sock); 362 return; 363 } 364 365 const request = recv_result.buf[0..recv_result.len]; 366 const method_line = getFirstLine(request); 367 368 if (std.mem.startsWith(u8, method_line, "OPTIONS ")) { 369 wsSend(sock, "HTTP/1.1 204 No Content\r\n" ++ CORS_HEADERS ++ "\r\n"); 370 _ = ws2.closesocket(sock); 371 return; 372 } 373 374 { 375 const headers_slice = if (findHeaderEnd(request)) |he| request[0..he] else request; 376 const tok = parseHeaderValue(headers_slice, "Authorization"); 377 var authorized = false; 378 if (tok) |t| { 379 if (t.len > 7 and std.mem.eql(u8, t[0..7], "Bearer ")) { 380 const bearer = t[7..]; 381 if (auth_token_len > 0 and bearer.len == auth_token_len and std.mem.eql(u8, bearer, auth_token[0..auth_token_len])) { 382 authorized = true; 383 } 384 } 385 } 386 if (!authorized) { 387 wsSend(sock, "HTTP/1.1 401 Unauthorized\r\n" ++ CORS_HEADERS ++ "Content-Length: 24\r\n\r\n{\"error\":\"Unauthorized\"}"); 388 _ = ws2.closesocket(sock); 389 return; 390 } 391 } 392 393 if (std.mem.startsWith(u8, method_line, "GET ")) { 394 const path = getPath(method_line); 395 if (std.mem.indexOf(u8, path, ".well-known") != null) { 396 wsSend(sock, "HTTP/1.1 404 Not Found\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n"); 397 _ = ws2.closesocket(sock); 398 return; 399 } 400 if (std.mem.eql(u8, path, "/sse")) { 401 handleSseConnect(sock); 402 return; 403 } 404 sendServerInfo(sock); 405 _ = ws2.closesocket(sock); 406 return; 407 } 408 409 if (std.mem.startsWith(u8, method_line, "POST ")) { 410 const path = getPath(method_line); 411 if (std.mem.indexOf(u8, path, ".well-known") != null) { 412 wsSend(sock, "HTTP/1.1 404 Not Found\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n"); 413 _ = ws2.closesocket(sock); 414 return; 415 } 416 417 if (findHeaderEnd(request)) |header_end| { 418 const body = request[header_end + 4 ..]; 419 const headers = request[0..header_end]; 420 421 if (std.mem.eql(u8, path, "/messages") or std.mem.startsWith(u8, path, "/messages?")) { 422 const session_id = parseQueryParam(path, "sessionId") orelse 423 parseHeaderValue(headers, "Mcp-Session-Id"); 424 handleSseMessage(sock, body, session_id); 425 } else { 426 handleJsonRpc(sock, body); 427 } 428 } else { 429 wsSend(sock, "HTTP/1.1 400 Bad Request\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n"); 430 } 431 _ = ws2.closesocket(sock);
▸工具注册表
工具面从 src/mcp/tools.zig:30(证据 src/evidence.md:13) 开始;前 10 个工具总是可见,src/mcp/tools.zig:112(证据 src/evidence.md:14) 后面才是需要 active debug session 的能力。schema helpers 在 src/mcp/tools.zig:607(证据 src/evidence.md:26) 附近组合参数定义,结果格式化和参数读取放在 src/mcp/tools.zig:761(证据 src/evidence.md:27) 一组 helper 里。
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,
▸bridge 边界
插件没有静态链接一堆 x64dbg API,而是在运行时解析:src/core/bridge.zig:267(证据 src/evidence.md:28) 声明函数指针,src/core/bridge.zig:338(证据 src/evidence.md:29) 根据 32/64 位选择 DLL 并解析符号,src/core/bridge.zig:419(证据 src/evidence.md:30)、src/core/bridge.zig:433(证据 src/evidence.md:31) 和 src/core/bridge.zig:437(证据 src/evidence.md:32) 提供表达式、读内存、写内存的薄封装。
338pub fn init() !void { 339 const bridge_dll: [*:0]const u8 = if (@sizeOf(usize) == 8) 340 "x64bridge.dll" 341 else 342 "x32bridge.dll"; 343 344 const plugin_dll: [*:0]const u8 = if (@sizeOf(usize) == 8) 345 "x64dbg.dll" 346 else 347 "x32dbg.dll"; 348 349 bridge_module = GetModuleHandleA(bridge_dll) orelse 350 return error.BridgeNotFound; 351 352 dbg_module = GetModuleHandleA(plugin_dll) orelse 353 return error.BridgeNotFound; 354 355 const m = bridge_module.?; 356 const d = dbg_module.?; 357 358 // Dbg functions (from x64bridge.dll / x32bridge.dll) 359 DbgCmdExec = resolve(@TypeOf(DbgCmdExec), m, "DbgCmdExec"); 360 DbgCmdExecDirect = resolve(@TypeOf(DbgCmdExecDirect), m, "DbgCmdExecDirect"); 361 DbgIsDebugging = resolve(@TypeOf(DbgIsDebugging), m, "DbgIsDebugging"); 362 DbgIsRunning = resolve(@TypeOf(DbgIsRunning), m, "DbgIsRunning"); 363 DbgIsRunLocked = resolve(@TypeOf(DbgIsRunLocked), m, "DbgIsRunLocked"); 364 DbgValFromString = resolve(@TypeOf(DbgValFromString), m, "DbgValFromString"); 365 DbgMemRead = resolve(@TypeOf(DbgMemRead), m, "DbgMemRead"); 366 DbgMemWrite = resolve(@TypeOf(DbgMemWrite), m, "DbgMemWrite"); 367 DbgGetLabelAt = resolve(@TypeOf(DbgGetLabelAt), m, "DbgGetLabelAt"); 368 DbgGetCommentAt = resolve(@TypeOf(DbgGetCommentAt), m, "DbgGetCommentAt"); 369 DbgGetModuleAt = resolve(@TypeOf(DbgGetModuleAt), m, "DbgGetModuleAt"); 370 DbgModBaseFromName = resolve(@TypeOf(DbgModBaseFromName), m, "DbgModBaseFromName"); 371 DbgMemFindBaseAddr = resolve(@TypeOf(DbgMemFindBaseAddr), m, "DbgMemFindBaseAddr"); 372 DbgSetCommentAt = resolve(@TypeOf(DbgSetCommentAt), m, "DbgSetCommentAt"); 373 DbgSetLabelAt = resolve(@TypeOf(DbgSetLabelAt), m, "DbgSetLabelAt"); 374 375 // Extended bridge functions (optional) 376 DbgDisasmFastAt = resolveOptional(DbgDisasmFastAtFn, m, "DbgDisasmFastAt"); 377 DbgGetBpList = resolveOptional(DbgGetBpListFn, m, "DbgGetBpList"); 378 DbgMemMap = resolveOptional(DbgMemMapFn, m, "DbgMemMap"); 379 DbgGetThreadList = resolveOptional(DbgGetThreadListFn, m, "DbgGetThreadList"); 380 BridgeFree = resolveOptional(BridgeFreeFn, m, "BridgeFree"); 381 GuiGetDisassembly = resolveOptional(GuiGetDisassemblyFn, m, "GuiGetDisassembly"); 382 DbgFunctionGet = resolveOptional(DbgFunctionGetFn, m, "DbgFunctionGet"); 383 384 // Plugin functions (from x64dbg.dll / x32dbg.dll) 385 _plugin_registercallback = resolve(@TypeOf(_plugin_registercallback), d, "_plugin_registercallback"); 386 _plugin_unregistercallback = resolve(@TypeOf(_plugin_unregistercallback), d, "_plugin_unregistercallback"); 387 _plugin_registercommand = resolve(@TypeOf(_plugin_registercommand), d, "_plugin_registercommand"); 388 _plugin_unregistercommand = resolve(@TypeOf(_plugin_unregistercommand), d, "_plugin_unregistercommand"); 389 _plugin_menuadd = resolve(@TypeOf(_plugin_menuadd), d, "_plugin_menuadd"); 390 _plugin_menuaddentry = resolve(@TypeOf(_plugin_menuaddentry), d, "_plugin_menuaddentry"); 391 _plugin_menuaddseparator = resolve(@TypeOf(_plugin_menuaddseparator), d, "_plugin_menuaddseparator"); 392 _plugin_menuseticon = resolveOptional(MenuSetIconFn, d, "_plugin_menuseticon"); 393 _plugin_menuclear = resolve(@TypeOf(_plugin_menuclear), d, "_plugin_menuclear"); 394 _plugin_menuentryseticon = resolveOptional(EntrySetIconFn, d, "_plugin_menuentryseticon"); 395 _plugin_logputs = resolve(@TypeOf(_plugin_logputs), d, "_plugin_logputs"); 396}
一次工具调用怎样落到调试器
JSON-RPC 入口在 src/core/mcp_server.zig:439(证据 src/evidence.md:33),其中 initialize、tools/list、tools/call 和 ping 是明确枚举的;工具列表在 src/core/mcp_server.zig:621(证据 src/evidence.md:15) 按 debug 状态过滤,调用时 src/core/mcp_server.zig:650(证据 src/evidence.md:16) 再做 debug-only 守卫。这种双层守卫让“看得到”和“能执行”都依赖运行态。
▸JSON-RPC 方法分发
439fn handleJsonRpc(sock: SOCKET, body: []const u8) void { 440 const parsed = std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, body, .{}) catch { 441 sendJsonRpcError(sock, null, -32700, "Parse error"); 442 return; 443 }; 444 defer parsed.deinit(); 445 446 const root = parsed.value; 447 const method_val = json_mod.getStringField(root, "method") orelse { 448 sendJsonRpcError(sock, null, -32600, "Invalid request: missing method"); 449 return; 450 }; 451 const id = json_mod.getObjectField(root, "id"); 452 453 if (id == null or std.mem.startsWith(u8, method_val, "notifications/")) { 454 wsSend(sock, "HTTP/1.1 202 Accepted\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n"); 455 return; 456 } 457 458 if (std.mem.eql(u8, method_val, "initialize")) { 459 handleInitialize(sock, id); 460 } else if (std.mem.eql(u8, method_val, "tools/list")) { 461 handleToolsList(sock, id); 462 } else if (std.mem.eql(u8, method_val, "tools/call")) { 463 handleToolsCall(sock, id, root); 464 } else if (std.mem.eql(u8, method_val, "ping")) { 465 sendJsonRpcResult(sock, id, "{}"); 466 } else { 467 sendJsonRpcError(sock, id, -32601, "Method not found"); 468 } 469}
▸工具调用守卫
650fn buildToolsCallResult(w: *JsonWriter, root: std.json.Value) void { 651 const params_obj = json_mod.getObjectField(root, "params"); 652 const tool_name = if (params_obj) |p| json_mod.getStringField(p, "name") else null; 653 if (tool_name == null) { 654 w.raw("{\"error\":\"Missing params.name\"}"); 655 return; 656 } 657 const name = tool_name.?; 658 const arguments = if (params_obj) |p| json_mod.getObjectField(p, "arguments") else null; 659 660 for (&tools_mod.tools) |*t| { 661 if (std.mem.eql(u8, t.name, name)) { 662 if (t.debug_only and !bridge.isDebugging()) { 663 w.raw("{\"error\":\"Tool requires an active debug session.\"}"); 664 return; 665 } 666 var out_buf: [65536]u8 = undefined; 667 const tr = t.handler(arguments, &out_buf); 668 669 w.beginObject(); 670 w.key("content"); 671 w.beginArray(); 672 w.beginObject(); 673 w.fieldStr("type", "text"); 674 w.key("text"); 675 w.writeString(tr.text); 676 w.endObject(); 677 w.endArray(); 678 w.raw(","); 679 if (tr.is_error) { 680 w.fieldBool("isError", true); 681 } 682 w.endObject(); 683 return; 684 } 685 } 686 w.raw("{\"error\":\"Tool not found.\"}"); 687}
▸危险能力的暂停约束
典型 handler 会先检查状态:src/mcp/tools.zig:989(证据 src/evidence.md:34) 的 ReadMemory 限制最大 4096 字节并要求目标暂停,src/mcp/tools.zig:1321(证据 src/evidence.md:35) 的 WriteMemToAddress 同样要求 active debug session 和 paused target,src/mcp/tools.zig:1403(证据 src/evidence.md:36) 的 Disassemble 用 GUI disassembly 辅助输出,src/mcp/tools.zig:1489(证据 src/evidence.md:37) 的 GetMemoryMap 读取并格式化内存页。
▸调用链
可验证点和边界条件
这个仓库没有测试目录,验证主要来自可读的运行态边界:src/core/config.zig:203(证据 src/evidence.md:38) 设默认端口和 bind address,src/core/config.zig:250(证据 src/evidence.md:39) 确保 token 存在,src/core/config.zig:286(证据 src/evidence.md:40) 保存配置,src/core/config.zig:550(证据 src/evidence.md:41) 保存后重启 MCP server。配置 UI 明确告诉用户 0.0.0.0 与 127.0.0.1 的差别,但默认值仍然偏开放。
▸配置持久化
203pub fn load() Config { 204 const default_port: u16 = if (@sizeOf(usize) == 8) 9094 else 9095; 205 var cfg = Config{ .port = default_port }; 206 const default_ip = "0.0.0.0"; 207 @memcpy(cfg.ip[0..default_ip.len], default_ip); 208 cfg.ip_len = default_ip.len; 209 210 const path = getConfigPath(); 211 const h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, null, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, null); 212 if (h == INVALID_HANDLE or h == null) return ensureToken(&cfg); 213 defer _ = CloseHandle(h); 214 215 var buf: [512]u8 = undefined; 216 var bytes_read: u32 = 0; 217 if (ReadFile(h, &buf, 512, &bytes_read, null) == 0) return ensureToken(&cfg); 218 219 const json_data = buf[0..bytes_read]; 220 221 // Parse "IpAddress":"<value>" 222 if (findJsonString(json_data, "IpAddress")) |ip_val| { 223 if (ip_val.len > 0 and ip_val.len < 64) { 224 @memcpy(cfg.ip[0..ip_val.len], ip_val); 225 cfg.ip_len = ip_val.len; 226 } 227 } 228 229 // Parse "Port":<number> 230 if (findJsonNumber(json_data, "Port")) |port_val| { 231 cfg.port = port_val; 232 } 233 234 // Parse "AutoStart":true/false 235 if (findJsonBool(json_data, "AutoStart")) |val| { 236 cfg.auto_start = val; 237 } 238 239 // Parse "AuthToken":"<value>" 240 if (findJsonString(json_data, "AuthToken")) |token_val| { 241 if (token_val.len > 0 and token_val.len <= 64) { 242 @memcpy(cfg.auth_token[0..token_val.len], token_val); 243 cfg.auth_token_len = token_val.len; 244 } 245 } 246 247 return ensureToken(&cfg); 248} 249 250fn ensureToken(cfg: *Config) Config { 251 if (cfg.auth_token_len == 0) { 252 var tok: [32]u8 = undefined; 253 generateToken(&tok); 254 @memcpy(cfg.auth_token[0..32], tok[0..32]); 255 cfg.auth_token_len = 32; 256 save(cfg); 257 } 258 return cfg.*;
▸事件观测
事件链路从 src/main.zig:344(证据 src/evidence.md:42) 的 debug started,到 src/main.zig:387(证据 src/evidence.md:43) 的 exception,再到 src/main.zig:421(证据 src/evidence.md:44) 的 breakpoint hit;同时 SSE 连接在 src/core/mcp_server.zig:499(证据 src/evidence.md:45) 建立,legacy 消息在 src/core/mcp_server.zig:530(证据 src/evidence.md:46) 回写。这为 AI 客户端提供反馈,但不是完整审计日志。
▸能力清单完整性
工具不仅有基础步进,还覆盖 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:2193(证据 src/evidence.md:50) run-to-address 与 src/mcp/tools.zig:2231(证据 src/evidence.md:51) trace。这说明它面向真实逆向工作流,而不是只包几个 demo 命令。
亮点之外,安全边界要前置
▸网络暴露风险
src/core/mcp_server.zig:37(证据 src/evidence.md:52) 默认 bind 到 0.0.0.0,src/core/mcp_server.zig:374(证据 src/evidence.md:22) 到 src/core/mcp_server.zig:388(证据 src/evidence.md:23) 有 bearer token 检查,但源码里没有 TLS、origin 绑定、速率限制或 per-tool 权限。对于一个能调试进程、读写内存、执行命令的服务,这个默认值需要被用户主动收紧。
▸工具授权粒度
危险能力很集中:src/mcp/tools.zig:943(证据 src/evidence.md:53) 允许执行任意 x64dbg command,src/mcp/tools.zig:1321(证据 src/evidence.md:35) 可以写内存,src/mcp/tools.zig:2506(证据 src/evidence.md:54) 能 dump memory,src/mcp/tools.zig:2775(证据 src/evidence.md:55) 能 dump module,src/mcp/tools.zig:2867(证据 src/evidence.md:56) 还加入 OEP 检测。现在的边界是“持有 token 即可”,还不是按工具、按进程、按会话授权。