REPOAUDIT:一种用于仓库级代码审计的自主LLM智能体

REPOAUDIT: An Autonomous LLM-Agent for Repository-Level Code Auditing

![[Pasted image 20250321163032.png]]

摘要

代码审计是一种以发现漏洞为目标的代码审查过程。大型语言模型(LLMs)在此任务中展现了巨大的潜力,能够无需编译即可分析程序,并根据指定提示实现定制化的漏洞检测。然而,将LLMs应用于仓库级代码审计存在显著挑战。LLMs固有的上下文限制和幻觉问题可能导致漏洞报告的质量较低。同时,软件仓库的大规模性带来了巨大的时间和token成本,阻碍了在现实场景中的效率和可扩展性。

本工作介绍了一种自主LLM智能体REPOAUDIT,旨在实现精确且高效的仓库级代码审计。通过配备智能体记忆功能,REPOAUDIT按需探索代码仓库,沿不同可行的程序路径分析单个函数中的数据流事实。它还引入了验证器,用于检查数据流事实以缓解幻觉,并检验潜在漏洞路径的路径条件可满足性,从而使得REPOAUDIT能够在代码审计中排除误报。实验表明,由Claude 3.5 Sonnet驱动的REPOAUDIT成功在15个真实系统中发现了38个真实漏洞,每个项目的平均耗时为0.44小时,花费为$2.54。

1. 引言

大型语言模型(LLMs)的快速创新显著提升了软件开发人员的生产力 [Wang等, 2021; Rozière等, 2023; Guo等, 2024]。诸如Copilot之类的LLM驱动IDE插件通过促进与LLMs的灵活交互,实现了敏捷的代码生成 [Barke等, 2023]。然而,在LLM时代,审计快速增长的代码库比编写代码更具挑战性。传统的程序分析技术,如动态分析和静态分析,主要关注观察运行时行为或符号化地推理编译期间生成的中间代码 [King, 1976; Cadar等, 2008; Calcagno等, 2009; Sui和Xue, 2016; Shi等, 2018a]。不幸的是,在人与LLM协作开发阶段,软件系统通常是非可执行的,甚至是不可编译的。此外,现有的分析技术需要深厚的专业知识,例如对编译器内部的深入理解 [Zhang等, 2024a; Zhou等, 2024]。因此,当前的程序分析方法往往无法满足现实世界中代码审计的实际需求 [Johnson等, 2013]。

近年来,大量研究集中在通过提示工程利用LLMs进行代码审计 [Fang等, 2024a; Hao等, 2023; Ding等, 2024]。与传统分析技术不同,LLM驱动的代码审计直接分析源代码,无需程序编译或执行。通过自然语言描述分析需求并在提示中提供少量示例,分析的定制化也得到了显著简化。然而,许多现有的LLM驱动代码审计技术大多局限于小型代码库,例如智能合约 [Sun等, 2024; Zhang和Zhang, 2024],缺乏支持复杂现实场景中仓库级代码审计的能力。

直接提示难以奏效

一种简单的解决方案是将仓库分解为更小的部分,并逐一提示模型处理这些部分。这种方法通常无法应对非局部漏洞,因为这类漏洞可能需要跨越大量相互关联的代码片段,涉及多个函数、类和文件进行推理。即使未来模型推理能力有显著提升,程序与transformer模型设计初衷所针对的自然语言文本之间的根本差异,仍使LLMs不足以进行全面的仓库分析。具体而言,代码仓库可以被概念化为一个巨大的图,其中节点表示单个语句,边捕获控制流、数据流及其他语句间的复杂依赖关系。这些关系在仓库级别上定义精确且极其复杂。例如,在项目shadowsocks-libev中,数据流边的数量超过一百万,远超任何LLM预训练数据样本中隐含图形结构的复杂性。这种分布差异导致直接提示无效,正如我们在第2.2节的实验所示。

此外,检测许多类型的漏洞需要沿特定程序路径推理属性。例如,当分配的内存未在某些程序路径上释放时即发生内存泄漏。检测此类所谓的路径敏感漏洞 [Shi等, 2018a] 需要将程序的图结构展开为单独的路径并逐一分析这些路径。然而,这会导致众所周知的路径爆炸问题,因为路径数量随语句数量呈指数增长。因此,直接提示类似于在一个巨大屏幕上展示一个大型项目,并期望人类审计员仅通过阅读和解释代码就能识别复杂且冗长路径上的漏洞——这一任务极不可能成功。

人工审计

实际上,人类审计员并非如此操作。现有认知科学文献表明 [Anicic等, 2012],人类在按顺序推理事件方面非常高效,例如按时间或空间顺序发生的事件。因此,人类审计员倾向于通过跟踪表示执行顺序的路径来探索代码中的复杂图形结构。为了避免过度探索,他们仅遍历与目标属性最相关的路径子集,并利用隐式抽象排除无关路径。有关检测空指针解引用的例子详见第2.1节。

我们的解决方案

普遍认为,LLMs的操作方式与人类相似,但具有更强的“耐力”和更广泛的知识获取能力 [Li等, 2023; Long等, 2024; Qian等, 2024]。基于这一观点,我们提出了一种新颖的基于LLM的仓库审计智能体,名为REPOAUDIT,灵感来源于人工审计实践。REPOAUDIT通过路径敏感和按需驱动的图遍历,解决了LLMs倾向于顺序推理与软件仓库固有复杂图形结构之间的基本错位问题。通过利用LLMs的抽象能力,REPOAUDIT通过排除无关代码区域和子路径来缓解路径爆炸问题。同时,它通过验证若干格式良好的属性对最终输出进行清理,从而最小化固有幻觉。

