文章总结: 本文介绍SyntaxFlow在C语言指针审计中的应用,通过控制流分析自动检测UAF、DoubleFree、NPD及内存泄漏等问题。引擎沿路径追踪指针状态,支持跨函数摘要传播,提供uaf、npd、memLeak等查询规则,简化规则编写并精确定位告警。建议优先绑定具体指针变量进行查询,注意当前主要支持C语言且跨函数仅跟一层。 综合评分: 85 文章分类: 代码审计,安全工具,漏洞分析,安全开发
指针审计:一组新的 SyntaxFlow Native Call
原创
Yak Yak
Yak Project
2026年9月3日 18:27 四川
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
在审计 C 时,我们常常会关注指针相关的问题,名字大家其实都熟:UAF、Double Free、NPD、内存泄漏。 以前写 SyntaxFlow,得自己把 free 捞出来,再去后面翻有没有人还在用。分支一多,规则就不好写。
SyntaxFlow 这边控制流分析一直在补:if 怎么分叉、从 A 能不能走到 B、中间必不经过某次判断,规则里已经能问了。顺着这条能力往下,指针审计也有了新的写法——引擎会跟着这些路径,把每个指针此刻还能不能用、是不是空记下来。规则直接问 <uaf()>、<npd()>就行,不用自己在规则里把控制流再走一遍。
目前先覆盖 C 的 malloc / calloc / realloc 和 free。
一、整体流程
源码先被编译。malloc 记成一次分配,free 记成一次释放,*p 记成一次解引用。if、return 会把函数拆成一段段,连成一张「代码会怎么往下走」的图。
接着引擎就顺着这张图走。每走到一块代码,就更新这块内存的状态:还活着、已经释放、空还是非空。碰到 if 就分叉记;两条路再并到一起。
规则这边只负责提问。写 <uaf()> 或 $p<npd()>,拿到的是已经走完路径的结论,不是在源码里搜字符串。告警一般落在真正出错的那一次使用上。UAF 还会尽量带上是哪次 free 把它放掉的,数据流图里可以从使用点点回释放点。
二、控制流怎么走进指针审计
程序的执行过程可以看作对一张共享状态表的持续演化。执行流依次进入各个函数,每个函数在被调用时读取当前状态,执行其逻辑,并将变更写回状态表。函数返回后,控制流并不重置或回溯,而是基于更新后的状态决定后续分支的走向。
换言之,这是一条单向推进的执行链:状态驱动决策,决策驱动路径,路径再次修改状态——循环往复,直至每个函数都会被走到。
查的东西不一样,表上记的也不一样:
| | | | | — | — | — | | 你在查 | 沿路记什么 | 两条路并到一起时 | | UAF、Double Free | 这块内存还活着,还是已经释放 | 有一条路已经释放,并起来就算已经释放 | | 内存泄漏 | 同样记活着 / 已释放 | 有一条路还活着,并起来就算可能还没释放 | | NPD | 空、非空,还是说不准 | if (p) 的两边先分开改,再继续走 |
沿路记什么 : 这块内存还活着,还是已经释放 ;记活着 / 已释放 ; 空、非空,还是说不准
YAK
2.1 UAF
来看下面这个和 UAF 相关的案例:
#include <stdlib.h>int main(int abrt) { int *p = (int*)malloc(sizeof(int)); *p = 10; if (abrt) { free(p); *p = 20; // 这条路上:刚 free 完又写,UAF return 1; } free(p); // 另一条路:free 完直接返回,没有再用 return 0;}
malloc 之后 p 指向一块活着的内存,因此 *p = 10 没问题。然后 if (abrt) 分成两叉。走进 then:先 free,再 *p = 20。状态已经是「释放了」,这次写入就是 UAF,接着 return 1,这条路结束。另一边只 free 再返回。后面没人再用,不报 UAF,也不报泄漏。
YAK
2.2 NPD
NPD 跟「有没有走过判空」绑在一起。if (p)、if (!p)、if (p == NULL) 会把后面分成两路:这一边可以当非空,那一边必须当空。
if (p) { *p = 1; } // 大括号里当非空,不报if (!p) { return; } *p = 1; // 判过空再解引用,一般安全
p = 0; p->x = 1; // 明明是空还解引用,要报if (c) { p = 0; } p->x = 1; // 有的分支置空,并起来说不准,也要报
函数调用时以指针作为参数,仅发生值拷贝(复制地址本身),不涉及对所指对象的访问,因此不构成解引用。解引用的语义边界在于:是否通过该地址发起了内存访问。具体触发解引用的操作包括:
- 间接访问运算符
*p—— 读取/写入所指对象的值 - 箭头运算符
p->field—— 基于地址偏移访问结构体成员 - 下标
p[i]—— 等价于*(p + i),同样触发访存
在此之前,指针只是一个整数值的容器,传参阶段不会触碰它指向的内存。
YAK
2.3 跨过程怎么跟进
函数里面的 if、return 是过程内的事。真正写规则时,释放和解引用经常不在同一个函数里:main 里 malloc,freep(p) 里才 free。引擎不会把 callee 整份控制流再嵌进 caller 走一遍,而是先给每个函数做一份形参摘要,再在调用点上用。
第一步:每个函数自己报一遍。
扫这个函数的指令,看形参后来发生了什么,记三件事:
- 这个形参会不会被
free - 会不会
free(*p)这种「顺着指针再释放里面那块」 - 会不会被解引用(
*p、p->field)
只记「第几个参数」,不把整条路径展开。void freep(int *p) { free(p); } 就会记成:第 0 个形参会被释放。
第二步:摘要互相传。
A 调用 B,B 已经知道自己会 free 第 0 个参数,那 A 里对应的那个实参,对 A 来说也算「会被释放」。包装再套一层也一样:wrap 调 freep,freep 调 free,摘要会往上传。这一步会多走几轮,直到没有新的「谁释放了谁」为止。
第三步:真正顺着 caller 走的时候,碰到一次调用。
比如 freep(p),先查出 freep 的摘要,再决定这次调用对 p 意味着什么:
- 摘要说会
free这个参数:这次调用就当成一次释放。后面再*p,报 UAF;再free(p),报 Double Free。泄漏那边则当成这块内存已经交出去了。 - 摘要说会解引用:若
p此时已经释放,这次调用本身就是 UAF;若可能是空,NPD 仍只在真正的*p/p->field上报,单把指针当参数传进去不算解引用。 - 有函数体,但只是把指针存起来、既不
free也不解引用:不当成一次使用,避免hold(p)这种误报。 - 找不到函数体(extern、函数指针、间接调用):偏保守,可能当成会使用。
入口处不会假设「调用者进来之前就已经 free 过了」。形参先当成还活着,只有这个函数里(或摘要认定的调用)真的释放了,才变成已释放。这样跨函数不容易误报。
NPD 还有一种跨过程:callee 里写了 *a = NULL,caller 会看到这次写入,之后再解引用就会按空来算。
对照一段很常见的包装:
void freep(int *p) { free(p); }
int main() { int *p = (int*)malloc(sizeof(int)); *p = 10; freep(p); // 摘要:freep 会释放第 0 个参数 → 这里当成 free *p = 20; // UAF return 0;}
流程是:先给 freep 打上「释放形参 0」;走 main 时碰到 freep(p),把 p 标成已释放;后面的 *p = 20 就是 UAF。规则仍然写 $p<uaf()> 即可,不必自己把 freep 展开。
跟不深的情况也要心里有数:函数指针、只在头文件里看到的声明、绕很多层才 free,摘要可能跟丢,结果是漏报,或者按 extern 走得偏保守。泄漏同样用这份摘要——包装函数里已经 free 了,caller 出口一般不再当泄漏。
三、指针审计和控制流查询
结合控制流相关的 native call(getCfg、cfgReachable、cfgDominates、cfgGuards 这类)可以实现从图上 A 走到 B,对中间过程进行更加准确的审计。
四:规则的使用
*<uaf()> // 整个程序扫一遍,先看看有没有$p<uaf()> // 只问已经拿到的 $p<uaf(target=$p)> // 和上一行一类,种子写在参数里
日常排查内存问题,最顺手的方式是先找到指针,再针对它提问。
<uaf()> 不带主语,适合先全局扫一眼,快速看看有没有释放后使用的风险。但真正写规则时,用 $p<uaf()> 更干净——把指针变量明确写在前面,报告结果直接绑定到这个具体的指针上,省得从一堆候选里再手动定位。
追问一个指针指向哪里,用 <pointsTo()>,写法是 $p<pointsTo()>。它必须跟在指针后面,因为分析器需要知道你问的是谁的指向关系。
判断两个指针是否指向同一块内存,用 a<aliases(target=$$b)>。这在检测双重释放时特别有用——先确认两个指针共享同一块内存,再看是否存在同一条路径上先后释放了两次。
一句话总结:先锁定指针,再下诊断;<pointsTo()> 跟在后面;比别名用 aliases,双 $ 不能省。
| | | |
| — | — | — |
| 写法 | 解决什么问题 | 结果落在哪 |
|
PS:只要 Double Free,可以写成
| | |
| — | — |
| 写法 | 在找什么 |
|
五、规则使用案例
全项目扫一遍
*<heapAlloc()> as $alloc*<freeCall()> as $free*<uaf()> as $uaf*<doubleFree()> as $df*<npd()> as $npd*<memLeak()> as $leak
alert $uaf for { title: "Use After Free", title_zh: "释放后使用", level: "high" }alert $df for { title: "Double Free", title_zh: "重复释放", level: "high" }alert $npd for { title: "Null Deref", title_zh: "空指针解引用", level: "high" }alert $leak for { title: "Memory Leak", title_zh: "内存泄漏", level: "low" }
UAF
不用自己判断 free 后面还有没有用。找到被 free 的指针,交给引擎:
free(* as $free_arg)$free_arg #-> as $ptr$ptr<uaf()> as $uaf
alert $uaf for { title: "Use After Free", title_zh: "释放后使用", level: "high", risk: "释放后使用",}
NPD
找到解引用,再问当时空不空:
*<npd()> as $npd
alert $npd for { title: "Null Pointer Dereference", title_zh: "空指针解引用", level: "high",}
只针对某一个指针
pa as $papb as $pb*<doubleFree(target=$pa)> as $df_pa*<doubleFree(target=$pb)> as $df_pb
泄漏
看函数结束时还在不在:
p as $focus$focus<memLeak()> as $leak
- malloc 完直接 return:还活着,也没交出去,报泄漏
- free 完再 return:不报
- if (abrt) free(p); 然后返回:有一条路没释放,报泄漏
- then、else 里都 free:不报
- return p;:所有权交给调用者,不报
别名
两个指针是不是同一块:
p as $focusq as $aliasr as $other
$focus<pointsTo()> as $obj$alias<aliases(target=$focus)> as $hit$other<aliases(target=$focus)> as $miss
q = p 时,hit 会留下 q、r 是另一次 malloc、miss 应为空。
和 CFG 拼在一起
<uaf()> 给出的命中已经考虑过分支。如果报告上还想标路径,可以再问:
p as $p$p<freeCall()> as $free$p<uaf()> as $use$use<cfgReachable(target=$free)> as $use2alert $use2
六、使用时要注意
- 现在主要是 C。别的语言如果没有同样的分配、释放信息,问了也可能是空的。
- 跨函数只跟一层。freep(p) 这种包装通常没问题;函数指针、绕很深的释放,可能会漏,或者偏保守。
- 先全项目扫可以,定规则的时候尽量绑到具体指针。
$p<uaf()>比*<uaf()>干净。 <derefSite()>只管写成*p的读取。p->field或*p = ...不一定出现在这个列表里,但<uaf()>、<npd()>还是可能从成员访问报出来。<nullCheck()>请跟在目标指针后面。写成*<nullCheck()>时,if (abrt) 这种普通条件有时也会被收进来。- 指针当参数传出去,NPD 不会把它当成解引用。
END
-
Memfit:是万径安全旗下Yaklang团队研发的面向长程渗透测试、漏洞挖掘、应急响应等全链路攻防任务的生产级安全领域Agent系统
-
Yakit:是万径安全旗下Yaklang 团队开源的一款完全国产的交互式安全测试平台
YAK官方资源
Yakit官网下载地址:
https://yaklang.com/
Github地址:
https://github.com/yaklang/yakit
Yakit 视频教程:
https://space.bilibili.com/437503777
Memfit下载地址:
https://memfit.ai/
IRify下载地址:
https://ssa.to/
YTray下载地址:
https://yaklang.io/ytray/
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Yak Project Yak Yak《指针审计:一组新的 SyntaxFlow Native Call》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论