用 AI agent 做二进制补丁对比:REA 比较两个版本的函数差异
本页目录
结论先行: REA 6.4.0 没有“把两个二进制整体一比、列出所有改动函数”的功能。它提供的是 MCP 上的函数级对比:由你(或 agent)挑出一对函数,分别在两个构建上跑 analyze_function,再把两条 Evidence 交给 compare_functions。结果分 8 个维度(identity、pseudocode、assembly、comments、calls、references、strings_names、cfg),每个维度是 unchanged、changed 或 unknown。compare_artifacts 在文件清单层面做同样的事;find_changed_behavior 把多份对比结果汇总成一份报告,并把静态候选和运行时观测分开。推荐的分工是:用项目的源码 diff 决定比哪些函数,用 REA 确认发布出来的代码里到底改了什么,unknown 一律当作待解决的问题,而不是“没变”。
这篇写给防守方和维护者:想知道一个安全版本在编译产物里改了什么,比如确认补丁构建里确实带上了修复、评估自己产品依赖的某个库的升级,或者核实 backport 有没有落地。它是 REA 的 Evidence、快照与版本对比 的函数级续篇。快照、证据包校验和 rea compare 在那篇里已经讲过,这里只简单带过。
示例目标是 zlib 1.2.11 和 1.2.12,都从源码自己编译。zlib 1.2.12 包含 CVE-2018-25032 的修复,这是 zlib 项目公开披露的 deflate 内存破坏问题。本文目标是防御性的:定位修复并描述它,不涉及如何触发漏洞。
前置条件
- REA 6.4.0 已接入 Claude Code、Cursor 或 Codex 等 MCP 客户端,见 安装 REA 并接入 Claude Code / Cursor / Codex。
- 已把 Ghidra 配置为 REA 的原生 provider,见 REA + Ghidra 免费原生二进制分析。两个版本必须用同一个 provider。provider 不同时,REA 不会把两边的伪代码和汇编文本当成可比的(后面会细说)。
- C 编译工具链、
git和jq。 - 留出 Ghidra 首次分析每个库的时间。之后有快照,就快了。如果分析较大的库时首次导入超时或内存不够,参考《Ghidra 无头 MCP 进阶》调整启动超时、堆内存,以及关闭后重开的恢复方法。
本文只对你自己编译的软件做静态分析。如果后续要在测试环境里运行任一版本,只能在你有权使用的系统上做,并先读 DISCLAIMER.md。
第 1 步:用同一套参数编译两个版本
补丁对比找的是两个构建之间的差异,而构建参数不一致本身就会制造差异。所以两个版本要用同一个编译器、同一组参数、同样的安装目录结构,并且不要 strip:
git clone https://github.com/madler/zlib.git /work/zlib-src
for v in 1.2.11 1.2.12; do
git -C /work/zlib-src worktree add /work/zlib-$v v$v
(cd /work/zlib-$v && CFLAGS="-O2 -g" ./configure --prefix=/work/zlib-$v/install && make && make install)
done
每个选择的理由:
- 参数一致。
-O级别不同,内联和基本块布局就会不同,结果会在 CFG 和汇编里冒出一堆和补丁无关的变化。 - 保留符号。 REA 按名字配对函数。它的对比代码会把
FUN_00102a40、sub_401000、fcn.00401000这类由地址生成的名字视为不稳定;只要任一侧是这种名字,identity 和 calls 维度就返回unknown,不去猜。带符号的构建能给出真实函数名,compress_block这样的 static 函数也有名字,除非被编译器内联掉了。 - 安装目录。
install/lib放共享库,install/include放头文件。每个版本一个目录,第 6 步可以直接拿来做清单对比。
第 2 步:用源码 diff 确定候选函数
zlib 是开源的,不需要在整个二进制里大海捞针,从提交历史入手:
git -C /work/zlib-src diff --stat v1.2.11 v1.2.12 -- '*.c' '*.h'
git -C /work/zlib-src log --oneline v1.2.11..v1.2.12 -- deflate.c deflate.h trees.c
你会发现 1.2.12 不是只改一行的补丁版本,CVE 修复之外还带了好几年积累的其他改动。这很常见,也正是整体 diff 噪声大的主要原因:你要的是修复涉及的函数,而不是所有动过的地方。CVE-2018-25032 的修复是 zlib 的提交 5c44459(“Fix a bug that can crash deflate on some input when using Z_FIXED”),它把原来相互重叠的 d_buf/l_buf 换成了独立的 sym_buf。在 deflate.c 里,这个提交改到了 deflateInit2_、deflatePrime、deflateCopy,以及 deflate_fast、deflate_slow、deflate_rle、deflate_huff 这几个压缩策略函数;在 trees.c 里改到了 init_block、_tr_tally 和 compress_block(_tr_flush_block 那一处只改了调试输出)。deflateInit2_ 和 compress_block 适合作为首批候选。其余函数里有好几个是 local(static)函数,而且非调试构建会用 deflate.h 里的 _tr_tally_lit/_tr_tally_dist 宏代替对 _tr_tally 的调用,所以其中一些可能被内联掉。请自己读一遍提交的 diff,以实际提交为准,不要只依赖这份清单。
把候选列表写下来。第 4 步让 agent 逐个处理,固定的列表能防止它跑偏。
第 3 步:用 CLI 缓存每个函数的 Evidence(可选)
完全可以让 agent 在 MCP 会话里做全部分析。不过要反复跑的话,建议先用 CLI 建快照。rea function 是 analyze_function 的 CLI 形式,支持 --snapshot:
mkdir -p /work/diff
for v in 1.2.11 1.2.12; do
for fn in deflateInit2_ compress_block; do
rea function /work/zlib-$v/install/lib/libz.so.$v "$fn" --provider ghidra \
--snapshot /work/diff/zlib-$v.snapshot.json --json > /work/diff/$fn.$v.json
done
done
每个输出文件都是一条完整的 analyze_function Evidence,绑定到库文件的 SHA-256。每个版本单独一个快照文件,原因见 Evidence 那篇;那篇也讲了怎么用 jq '.evidence_bundle' 把快照里的证据包取出来归档。
继续之前先做个检查:
jq '{op: .operation, name: .normalized_result.procedure.name, limits: .normalized_result.limitations}' \
/work/diff/compress_block.1.2.12.json
如果 name 是 FUN_… 这类地址名,或者找不到这个函数,多半是这个构建里它被内联了。记下来,跳过即可。不要因为地址看着接近,就拿另一个函数来凑对。
第 4 步:让 agent 逐对比较函数
在 6.4.0 里,compare_functions、compare_artifacts 和 find_changed_behavior 都只能通过 MCP 调用。它们在发布包的 MCP_TOOLS_WITHOUT_DEDICATED_CLI 列表里,所以没有 rea compare-functions 这样的命令。给 agent 的提示可以这样写:
Using REA over MCP, compare these functions between the two zlib builds:
deflateInit2_, compress_block.
For each function:
1. open_binary /work/zlib-1.2.11/install/lib/libz.so.1.2.11 with provider ghidra
(snapshot_path /work/diff/zlib-1.2.11.snapshot.json), run analyze_function,
keep the Evidence.
2. Do the same for /work/zlib-1.2.12/install/lib/libz.so.1.2.12.
3. Pass both complete Evidence records to compare_functions as left and right.
4. Report status, function_match, and every dimension whose status is changed or
unknown, with its limitations and the two evidence_links.
Do not merge unknown into unchanged. Do not compare a function against a
differently named one unless I ask.
几个容易出错的地方:
- 输入是完整的 Evidence 记录,不是 ID。
compare_functions的left和right都必须是完整的analyze_functionEvidence。MCP 契约的说法是:把返回的 Evidence 原样传过去。如果某个函数的 Evidence 超过 MCP 响应预算,REA 会把它留在会话里,并给 agent 一个inspect_analysis_view引用,用来分页读取。但对比本身仍然需要完整记录。 open_binary带snapshot_path会导入 CLI 生成的快照,完全命中时不用再启动 Ghidra。命中要求字节、操作、参数、provider 和分析配置全部一致。- 配对是精确匹配。 两边名字相同且都不是地址名时,
function_match为matched,方式为symbol;名字不同是mismatched;两边都是同一个地址名则是ambiguous,而 ambiguous 会让整体状态变成unknown。
第 5 步:逐个维度读结果
整体 status 的规则很严格:任一维度是 unknown(或配对是 ambiguous)就是 unknown;任一维度 changed 就是 changed;8 个维度全一致才是 unchanged。真正有用的信息都在各维度那几行里:
| 维度 | 比较内容 | 常见的 unknown 原因 |
|---|---|---|
identity |
名字、签名、局部变量描述 | 任一侧是地址名 |
pseudocode |
provider 伪代码文本,做有上限的逐行 diff | provider 不同,或 diff 超出时间/规模上限 |
assembly |
provider 汇编文本 | provider 不同。即使 provider 相同,也没有重定位归一化,地址平移会显示为变化 |
comments |
按函数内偏移比较注释 | 注释地址无法归一化 |
calls |
调用者和被调用者的名字 | 调用者或被调用者是地址名 |
references |
入/出引用的端点和类型 | provider 没有提供引用类型 |
strings_names |
按偏移比较引用到的字符串和名字 | 源地址无法归一化 |
cfg |
基本块大小和后继拓扑(地址已归一化) | 基本块地址无法归一化 |
文本类维度会给出 text_delta,包含 added_lines、removed_lines 和 hunks。如果两边用了不同的 provider,changed 维度的 conclusion_kind 会是 "contradiction" 而不是 derived_relationship,结果的 limitations 也会写明没有把两种 provider 的表示等同起来。这又是一个坚持用同一 provider 的理由。
6.4.0 的一个已知局限。 在 6.4.0 发布包里,calls 维度只比较报告出来的调用者和被调用者名字列表,不检查列表是否完整。函数档案里可能有 unresolved_calls(找不到目标的调用点)和 limitations,但在 6.4.0 中,calls: unchanged 仍可能建立在不完整的覆盖之上。这就是 issue #2222(“compare_functions treats incomplete call coverage as unchanged”):它指出,两侧调用者/被调用者列表都为空时,即使其中一侧的调用图是 unknown,也会得出 calls: unchanged,而且 unresolved_calls 里的调用点会被忽略。该 issue 已于 2026-10-12 关闭,也就是 6.4.0 发布的第二天;修复合入 main,把缺失、部分、未知或未解析的调用覆盖一律视为不足以证明调用集合相同。这个修复不在 6.4.0 里。升级之前,让 agent 在采信 calls 为 unchanged 之前,先检查两侧输入的 normalized_result.unresolved_calls 和 limitations。
判读一个修复时,可以参考:
- 源码 diff 点名的函数里,
pseudocode和cfg都变了:说明补丁确实进了二进制。 pseudocode变了、cfg没变:通常是某个常量或尺寸计算变了,去读伪代码的 hunk。- 候选函数的所有维度全是
unchanged:修复可能在被内联的辅助函数里,或者在你没列出的函数里,也可能构建根本没带上改动。不要凭一个“没动静”的函数就下结论说“没打补丁”。
第 6 步:用 compare_artifacts 做清单级对比
函数级工作之前或之后,都可以先比一比两个构建实际安装了什么。CLI 可以给目录做清单:
rea inspect-artifact /work/zlib-1.2.11/install --json > /work/diff/inv-1.2.11.json
rea inspect-artifact /work/zlib-1.2.12/install --json > /work/diff/inv-1.2.12.json
jq '.normalized_result.substeps[0].evidence.operation' /work/diff/inv-1.2.12.json
compare_artifacts 要的是嵌在每个 inspect_artifact 结果里的清单 Evidence(位于 normalized_result.substeps[0].evidence),而不是外层那条记录。在 MCP 里,agent 可以自己对每个目录做 open_binary 和 inspect_artifact,再调用 compare_artifacts。每条变化按逻辑路径归类为 added、removed、changed、unknown 或 contradiction,附带 content、size、metadata 等维度,并同时引用两侧输入。覆盖不完整时给出 unknown 或 truncated,绝不会算作相同。对 zlib 来说,你会看到带版本号的 .so 文件名变了,这在意料之中。换成更大的产品,这一步常常能在深入函数之前,先发现新打包进来的辅助程序或改过的配置文件。
第 7 步:汇总并写变更摘要
把对比得到的 Evidence(compare_functions 和 compare_artifacts 的结果)作为 comparisons 传给 find_changed_behavior。它输出的每条 finding 带有 scope(这里都是 static_candidate)、分类(derived_relationship、contradiction、unresolved_branch)和来源对比的 ID。输入里没有进程对比时,behavior_status 保持 unknown,limitations 里也会写明:静态差异只是行为候选,不是运行时观测。你的报告里也应该保留这句话。
然后让 agent 写摘要,规则只有一条:每句话都要引用一个 evidence_id。例如:
Write a change summary for zlib 1.2.11 -> 1.2.12 limited to the CVE-2018-25032
fix. For each claim, cite the compare_functions evidence_id and the two
analyze_function evidence_ids it links. List every unknown dimension separately
under "Open questions" with its limitation text. Do not describe how to trigger
the bug.
最后,在 close_binary 之前用绝对路径调用 export_evidence_bundle,把证据包和摘要放在一起归档。Evidence 那篇 里的规范化导出和对比步骤可以原样使用。如果有需要跨几轮审查跟踪的待解问题,可以用 record_unknown 记进同一个证据包。
和 BinDiff、Diaphora、ghidriff 的区别
专门的 diff 工具会用结构启发式把两个二进制里的所有函数配起来,再给改动排序。REA 6.4.0 不做整程序自动配对:它只比较你指定的函数对,按名字精确匹配,不用猜测去填空。实际使用时:
- 没有源码、也不知道从哪看起时,用整程序 diff 工具。
- 已经有候选列表(来自源码历史、安全公告或 diff 工具的输出),并且需要每条结论都附证据、不确定性明确标出来时,用 REA。
- 两者可以配合:先取 diff 工具排在前面的函数对,再逐个用
compare_functions确认。
常见问题和排错
compare_functions 拒绝输入。 它只接受带产物主体的 analyze_function Evidence。传 decompile 的结果或者 ID 字符串都会失败。
所有维度都是 unknown。 先看 function_match。两边都是地址名时,identity 和 calls 必然 unknown。带符号重新编译,或者改比导出函数。
assembly 总是 changed。 代码挪了位置就会这样,因为没有重定位归一化。判断时更多参考 cfg 和 pseudocode。
MCP 里没复用快照。 provider 或分析配置和 CLI 那次不一致,或者会话里做过元数据修改。关闭目标再重新打开。
agent 比错了函数对。 把第 2 步的列表明确交给它,并要求不许替换名字。
下一步
- 快照、证据包和
rea compare的完整说明:REA 的 Evidence、快照与版本对比。 - 为大型库配好 Ghidra:REA + Ghidra 免费原生二进制分析。
- 托管代码的对应做法
compare-managed-members:.NET 程序集静态分析。 - REA 的证据模型和其他 MCP 有什么不同:REA 与 GhidraMCP、IDA Pro MCP 对比。
- 参考文档:docs/mcp-contracts.md。