指针审计:一组新的SyntaxFlowNativeCall

admin 2026-09-04 05:06:08 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍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&nbsp;<stdlib.h>int&nbsp;main(int&nbsp;abrt)&nbsp;{&nbsp; &nbsp;&nbsp;int&nbsp;*p = (int*)malloc(sizeof(int));&nbsp; &nbsp; *p =&nbsp;10;&nbsp; &nbsp;&nbsp;if&nbsp;(abrt) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;free(p);&nbsp; &nbsp; &nbsp; &nbsp; *p =&nbsp;20; &nbsp;&nbsp;// 这条路上:刚 free 完又写,UAF&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;1;&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;free(p); &nbsp; &nbsp; &nbsp;&nbsp;// 另一条路:free 完直接返回,没有再用&nbsp; &nbsp;&nbsp;return&nbsp;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&nbsp;(p) {&nbsp;&nbsp; &nbsp; *p =&nbsp;1;&nbsp;} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 大括号里当非空,不报if&nbsp;(!p) {&nbsp;&nbsp; &nbsp;&nbsp;return;&nbsp;}&nbsp;*p =&nbsp;1; &nbsp;// 判过空再解引用,一般安全
p =&nbsp;0;&nbsp;p->x =&nbsp;1; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 明明是空还解引用,要报if&nbsp;(c) {&nbsp;&nbsp; &nbsp; p =&nbsp;0;&nbsp;}&nbsp;p->x =&nbsp;1; &nbsp;// 有的分支置空,并起来说不准,也要报

函数调用时以指针作为参数,仅发生值拷贝(复制地址本身),不涉及对所指对象的访问,因此不构成解引用。解引用的语义边界在于:是否通过该地址发起了内存访问。具体触发解引用的操作包括:

  • 间接访问运算符 *p —— 读取/写入所指对象的值
  • 箭头运算符 p->field —— 基于地址偏移访问结构体成员
  • 下标 p[i]—— 等价于 *(p + i),同样触发访存

在此之前,指针只是一个整数值的容器,传参阶段不会触碰它指向的内存。

YAK

2.3 跨过程怎么跟进

函数里面的 ifreturn 是过程内的事。真正写规则时,释放和解引用经常不在同一个函数里:main 里 mallocfreep(p) 里才 free。引擎不会把 callee 整份控制流再嵌进 caller 走一遍,而是先给每个函数做一份形参摘要,再在调用点上用。

第一步:每个函数自己报一遍。

扫这个函数的指令,看形参后来发生了什么,记三件事:

  • 这个形参会不会被 free
  • 会不会 free(*p) 这种「顺着指针再释放里面那块」
  • 会不会被解引用(*pp->field

只记「第几个参数」,不把整条路径展开。void freep(int *p) { free(p); } 就会记成:第 0 个形参会被释放。

第二步:摘要互相传。

A 调用 B,B 已经知道自己会 free 第 0 个参数,那 A 里对应的那个实参,对 A 来说也算「会被释放」。包装再套一层也一样:wrap 调 freepfreep 调 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&nbsp;freep(int&nbsp;*p)&nbsp;{ free(p); }
int&nbsp;main()&nbsp;{&nbsp; &nbsp;&nbsp;int&nbsp;*p = (int*)malloc(sizeof(int));&nbsp; &nbsp; *p =&nbsp;10;&nbsp; &nbsp; freep(p); &nbsp;&nbsp;// 摘要:freep 会释放第 0 个参数 → 这里当成 free&nbsp; &nbsp; *p =&nbsp;20; &nbsp; &nbsp;// UAF&nbsp; &nbsp;&nbsp;return&nbsp;0;}

流程是:先给 freep 打上「释放形参 0」;走 main 时碰到 freep(p),把 p 标成已释放;后面的 *p = 20 就是 UAF。规则仍然写 $p<uaf()> 即可,不必自己把 freep 展开。

跟不深的情况也要心里有数:函数指针、只在头文件里看到的声明、绕很多层才 free,摘要可能跟丢,结果是漏报,或者按 extern 走得偏保守。泄漏同样用这份摘要——包装函数里已经 free 了,caller 出口一般不再当泄漏。

三、指针审计和控制流查询

结合控制流相关的 native callgetCfg、cfgReachable、cfgDominates、cfgGuards 这类)可以实现从图上 A 走到 B,对中间过程进行更加准确的审计。


四:规则的使用

*<uaf()> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//&nbsp;整个程序扫一遍,先看看有没有$p<uaf()> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//&nbsp;只问已经拿到的&nbsp;$p<uaf(target=$p)> &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//&nbsp;和上一行一类,种子写在参数里

日常排查内存问题,最顺手的方式是先找到指针,再针对它提问。

<uaf()> 不带主语,适合先全局扫一眼,快速看看有没有释放后使用的风险。但真正写规则时,用 $p<uaf()> 更干净——把指针变量明确写在前面,报告结果直接绑定到这个具体的指针上,省得从一堆候选里再手动定位。

追问一个指针指向哪里,用 <pointsTo()>,写法是 $p<pointsTo()>。它必须跟在指针后面,因为分析器需要知道你问的是谁的指向关系。

判断两个指针是否指向同一块内存,用 a<aliases(target=$$b)>。这在检测双重释放时特别有用——先确认两个指针共享同一块内存,再看是否存在同一条路径上先后释放了两次。

一句话总结:先锁定指针,再下诊断;<pointsTo()> 跟在后面;比别名用 aliases,双 $ 不能省。

| | | | | — | — | — | | 写法 | 解决什么问题 | 结果落在哪 | | | 有没有 UAF(Double Free 也算一类) | 出错的那次使用 | | | 有没有第二次 free | 第二次 free | | | 可能为空的时候有没有解引用 | 解引用的位置 | | | 函数结束时这块堆内存是不是还可能没放、也没交出去 | 那次分配 |

PS:只要 Double Free,可以写成

| | | | — | — | | 写法 | 在找什么 | | | malloc 这类分配 | | | free | | | 写成 *p 的读取(p->x 不一定在这里) | | | if (p) 这种判空 | | | 这个指针可能指向哪 | | | 会不会和 $b 是同一块内存 |

五、规则使用案例

全项目扫一遍

*<heapAlloc()>&nbsp;as&nbsp;$alloc*<freeCall()>&nbsp;as&nbsp;$free*<uaf()>&nbsp;as&nbsp;$uaf*<doubleFree()>&nbsp;as&nbsp;$df*<npd()>&nbsp;as&nbsp;$npd*<memLeak()>&nbsp;as&nbsp;$leak
alert&nbsp;$uaf&nbsp;for&nbsp;{ title:&nbsp;"Use After Free", title_zh:&nbsp;"释放后使用", level:&nbsp;"high"&nbsp;}alert&nbsp;$df&nbsp;&nbsp;for&nbsp;{ title:&nbsp;"Double Free", &nbsp; &nbsp;title_zh:&nbsp;"重复释放", &nbsp; level:&nbsp;"high"&nbsp;}alert&nbsp;$npd&nbsp;for&nbsp;{ title:&nbsp;"Null Deref", &nbsp; &nbsp; title_zh:&nbsp;"空指针解引用", level:&nbsp;"high"&nbsp;}alert&nbsp;$leak&nbsp;for&nbsp;{ title:&nbsp;"Memory Leak", &nbsp; title_zh:&nbsp;"内存泄漏", &nbsp; level:&nbsp;"low"&nbsp;}

UAF

不用自己判断 free 后面还有没有用。找到被 free 的指针,交给引擎:

free(*&nbsp;as&nbsp;$free_arg)$free_arg&nbsp;#->&nbsp;as&nbsp;$ptr$ptr<uaf()>&nbsp;as&nbsp;$uaf
alert&nbsp;$uaf&nbsp;for&nbsp;{&nbsp; &nbsp; title:&nbsp;"Use After Free",&nbsp; &nbsp; title_zh:&nbsp;"释放后使用",&nbsp; &nbsp; level:&nbsp;"high",&nbsp; &nbsp; risk:&nbsp;"释放后使用",}

NPD

找到解引用,再问当时空不空:

*<npd()>&nbsp;as&nbsp;$npd
alert&nbsp;$npd&nbsp;for&nbsp;{&nbsp; &nbsp; title:&nbsp;"Null Pointer Dereference",&nbsp; &nbsp; title_zh:&nbsp;"空指针解引用",&nbsp; &nbsp; level:&nbsp;"high",}

只针对某一个指针

pa&nbsp;as&nbsp;$papb&nbsp;as&nbsp;$pb*<doubleFree(target=$pa)>&nbsp;as&nbsp;$df_pa*<doubleFree(target=$pb)>&nbsp;as&nbsp;$df_pb

泄漏

看函数结束时还在不在:

p&nbsp;as&nbsp;$focus$focus<memLeak()>&nbsp;as&nbsp;$leak
  • malloc 完直接 return:还活着,也没交出去,报泄漏
  • free 完再 return:不报
  • if (abrt) free(p); 然后返回:有一条路没释放,报泄漏
  • then、else 里都 free:不报
  • return p;:所有权交给调用者,不报

别名

两个指针是不是同一块:

p&nbsp;as&nbsp;$focusq&nbsp;as&nbsp;$aliasr&nbsp;as&nbsp;$other
$focus<pointsTo()>&nbsp;as&nbsp;$obj$alias<aliases(target=$focus)>&nbsp;as&nbsp;$hit$other<aliases(target=$focus)>&nbsp;as&nbsp;$miss

q = p 时,hit 会留下 q、r 是另一次 malloc、miss 应为空。

和 CFG 拼在一起

<uaf()> 给出的命中已经考虑过分支。如果报告上还想标路径,可以再问:

p&nbsp;as&nbsp;$p$p<freeCall()>&nbsp;as&nbsp;$free$p<uaf()>&nbsp;as&nbsp;$use$use<cfgReachable(target=$free)>&nbsp;as&nbsp;$use2alert&nbsp;$use2

六、使用时要注意

  1. 现在主要是 C。别的语言如果没有同样的分配、释放信息,问了也可能是空的。
  2. 跨函数只跟一层。freep(p) 这种包装通常没问题;函数指针、绕很深的释放,可能会漏,或者偏保守。
  3. 先全项目扫可以,定规则的时候尽量绑到具体指针。$p<uaf()> 比 *<uaf()> 干净。
  4. <derefSite()> 只管写成 *p 的读取。p->field 或 *p = ... 不一定出现在这个列表里,但<uaf()>、<npd()> 还是可能从成员访问报出来。
  5. <nullCheck()> 请跟在目标指针后面。写成 *<nullCheck()> 时,if (abrt) 这种普通条件有时也会被收进来。
  6. 指针当参数传出去,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》

一个BOLA漏洞 网络安全文章

一个BOLA漏洞

文章总结: 本文详细披露了在网络安全AI聊天平台中发现的对象级授权(BOLA)漏洞,任何认证用户可查看或篡改其他用户的私聊记录,影响严重。作者通过双账户测试验证
评论:0   参与:  0