更具体地说,智能体REPOAUDIT由三个组件组成,包括启动器、探索器和验证器。首先,启动器根据调查中的属性识别起点,例如在扫描空指针解引用漏洞时的NULL值。其次,探索器按需遍历相关函数。类似于人类审计员逐函数分析代码的方式,探索器通过包含这些起点的函数向LLMs发起查询开始。与传统基于编译器的自动化扫描工具显式且程序化地枚举函数内的单个路径不同,探索器利用LLMs固有能力隐式区分相关路径与无关路径,并仅推导前者。如果在分析函数内路径后发现其他函数调用和函数内返回相关——例如,当空指针值通过这些函数边界传播时——系统会合成后续提示,以根据需要将扫描扩展到被调用函数或调用函数。第三,REPOAUDIT在将探索器的输出存储到智能体记忆之前对其进行检查,并通过检查错误程序路径的路径条件来审查漏洞报告候选。这种验证设计可以显著提高REPOAUDIT的精度。
我们以原型形式呈现了REPOAUDIT,并使用Claude 3.5 Sonnet为其提供支持。我们在三种关键漏洞类型上进行了测试:空指针解引用、内存泄漏和释放后使用(use-after-free)。实验对象包括15个真实世界的软件系统,平均代码行数为251,259行。结果表明,REPOAUDIT有效重现了21个先前检测到的漏洞,并发现了17个新漏洞,其中四个已在最新提交中修复,达到了65.52%的精度。此外,实验还表明REPOAUDIT表现出高效率且产生较低的token成本,单个软件系统的分析平均耗时0.44小时,花费$2.54。此外,我们与两种工业级静态漏洞检测工具进行了对比分析,即Meta INFER (Meta, 2025) 和 Amazon CODEGURU (Amazon, 2025)。结果发现,Amazon CODEGURU报告了18个误报,未发现任何真实漏洞;而Meta INFER报告了7个真实漏洞及2个误报。我们还使用另外两个LLMs对REPOAUDIT进行了评估,分别是DeepSeek R1和GPT-4 Turbo。特别是由DeepSeek R1驱动的REPOAUDIT检测到了44个真实漏洞,达到了75.86%的精度,而每个项目的平均花费仅为$0.57。值得注意的是,REPOAUDIT能够在开发阶段促进IDE级别的代码审计,这是Meta INFER和其他依赖编译的漏洞检测工具无法支持的。据我们所知,REPOAUDIT是首个完全基于LLM驱动并能够扩展到真实软件系统的代码审计技术。我们期望我们的努力能为其他LLM驱动的仓库级编码任务提供宝贵的见解。我们已发布了漏洞报告 [Guo等, 2025],并在接受后将公开REPOAUDIT的实现。

2. 前提条件

在本节中,我们首先讨论仓库级代码审计的本质。接下来,我们阐述LLMs在此任务中的局限性。最后,我们强调LLMs在处理基础任务时的若干内在优势,这些优势可以用于构建我们的仓库级审计工具。

2.1 审计需要对复杂图进行路径敏感推理

尽管某些类型的漏洞(例如API误用 [Li等, 2021])只需要对抽象语法树(ASTs)进行局部推理,并且可以通过高效的扫描工具相对容易地检测到,但许多关键漏洞类型要求将整个项目建模为一个巨大的图,并沿该图中的各个路径进行属性推理。例如,检测空指针解引用(Null Pointer Dereference, NPD)漏洞依赖于构建和分析一种称为数据依赖图(DDG)的专用图结构 [Ferrante等, 1984],其中节点表示语句,边表示特定语句中不同程序值之间的数据流事实。具体而言,如果变量u在语句sta处的值可能会影响变量v在语句stb处的值(沿某些程序路径),则存在从变量u在语句sta到变量v在语句stb的数据流事实,记作 u@sta→v@stbu@sta \rightarrow v@stbu@stav@stb。程序路径是按执行顺序排列的语句序列。如果存在满足路径上所有条件检查的输入,则该路径是可行的。因此,数据流事实可能发生在相距较远的两个语句之间,例如当全局变量在不同的源目录中被读取和写入时。

确定某个数据流事实在程序中是否可能成立需要收集可行的程序路径并沿路径分析数据流事实。以空指针解引用(NPD)检测为例。给定DDG,代码审计员应在可行程序路径上找到一条从空值(作为源值)到解引用指针(作为汇点值)的数据流事实链。
![[Pasted image 20250321163548.png]]

例如,在图1中,函数field2json在第4行初始化了一个空值,引发了数据流事实NULL@s4→json@s4NULL@s4 \rightarrow json@s4NULL@s4json@s4,在图1中标记为1。当repeated为false时,第4行json的值传播到第14行的返回语句。这一数据流事实记作json@s4→json@s14json@s4 \rightarrow json@s14json@s4json@s14,标记为2。随后,在函数parse_msg中,field2json的返回值被赋值给第7行的指针field_json,并在函数parse_msg的第8行进一步解引用,最终导致了一个NPD漏洞。

此外,我们研究了《2024 CWE Top 25最危险软件弱点》列表,这是一个精选的清单,涵盖了2024年报告的31,770个常见漏洞和暴露(CVE)记录中最关键和最常见的漏洞。我们的研究表明,25个弱点类别中有19个(占76%)需要对调用图、DDGs和控制流图(CFGs)进行全局的路径敏感推理以判断源-汇可达性,而只有6个类别可以通过基于ASTs的局部分析有效识别。因此,用于安全漏洞检测的代码审计需要对复杂图进行有效的路径敏感推理。

2.2 LLMs的不足

