第一次编译与查询
本节用一个 Java 命令执行示例,完成从源程序到查询结果的最小流程。目标是定位 Runtime.exec 调用、追踪传入参数,并把结果映射回源码。
准备环境与示例
先按照 CLI 总览安装 Yak,再检查本地版本:
yak version
本文的命令与编译与查询指南使用同一套接口。具体版本支持的参数可以通过 yak ssa-compile --help 和 yak ssa-query --help 检查。
克隆练习项目,进入第一课:
git clone https://github.com/yaklang/syntaxflow-zero-to-hero
cd syntaxflow-zero-to-hero/lesson-1-hello-world
这个目录中,HelloWorld.java 是待分析程序,lesson-1.sf 是查询规则。先阅读源码,再运行分析,以便理解每个命中值对应的实际代码。
编译为 Program
yak ssa-compile -t . -p lesson1 -l java --re-compile
| 参数 | 作用 |
|---|---|
-t . | 以当前目录为编译目标 |
-p lesson1 | 将编译结果保存为名为 lesson1 的 Program |
-l java | 指定源程序语言 |
--re-compile | 允许重新编译同名 Program |
编译完成后,检查输出中是否存在失败信息。Program 名称是后续查询的入口;它与源程序中的类名或项目目录名并不是同一个概念。
阅读一条数据流查询
示例规则的核心表达式如下:
lesson-1.sf · 查询部分
Runtime.getRuntime().exec(* #-> * as $source) as $sink;
check $source then "找到命令执行参数来源" else "未找到参数来源";
check $sink then "找到命令执行点" else "未找到命令执行点";
从左到右阅读:
Runtime.getRuntime().exec定位关注的调用。- 括号内的匹配表达式选择调用参数。
#->沿定义链向上追踪影响参数的值。as $source保存追踪结果;as $sink保存调用匹配结果。check根据命名结果是否存在输出说明。
这里的 #-> 表达数据依赖追踪,不表示控制流图中的“支配者”。追踪方向和配置项可继续查阅数据流运算。
执行规则
使用已经编译的 Program:
yak ssa-query -p lesson1 --sf lesson-1.sf
也可以使用同一查询接口的别名:
yak sf -p lesson1 --sf lesson-1.sf
-p lesson1 必须与编译时的 Program 名称一致。修改源码后应重新编译;修改规则后则可以在已有 Program 上再次查询。
从结果回到代码
查询输出包含命名变量及其匹配的值。优先检查:
$source: 是否指向示例中的方法参数cmd,以及该参数来自何处。$sink: 是否对应调用exec(cmd)的位置。- 源码范围: 文件名与行列范围是否与本地练习项目一致。
输出中的 IR 编号和行列位置可能随版本与源码变化。它们是定位证据,不能仅凭某个编号或示例日志判断查询是否正确。
从匹配到结论
找到调用与参数来源,说明规则发现了一条值得检查的数据依赖。是否存在可利用的命令注入,还需要确认参数是否可控、路径是否可达,以及输入是否经过有效约束。