Skip to main content

Why gdb-mcp?

·4892 words·10 mins·
工具 MCP GDB Reverse Agent Vulnerability
Table of Contents

最近写了一个项目:gdb-mcp

一句话概括,它是一个面向 MCP 客户端的 GDB server,让 Codex、Claude Code 之类的 agent 可以通过结构化 tool 调用来控制 GDB,而不是把 GDB 当成普通 shell 程序来用。

TL;DR
#

gdb-mcp 想解决的问题是:让 agent 能够稳定地进入动态调试循环。

  • 用 GDB/MI 和 MCP tool 封装 GDB,而不是让 agent 自己拼裸命令、解析终端输出
  • 显式维护多 GDB session,避免“当前调试目标”这种隐式状态
  • 为 run / continue / step 这类操作返回 compact context,包括当前位置、backtrace、locals 和 stop reason
  • 默认关闭写内存、调用函数、任意 GDB 命令等危险能力
  • 支持本地程序、attach、core dump、gdbserver 和 reverse debugging 等逆向/漏挖常见场景

我的理解是,agent 想做自动化逆向和漏洞挖掘,不能只有静态分析能力。很多关键事实只存在于运行时,而 GDB 正是最通用的运行时观察入口。

初心
#

我一开始想做这个东西,是因为我觉得 GDB MCP 对 agent 自动化逆向和漏洞挖掘很重要

逆向和漏挖并不只是“看静态代码”。很多时候真正有用的信息来自动态过程:

  • 一个输入走到了哪条分支
  • 某个变量在崩溃前被改成了什么
  • 堆块、寄存器、栈帧、线程状态现在是什么样
  • patch 一个条件、改一个变量、倒回一步之后行为是否变化
  • core dump 里保存的现场能不能还原出漏洞路径

这些工作以前主要由人来驱动 GDB:下断点、运行、单步、看 backtrace、看 locals、读内存、继续跑,再根据结果调整下一步。Agent 想进入这个循环,就需要一个稳定的调试接口。

如果没有这样的接口,agent 只能把 GDB 当成一个普通命令行程序来用。这会带来几个问题:输出格式不稳定、上下文太长、状态不清晰,并且很难区分“读一下状态”和“修改目标程序”。

市面上已经有一些 GDB MCP,但我试下来都不太符合自己想要的工作流。主要问题如下:

  • 会话模型不够明确,多目标调试时容易混在一起
  • 过度依赖裸 gdb_execute,agent 需要自己拼命令、自己解析输出
  • 输出没有为 LLM 上下文做压缩,一次 backtrace 或 disassemble 就可能塞进很多无关内容
  • 安全边界不清楚,读状态、控制执行、写内存、调用 inferior 函数混在一起
  • 对 attach、core、gdbserver、reverse debugging 这些逆向/漏洞场景常用能力支持不足

于是我决定自己(vibe)写一个。

设计目标
#

gdb-mcp 的目标不是把所有 GDB 命令原样搬到 MCP 里,而是给 agent 一个更适合自动化推理的调试抽象。

目前它的定位大概是:

  • Linux-first,优先保证本地 GDB / core / attach / gdbserver 的常见路径可靠
  • 每个调试目标都有显式 session_id,不存在隐式当前会话
  • 常用操作都做成专用 tool,而不是让 agent 到处执行裸命令
  • 执行类工具尽量直接返回当前位置、栈和局部变量,减少 agent 再发多轮查询
  • 默认安全,写内存、调用函数、任意 GDB 命令必须显式开启 unsafe
  • 支持 Codex、Claude Code 以及任何标准 MCP client

目前版本到了 0.3.0,工具数量五十多个,覆盖 session 管理、执行控制、断点、线程/栈帧、表达式求值、反汇编、内存读取、core、attach、gdbserver、reverse debugging 和诊断信息。

技术路线
#

整体结构并不复杂:

MCP client
  |
  | stdio
  v
gdb-mcp lazy proxy
  |
  | first gdb_* tool call
  v
gdb-mcp backend
  |
  | GDB/MI
  v
GDB / gdbserver / target program

这里有两个关键选择:一是用 GDB/MI,而不是直接解析普通 CLI 输出;二是默认入口是一个 lazy stdio proxy。

GDB/MI
#