根据 [Rozière等, 2023; OpenAI, 2023],许多基础模型最初是在相对较短的文本或代码片段上进行预训练的。对于长上下文,像DeepSeek-V3 [Liu等, 2024]、Llama3系列 [Dubey等, 2024] 和 QWen2.5-Coder [Hui等, 2024] 这样的模型通常采用NTK感知的长度插值技术(如YaRN [Peng等, 2024])将上下文窗口从最初的4K/8K逐步扩展到128K个token。尽管这些模型在“大海捞针”(Needle in a Haystack)评估中表现良好 [Kamradt, 2023],但该任务旨在服务于RAG(检索增强生成),与我们在应用中所需的路径敏感程序理解能力并不完全对齐。
![[Pasted image 20250321163625.png]]

为验证我们的推测,我们进行了一个对照实验,其中向Claude 3.5 Sonnet提供了图1中显示的所有五个函数,以识别NPD漏洞,实验方法类似于最近的一项研究 [Fang等, 2024a]。这是一个对照实验,因为在实际操作中,我们无法保证能够全面了解与某个漏洞相关的所有函数集合。模型表现出显著的幻觉现象,报告称几乎所有解引用指针都具有空值。即使我们通过提供若干示例和解释来改进提示(例如如何发生空指针解引用漏洞),模型仍然产生幻觉,导致误报和错误解释。更多数值结果详见第4.3节。

2.3 LLMs的内在优势

从积极方面来看,我们观察到LLMs在范围受限的情况下可以有效执行基本分析。这种能力使我们能够超越传统程序分析方法,后者需要编译且难以高效扩展。具体而言,我们确定了审计中关键的几种基础能力:程序抽象、指针处理和可行程序路径探索。虽然传统审计工具依赖重量级但严格的技术来实现这些能力,熟练的人类审计员往往依靠直觉在有限的分析范围内应对这些挑战。我们在LLMs中也观察到了类似的特性。

2.3.1 程序抽象

抽象是程序分析可扩展性的关键。给定一个属性、一组初始程序点和定义的范围(例如,一个函数),抽象会识别出范围内与该属性相关的语句子集。这些语句形成一个自包含的小型程序,从而显著减少需要分析的路径数量。例如,在NPD检测中(如第2.1节所述),抽象目标是跟踪空值在程序中的传播,并关注关键语句,例如空指针赋值、值传播、保护传播的条件检查以及跨函数传播(例如,通过参数将指针传递给被调用函数、将其返回给调用函数或将它写入全局变量)。
![[Pasted image 20250321163659.png]]

在图2(a)所示的初步实验中,我们将图1中的函数field2json输入Claude 3.5 Sonnet并要求其对程序进行抽象。可以观察到,LLM返回的程序仅包含与空值传播相关的关键语句,而无关语句(如图1中field2json函数第8至12行的switch语句)已被移除。
![[Pasted image 20250321163637.png]]

值得注意的是,人类审计员通常会隐式地执行此类抽象,从而使他们能够在不进行过度路径探索的情况下分析复杂代码,避免了符号执行 [Cadar等, 2008] 和软件模型检查 [Clarke, 1997] 等经典工具的弊端。

2.3.2 指针处理

C语言中的指针变量和Java中的引用变量可能根据其运行时值指向不同的内存对象。因此,通过指针解引用进行的读写操作会动态生成基于指针值的数据流事实。确定指针变量可能指向的内存对象集合被称为指针分析(points-to analysis)[Smaragdakis等, 2015],这是下游静态分析(如DDG构建)中最困难的问题之一。传统的指针分析技术通常依赖保守和关系化的方法,往往会显著高估可能的内存对象集合,从而复杂化下游分析任务。相比之下,人类审计员通常能够通过理解程序语义直观且准确地确定特定指针变量的指针事实。高级LLMs在单个函数范围内也表现出类似的能力。例如,在图2(b)中,我们向Claude 3.5 Sonnet提供了图1中的函数field2json,并查询该函数返回值的指针事实。模型准确识别了函数内的两个可能的指针事实,甚至识别了使相应指针事实成立的路径约束。相比之下,符号静态分析器SVF [Sui和Xue, 2016] 默认在缺乏语义分析的情况下计算无路径条件的指针事实。

2.3.3 可行程序路径探索

只有当漏洞报告提供了一条可行的程序路径(从根因到症状)作为证据时,才被视为有效。为了确定可行性,传统工具依赖于将路径上的条件检查建模为符号约束(例如,以一阶逻辑公式的形式),并通过定理证明器(例如SMT求解器Z3 [de Moura和Bjørner, 2008])查询是否存在满足约束的输入。这一过程计算成本高昂且容易失败,因为将程序路径转换为逻辑公式需要探索大量程序路径,并建模缺乏直接逻辑表示的广泛程序行为(如循环、数组索引、别名、指针算术和无限字符串操作)。相比之下,人类及LLMs依赖抽象和直观的逻辑推理来评估可行性,这在有限范围内非常有效。
![[Pasted image 20250321163720.png]]

在图2©中,我们展示了使用Claude 3.5 Sonnet对函数field2json进行可行程序路径探索的示例。可以观察到,模型能够跳过无关语句(如switch语句),避免探索大量程序路径。它还发现了第5行和第13行分支条件之间的矛盾,排除了同时覆盖第6行和第14行的程序路径,从而报告了图2©中的三条可行程序路径。

3. REPOAUDIT

