7.1 CPG:一张图看懂代码


7.1 CPG:一张图看懂代码

本节摘要:要理解 Joern,先理解它脚下的数据结构——代码属性图(Code Property Graph,CPG)。本节从三张「旧地图」讲起:AST 只管结构、CFG 只管流向、PDG 只管依赖,各自都有系统性盲区;然后看 CPG 如何用一个 schema 把三者融合:节点复用 AST 节点集,控制流边与数据依赖边挂在同一批节点上;接着解释「一张图」为什么改变了分析方式——结构、流向、依赖可以在一次遍历里同时询问,并举出三个维度逐级加深的问题实例;再用一个「审阅一段代码要查几次图」的小对照,量化分立图与统一图的效率差;最后与 CodeQL 的代码数据库做同异对比:都是「代码可查询」路线,一个用关系表加逻辑谓词,一个用属性图加遍历 DSL。编译原理术语的出处与深讲见《build-your-own-x》第 09 章《造操作系统与编译器》。

学习目标

  • 分别说出 AST、CFG、PDG 能回答与不能回答的问题类型。
  • 描述 CPG 的融合思路:一套节点、多种边。
  • 解释「一次遍历混合问三个问题」的具体分析收益。
  • 对比 CPG 与 CodeQL 数据库的表达力与工程差异。

一、三张旧地图与各自盲区

编译器与逆向工程各画过一张地图:

地图 回答的问题 看不见的东西
AST 抽象语法树 结构:谁包含谁、括号怎么配 执行顺序、数据影响
CFG 控制流图 流向:从哪执行到哪、跳转关系 数据从哪来到哪去
PDG 程序依赖图 依赖:谁影响谁(数据与控制依赖) 结构层级、直观执行序

拿一段三行代码看盲区(示意):

# three_maps.txt —— 同一段代码在三种图里的成像 if (auth_ok) ── CFG 看见:两个分支与汇合点 strcpy(buf, input); ── AST 看见:调用表达式嵌套在 if 内 log_access(user); ── PDG 看见:input 影响 buf,user 影响 log_access 的参数 ​

单一地图都答不出安全分析里最常见的那类问题:「input 这个外部数据,是否可能在 auth_ok 为真的路径上流进 strcpy?」——它同时需要依赖(PDG:input 影响什么)、路径(CFG:这条边在不在检查之后)与结构位置(AST:调用发生在哪个函数的哪层块里)。

把三张地图对同一问句的「哑区」排开看更直观:

问句 AST 答 CFG 答 PDG 答 需要的组合
strcpy 在哪个函数里 能 绕 绕 AST 单独可答
检查一定在调用前吗 不能 能 不能 CFG 单独可答
input 影响 buf 吗 不能 不能 能 PDG 单独可答
受控 input 能流进 strcpy 且过检查吗 不能 不能 不能 三图联合

传统做法是把三张图各自建好,然后在三个数据结构之间来回切换分析逻辑:先在 PDG 上查到数据流候选,再切到 CFG 验证路径,再回 AST 找上下文——每次切换都是一次「导出、对齐、再导入」的工程损耗,而节点编号在三种图里往往还不一致。

二、CPG:一套节点、三种边

CPG 的 schema 思想极其朴素:节点集合直接复用 AST 的节点(函数、调用、赋值、标识符……),再往同一批节点上加挂不同标签的边——AST 边(包含关系)、CFG 边(执行顺序)、PDG 边(数据与控制依赖)。示意图:

# cpg_schema.txt —— CPG:AST 节点为底,三种边并存 ┌────────────┐ │ if 语句节点 │ └─────┬──────┘ AST 边 ┌───────┴────────┐ AST 边 ▼ ▼ ┌──────────┐ ┌────────────┐ │ 条件表达式 │ │ 调用 strcpy │ └────┬─────┘ └──┬─────┬───┘ │ PDG 边 │CFG边 │ AST 边 │ ┌────────────┘ ▼ ▼ ▼ 参数节点 标识符 input ──PDG 边──▶ 参数 buf ​

三种边并存意味着一个节点的「邻居」天然携带三种语义:往 AST 边走是看结构,往 CFG 边走是看路径,往 PDG 边走是看影响。节点上的属性(源码位置、代码文本、类型信息若有)让每一步遍历的产出都能直接落回源码行——分析结果永远可以说「在第几行」。