GDB 普通 CLI 输出是给人看的,不是给程序解析的。同样是一个栈帧、一个断点、一次停止原因,CLI 输出经常依赖格式化文本。Agent 当然可以“读懂”文本,但如果要做自动化,就不能每一步都赌模型的自然语言解析能力。

GDB/MI 的输出更接近结构化协议,例如 result record、exec async record、stream record 都有固定形态。项目里写了一个轻量 MI parser,把 GDB 输出解析成统一的 MIRecord

  • ^done^error 这类 result record 用来判断命令是否成功
  • *stopped*running 这类 async record 用来维护 inferior 状态
  • ~@& 分别对应 console、target、log stream
  • token 用来把命令和返回结果关联起来

这样 MCP tool 返回给 agent 的就不是一整段终端文本,而是 JSON 结构:okresult_classresultsstoppedconsoletargetasyncraw 等字段都被拆出来了。

多会话
#

另一个设计点是多会话。

我不希望这个工具存在“当前正在调试的程序”这种隐式全局状态。Agent 只要跑久一点,就很容易出现:

  • 上一轮开了一个 GDB 没关
  • 用户又让它看另一个 binary
  • 过程中还 attach 了一个进程
  • 后面一句“继续运行”到底指哪个目标不明确

所以 gdb-mcp 的所有 session 操作都围绕显式 session_id 展开。SessionManager 负责创建、查找、关闭 session;每个 GdbSession 内部维护自己的 GDB 子进程、命令 token、pending command、最近事件、最近命令、last stop 和可选的 gdbserver 子进程。

这对于 agent 来说很重要。它可以先 gdb_list_sessions 看当前有哪些会话,再决定复用还是新建;多个程序并行分析时,也不会因为“当前 GDB”被覆盖而混乱。

命令路由
#

GDB/MI 比较麻烦的一点是异步。一次命令发出去之后,可能先收到 ^running,随后收到若干 stream record,最后收到 *stopped;也可能命令本身已经 ^done,但 inferior 还没有停。

gdb-mcp 的处理方式是:

  1. 每条命令分配一个递增 token。
  2. _PendingCommand 记录这个 token 对应的 future、result record、stopped record 和中间 records。
  3. reader task 持续读取 GDB stdout,解析成 MIRecord
  4. 根据 token 和 record 类型把输出路由到对应 pending command。
  5. runcontinuestep 这类命令,可以设置 wait_for_stop=True,等到真正停止后再返回。

这比简单地“发一行命令,读到 prompt 就结束”要麻烦,但对调试是必要的。因为 agent 关心的往往不是命令是否发出成功,而是程序现在停在了哪里、为什么停、还能看到什么现场。

Demo
#

仓库里放了一个最小 demo,用来验证最核心的调试路径:编译一个简单 C 程序,下断点,运行到目标函数,然后让 agent 一次性拿到现场。

demo 程序如下:

#include <stdio.h>

static int add(int a, int b) {
    int total = a + b;
    return total;
}

int main(void) {
    int value = add(2, 40);
    printf("value=%d\n", value);
    return value == 42 ? 0 : 1;
}

先编译:

cc -g -gdwarf-4 -O0 examples/hello.c -o /tmp/gdb-mcp-hello

然后在 Codex 或 Claude Code 里,其实只需要给一个比较自然的任务:

Use GDB MCP to debug /tmp/gdb-mcp-hello.
Set a breakpoint at add, run, show the current location, backtrace, locals,
then continue once.

对应到 MCP tool flow,大致是:

  1. gdb_create_session 加载 /tmp/gdb-mcp-hello
  2. gdb_set_breakpointadd 下断点
  3. gdb_run_and_context 运行到断点并返回现场
  4. gdb_continue_and_context 继续运行到程序退出
  5. gdb_close_session 关闭 session

关键是第三步。传统 GDB wrapper 可能只会告诉 agent “程序停了”,然后 agent 还需要继续问当前 frame、backtrace、locals。gdb_run_and_context 会把这些信息一起返回,结果类似:

action: run
stop: breakpoint-hit
location: add at examples/hello.c:3
backtrace: #0 add:3 <- #1 main:8
locals: a=2, b=40