基于前一节的发现,我们提出了REPOAUDIT,一种自主的LLM驱动智能体,旨在通过模拟手动审计过程实现仓库级代码审计。其核心设计理念如下:由于LLMs难以在大规模程序图中推理路径敏感属性,REPOAUDIT采用以智能体为中心的方法,从外部导航这些图,并一次向模型提示一个单元(例如,一个函数)。这种按需驱动的导航根据模型对每个函数的响应进行调整,而专用的智能体记忆确保跨函数的分析结果能够无缝共享。这种方法使导航过程有效地复制了对原始大规模图进行路径敏感探索的意图。为了在函数级别实现高效且路径敏感的分析,REPOAUDIT通过提供明确的提示来利用LLMs的内在能力,这些提示涵盖程序抽象、指针处理和可行程序路径探索。
![[Pasted image 20250321163736.png]]

图3展示了我们智能体的架构。启动器工具(1)以指定的漏洞定义和目标仓库作为输入,识别分析所需的源值,例如用于NPD检测的空值。每个源值都会触发一个扫描过程。探索器工具(2)通过对模型进行迭代、按需驱动的仓库探索,一次分析一个函数。每次分析的结果存储在智能体记忆中,指导进一步的探索。分析提示是为每个函数动态生成的,提供了针对特定函数量身定制的详细抽象指令,并根据先前分析的结果进行调整。虽然智能体在单个函数分析中仍可能出现幻觉并生成误报漏洞报告,但一组验证器工具(3)从多个角度验证探索器的结果,包括控制流顺序的有效性和跨函数路径条件的可满足性。

3.1 启动器

正如第2.1节所讨论的,检测漏洞,特别是最普遍的CWE漏洞类型,涉及从明确定义的源值作为起点遍历特定图中的路径,以确定是否有任何汇点值可达。启动器识别扫描的起点(即源)。在我们的实现中,我们使用tree-sitter解析库创建了一套定制的模式匹配器,用于支持REPOAUDIT的漏洞类型的源。这些匹配器简洁明了,通常仅由几行代码组成,并且需要一次性实现工作。或者,最近的进展提供了一种有前景的方向,即使用LLMs合成此类匹配器 [Wang等, 2024a]。

3.2 探索器

对于启动器识别的每个源值,探索器对仓库进行一轮扫描。在每一轮中,探索器按需遍历一部分函数,从已识别的源开始。它查询LLM逐一分析函数,并将结果存储在智能体记忆中。探索器执行多项操作,每个操作都由相应的提示引导。这些操作包括:分析单个函数、选择要探索的函数以及生成漏洞报告候选。我们将详细介绍以下三项操作。

3.2.1 分析单个函数

如第2.1节所述,大多数漏洞类型的扫描可以简化为遍历有限的一组图,例如DDG和CFG。这使得可以为这些图类型使用通用分析提示,而无需针对特定漏洞的提示。此外,我们不显式枚举和分析单个路径,而是利用LLMs的内在能力区分路径并进行路径敏感推理。这种方法的关键在于第2.3.2节和第2.3.1节分别展示的指针处理和程序抽象,它们隐式地将函数缩减为显著更小的代码片段,路径数量大幅减少。这最终有助于第2.3.3节展示的可行程序路径的高效探索。
![[Pasted image 20250321163823.png]]

我们在图4中设计了用于分析单个函数的提示模板。在开头描述任务后,我们向LLM提供三步提示,逐步引导其在指针处理、抽象和可行程序路径探索中发挥能力。通过提供若干示例,我们在最后提出问题,要求LLM沿着不同可行程序路径从感兴趣的初始值开始识别数据流事实。

以函数field2json和第4行的初始空值为例。利用LLMs的内在优势,REPOAUDIT收集了图2©中显示的三条可行程序路径。通过沿第一条程序路径(记作p1)模拟程序执行,LLM识别出数据流事实NULL@s4→json@s4NULL@s4 \rightarrow json@s4NULL@s4json@s4json@s4→json@s14json@s4 \rightarrow json@s14json@s4json@s14。对于第二条和第三条程序路径(分别记作p2和p3),LLM仅识别出数据流事实NULL@s4→json@s4NULL@s4 \rightarrow json@s4NULL@s4json@s4

智能体记忆
在分析完一个函数后,探索器会为每个可行程序路径收集一组数据流事实,并将其存储到智能体记忆中。具体而言,智能体记忆是一个与函数f和程序值v@s相关的函数M。M(f, v@s)中的每个元素是一对程序路径和一组数据流事实。例如,在分析完函数field2json和第4行的空值后,记忆将(field2json, NULL@s4)映射如下:
M(field2json,NULL@s4)={(p1,{NULL@s4→json@s4,json@s4→json@s14}),(p2,{NULL@s4→json@s4}),(p3,{NULL@s4→json@s4})} M(\text{field2json}, \text{NULL@s4}) = \{ (p1, \{\text{NULL@s4} \rightarrow \text{json@s4}, \text{json@s4} \rightarrow \text{json@s14}\}), (p2, \{\text{NULL@s4} \rightarrow \text{json@s4}\}), (p3, \{\text{NULL@s4} \rightarrow \text{json@s4}\}) \} M(field2json,NULL@s4)={(p1,{NULL@s4json@s4,json@s4json@s14}),(p2,{NULL@s4json@s4}),(p3,{NULL@s4json@s4})}

3.2.2 选择要探索的函数

在分析完一个函数后,如果目标程序值跨函数边界传播,探索器会查询底层调用图以识别相关函数以进行进一步探索。这种后续探索由逃离当前函数边界的程序值引导。例如,在图1中,field2json第4行的空值传播到返回值。因此,在下一步中,探索器分析field2json的调用函数parse_msg,并检查返回值是如何传播的。

