MCP(Model Context Protocol)是当下 AI 工具链的热门话题。当 unidbg 社区引入 MCP 功能后,不少人觉得这是个好方向——让 AI 直接操作调试器,下断点、读内存、单步执行,听起来很美好。
事实上,在 unidbg 官方加入 MCP 之前,笔者就已经尝试过给 unidbg 做 MCP 接入。结果发现问题比想象中多得多,最终放弃了这条路线,转而采用 Claude Code 的 Skill(自定义命令)机制来辅助逆向分析。
本文记录这个过程中的思考和踩坑,探讨为什么 unidbg 这类模拟器场景下,MCP 可能并不是最佳选择。
设想是这样的:unidbg 启动后暴露一组 MCP 工具,AI 可以:
逆向工程师只需要对话:"帮我看看这个函数第三个参数指向什么结构体",AI 自动下断点、读数据、推理分析。
后来 unidbg 官方确实做了这件事——一个完全自研的 HTTP+SSE 服务器(约 2600 行代码),暴露了 50+ 个调试工具:
实现得很完整,本质上是一个 AI 驱动的 GDB。
但当我们把它放到实际的逆向分析工作流中一评估,就发现了一系列结构性问题。
用过 unidbg 的人都知道,让一个 SO 跑起来最痛苦的不是逆向分析本身,而是补环境——手动模拟 SO 依赖的所有 Java/Android 环境。
unidbg 不是真机,它只模拟 ARM CPU。当 native 代码通过 JNI 回调 Java 层时,unidbg 会抛出异常告诉你:"这个 JNI 方法我不认识,你得自己写 mock。"
这是一个典型的循环:
一个典型的网络请求签名 SO,最终需要 mock 的 JNI 接口清单可能是这样的:
每个 mock 还不是简单返回 null 就行。比如 SharedPreferences->getString() 需要根据 key 返回特定的值(密钥、配置等),Headers->name()/value() 需要维护一个完整的请求头列表并按索引返回。有些 mock 之间还有状态依赖——Buffer->writeString() 写入的数据,后面 Buffer->readByteArray() 要能读出来。
MCP 的工具集全部是运行时调试工具——read_memory、set_register、add_breakpoint 等等。但补环境需要的是:
1. 修改 Java 源码
MCP 没有任何工具能编辑 .java 文件。当 SO 崩溃在一个未实现的 JNI 方法上时,你需要在继承 AbstractJni 的测试类里加一个 case 分支。这是纯粹的代码编辑操作,不是调试操作。
2. 触发编译
改完代码后需要 mvn compile 或 IDE 增量编译。MCP 不接入构建系统。
3. 读取编译/运行报错
unidbg 的报错信息打印在 Java 的 stdout/stderr 上,不是在调试器控制台里。MCP 看不到这些输出。
4. 理解 JNI 语义
补环境不仅是"返回一个值",还需要理解 SO 期望的语义。比如:
这需要理解 OkHttp 的 API 设计,不是简单的内存读写能解决的。
5. 反复重启
补环境的循环是"改一个 mock → 重启整个模拟器 → 看下一个报错"。每次都是完整的进程生命周期。MCP 连接的是一个运行中的调试会话,一旦进程结束,MCP 会话也就断了。
需要的是一个能读报错→改代码→触发编译→重新运行的循环。这恰好是 AI 代码编辑工具(Claude Code、Cursor 等)的核心能力,而不是 MCP 调试工具的能力。
SO 跑通后进入逆向分析阶段。核心工作是"下断点捕获数据→验证算法假设"。
但 hook 点经常选错,这是常态:
最终写出来的代码可能是这样的:
或者更常见的:
unidbg 模拟器每次运行都是从头开始:初始化 → 加载 SO → JNI_OnLoad → 调用目标函数 → 结束。整个生命周期可能只有几秒。错过一个断点,就得重头再来。但 MCP 没有"重启模拟器"的能力。
而代码是声明式的——callCount == 0 写一次,改个数字重跑 3 秒搞定。
每次迭代成本极低,而且之前所有的 mock 环境、初始化逻辑都在代码里,不会丢失。
逆向不是"看数据",而是"提出假设→验证→修正"的循环。
比如你怀疑 SO 里有一段魔改 AES,验证方式是:
然后还有 InvMixColumns、AES-CBC 解密、PKCS#7 验证... 总计 200 行纯算法代码。
AI 通过 read_memory 拿到轮密钥的 hex dump 后,需要在"脑中"做 GF(2^8) 乘法、矩阵运算、S-box 查表... 这不现实。最终 AI 还是会说:"我帮你写个 Python 脚本验证吧"——又绕回了代码生成的路子。
MCP 工具集里没有"执行任意计算"的工具。它能读数据,但不能处理数据。
unidbg 的 trace 输出量极大。一个包含自定义虚拟机的 SO,一次函数调用可能产生数十万条指令。
MCP 的 trace_code 实现将每条指令都作为一个 JSON 事件推入无界队列:
然后 poll_events 一次性全部拼成字符串返回:
10 万条指令 × 每条 ~300 字节 = 30MB JSON。两个问题:
写文件 + 离线过滤,天然支持任意大小的 trace 数据。
Claude Code 的 Skill 是一种自定义命令机制。在项目目录下创建 .claude/skills/<name>/SKILL.md,定义一个 prompt 模板。使用时输入 /skill-name 触发 AI 按模板生成代码。
核心区别:MCP 让 AI 操作调试器,Skill 让 AI 写代码。
/hook [地址] [描述] — 断点捕获代码生成
/trace [范围] [关注点] — Trace 捕获 + 分析脚本
/verify [算法假设] — 算法验证脚本
/jni [报错信息] — JNI Mock 自动补全
以"捕获 AES 轮密钥"为例:
MCP 方式(交互 8 轮,最终还是要写代码):
Skill 方式(1 轮):
MCP 的产出是一次性操作——"在某地址读 X2 寄存器"执行完就消失了。
代码的产出是永久资产——一个验证脚本不仅是工具,更是算法还原的完整文档。一个典型项目可能积累近 10 个 Java 调试脚本和 30+ 个 Python 验证脚本,每个都能独立重跑,合在一起就是整个算法还原过程的知识沉淀。
代码是声明式的——"我要在第 1 次调用时读 X2 指向的 0xF0 字节"写一次就行,反复执行结果一致。
MCP 是命令式的——每次都要手动走一遍"下断点→continue→poll→读数据"流程。走错一步就废了,而且没有重来的机会。
和 GDB/Frida/IDA 不同,unidbg 是模拟器,每次运行都是从头开始。MCP 适合的是长生命周期进程调试——目标进程持续存在,断点错了可以改,状态随时可查。unidbg 的"短生命周期、反复重跑"特性,天然适合代码驱动而不是交互驱动。
公平起见,MCP 并非一无是处。它适合:
unidbg 逆向的核心循环是"写代码→跑→看结果→改代码",从最初的补环境到最终的算法验证,每一步都是代码驱动。AI 最大的价值不是替你操作调试器,而是替你写代码。
对于 unidbg 这种"短生命周期、需要补环境、反复重跑、需要算法验证"的场景,让 AI 生成代码(Skill)比让 AI 操作调试器(MCP)更合适。
把 AI 当作一个极其高效的代码生成器,远比把它当作一个远程调试操作员更有价值。
运行 → 崩溃: "callObjectMethodV not implemented: okhttp3/Interceptor$Chain->request()"
→ 补上这个 mock → 重新编译运行
→ 崩溃: "callObjectMethodV not implemented: okhttp3/Request->url()"
→ 补上这个 mock → 重新编译运行
→ 崩溃: "callObjectMethodV not implemented: okhttp3/HttpUrl->toString()"
→ ...(重复 20-30 次)
运行 → 崩溃: "callObjectMethodV not implemented: okhttp3/Interceptor$Chain->request()"
→ 补上这个 mock → 重新编译运行
→ 崩溃: "callObjectMethodV not implemented: okhttp3/Request->url()"
→ 补上这个 mock → 重新编译运行
→ 崩溃: "callObjectMethodV not implemented: okhttp3/HttpUrl->toString()"
→ ...(重复 20-30 次)
"okhttp3/Interceptor$Chain->request()"
"okhttp3/Request->url()"
"okhttp3/Request->method()"
"okhttp3/Request->headers()"
"okhttp3/Request->body()"
"okhttp3/Request->newBuilder()"
"okhttp3/Request$Builder->addHeader()"
"okhttp3/Request$Builder->build()"
"okhttp3/HttpUrl->toString()"
"okhttp3/HttpUrl->encodedPath()"
"okhttp3/HttpUrl->encodedQuery()"
"okhttp3/Headers->size()"
"okhttp3/Headers->name()"
"okhttp3/Headers->value()"
"okio/Buffer-><init>()"
"okio/Buffer->writeString()"
"okio/Buffer->readByteArray()"
"okio/Buffer->clone()"
"okio/Buffer->read()"
"android/content/Context->getSharedPreferences()"
"android/content/SharedPreferences->getString()"
"/proc/self/status" → 需要伪造 TracerPid: 0(反调试)
"/proc/self/cmdline" → 需要返回正确的包名
"okhttp3/Interceptor$Chain->request()"
"okhttp3/Request->url()"
"okhttp3/Request->method()"
"okhttp3/Request->headers()"
"okhttp3/Request->body()"
"okhttp3/Request->newBuilder()"
"okhttp3/Request$Builder->addHeader()"
"okhttp3/Request$Builder->build()"
"okhttp3/HttpUrl->toString()"
"okhttp3/HttpUrl->encodedPath()"
"okhttp3/HttpUrl->encodedQuery()"
"okhttp3/Headers->size()"
"okhttp3/Headers->name()"
"okhttp3/Headers->value()"
"okio/Buffer-><init>()"
"okio/Buffer->writeString()"
"okio/Buffer->readByteArray()"
"okio/Buffer->clone()"
"okio/Buffer->read()"
"android/content/Context->getSharedPreferences()"
"android/content/SharedPreferences->getString()"
"/proc/self/status" → 需要伪造 TracerPid: 0(反调试)
"/proc/self/cmdline" → 需要返回正确的包名
case "okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;":
return responseClass.newObject(null);
case "okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;":
return responseClass.newObject(null);
debugger.addBreakPoint(module.base + 0xABCDE, new BreakPointCallback() {
int callCount = 0;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
if (callCount == 0) {
RegisterContext ctx = emulator.getContext();
long x2 = ctx.getLongArg(2);
byte[] keyCtx = emulator.getBackend().mem_read(x2, 0xF0);
}
callCount++;
return true;
}
});
debugger.addBreakPoint(module.base + 0xABCDE, new BreakPointCallback() {
int callCount = 0;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
if (callCount == 0) {
RegisterContext ctx = emulator.getContext();
long x2 = ctx.getLongArg(2);
byte[] keyCtx = emulator.getBackend().mem_read(x2, 0xF0);
}
callCount++;
return true;
}
});
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!