这说明 agent 已经知道程序停在 add,参数 ab 分别是 240,调用者是 main。如果继续执行,gdb_continue_and_context 会返回程序输出:

value=42

Context Tool
#

早期只做基础 GDB wrapper 的时候,我很快发现一个问题:agent 调试程序时,常常会重复发这样的工具序列:

  1. run / continue / step
  2. current location
  3. backtrace
  4. locals
  5. 根据结果决定下一步

如果每一次执行控制都要再查三次状态,成本高,且上下文容易碎。所以现在提供了一组 *_and_context 工具:

  • gdb_run_and_context
  • gdb_continue_and_context
  • gdb_step_and_context
  • gdb_next_and_context
  • gdb_reverse_continue_and_context
  • gdb_reverse_step_and_context
  • gdb_reverse_next_and_context
  • gdb_reverse_finish_and_context

这些工具在执行后会自动收集:

  • 当前 frame
  • backtrace
  • locals
  • stop reason
  • target output
  • 一段 compact summary

例如一个典型结果会包含类似这样的 summary:

action: run
stop: breakpoint-hit
location: add at /tmp/hello.c:3
backtrace: #0 add:3 <- #1 main:8
locals: a=2, b=40

这其实是给 agent 的“最小可用现场”。它不需要立刻看到所有 MI 原始结果,但如果确实需要,也可以传 include_raw=true 拿完整 payload。

我的理解是,这是 GDB MCP 和普通 shell GDB 最大的差别之一:它不只是让 agent 能“调用 GDB”,而是把调试循环中最常用的观察结果整理成适合模型消费的形态。

工具分层
#

目前工具大致分成几类。

Session 管理
#

包括:

  • gdb_create_session
  • gdb_attach
  • gdb_load_core
  • gdb_connect_gdbserver
  • gdb_launch_gdbserver
  • gdb_list_sessions
  • gdb_status
  • gdb_close_session

这里比较在意的是 attach、core 和 gdbserver。因为真实逆向/漏洞分析里,很多目标并不是简单 gdb ./a.out 就能覆盖:

  • fuzzing 产生 core,需要加载 core 还原现场
  • 服务进程已经在运行,需要 attach
  • 目标跑在容器、虚拟机、远程环境里,需要 gdbserver
  • 某些场景希望 gdbserver :0 自动选择端口,再由 MCP 解析实际端口连接

gdb_launch_gdbserver 会启动本地 gdbserver、解析 banner 里的端口、创建 GDB session 并连接上去。如果连接失败,会清理刚创建的 GDB 和 gdbserver,避免留下僵尸进程。

执行控制
#

包括 run、continue、interrupt、signal、step、next、detach、kill,以及 reverse debugging 相关工具。

Reverse debugging 对自动化分析挺有意义。漏洞触发后,agent 经常知道“这里坏了”,但不知道“什么时候开始坏的”。如果目标和 GDB 支持 process record,就可以从崩溃点往回走:

  • gdb_start_recording
  • gdb_reverse_continue
  • gdb_reverse_step
  • gdb_reverse_next
  • gdb_reverse_finish

这类能力手动用起来已经很有价值,放到 agent workflow 里会更有想象空间。

观察与分析
#

常用的包括:

  • gdb_context
  • gdb_backtrace
  • gdb_locals
  • gdb_eval_expression
  • gdb_registers
  • gdb_read_memory
  • gdb_read_c_string
  • gdb_disassemble
  • gdb_disassemble_current_frame
  • gdb_source
  • gdb_memory_mappings
  • gdb_shared_libraries
  • gdb_info_files

这些工具的原则是:能用更窄、更语义化的接口解决,就不要让 agent 自己拼 GDB 命令。例如读字符串就给 gdb_read_c_string,读内存就给 gdb_read_memory,看当前 PC 附近反汇编就给 gdb_disassemble_current_frame

这会牺牲一部分 GDB 的自由度,但换来的是更稳定的自动化路径。

安全边界
#

调试器天然危险。

GDB 可以 attach 到进程,可以读内存,可以改变量,可以写内存,可以调用 inferior 函数,甚至可以通过 CLI 执行 shell 命令。把这种能力交给 agent,如果没有边界,风险很高。