需要注意的是,如果程序值未逃逸,则分析不会导致对其他函数的探索。例如,如果我们考虑第二和第三条程序路径(即第3.2.1节示例中的p2和p3),函数field2json第4行中json的值并未传播。因此,探索器不会进入任何其他函数。此外,我们利用存储在记忆中的现有数据流事实作为缓存,以避免冗余分析。具体而言,在分析特定程序值在函数中的传播之前,探索器首先检查智能体记忆,并确定是否已经完成过该分析。在我们的评估中,我们将量化这种缓存策略如何减少计算成本。

3.2.3 生成漏洞报告候选

在分析完一个函数后,探索器评估是否可以识别出任何新的漏洞候选。以NPD检测为例,探索器检查是否识别出任何到达汇点值的新数据流事实,即解引用指针。如果是,则通过组装跨函数的完整数据流事实轨迹以及相应的跨过程程序路径生成漏洞报告,这涉及链接存储在智能体记忆中的数据流事实。例如,探索器进入函数parse_msg并检查field2json返回值的传播。基于图1中发现的两个数据流事实(即标记为3和4的事实),探索器到达一个汇点值,即第8行的解引用指针。因此,探索器识别出一个潜在漏洞,并通过连接数据流事实1、2、3和4来报告它。对于某些漏洞类型(如内存泄漏漏洞),如果探索器无法沿某些可行路径到达任何汇点(例如free函数调用的参数),则会报告一个潜在漏洞。

3.3 验证器

为了提高漏洞报告的质量,我们为探索器引入了两种验证机制。

数据流事实与控制流的对齐验证
在处理复杂代码时,LLM可能会产生幻觉,并错误地推断某个数据流事实u@s1→v@s2u@s1 \rightarrow v@s2u@s1v@s2沿某条程序路径p违反了控制流顺序。具体来说,这意味着语句s2必须在程序路径p中出现在另一个语句s1之后,但模型得出结论认为在s2处定义的变量可被s1使用。为了检测此类错位,我们采用基于解析的分析器来验证控制流顺序。只有符合控制流顺序的数据流事实才会存储在智能体记忆中。

跨过程程序路径的可行性验证
虽然单个函数内的路径可行性由探索器本身进行检查,但跨函数的条件检查可能会出现矛盾,从而导致不可行的跨过程程序路径。回想一下,每个漏洞报告是对多个数据流事实的串联,例如图1中的1、2、3和4。不同函数中的数据流事实在相应程序路径的特定条件下成立。只有当不同函数中的路径条件不矛盾(即其逻辑合取可满足)时,漏洞报告候选才有效。为了检查有效性,我们向LLM提供跨过程路径,并要求其识别路径条件中的任何矛盾。如果发现矛盾,则丢弃该漏洞报告。

4. 评估

我们利用tree-sitter解析库为REPOAUDIT提供了一组基础工具,例如调用图构造器和控制流顺序验证器。我们选择LLM Claude 3.5 Sonnet为REPOAUDIT、基线和消融实验提供支持。我们使用另外两个LLMs(GPT-4 Turbo和DeepSeek R1)评估REPOAUDIT的有效性,并在附录中呈现主要结果。为了减少提示的随机性,我们将温度设置为0.0。遵循审计中的常见做法 [Heo等, 2017],我们为调用上下文引入一个上限K并将其设置为4,即REPOAUDIT最多调查四个函数之间的数据流事实。

4.1 漏洞类型和数据集

我们专注于三类漏洞:NPD、ML和UAF,它们均属于CWE Top 25最危险弱点。我们的评估首先旨在重现先前工作中报告的漏洞。具体而言,我们调查计算机安全和软件工程领域的近期工作,并收集作者发布的漏洞报告 [Huang等, 2024; Shi等, 2021; 2018b]。此外,我们尝试在目标代码仓库中检测新漏洞。如表1所示,我们选择了五个维护良好的项目。
![[Pasted image 20250321163909.png]]

4.2 REPOAUDIT的性能

设置和指标
在检测NPD漏洞时,我们将字面值NULL识别为源,将解引用指针识别为汇点。在检测ML和UAF漏洞时,我们重点关注报告漏洞中使用的内存分配/释放函数,并通过解析识别其返回值/参数。为了重现先前研究中报告的漏洞,我们检出仓库并在相应的漏洞提交中分析代码。对于新发现的漏洞,我们手动检查它们是真实漏洞(TPs)还是误报(FPs),以此计算REPOAUDIT的精度。此外,如果新发现的漏洞仍存在于仓库的最新版本中,我们会将其报告给开发者。为了量化代码审计的资源消耗,我们在分析每个仓库时测量输入/输出token成本、时间成本和财务成本。

结果
![[Pasted image 20250321163926.png]]

如表2所示,MagickCore项目的基准报告包含七个漏洞,而其他项目的基准报告各识别一个漏洞。结果表明,REPOAUDIT成功检测到所有基准报告中列出的漏洞。此外,REPOAUDIT还发现了17个新漏洞,其中4个已在最新提交中修复,而13个漏洞在最新版本中仍未解决。总计,REPOAUDIT报告了38个TPs和20个FPs,精度为65.52%。值得注意的是,REPOAUDIT检测到的20个TPs是跨过程漏洞。通过利用LLM揭示的数据流事实,REPOAUDIT可以探索相关函数并追踪跨不同函数的错误程序路径。最后,REPOAUDIT展现了高效性和成本效益。平均而言,使用REPOAUDIT分析一个程序仅需0.44小时(即1,577.22秒),在100.67轮提示内完成代码审计。根据Claude 3.5 Sonnet的定价政策,每个项目审计的平均成本为$2.54,每次真实漏洞检测的成本仅为$1.03。

4.3 与LLM驱动的漏洞检测器的比较

