x64dbgMCP
§ 00 · Source-level Observatory

x64dbg-mcp-server Debugger MCP bridge

一个原生 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

它解决什么,为什么突然火

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。这个路径短,依赖少,但也意味着并发、认证、网络暴露和调试器状态都落在插件自己的边界里。

▸核心数据

架构模式
Native x64dbg plugin + embedded MCP HTTP/SSE server + JSON-RPC tool dispatcher。
核心抽象
ToolDef 注册表、Bridge wrappers、Config 持久化、SSE client list、event ring buffer。
证据密度
本文引用真实 Zig 源码行,同时用 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 都必须认真做运行态检查。

§ 01 · ARCHITECTURE

插件内嵌服务器:不是外置 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。因为生命周期跟插件同进程,服务挂掉不会只是外部进程重启的问题,而会直接影响调试器扩展体验。

▸系统拓扑

x64dbg menu / commands→cmdStartServer / cmdStopServer
mcp.start→serverLoop thread
HTTP request→handleClient
JSON-RPC method→tools/list or tools/call
tool handler→bridge.* x64dbg API
debug event→event ring + SSE

▸插件入口和命令注册

src/main.zig · L59-80Zig
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 服务启动

src/core/mcp_server.zig · L179-210Zig
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 都共享调试器进程里的地址空间。

深度阅读 · HTTP/SSE transport
§ 02 · CORE MODULES

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 和路径分支都在同一个函数里显式展开。

src/core/mcp_server.zig · L358-431Zig
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 里。

src/mcp/tools.zig · L30-75Zig
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) 提供表达式、读内存、写内存的薄封装。

src/core/bridge.zig · L338-396Zig
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}
深度阅读 · Tool registry
§ 03 · KEY FLOW

一次工具调用怎样落到调试器

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 方法分发

src/core/mcp_server.zig · L439-469Zig
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}

▸工具调用守卫

src/core/mcp_server.zig · L650-687Zig
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 读取并格式化内存页。

▸调用链

POST /→handleJsonRpc
method = tools/call→buildToolsCallResult
name lookup→ToolDef.handler
state guard→is_debug_only + bridge.isDebugging
handler→bridge.cmdExec / memRead / memWrite
深度阅读 · Bridge and security boundary
§ 04 · VALIDATION

可验证点和边界条件

这个仓库没有测试目录,验证主要来自可读的运行态边界: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 的差别,但默认值仍然偏开放。

▸配置持久化

src/core/config.zig · L203-258Zig
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 命令。

§ 05 · RISKS

亮点之外,安全边界要前置

▸网络暴露风险

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 即可”,还不是按工具、按进程、按会话授权。

▸工程亮点

依赖极少
Zig + Win32 API 让交付形态很轻,适合 drop-in plugin。
MCP 兼容清晰
HTTP 与 SSE 两条路径都实现,覆盖新旧 MCP 客户端。
状态守卫普遍
大量 debug-only handler 检查 active session 和 paused state。
治理还可增强
建议默认 loopback、加入 per-tool allowlist、访问日志和一次性 session token。