Joern 把这个 schema 实现为可落盘、可增量加载的图数据库,并配上 Scala 语言的查询 DSL——它自称 Bug Hunter 的 Workbench(猎虫者的工作台),定位就是给「愿意写几行查询的安全研究员」用的平台。学术源头可以追到代码属性图的原始论文(Yamaguchi 等,2014,把三种图融合用于漏洞模式挖掘),Joern 是这一思想的工程化旗舰。

三、为什么「一张图」改变了分析方式

变化不在数据量,在提问方式。分立的图只能回答单维度问题;CPG 允许把三个维度写进一条遍历里。三个维度逐级加深的真实问题:

  • 单维度问题:库里有几次 strcpy 调用?(AST 就够——按结构找调用节点)
  • 双维度问题:strcpy 的参数里有没有来自函数形参的数据?(AST 加 PDG——先定位调用,再沿依赖边回溯)
  • 三维度问题:形参 taint 到 strcpy 的路径上,是否必然经过 auth 检查?(AST 加 PDG 加 CFG——数据流回溯叠加路径约束)

第三类问题正是「可达性判定」的雏形——第 06 章里 semgrep 污点模式勉强答、CodeQL 污点查询认真答的问题,在 CPG 里变成一次图遍历:先按结构定位调用点,沿 PDG 边回溯污染源,再用 CFG 边检查路径上是否经过净化检查。

对研究者更重要的差别是可探索性:CodeQL 的查询面向「已想清楚的模式」,一次成稿;CPG 遍历面向「边走边看」的探索,一条查询的输出可以直接变成下一条查询的输入——

# explore_loop.txt —— 探索式分析的滚动循环 查询一:列出全库危险调用 │ 看到输出,挑一个可疑函数 ▼ 查询二:该函数的参数从哪来(PDG 回溯) │ 发现来自入口函数 ▼ 查询三:入口到该函数的路径上有无净化(CFG 检查) │ 结论成型 ▼ 固化成 .sc 脚本,入库复用(第 7.2 节) ​

逆向工作恰恰是探索密集型的:你几乎从不知道要找什么,直到你看见它。这是「一张图」最深层的价值。

四、与 CodeQL 数据库的异同

两者同属「把代码变成可查询模型」的路线,工程形态不同:

维度 CodeQL CPG(Joern)
模型形态 关系数据库(表与谓词) 属性图(节点与边)
查询语言 QL(声明式逻辑) Scala DSL(图遍历)
风格 写清条件,引擎推导 亲自遍历,逐步试探
污点分析 引擎级,成熟 内置污点引擎,可用
建库门槛 编译型语言要真实构建 源码可直接导入,无需构建
二进制支持 不涉及 经 Ghidra 前端可导入 x86

(语言成熟度以 Joern 官方文档为准:C、C++、Java 很高;JavaScript、Python 与 x86 二进制属于高;Kotlin、PHP、Go、Swift 属于中等——2026-10 官方口径。)

一句话选型直觉:模式已明确、要大规模复跑,用 CodeQL;模式还在成形、要交互式探索、或要碰二进制,用 Joern。第 6.3 节的流水线图可以再添一格:CPG 探索位于「快筛之后、深挖前后皆可」,它的产出(一条想清楚的遍历)既可沉淀成 QL 查询,也可以直接进第 10 章的 AI 验证器清单。

三种「分析姿态」的对照当作本章心法:

姿态 代表 你写什么 引擎干什么
规则 semgrep 模式 逐文件匹配
声明 CodeQL 条件 全库逻辑推导
遍历 Joern 路线 沿边走给你看

三种姿态没有高下,只有阶段:探索期用遍历形成假设,稳定期把假设写成声明或规则——第 10 章的 AI 方法论里,模型负责的正是「遍历期的阅读」,引擎负责的正是「稳定期的判定」。

本节要点回顾

  • AST 管结构、CFG 管流向、PDG 管依赖;单一地图答不出跨维度的可达性问题。
  • CPG 用一套节点加三种边完成三合一,Joern 是它的代表平台(学术源头为 2014 年的代码属性图论文)。
  • 一张图的核心收益是提问方式:一次遍历混合问结构、路径、影响;探索式滚动循环是逆向的天然节奏。
  • 与 CodeQL 是「图遍历对关系推导」:探索用 CPG,复跑用 QL。

图的道理讲完了,接下来动手:装上 Joern,把一个真实代码库导进去,在 REPL 里亲手写第一遍历——你会发现在图上「问问题」比想象中便宜得多。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U