设置和指标
根据我们的调查,存在两类LLM驱动的代码审计技术,即端到端的少样本链式思维(CoT)提示方法和以智能体为中心的方法。具体而言,端到端少样本CoT提示方法可以分为两类,包括单函数级别漏洞检测和多函数级别漏洞检测。首先,单函数级别漏洞检测被许多近期研究广泛采用和评估 [Chen等, 2023; Ding等, 2024]。由于单个函数的长度有限,上下文长度不会影响该技术的适用性。为了评估单函数级别漏洞检测,我们收集了所有包含导致LLM生成真实漏洞(TPs)的源值的函数,并添加少量示例,要求LLM确定该函数是否可能引入特定类型的漏洞。其次,多函数级别漏洞检测器尝试将整个程序输入LLM,以便包含漏洞函数的调用上下文 [Wang等, 2024b]。然而,现实世界软件系统的巨大规模使得提示的长度超过了LLM的上下文长度限制。因此,在我们的评估中,我们仅将覆盖漏洞程序路径的相关函数输入LLM。在以智能体为中心的方法中 [Wang等, 2024a; Li等, 2024a;b],我们选择LLMDFA [Wang等, 2024a] 作为基线进行评估,因为它支持无需编译且可定制的分析。由于LLMDFA无法支持内存泄漏(ML)检测,我们将REPOAUDIT与LLMDFA的比较集中在NPD和UAF漏洞检测上。需要注意的是,LLMDFA需要总结每个函数中源、参数、输出值与汇点、参数或返回值之间的数据流事实。因此,整体计算成本——时间、token和财务成本——可能会显著增加。为避免高昂的计算成本,我们在两种设置下进行了一组对照实验。在第一种设置中,LLMDFA应用于覆盖漏洞程序路径的函数,为这些函数生成数据流摘要。在第二种设置中,LLMDFA的任务是从每个源值生成数据流摘要。我们将这两种设置分别称为LLMDFA-PATHSCAN和LLMDFA-SRCSCAN。值得注意的是,LLMDFA-PATHSCAN和LLMDFA-SRCSCAN的提示轮次和计算成本低于LLMDFA,因为前两者仅推理仓库中的一部分函数。通过将其成本与REPOAUDIT的成本进行比较,我们可以突出我们的方法相对于现有以智能体为中心技术的优越效率。

结果
![[Pasted image 20250321163951.png]]

图5展示了单函数级别漏洞检测器、多函数级别漏洞检测器和REPOAUDIT之间的比较结果。单函数级别漏洞检测器只能检测一个过程内NPD漏洞。主要原因有两点:第一,它仅访问漏洞路径中的最后一个函数,缺乏该函数的调用上下文。例如,在NPD检测中,单函数级别检测器无法确定参数是否为空,这使其无法检测跨过程漏洞。第二,模型可能忽略函数内的关键控制流,导致其无法准确识别特定值的数据流事实,从而在过程内漏洞检测中召回率较低。例如,如附录2中的代码清单1所示,LLM忽略了第6行的错误处理分支,这导致函数在未释放由damon_new_ctx()分配的内存对象的情况下返回,进而未能检测到此内存泄漏漏洞。
![[Pasted image 20250321164015.png]]

表3显示了LLMDFA和REPOAUDIT的比较结果。平均而言,LLMDFA-PATHSCAN的提示轮次数是REPOAUDIT的165倍,输入token数量是123倍。类似地,对于LLMDFA-SRCSCAN,提示轮次数是REPOAUDIT的1,737倍,输入token数量是1,193倍。需要注意的是,在实际场景中,LLMDFA的实际成本会更高。这种显著的性能差距源于LLMDFA在每个函数内对特定程序值进行成对分析。例如,为了检测图1中示例程序中的NPD漏洞,LLMDFA从函数field2json第4行的空值开始,分析其与该函数内所有参数、返回值和解引用指针(例如第8行的field)之间的依赖关系。相比之下,REPOAUDIT仅需两轮与LLM的交互即可分析函数field2json和parse_msg以检测此NPD漏洞。

通过访问完整的调用上下文,多函数级别漏洞检测器能够检测到10个漏洞。这表明提供额外的调用上下文可以提高LLM的漏洞检测能力。然而,改进仍然有限。由于幻觉问题,LLM可能仍会错误地分析某些过程内和跨过程的数据流事实,导致大量误报(FPs)。相比之下,REPOAUDIT通过采用程序抽象进行路径敏感推理,有助于精确发现用于代码审计的数据流事实。

4.4 与工业级漏洞检测器的比较

设置和指标
我们选择了两种典型的静态漏洞检测工具作为工业工具的代表,分别是Meta INFER (Meta, 2025) 和 Amazon CODEGURU (Amazon, 2025)。具体而言,Meta INFER是Meta开发的一种静态分析工具,用于检测程序漏洞。得益于其复杂的内存模型 [Calcagno等, 2009],Meta INFER在内存漏洞检测方面表现出色。我们评估中的三种漏洞类型均受Meta INFER支持。值得注意的是,Meta INFER仅适用于能够成功编译的项目。Amazon CODEGURU作为一项AWS服务,结合了机器学习和自动化推理来识别潜在漏洞。与Meta INFER不同,Amazon CODEGURU可以直接分析源代码而无需编译。由于Amazon CODEGURU不支持ML检测,我们仅对其在实验对象上进行NPD和UAF检测的效果进行评估。运行这两种工业级漏洞检测工具后,我们手动检查漏洞报告,标记真实漏洞(TPs)和误报(FPs),并将TPs与第4.2节中REPOAUDIT发现的结果进行比较。

