Ghidra MCP:用 REA 让 AI agent 无界面调用 Ghidra 反编译
本页目录
想让 AI agent 不开图形界面就用上 Ghidra,做法是让一个 MCP 服务器指向已解压的 Ghidra,由它替你跑无头分析器。第三方开源工具 REA 把这件事做成了 provider:设置 GHIDRA_INSTALL_DIR,用 rea doctor --provider ghidra 检查,然后在 CLI 或 MCP 里请求 decompile、xrefs、trace。REA 会把二进制导入一个临时 Ghidra 项目,等自动分析完成,再连同 Evidence 记录一起返回结果。它不会下载或安装 Ghidra。
本页实际跑了什么。 下面的内容都在 2026-11-02 实际跑过:REA 6.7.0 搭配 Ghidra 12.1.4,macOS 27.0.1(Apple silicon),OpenJDK 25.0.2,目标是用 Apple clang 21.0.0 编译的 -O0 样本 vault(SHA-256 5c3149…4ba4)。所有命令退出码都是 0。输出复制自这次运行,删减处标 …;本地路径缩写为 ./vault 和 ./ghidra_12.1.4_PUBLIC。nm/otool 给出的地址是标准答案,可以拿来核对 Ghidra。rea instructions 和 rea trace-native-values 这次没有跑。rea 命令取自 6.7.0 的 CLI 和 REA 的 Ghidra provider 文档。
环境要求
以下来自 REA 的安装文档,rea doctor 会逐项检查:
- Linux 或 macOS(x64 或 arm64)。Windows x64 是实验性的只读支持,只接受本地 NTFS 卷上的原生 x86/x86-64 PE 文件(Windows 说明)。
- 已解压的 Ghidra 12.1.x(桥接在 12.1.4 上验证过)。
- 64 位完整 JDK 21 或更新版本,要带
javac。 - 本机架构对应的 Ghidra 原生反编译器,位于
Ghidra/Features/Decompiler/os/<platform>/。12.1.4 的压缩包只带了linux_x86_64和win_x86_64两个,所以在 Mac(包括 Apple silicon)上要自己编译一次,做法见文末。 - Node.js 22.19+、24.11+ 或 26+。
配置并检查
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
export JAVA_HOME=/absolute/path/to/jdk-21 # optional if java/javac are on PATH
rea doctor --provider ghidra --json
一定要加 --provider ghidra:不带它的 rea doctor 会检查所有引擎和 agent 注册,会因为无关的项目报不健康。在我们的机器上编好 arm64 反编译器之后,限定范围的检查用时 5 秒,返回 "healthy": true,九条 scope_checks 全部通过(节选):
{
"healthy": true,
"environment_healthy": false,
"scope": {
"mode": "explicit",
"providers": [
"ghidra"
]
},
"scope_checks": [
…
{
"name": "ghidra-version",
"ok": true,
"classification": "healthy",
"detail": "12.1.4"
},
…
{
"name": "ghidra-native_decompiler",
"ok": true,
"classification": "healthy",
"detail": "./ghidra_12.1.4_PUBLIC/Ghidra/Features/Decompiler/os/mac_arm_64/decompile reports mach-o arm64, which matches darwin/arm64"
},
{
"name": "ghidra-java",
"ok": true,
"classification": "healthy",
"detail": "25.0.2; 64-bit; JDK; /opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home; accepted a 64-bit full JDK 21 or newer"
}
]
}
九条检查是 node、ghidra、ghidra-platform、ghidra-architecture、ghidra-installation、ghidra-version、ghidra-headless、ghidra-native_decompiler 和 ghidra-java。environment_healthy: false 来自另一个 informational_checks 列表:25 项里有 24 项失败,都是本文用不到的东西(Hopper、IDA、JADX、Kuna、REA skill 和 19 个 agent 注册)。注意原生反编译器检查会比对可执行文件架构和本机架构,所以拷一个 x86-64 的 decompile 过来,在 Apple silicon 上是过不了的。
Cursor 这类图形界面 agent 不会继承你在终端里 export 的变量。rea setup 会在你确认后把 GHIDRA_INSTALL_DIR/JAVA_HOME 写进各个 agent 的 REA 注册里,写完重启 agent。如果同时配置了 Hopper 或 IDA,每条命令加 --provider ghidra,或设置 REA_ANALYSIS_PROVIDER=ghidra,否则可能收到列出 details.candidate_ids 的 ambiguous 错误。
一个有标准答案的样本
vault.c 把 8 字节口令藏在一个字节混淆函数后面:
/* vault.c - a tiny practice target for REA + Ghidra */
#include <stdio.h>
#include <string.h>
#define KEY_LEN 8
/* "rea-demo" after mix_byte() */
static const unsigned char kExpected[KEY_LEN] = {
0x28, 0x42, 0x41, 0x80, 0x4A, 0x4E, 0x49, 0x4A
};
static unsigned char mix_byte(unsigned char c, int i) {
return (unsigned char)((c ^ 0x5A) + i * 3);
}
int check_passphrase(const char *input) {
if (strlen(input) != KEY_LEN)
return 0;
for (int i = 0; i < KEY_LEN; i++) {
if (mix_byte((unsigned char)input[i], i) != kExpected[i])
return 0;
}
return 1;
}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <passphrase>\n", argv[0]);
return 2;
}
if (check_passphrase(argv[1])) {
puts("Vault opened");
return 0;
}
puts("Wrong passphrase");
return 1;
}
$ cc -O0 -g -o vault vault.c
$ ./vault rea-demo; echo $?
Vault opened
0
$ ./vault wrong; echo $?
Wrong passphrase
1
这个 -O0 Mach-O 构建的标准答案,来自 nm 和 otool:
$ nm vault
00000001000004b0 T _check_passphrase
0000000100000660 s _kExpected
0000000100000590 T _main
000000010000055c t _mix_byte
U _puts
U _strlen
(4 lines trimmed)
$ otool -v -s __TEXT __cstring vault
0000000100000668 usage: %s <passphrase>\n
0000000100000680 Vault opened
000000010000068d Wrong passphrase
$ otool -tV vault | grep -B1 'Wrong passphrase'
0000000100000614 adrp x0, 0 ; 0x100000000
0000000100000618 add x0, x0, #0x68d ; literal pool for: "Wrong passphrase"
也就是说,正确的分析应该找到位于 0x10000068d 的 Wrong passphrase,它在 main 的 0x100000614/0x100000618 处被引用,紧跟在 0x1000005f4 的 bl _check_passphrase 之后;8 个期望字节在 __TEXT,__const 的 0x100000660。Mach-O 上 C 符号带前导下划线,名字照工具返回的原样使用。
命令一览
每次 rea CLI 调用都是独立进程:导入、自动分析、回答一个问题、关闭。走 MCP 时 agent 只打开一次(open_binary),后续问题复用已分析的程序,所以只有第一个问题慢。
BIN="$PWD/vault"
rea analyze "$BIN" --provider ghidra --json # overview (MCP: binary_overview)
rea search "$BIN" "passphrase" --provider ghidra --json
rea search "$BIN" check --kind procedures --provider ghidra --json
rea decompile "$BIN" _check_passphrase --provider ghidra --json
rea function "$BIN" _check_passphrase --provider ghidra --json
rea instructions "$BIN" _check_passphrase --provider ghidra --json
rea xrefs "$BIN" 0x10000068d --provider ghidra --json
rea trace "$BIN" "Wrong passphrase" --provider ghidra --json
rea trace-native-values "$BIN" _check_passphrase --provider ghidra --json
下一节给出除 instructions 和 trace-native-values 之外每条命令的实际结果。
按 provider 文档,各自的用途:
search默认按字面、不区分大小写搜字符串;--kind procedures搜函数名,--mode regex接受 Java 正则。只搜 Ghidra 定义为字符串数据的内容,打包或未定义的字面量(Rust 字符串是已知情况)可能搜不到,所以搜不到不代表不存在。decompile返回单个函数的伪代码;function额外给出汇编、调用方与被调用方、引用、基本块和有界的 p-code 值流;instructions不调用反编译器,直接列原始指令。0x前缀表示地址,否则精确的符号名优先。xrefs只返回来源地址列表。它自己的limitations写明“does not expose their kinds; procedure_references and analyze_function preserve available kind metadata”,即引用类型(call、jump、数据读、写、computed 等)要用procedure_references或analyze_function才能看到。合成的入口点引用会被省略,没有已知目标的计算调用保持 unknown,不会乱猜。trace(MCP 里叫trace_feature)按字面匹配查询,沿 xrefs 找到所在函数,并列出解析不了的部分。trace-native-values是有界的静态数据流追踪,别名效应和持久状态仍是未知。- 以短横线开头的参数要用
=形式,比如--procedure=-name、--query=-text。
Ghidra 对 vault 的实际输出
每次 CLI 调用都是冷启动(导入、自动分析、启动桥接),耗时主要花在启动上,而不是问题本身。这台机器上:analyze 18 秒,两次 search 各 18 秒,decompile 21 秒,function 49 秒,xrefs 0x10000068d 76 秒,xrefs _check_passphrase 68 秒,trace 69 秒。即使是很小的二进制,每次 xrefs 或 trace 也要预留一分钟左右。
analyze(binary_overview,只保留计数):
{
"document": "target-5c3149797769.bin",
"image_base": "0x100000000",
…
"segment_count": 11,
"procedure_count": 9,
"string_count": 30
}
search "passphrase" 找到了 otool 地址上的两个程序字符串,另外还有两个位于 __LINKEDIT(同一份概览里它从 0x100008000 开始)的符号名字符串:
{
"operation": "search_strings",
"parameters": {
"pattern": "passphrase",
"mode": "literal",
"case_sensitive": false
},
"normalized_result": [
{
"address": "0x100000668",
"value": "usage: %s <passphrase>\n"
},
{
"address": "0x10000068d",
"value": "Wrong passphrase"
},
{
"address": "0x1000080b5",
"value": "check_passphrase"
},
{
"address": "0x1000082be",
"value": "_check_passphrase"
}
]
}
search check --kind procedures 只有一个结果 {"address": "0x1000004b0", "value": "_check_passphrase"},和 nm 一致。
decompile _check_passphrase(normalized_result,confidence: "observed"):
undefined4 _check_passphrase(char *param_1)
{
uint uVar1;
size_t sVar2;
int local_24;
undefined4 local_14;
sVar2 = _strlen(param_1);
if (sVar2 == 8) {
for (local_24 = 0; local_24 < 8; local_24 = local_24 + 1) {
uVar1 = _mix_byte(param_1[local_24],local_24);
if (uVar1 != (byte)(&_kExpected)[local_24]) {
return 0;
}
}
local_14 = 1;
}
else {
local_14 = 0;
}
return local_14;
}
结构和源码一致:长度和 8 比较、循环调用 _mix_byte、逐字节和 _kExpected 比较。但它不会给出那 8 个字节本身,所以下面给 agent 的提示要求用 read_bytes。
function _check_passphrase 补上调用关系:调用方只有一个,0x100000590 的 entry;被调用方两个,0x10000055c 的 _mix_byte 和 0x10000063c 的 _strlen thunk。entry 是 Ghidra 给同一地址起的名字,nm 里那里是 _main。
xrefs(两次调用各自的 normalized_result):
$ rea xrefs ./vault 0x10000068d --provider ghidra --json
["0x100000618"]
$ rea xrefs ./vault _check_passphrase --provider ghidra --json
["0x1000005f4","0x1000080d0"]
0x100000618 就是 otool 里的 add x0, x0, #0x68d,0x1000005f4 是 bl _check_passphrase。0x1000080d0 是来自 __LINKEDIT 的数据引用,不是第二个调用方。
trace "Wrong passphrase"(trace_feature,函数细节已删减)一次调用就把字符串和所在函数对上了:
{
"query": "Wrong passphrase",
"search_mode": "literal",
"matches": [
{
"type": "string",
"address": "0x10000068d",
"value": "Wrong passphrase"
}
],
"references": [
{
"target_address": "0x10000068d",
"source_address": "0x100000618",
"containing_procedure": {
"query_address": "0x100000618",
"found": true,
"procedure": {
"address": "0x100000590",
"name": "entry",
…
"body": {
"ranges": [
{
"start": "0x100000590",
"end": "0x10000063b"
}
],
"total_bytes": 172
…
}
}
}
}
],
"truncated": false,
"residual_unknowns": []
}
所有地址都和上面 nm/otool 的标准答案一致,residual_unknowns 为空。
同一个二进制直接跑 analyzeHeadless。 作为交叉核对,我们直接运行 Ghidra 无头分析器,配一个我们自己写的小后置脚本(VaultEvidence.java,不属于 REA 也不属于 Ghidra)。导入、分析加脚本共 27 秒。应用 DWARF 后,Ghidra 用调试信息给函数和参数命名,而上面 REA 的结果用的是 Mach-O 符号名和 param_1:
=== DECOMPILE check_passphrase @ 1000004b0 ===
int check_passphrase(char *input)
…
/* Unresolved local var: int i@[DW_OP_breg31(sp): +12] */
for (local_24 = 0; local_24 < 8; local_24 = local_24 + 1) {
uVar1 = mix_byte(input[local_24],local_24);
if (uVar1 != kExpected[local_24]) {
…
=== XREFS to "Wrong passphrase" @ 10000068d ===
100000618 PARAM in main
=== XREFS to check_passphrase @ 1000004b0 ===
Entry Point EXTERNAL in <no function>
1000080d0 DATA in <no function>
1000005f4 UNCONDITIONAL_CALL in main
(已删去 INFO VaultEvidence.java> 前缀。)引用和前面一样,这次带上了类型;Entry Point 那条就是 REA 省略的合成引用。无头日志在导入 DWARF 时还会打出这一段:
ERROR DWARF un-recoverable expressions: (DWARFImportSummary)
ERROR DW_OP_fbreg -> 3 (DWARFImportSummary)
ERROR DW_OP_breg31 -> 3 (DWARFImportSummary)
ERROR DW_OP_reg31 -> 1 (DWARFImportSummary)
ERROR DW_OP_reg29 -> 2 (DWARFImportSummary)
这是无害的噪音:Ghidra 没能映射部分局部变量的位置(所以才有 Unresolved local var 注释),同一份日志里仍然报告 REPORT: Analysis succeeded。
给 agent 的话,一条提示就够了。最后一句最重要,因为 kExpected 在数据里,不在代码里:
Use REA with the Ghidra provider on /absolute/path/to/vault. Find what prints
"Wrong passphrase", decompile the check that decides it, and work out which input
passes. Read the data bytes it compares against (read_bytes) instead of guessing,
and cite the addresses for each step.
对这些字节反推 mix_byte 得到 rea-demo,可以用 ./vault rea-demo 验证。agent 做的重命名和注释(annotate_native_function)只存在于临时 Ghidra 数据库里,可执行文件本身不会被改。
-O2 反而让这个样本更简单
我们原以为优化后的构建会藏得更深,结果正相反。用 cc -O2 编译后,nm 里已经没有 _mix_byte 和 _kExpected:编译器在编译期就把整个校验反算出来了,直接逐字节和明文比较:
$ otool -tV vault-o2 | sed -n '/_check_passphrase:/,/ret/p'
0000000100000470 bl 0x1000005f0 ; symbol stub for: _strlen
0000000100000474 cmp x0, #0x8
000000010000047c ldrb w8, [x19]
0000000100000480 cmp w8, #0x72
0000000100000494 ldrb w8, [x19, #0x2]
0000000100000498 cmp w8, #0x61
(b.ne branches and the other six byte compares trimmed: 0x65 0x2d 0x64 0x65 0x6d 0x6f)
0x72 0x65 0x61 0x2d 0x64 0x65 0x6d 0x6f 就是 ASCII 的 rea-demo。任何反汇编器都能看到,反编译器在这里帮不上什么忙。这种期望值是常量的校验不适合用来测反编译器,换一个期望值在运行时才算出来的目标。
脚本化运行用快照
每次 CLI 调用都会重新导入,所以可以加 --snapshot /absolute/path/vault.json 缓存精确结果:字节、参数和 provider 设置都相同的同一个查询,REA 直接返回保存的答案,不再启动 Ghidra。它不是保存下来的 Ghidra 数据库,新问题仍会启动 provider。二进制补丁比对用它来比较两个构建。
排错(来自 REA 文档)
macOS 上报 native_decompiler 缺失。 Ghidra 12.1.4 的发行包没有预编译的 Apple silicon 反编译器(Decompiler/os/ 下只有 linux_x86_64 和 win_x86_64),所以在 arm64 macOS 上要自己编译一次,需要 Xcode Command Line Tools、make 和 bison。Apple silicon 上:
cd "$GHIDRA_INSTALL_DIR/Ghidra/Features/Decompiler/src/decompile/cpp"
make ghidra_opt ARCH_TYPE="-arch arm64" OSDIR=mac_arm_64
decompiler_dir="$GHIDRA_INSTALL_DIR/Ghidra/Features/Decompiler/os/mac_arm_64"
mkdir -p "$decompiler_dir"
test ! -e "$decompiler_dir/decompile" && test ! -L "$decompiler_dir/decompile" &&
install -m 755 ghidra_opt "$decompiler_dir/decompile"
本文用的就是这次编译;装好后 file 显示 decompile 为 Mach-O 64-bit executable arm64,rea doctor 的 ghidra-native_decompiler 检查通过。Intel Mac 用普通的 make ghidra_opt 和 mac_x86_64 目录。
agent 第一个问题就超时。 REA 给 Ghidra 的导入、自动分析和桥接启动留了 330,000 毫秒,但 MCP 客户端可能更早取消这次工具调用。要同时调大 REA 服务器条目里的 REA_GHIDRA_STARTUP_TIMEOUT_MS 和客户端自己的工具超时,两者互不影响。time rea analyze "$BIN" --provider ghidra --json > /dev/null 能在没有客户端限时的情况下测出真实冷启动时间(本机 analyze 18 秒,xrefs 和 trace 约 70 秒)。客户端中途放弃后,先 close_binary,再用 provider_id: "ghidra" 重新 open_binary,然后重试(详情)。
内存不够。 JVM 堆大小取自 GHIDRA_HEADLESS_MAXMEM(默认 2G,文档称小目标上 512M 可用)。它必须设在 REA 服务器的环境里,而且是唯一的内存开关:REA 会清掉 _JAVA_OPTIONS、JAVA_TOOL_OPTIONS、JDK_JAVA_OPTIONS 和 GHIDRA_JAVA_OPTIONS。
语言识别错误,或代码只能通过计算跳转到达。 设置 REA_GHIDRA_LANGUAGE_ID(例如 x86:LE:32:default)。漏掉的代码可以用 REA_GHIDRA_SEED_FILE 指向一个绝对路径的制表符分隔文件,每行 address<TAB>kind[<TAB>name](kind 为 function、code 或 label);REA 会记录它的 SHA-256,种子地址写错时会给出 function_failed 计数。
artifact_changed。 会话打开期间重新编译了二进制,重新打开即可。
想比较 REA 和其他 Ghidra、IDA 桥接,参见 REA 与 GhidraMCP、IDA Pro MCP 对比。