本节摘要:本章一路建立的 CPG 能力,本节推进到二进制世界:Joern 经 Ghidra 前端把 x86 ELF 导入为代码属性图——节点是函数与调用,边仍是那三种语义。意义在于「同语言」三个字:你在 7.2 节学的每条 Scala 查询,在二进制图上一字不改照样能跑;源码图谱与二进制图谱终于可以在同一个平台里对照。本节先讲前端的工作原理与导入流程(命令细节以 Joern 官方文档为准);再用同一套查询语言在 stripped 二进制上做函数清单、交叉引用与可达性查询;然后演示源码图与二进制图的对照分析——一致性核对、结构比对命名、差异聚焦三类用途;最后坦白这条路线的成熟度边界与导入成本的现实。这是本书「源码与二进制打通」差异化路线的落点。
第 01 章《Ghidra地基》讲过:Ghidra 把指令流反汇编、分析函数边界、恢复调用关系——这些产出本质上是「二进制世界的事实」。Joern 的二进制前端做的事情是把这些事实翻译进 CPG 的 schema:函数节点、调用节点照建,控制流边挂基本块,数据依赖按 Ghidra 的分析结果近似恢复。
于是同一个查询心智模型横跨两个世界:
# src_bin_bridge.txt —— 源码与二进制在 CPG 层会师 源码 C/C++/Java/Python ... x86 ELF/PE │ │ importCode 直接解析 Ghidra 前端反汇编与分析 │ │ ▼ ▼ 源码 CPG ◀──── 同一 schema ────▶ 二进制 CPG 同一套 Scala DSL 查询
注意这层关系与第 03 章《GhidraMCP实战》不冲突:MCP 把 Ghidra 的单点能力(反编译某个函数、查某个交叉引用)暴露给模型,是问答式的;CPG 前端则把整个二进制变成可批量遍历的图,是扫描式的。前者适合「帮我看看这个函数」,后者适合「把整个二进制里所有符合某模式的调用列出来」——两者互补,甚至常常串联:图查询缩小范围,MCP 反编译逐个细看。
以一个自编译的 crackme 二进制为例(编译方法见第 8.1 节,先 strip 练习 stripped 场景):
# bin2cpg.sh —— 二进制入图流程示意(命令细节以 Joern 官方文档为准) # 步骤一:在 Joern 控制台对 ELF 执行导入,Ghidra 前端自动接管二进制输入 # importCode("/path/to/crackme.elf") ← 生成二进制 CPG 工作区 # 步骤二:后续会话直接加载,避免重复导入 # loadCpg("crackme.elf") ← cpg 变量即二进制图谱
导入完成后先用三条确认查询核实规模:函数数、调用数、外部导入函数数(7.2 节的摸底三查询原样可用)。成本要有预期:
| 样本规模 | 自动分析耗时(量级预期) | 建议 |
|---|---|---|
| 教学级(几十 KB) | 分钟级 | 直接入图 |
| 中型组件(数 MB) | 数十分钟 | 入图一次、loadCpg 复用 |
| 大型产物(几十 MB 以上) | 可能以小时计 | 先 headless Ghidra 单独分析评估,再决定入图 |
提示:未知样本的导入与后续分析请在隔离环境进行,纪律见《Agent 沙箱与运行时隔离》第 02 章《从零搭一个Agent沙箱》。
导入失败的常见原因速查(按出现频率排序):
| 症状 | 首查 | 处置 |
|---|---|---|
| 导入即报解析错误 | 文件是否真是 ELF 或 PE | file 命令确认格式,加壳样本先脱壳研究 |
| 导入成功但函数数为个位数 | 是否静态链接巨无霸或加壳 | 先看 Ghidra 单独分析的产出再评估 |
| 图查询全部为空 | loadCpg 是否加载了正确工作区 | 重新 importCode 后立即跑摸底三查询 |
| 导入超时 | 样本规模超出预期 | 按第二节表格改走 headless Ghidra 预分析路线 |
图谱就位,7.2 节的语法原样生效。函数清单(过滤动态链接桩):
# bin_functions.sc —— 二进制 CPG:函数清单(过滤 PLT 桩) cpg.method.nameNot(".*@plt.*").name.l
交叉引用式查询——谁调用了目标函数、它又调用了谁:
# bin_xref.sc —— 二进制 CPG:目标函数的调用双方 val target = cpg.method.name(".*40119.*") // Ghidra 风格的地址命名 target.caller.name.l // 上行交叉引用 target.callee.name.l // 下行交叉引用
数据流查询同样可用:函数参数到危险函数实参的可达性,语法与 7.2 节完全一致——只是节点从「源码形参」变成「Ghidra 恢复的寄存器传递近似」。还要补一类二进制特有的查询:按字符串反查函数。程序自述文本(提示语、错误信息)往往是最可靠的语义锚点:
# str_anchor.sc —— 二进制 CPG:以字符串为锚点反查引用函数 cpg.literal.code("\"Access granted\"").method.name.l
二进制侧的数据流精度天然低于源码侧(指针别名、间接跳转都是敌人),所以这里的可达性结果应当作「值得人工复核的线索」,而非定论;确认手段是第 08 章的符号执行与动态调试。
二进制查询的三条注意事项,先立在这里少走弯路:
「打通」的真正威力在对照。对有源码的自研或开源组件,建两张图后可以做三类工作:
# dual_cpg.txt —— 源码图与二进制图的对账 源码 CPG 二进制 CPG check_license 出度 3 ◀─结构匹配─▶ FUN_00401190 出度 3 长度校验被调用 ◀─一致性──▶ 同地址段调用存在? v1.1 与 v1.2 差异查询 ◀─差异聚焦─▶ 只有两个函数的邻域变了
这套对照不是玩具:软件成分分析、供应链审计、开源组件合规检查都在用同型方法。本书把它作为第 09 章命名恢复的机器辅助基础——结构匹配给出的候选名,再交给人与大模型裁决;比对命中且行为验证通过的,才按第 9.2 节流程转正。
按官方口径(2026-10),二进制支持经 Ghidra 前端覆盖 x86,成熟度属于「高」一档,但「高」不等于「全」。四条边界要心里有数:
即便有这些边界,这条路线的独特价值依然成立:一个查询心智模型、一套脚本资产、一个平台,同时覆盖源码审计与二进制逆向。对比之下,第 06 章两台引擎都只活在源码侧,传统二进制工具(第 01、02 章)又不具备图查询的表达力——Joern 的 Ghidra 前端正好补上中间这块拼图,这也是本书把「源码与二进制打通」作为差异化主线的技术底气。
图回答的是「结构上可能」:可达性查询说有一条数据流路径存在,但它不保证真实输入能走通、更不给出走通的输入。下一章请出符号执行引擎 angr——让变量「同时取所有值」,把路径可行性变成求解器可判定的问题,再让大模型负责提假设、求解器负责下结论。