结果
![[Pasted image 20250321164026.png]]

表4展示了Meta INFER和Amazon CODEGURU的结果。如表4的“Build”列所示,13个项目可以成功编译。然而,其中五个项目仍然导致Meta INFER崩溃,即ID为N3、M3、M5、U1和U2的项目,即使我们尝试了多个版本的Infer(包括v1.2.0(最新版)、v1.0.0和v0.9.0)。我们已将这些问题报告给Meta INFER,但尚未收到任何回应。编译失败以及对成功编译项目的分析失败表明,这种依赖编译的漏洞检测工具适用性受限且稳定性不足。最终,我们仅成功使用Meta INFER分析了八个项目,总共获得七个TPs和两个FPs。值得注意的是,对于项目ID为N4的项目libfreenect,Infer生成的五个TPs基于假设:外部API realloc和malloc在分配失败时可能返回空指针。然而,这一假设并不总是成立。在我们的实现中,REPOAUDIT不依赖此类假设,而是从字面空值或其他用户定义的API开始分析。最后,尽管Meta INFER以其强大的内存模型著称,它仍未能检测到REPOAUDIT发现的任何ML或UAF漏洞。工业静态漏洞检测工具的不精确性使开发人员在生产线上遗漏了许多关键漏洞。相比之下,REPOAUDIT不仅支持无需编译的分析,具有更广泛的适用性,还展现出更强的检测能力,总共识别出38个TPs。

如表4的最后两列所示,Amazon CODEGURU在目标的10个项目中未检测到任何TPs,同时生成了18个FPs。由于其固有的形式化推理技术和机器学习模型的局限性,CODEGURU只能检测某些模式的漏洞,并且对代码中保持语义不变的变异缺乏鲁棒性。相比之下,REPOAUDIT利用LLMs解释程序语义,使其能够发现数据流事实以实现有效的漏洞检测。

4.5 消融研究

设置和指标
为了评估每项技术设计的有效性,我们引入了REPOAUDIT的三个消融变体,分别是REPOAUDIT-NOABS、REPOAUDIT-NOVAL和REPOAUDIT-NOCACHE。具体而言,REPOAUDIT-NOABS跳过程序抽象,即删除图4提示模板中的第二步。REPOAUDIT-NOVAL移除探索器发现的数据流事实的验证,并跳过对漏洞报告的检查。REPOAUDIT-NOCACHE在探索器分析单个函数时禁用智能体记忆的缓存策略。

结果
![[Pasted image 20250321164036.png]]

表5展示了REPOAUDIT-NOVAL和REPOAUDIT-NOABS的结果。在没有程序抽象的情况下,REPOAUDIT-NOABS使TP数量减少了44.74%,而报告的FP数量增加了105%,导致精度下降至33.87%。这一下降归因于函数内众多条件分支、循环结构及其嵌套组合所形成的复杂控制流和多条执行路径。在分析这些情况时,模型更容易产生幻觉,导致漏检错误传播路径或错误识别不存在的路径。

当验证器被禁用时,FP的数量增加到41个,增幅为105%。这一增长主要由各种条件分支和跳转语句(如if-else和switch)以及某些分支中的早期退出(例如错误处理)引起。没有验证器的情况下,LLM可能会产生幻觉,无法考虑这些关键分支,从而导致大量不符合可达性要求的虚假数据流事实。此外,LLM发现的分支条件之间的冲突也有助于提高REPOAUDIT的精度。
在这里插入图片描述

REPOAUDIT-NOCACHE的结果见表6。对于项目icu,分析时间超过72小时,提示轮次超过20,000次,是REPOAUDIT的30多倍。对于其余项目,平均而言,提示轮次增加了3.55倍,财务成本增加了3.48倍,分析时间增加了3.75倍。此外,我们观察到缓存命中率与项目的规模和DDG的密度密切相关。在具有复杂调用图和密集DDG的项目中,单个函数可能出现在多个错误值传播路径中。例如,在icu项目中,与解析相关的函数在缓存中命中了623次。在这种情况下,缓存策略有效降低了分析成本,并将分析时间保持在可接受范围内。

4.6 局限性与未来工作

尽管REPOAUDIT在分析真实世界的软件系统方面展现出显著潜力,但它仍有两个主要缺点。首先,REPOAUDIT的开销受到所分析仓库中源数量的严重影响。在存在大量源的情况下,REPOAUDIT可能会产生巨大的时间、token和财务成本。其次,REPOAUDIT在检测跨过程数据流事实时并不完全精确,因为它仅检查最多四个函数之间的流动,这可能导致其遗漏涉及更长调用链的复杂漏洞。未来,我们计划通过并行化优化仓库探索,从而减少整体分析的时间成本。此外,微调小型模型作为调用者/被调用者选择的规划器也是促进仓库级代码审计的一个有前途的方向。效率的提升和财务成本的降低将允许对具有更长调用链的仓库进行探索。

5. 相关工作

