前言
又是一年元旦至,一般来说这个时间是该写年终总结了,我朋友圈里很多师傅也纷纷挂出了自己的年终总结。只不过从我往年的年终总结时间可以看出来,我写年终总结的时间是一年比一年晚,22年是一月底写的,23年是二月底写的,24年甚至是四月份写的,所以今年写年终总结的时间连我自己也不知道,并且我比较有逆反心理,大家都在这个时候写年终总结我偏不写,来写点我对于技术的感悟。
研一选的几门课里,有三门的期末作业是写综述,这三个综述的主题我都不约而同的选择了我最感兴趣的“LLM+SAST”主题,恰好今年也自己开发了一个SAST工具,也看了不少相关的文章,和许多师傅交流过,对于这一块也有不少有趣的思考,所谓情之所至,下笔不能自休,情不自禁的想写个随笔聊聊我的看法。
SAST
什么是静态分析(SAST)?静态分析就是通过分析静态程序本身来推测程序的行为,并判断程序是否满足某些特定的性质,对于代码审计这个领域,最让大家熟知的 SAST 技术莫过于污点分析了,所谓污点分析,就是利用程序分析的方法判断从 Source 到 Sink 之间是否存在可达的路径,一般来说 Source 点指的是用户的可控输入点,在 PHP 里可能是 $_POST、$_GET 这类取值用户可控的变量,在 Java 里可能是 HttpServletRequest.getParameter()、@RequestParam 注解修饰的参数等获取 HTTP 请求内容的方法与对象;而 Sink 点就是危险函数,比如 PHP 里的 system、Java里的 Runtime.getRuntime().exec()等等。如果存在一条可控的从 Source 到 Sink 的路径,我们自然能直观的感受到用户可以直接控制这些危险函数的执行过程,自然存在严重的安全漏洞。
上面这类漏洞在学术界有个专有名词,叫做 Taint-Style Vulnerabilities,即污点类型的漏洞。但作为安全工程师的我们知道,Taint-Style Vulnerabilities 其实只是广阔的 Web 漏洞海洋中的一部分罢了,虽然它确实占了很大一块面积,但还有很多类型的漏洞是不能用简单的 Taint-Style 概括的,比如越权型的漏洞、条件竞争导致的漏洞、业务逻辑带来的漏洞等等。
LLM
LLM(大语言模型)想必大家都不陌生,自从 Chatgpt 的一炮而红,LLM 在人们的生产生活了带来了不可磨灭的影响,有了 LLM 之后无论是学习还是挖洞都轻松了许多,现在我甚至已经很难想象在那个没有 LLM 的时代我是怎么坚持下来的了。LLM的全称是Large Language Model,之所以说它 Large,是因为它在训练时阅读了海量的文本数据(书籍、网页、代码、论文等),其阅读量远超人类几辈子的总和,OpenAI在训练chatgpt的时候用了几乎所有你能在互联网上搜索到的东西来训练模型;而它之所以是 Language 的,是因为它的核心能力是处理人类语言,它不仅能看懂中文、英文,还能看懂编程语言,具有很强的语义理解能力;最后它是一个 Model,它的本质是一个数学统计模型。之所以 LLM 能与我们对话甚至回答我们的问题、帮我们写代码,这是因为它的工作原理本质上是“文字接龙”,即根据输入不断的预测下一个词,当你输入“床前明月光”时,模型通过概率计算,觉得后面接“疑是地上霜”的概率最大,这也是一些输入法能做到的联想程度,当它读的书足够多、脑容量足够大时,这种“接龙”能力发生了质变(学术界叫做涌现)。它不再只是简单补全句子,而是能根据上下文“接龙”出一段代码、一篇论文摘要、甚至是一个复杂的逻辑推理过程。
现在的实践
SAST 是机械的、是强建模的,我们需要用利用一套预定的规则来拟合代码的逻辑,比如什么时候控制流会从方法 A 传播到方法 B、什么是Source 什么是Sink、一个预定的表达式到底应当解析成怎样的中间表示。即使我们有了一套非常完美的规则,能拟合代码可能出现的所有可能,对代码执行逻辑给出绝对精确的判断,一个问题来了,那就是执行效率。真实的代码逻辑是复杂的,Source的传播过程可能有无数可能的执行路径,更别说真实的逻辑里会有极其复杂的过程间调用过程,从A调用到B、又调用到C、以此往复,即使你有非常精确的SAST工具,也需要非常漫长的时间才能获取分析结果,甚至是内存爆炸,在企业里这样的开销是不被允许的,一个即将上线的业务怎么可能因为你这个莫名其妙的安全审计环节就拖延几天。
不同的上下文会获取不同的分析结果,比如假设A是Source、B是Sink,A->B的执行结果和A->C->B、A->C->D->B显然可能是不一样的,这就是因为不同的上下文,哪怕是同一个方法,上下文不同执行的结果也可能会不同,对于上下文敏感的污点分析,它会追踪方法的调用路径给出精确的执行结果。这自然是我们想要的,但过于追求精准度可能带来极为庞大的开销,甚至导致什么也分析不出来,也因此,在标准的 Tai-e 实现中,上下文敏感度被限制为仅两次调用。关于这个问题,LFY 学长的 MScan 给出了一种有趣的解决办法,那就是距离引导的污点分析,利用 Source 到 Sink 间的距离指导分析精度,污点路径附近节点有更高敏感度;远端节点则转化为低精度的分析,开销更低。
LLM 天然具有语义理解能力,能够帮助我们跳脱预定的规则,从代码的逻辑中理解代码,辅助我们进行分析。也因此,在这个 AI 的时代,许多人都试图将 LLM 引入传统的 SAST ,增益传统的代码审计流程。今年我看一些大厂人才计划里安全岗的描述,很多都提到需要让这些最优秀的人才去解决 LLM+SAST 的问题,利用 LLM 赋能传统的 SDL 过程,让 SDL 更快捷和高效。
如何做 LLM+SAST 的结合呢?我将这种结合分为两种流派,一种流派是 SAST 为主,LLM 为辅,利用 LLM 解决传统 SAST 中难以解决的痛点,另一种流派是 LLM 为主,SAST 为辅,甚至完全没有 SAST 参与,有些人也把这种流派叫做 LLM Native 的代码审计。我和许多师傅交流过这个问题,发现了一些有趣的东西,我们国内大部分人都是走的流派一,也就是用 LLM 辅助传统的 SAST,偏向于用 LLM 解决已有的问题,但国外有许多人其实走的是流派二,也就是 LLM 为主,甚至他们会认为随着 LLM 的发展,传统的 SAST 其实没有多少路能走了,说不定未来的 LLM 就能发展到你一个指令就帮你精确的发现所有漏洞,而且传统的 SAST 走到头也很难超过 CodeQL,要想走出一条新路子,只能从 LLM 下手。
流派一:SAST为主
这里我把两种流派都用我浅薄的认识来简单介绍介绍。流派一以 SAST 为主,LLM 只起辅助作用,既然是用 LLM 解决问题,我们就需要思考传统的 SAST,比如污点分析,到底有哪些问题呢?其实第一个最直观的想法就是找 Source 和 Sink,或许这么久以来我们已经有了非常强的污点分析引擎,但正因为我们不能精确的寻找到所有 Source 和 Sink 才导致了漏洞的产生,比如 Log4j。
长期以来这个过程其实纯粹靠专家经验,靠我们总结的一套可能的规范,我看过腾讯早期2021年的文章,当时还没有LLM,这个问题他们也非常头疼,也只能通过尽可能全面的手工标注来确保不遗漏 Source 和 Sink:

当然现在来到了 LLM 的时代,这种需要语义理解的问题正是 LLM 擅长的领域。今年腾讯云鼎的Blackhat议题里就提到了他们是怎么利用 LLM 解决这个问题的:从人工困局到智能破局 – 大模型在代码安全审计的探索与实践,简单来说就是利用 LLM 代替之前的人工标注过程去分析源码/文档,确保不遗漏 Source 和 Sink,在这个文章末尾他们也暗戳戳的提到了他们是怎么超越传统工具 CodeQL 的,那就是因为传统工具对于 Sink 太依靠建模了,根本不能 Cover 完全:

除了找 Source/Sink 还有什么问题吗?上面提到了MScan ,其实 MScan 就是一个典型的解决传统 SAST 缺陷的工作,MScan 瞄准的场景是微服务,上面提到过,即使对于传统的单项目的服务,想做污点分析也需要极大的开销,而对于横跨多个项目的微服务,污点分析的难度和开销是呈指数上升的,想用传统方法很难解决这个问题。Mscan的解决思路其实是三个设计:用 LLM 找微服务入口点、建立跨服务的调用图、距离引导的污点分析,第三个设计上面已经介绍过了,但我个人认为其实设计一和设计二都能用 LLM 实现,甚至还能包装个用 LLM 找 Sink 点,一个有趣的现象是,学长这篇工作当时设计一没用LLM解决,纯靠人力解决,然后被拒了,最后换成LLM直接拿了四大的Distinguished Paper Award,看来学术界也是很吃”利用LLM解决传统SAST难题”这一套的。
除了上面提到的这些,其实还有很多传统 SAST 难以解决的场景适合上 LLM,比如业务逻辑的洞、利用 LLM 识别 Sanitizer、解决 Java 中反射这类传统 SAST 会断边的情况等等,瞄准具体的问题。利用 LLM 解决,这便是流派一的主要思路。
流派二:LLM 为主
流派二以 LLM 为主,通过充分发挥 LLM 语义理解的能力来实现代码审计,但直接把代码打包发给 LLM 然后问类似”这个代码有漏洞吗?”的思路显然是行不通的,这必然会超过 LLM 的上下文,即使未来 LLM 不断的发展,上下文能到几个G,我认为这样的想法也是不可行的,毕竟用过 LLM 的都知道 LLM 存在极其严重的幻觉问题,要是上下文到了G这个数量级,很难想象这个幻觉会到什么程度,指望 LLM 帮我们直接实现端到端的代码审计是不切实际的。
那为什么我要说现在有种流派叫 LLM 为主呢?意思其实就是上面的做法不可行,但是还是有可行的做法的,至少我今年就看到了两个让人眼前一亮的实践。说到底之所以 LLM 不能直接端到端的帮我们发现漏洞是因为上下文和代码引入的噪声,如果我能对原有的代码做一次精简,是不是就能解决这个问题了呢?
今年USENIX 2025上有一篇文章就很好的体现了这个思路,他的做法是用代码属性图 (CPG),CPG 是一种将多种代码表示形式合并为单一图结构的统一程序分析方法,它将三种经典的静态分析图融合在了一起:
- 抽象语法树 (AST):保留代码的语法结构 。
- 控制流图 (CFG):表示程序的执行路径信息 。
- 程序依赖图 (PDG):追踪数据依赖和控制依赖关系
通过将代码转化为图,安全分析师可以使用图查询语言(如 Joern 工具使用的 CPGQL)来遍历图结构,从而通过特定查询来查找复杂的代码模式,并且由于 CPG 同时包含语法、控制流和数据流信息,它能非常精准地描述复杂的漏洞模式,这是传统单一分析方法难以做到。这个工作通过先使用静态分析工具 Joern 在CPG 上执行查询,提取潜在的漏洞执行路径及其交互变量,移除无关的干扰代码,构建出仅包含漏洞相关逻辑的“代码切片”,将精简后的代码切片交由 LLM 来判断代码是否存在安全问题,这也是为什么这个论文叫做 LLMxCPG 的原因,不是直接交代码,而是切片出可能有漏洞的执行路径交由 LLM 做分析,当然这个做法也不是十全十美的,毕竟从 CPG 提取代码切片的过程就由回到传统 SAST 上了,传统 SAST 会有的问题这里也会有,比如 Source/Sink 的完备性、控制流断边等等。
另一个有趣的工作不是发在安全顶会上的,而是发在 AI 顶会 NeurIPS 上的,这个工作叫做 LLMDFA,作者想到了一种更加别出心裁的思路,他会用 LLM 分析单个函数内部的数据流依赖关系,对每个函数提取数据流摘要,引导LLM识别函数内的变量依赖(如赋值、别名等),最后将函数抽象成变量的传播关系,形成跨过程的数据流路径,最后验证从 Source 到 Sink 的数据流路径在逻辑上是否可行。这个工作有几个亮点,比如因为他不是传统的SAST,所以可以分析不完整的代码片段,比如它通过自然语言定义 Source 和 Sink ,比如它甚至是语言无关的,能对不同的编程语言都做分析,缺点就是可能会消耗非常巨大的 token,并且实际效果存疑,但整个 idea 是非常让人眼前一亮的。
总结
无论是 LLM 还是 SAST,他的作用总归是帮助我们解决问题,我也相信这两种不同的流派在自动化代码审计的路上都能有不错的效果,也希望我未来能做一些有趣的实践。
原理好高深,我这种脚本小子,看起来只能用大佬开发出的工具了
25年12月blackhat上 https://www.cyberark.com/resources/threat-research-blog/vulnhalla-picking-the-true-vulnerabilities-from-the-codeql-haystack 也很值得参考 但是llm对于漏洞理解能力还是有偏差的 毕竟是通过预测推理 如果遇到一些bypass 或者组合、多步骤利用的方式 感觉凭现有的llm应该是很难识别出来 但是llm sast融入到cicd做扫描或者门禁 感觉也是一种可以落地的方案
ai是解决已知的未知,不是未知的未知 — AI for coding