所以 gdb-mcp 默认区分几类能力:

  • Read:读取调试状态或目标状态
  • Execution:运行、继续、中断、attach、detach、kill
  • Mutation:修改调试器状态,例如断点、frame、路径
  • Unsafe:可能执行目标代码或修改 inferior 状态

默认情况下,以下能力是关掉的:

  • gdb_execute
  • gdb_call_function
  • gdb_set_variable
  • gdb_write_memory
  • gdb_breakpoint_commands

必须显式使用:

gdb-mcp --unsafe

或者:

GDB_MCP_ALLOW_UNSAFE=1 gdb-mcp

此外,安全模式下的表达式求值也做了保守过滤。比如 gdb_eval_expressiongdb_printgdb_set_watchpoint 会拒绝明显的函数调用、赋值、自增自减和控制字符。这个过滤不是 sandbox,不能当作对恶意输入的完整隔离,但它能防止 agent 在普通“读一下变量”的任务中顺手执行了 puts() 或改掉目标状态。

Lazy Proxy
#

0.3.0 里一个比较重要的变化是默认入口变成 lazy stdio proxy。

MCP client 启动时一般会枚举 server 的工具。如果 server 一启动就拉起完整 GDB backend,体验会比较差:用户可能只是打开了一个 Codex 会话,并不一定真的要调试程序,但这时后端已经启动、依赖已经检查、进程也挂着了。

现在的默认 gdb-mcp 命令只做一件事:立刻向 MCP client 暴露完整 tool schema,但真正的 backend 等到第一次调用 gdb_* 工具时才启动。

这样有几个好处:

  • MCP client 启动更快
  • 不调试时没有多余 GDB 后端进程
  • 仍然保留完整工具发现能力
  • 需要时可以切到独立 backend 或 HTTP backend

如果想显式运行后端,也可以用:

gdb-mcp-backend --transport streamable-http --host 127.0.0.1 --port 8000
GDB_MCP_BACKEND_URL=http://127.0.0.1:8000/mcp gdb-mcp

HTTP transport 默认也应该只绑定 loopback。调试器服务不应该裸露到不可信网络里。

目前的状态
#

截至现在,gdb-mcp 已经具备了我最开始想要的核心形态:

  • agent 可以创建多个隔离的 GDB session
  • 可以调试本地程序、attach 进程、加载 core、连接或启动 gdbserver
  • 可以用 context tool 低成本获得当前位置、栈和局部变量
  • 可以做常见的断点、watchpoint、寄存器、内存、反汇编、source 查看
  • 可以在显式 unsafe 模式下进行更强的修改和调用
  • 可以通过 lazy proxy 适配 Codex / Claude Code 这类 MCP client 的启动模型

当然它还远不是终点。后面我比较想继续做的方向包括:

  • 更好的 crash triage workflow,比如从 core 自动提取崩溃点、输入、调用链和关键内存
  • 更适合漏洞挖掘的 heap / allocator 辅助工具(pwndbg?)
  • 对常见 sanitizer、fuzzer 输出的整合
  • 更强的远程调试和容器隔离方案
  • 针对 agent 的调试策略 prompt / workflow 示例
  • 更细粒度的权限模型,而不是只有 safe / unsafe 两档

小结
#

我对这个项目的判断是:agent 做自动化逆向和漏洞挖掘,需要稳定的动态调试接口

纯静态分析当然有价值,但很多关键事实只存在于运行时。以前人类逆向工程师会通过 GDB 把这些事实一点点挖出来;如果 agent 要参与这个过程,它就需要一个能保持会话、能结构化观察、能控制风险、能压缩上下文的 GDB 接口。

gdb-mcp 目前就是朝这个方向做的一版实现。它还不复杂,也不完美,但已经能支撑实际的 agent 调试循环了。

后面应该会继续沿着“少一点裸命令,多一点面向分析任务的工具”这个方向迭代。

BeaCox
Author
BeaCox
Stay humble, remain critical.

Related

Sk2Decompile 论文阅读笔记
·1439 words·3 mins
论文 LLM Reverse Decompile RL SFT
第十七届全国大学生信息安全竞赛创新实践能力赛初赛 Writeup
·3264 words·7 mins
CTF PWN Crypto Misc Reverse
TBTL CTF 2024 WriteUp
·1874 words·4 mins
CTF PWN Crypto Misc Reverse