用于代码审计的LLMs
大量文献集中于利用LLMs进行代码审计。近年来,BigVul [Fan等, 2020]、PrimeVul [Ding等, 2024] 和 DiverseVul [Chen等, 2023] 等各种基准测试已建立。然而,这些基准测试缺乏漏洞函数的调用上下文,从而降低了此类函数级代码审计技术的有效性 [Risse和Böhme, 2024]。对于仓库级代码审计,现有技术通常可分为两类。第一类技术利用LLMs为符号代码分析器提供专门的先验知识或审查初始漏洞报告,而代码库的主要扫描仍由传统的符号分析器完成 [Wang等, 2024c; Li等, 2024b;a]。一个典型例子是IRIS [Li等, 2024b],它使用LLMs定位程序中的敏感值,帮助CODEQL [Avgustinov等, 2016] 等传统分析器进行污点式漏洞检测。然而,由于依赖基于编译的符号分析,这些技术缺乏无需编译即可进行IDE级别分析的能力。第二类方法利用LLMs作为代码解释器,通过提示工程提取语义属性 [Fang等, 2024a; Wang等, 2024b;a; Sun等, 2024]。通常,LLMDFA [Wang等, 2024a] 和 LLMSAN [Wang等, 2024b] 结合少样本CoT提示生成数据流路径以进行漏洞检测。我们的工作REPOAUDIT属于后者,利用LLMs进行无需编译的语义分析以及结合符号方法。与LLMDFA不同,REPOAUDIT采用按需驱动策略进行代码库探索,避免为所有函数生成详尽的数据流摘要,从而增强分析的可扩展性。此外,REPOAUDIT通过单独分析函数而不是仓库级端到端提示,在召回率方面优于LLMSAN,有效缓解整体分析中的幻觉问题。

用于编码任务的LLM智能体
大量研究集中在为各种编码任务建立LLM智能体,涵盖程序修复 [Zhang等, 2024b; Bouzenia等, 2024]、测试 [Fang等, 2024b; Xia等, 2023] 和分析 [Li等, 2024a; Wang等, 2024b;a]。与端到端解决方案相比,以智能体为中心的设计在缓解LLMs固有缺陷(包括上下文限制和幻觉)方面被证明是有效的。具体而言,以智能体为中心的设计通常包含三个关键设计方面。首先,许多现有工作利用领域专家知识促进程序分解,使智能体能够解决更易管理的子问题,同时尽量减少幻觉 [Wang等, 2024a; Zhang等, 2024b; Xia等, 2024]。例如,PYDEX模拟手动程序调试和修复过程,分别解决语法和语义正确性问题 [Zhang等, 2024b]。其次,近期研究致力于利用LLMs在规划方面的能力 [Bouzenia等, 2024; Li等, 2024a]。例如,基于智能体的黑客工具 [Google, 2024a;b; Fang等, 2024b] 根据LLM反馈检查调用者和被调用者来探索程序。另一方面,REPAIRAGENT利用LLMs选择适当的行动以理解并修复漏洞 [Bouzenia等, 2024]。与第一类不同,具有规划能力的智能体可以以最少的人工干预自主解决问题。第三,许多现有方法向智能体引入了多种专家工具,有效缓解LLM幻觉。除了常用的工具(如Python解释器和编译器),一些智能体还结合了手工制作的特定领域工具,包括专用静态分析器 [Li等, 2024a],甚至利用通用领域专业知识合成工具 [Wang等, 2024a; Yang等, 2024]。在这项工作中,REPOAUDIT模拟手动代码审计过程,将问题分解为分析单个函数中的数据流事实的问题。与基于智能体的黑客工具不同,REPOAUDIT在其记忆中维护数据流事实,并促进验证以缓解幻觉。作为仓库级代码审计的智能体,其设计有可能启发其他编码任务中的以智能体为中心的设计。

6. 结论

本文介绍了REPOAUDIT,一种自主的LLM智能体,能够实现精确且高效的仓库级代码审计。通过模拟手动代码审计过程,REPOAUDIT利用LLM的内在优势(如程序抽象),并对仓库中的程序进行路径敏感推理。我们的实验表明,REPOAUDIT具有高精度和高效率,达到了65.52%的精度,并且每个仓库的平均分析时间仅为0.44小时。REPOAUDIT成功重现了21个先前检测到的漏洞,并发现了17个潜在漏洞,其中4个已经修复。我们的研究展示了基于LLM的代码审计的巨大潜力,为灵活且可配置的代码安全分析提供了一条有前景的路径。

影响声明

本文展示的工作旨在推进机器学习领域的发展,目标是解决一个复杂的代码推理任务,即仓库级代码审计。我们已在上文中阐明了我们工作的局限性。我们预计我们的工作不会带来广泛的负面影响,尽管将LLMs用于仓库级代码审计可能存在某些风险,例如私有组织中的源代码泄露和潜在的高额token成本。同时,值得进一步讨论的是,我们的工作有可能通过LLMs的力量显著改变软件工程领域。具体而言,基于LLM的代码审计不仅能够以少量定制化实现对不完整程序的分析,还解决了代码审计中的其他挑战。

首先,传统的代码审计主要依赖于特定版本的中间表示(IRs)。随着编译基础设施的演进,IR代码的版本会发生变化,需要迁移实现以支持新版本IR的分析。例如,Clang编译器在过去十年中经历了十次重大版本更新,导致不同编译器版本生成的IR之间存在差异。这些IR差异在长期内需要大量的人工努力来迁移代码审计实现以适应每次版本更新。然而,基于LLM的代码审计直接操作源代码,并支持不同的语言标准。

其次,传统代码审计依赖于各种抽象和精度设置,尤其是指针分析——这是传统代码审计工作流程中的基本预分析步骤。具体而言,代码审计工具的开发者必须考虑不同的精度设置并相应地实现分析算法。这一过程需要大量的实现工作。相比之下,与程序语义对齐的LLMs充当了程序语义的解释器,消除了提出抽象和在特定精度设置下实现分析的需求。

第三,传统代码审计需要推理IR的语义,并针对不同语言重新实现相同的算法。相比之下,LLMs作为通用代码解释器,在理解短代码片段方面表现出色,无论使用何种编程语言。通过遵循类似的提示策略,我们可以轻松扩展REPOAUDIT以分析其他语言的程序,包括但不限于C/C++、Python和JavaScript。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