From Naptime to Big Sleep: Using Large Language Models To Catch Vulnerabilities In Real-World Code
介绍
在我们之前的文章中,项目午睡:评估大型语言模型的进攻安全能力,我们介绍了大型语言模型辅助的漏洞研究框架,并通过提升 Meta 的 CyberSecEval2 基准的最先进性能展示了其潜力。从那时起,午睡演变为大睡,这是谷歌项目零与谷歌 DeepMind 之间的合作。
今天,我们很高兴分享大睡代理发现的第一个现实世界漏洞:在SQLite中存在一个可利用的栈缓冲区下溢漏洞,SQLite 是一个广泛使用的开源数据库引擎。我们在十月初发现了这个漏洞并向开发者报告,他们在同一天修复了它。幸运的是,我们在其正式发布之前就发现了该问题,因此 SQLite 用户未受到影响。
我们相信这是 AI 代理首次发现广泛使用的现实世界软件中先前未知的可利用内存安全问题的公开示例。今年早些时候,在 DARPA AIxCC 活动上,亚特兰大团队在 SQLite 中发现了一个空指针解引用,这激励我们进行测试,以查看是否能找到更严重的漏洞。
我们认为这项工作具有巨大的防御潜力。在软件发布之前发现漏洞,意味着攻击者没有利用的机会:漏洞在攻击者有机会利用之前就被修复。模糊测试已经帮助了很多,但我们需要一种方法来帮助防御者发现那些难以(或不可能)通过模糊测试发现的错误,我们希望 AI 能够缩小这个差距。我们认为这是一个有前途的路径,最终实现防御者的非对称优势。
该漏洞本身非常有趣,现有的 SQLite 测试基础设施(通过 OSS-Fuzz 和项目自身的基础设施)并未发现此问题,因此我们进行了进一步的调查。
方法论
午睡和现在的大睡的一个关键动机因素是持续的野外发现已发现并修补的漏洞的变种。随着这一趋势的持续,显然模糊测试未能捕捉到这些变种,而对于攻击者来说,手动变种分析是一种具有成本效益的方法。
我们还认为,这种变种分析任务比更一般的开放式漏洞研究问题更适合当前的 LLM。通过提供一个起点——例如先前修复漏洞的详细信息——我们消除了漏洞研究中的许多模糊性,并从一个具体的、良好基础的理论开始:“这是一个先前的错误;可能在某处还有另一个类似的错误”。
我们的项目仍处于研究阶段,目前我们正在使用已知漏洞的小程序来评估进展。最近,我们决定通过在 SQLite 上进行第一次广泛的现实世界变种分析实验来测试我们的模型和工具。我们收集了一些最近的 SQLite 代码库提交,手动删除了琐碎和仅文档更改。然后,我们调整了提示,向代理提供了提交消息和更改的差异,并要求代理审查当前代码库(在HEAD)中可能未修复的相关问题。
发现的漏洞
该漏洞非常有趣,因为在一个(否则)为索引类型的字段 iColumn 中使用了特殊的哨兵值 -1:
7476: struct sqlite3_index_constraint {
7477: int iColumn; /* Column constrained. -1 for ROWID */
7478: unsigned char op; /* Constraint operator */
7479: unsigned char usable; /* True if this constraint is usable */
7480: int iTermOffset; /* Used internally - xBestIndex should ignore */
7481: } *aConstraint; /* Table of WHERE clause constraints */
该模式导致了一个潜在的边缘情况,所有使用该字段的代码均需处理,因为有效的列索引应为非负值。
函数 seriesBestIndex 未能妥善处理此边缘情况,导致在处理对 rowid 列的约束查询时,写入了负索引的栈缓冲区。在我们提供给代理的构建版本中,启用了调试断言,并在第 706 行检查了该条件。
619 static int seriesBestIndex(
620 sqlite3_vtab *pVTab,
621 sqlite3_index_info *pIdxInfo
622 ){
...
630 int aIdx[7]; /* Constraints on start, stop, step, LIMIT, OFFSET,
631 ** and value. aIdx[5] covers value=, value>=, and
632 ** value>, aIdx[6] covers value<= and value< */
633 const struct sqlite3_index_constraint *pConstraint;
...
642 for(i=0; i<pIdxInfo->nConstraint; i++, pConstraint++){
643 int iCol; /* 0 for start, 1 for stop, 2 for step */
644 int iMask; /* bitmask for those column */
645 int op = pConstraint->op;
...
705 iCol = pConstraint->iColumn - SERIES_COLUMN_START;
706 assert( iCol>=0 && iCol<=2 );
707 iMask = 1 << iCol;
...
713 if( pConstraint->usable==0 ){
714 unusableMask |= iMask;
715 continue;
716 }else if( op==SQLITE_INDEX_CONSTRAINT_EQ ){
717 idxNum |= iMask;
718 aIdx[iCol] = i;
719 }
720 }
在发布版本中,该断言并不存在。在我们的测试中(结果可能因编译器和优化级别而异),第 718 行的后续写入将导致 aIdx 缓冲区下溢,破坏 pConstraint 指针的最低有效 32 位,该指针将在循环的下一次迭代中被解引用,从而引发潜在的可利用条件。
考虑到该漏洞的性质,对于我们这些人类研究者而言,确切理解如何触发它并不简单。显然,对 ROWID 列的约束将是一个良好的起点,但需要更深入地分析代码。代理对 SQLite 的理解显然超出我们的水平,因此可以省略一些步骤。
一个常见的情况是,模型在生成测试用例时会直接使用 generate_series 虚拟表。(我们还观察到模型首先研究可用的虚拟表,但显然它能够在此应用已有的知识)。
轨迹亮点
在基于 Gemini 1.5 Pro 的成功运行中,种子提交为 [1976c3f7];这是一个相对较大且不明显的变更。代理发现的漏洞与种子提交中的变更仅 loosely 相关——在手动变体分析中,这并不罕见,理解代码库中的一个漏洞往往会导致研究者发现其他问题。
以下是选定的亮点,我们的评论以斜体显示——所有 ASSISTANT 块中的文本均来自代理。
代理对差异中的变化进行了合理评估,并以此作为研究的起点:
ASSISTANT
该 CL 通过引入 termFromWhereClause 函数改善了虚拟表查询中 OR 分支共享的 WHERE 条件的处理。该函数从 WhereClause 中检索特定条件,考虑嵌套子句。在此更改之前,由于条件索引的映射不正确,常见条件可能未得到正确处理,可能导致低效的查询计划或不正确的结果。最可能的漏洞涉及在 allocateIndexInfo 和 whereLoopAddVirtualOne 函数中对 WHERE 子句约束的错误处理,CL 修改了如何使用新辅助函数的约束偏移量。我将首先检查 allocateIndexInfo 函数……
经过几步(省略)探索相关代码后,代理形成了一个更具体的假设,并尝试通过运行补丁中的修改版测试用例来验证,但由于补丁中包含的测试用例依赖于我们配置中不可用的扩展,因此未能成功:
TOOL(debugger_run)
程序在没有崩溃或触发断点的情况下退出。退出代码为 1。
STDERR 上的输出是:
Runtime error near line 2: no such module: tcl
Parse error near line 3: no such table: x1
ASSISTANT
程序终止的原因是 TCL 模块不可用。该测试依赖于 TCL 虚拟表模块,而该模块在编译 SQLite 时需启用。提供的程序输入基于 bestindexC.test 文件,该文件使用 TCL 模块来测试虚拟表的最佳索引选择。
为了解决此问题,程序输入需要更改为使用不依赖于 TCL 模块的其他虚拟表。然而,漏洞与虚拟表查询规划相关,因此我们仍需一个虚拟表来触发该问题。
我将使用 code_browser_source 工具搜索 SQLite 中内置虚拟表的示例,并调整输入以使用其中一个,同时在 whereLoopAddVirtualOne 中再次设置断点……
代理优雅地适应了这一挫折,认识到修复测试用例所需的步骤,并在几步之后获取了构建到达 whereLoopAddVirtualOne 的测试用例所需的所有信息:
ASSISTANT
sqlite3_create_module 函数用于注册虚拟表模块。shell.c 文件中包含多个实例,包括 generate_series、fsdir、completion 和 expert 模块。json 表也值得关注。现在我将重点放在 generate_series 上作为起点,尝试构建一个使用该模块的查询,以可能触发不正确的约束处理。
……
请注意,在最初发现和重现漏洞的过程中,代理对其试图触发的“错误约束处理”描述相对模糊;在这种情况下,完整的轨迹读起来有些像人类研究者——从一个想法开始,在调查过程中想到其他几个想法,最终产生一个重现稍微不同错误的测试用例。在这种情况下,当在 generate_series 表上调用 whereLoopAddVirtualOne 时,我们最终进入 seriesBestIndex,在那里约束确实处理不当。
然而,特别有趣的是,一旦程序崩溃,模型在解释事情方面表现得更为出色——在生成根本原因分析和完整的崩溃报告时,我们可以看到代理清晰地构建了一个测试用例,以触发特定的已识别边缘情况:
ASSISTANT
相关字段是 sqlite3_index_constraint 结构中的 iColumn,该字段表示约束的列号。要导致断言失败,我们需要对索引大于 3 或小于 1 的列施加约束。
让我们构建一个涉及这些列约束的 SQL 查询。generate_series 表包含名为 value、start、step 和 stop 的列。索引为:
- value: 0
- start: 1
- step: 2
- stop: 3
要触发这个漏洞,我们可以在 ROWID 上添加一个约束。ROWID 上的约束使用 iColumn = -1。
以下是一个示例查询:
SELECT * FROM generate_series(1,10,1) WHERE ROWID = 1;
这个查询应该会导致 seriesBestIndex 函数崩溃……
最后,我们得到了一个清晰准确的问题总结,几乎可以直接报告!
TOOL(debugger_run)
程序崩溃:SIGABRT at :0。
STDERR 上的输出是:
sqlite3: src/shell.c:6816: int seriesBestIndex(sqlite3_vtab *, sqlite3_index_info *): Assertion `iCol>=0 && iCol<=2' failed.
ASSISTANT(报告成功)
程序在执行 SELECT * FROM generate_series(1,10,1) WHERE ROWID = 1; 时崩溃,原因是 seriesBestIndex 函数中的断言失败。
该断言 assert(iCol>=0 && iCol<=2) 失败,因 sqlite3_index_info 结构中的 iColumn 字段可能包含超出有效列索引范围的值,例如在约束涉及 ROWID 时为 -1。
这导致 iCol 被计算为 -2,从而引发断言失败。
关于模糊测试?
考虑到重现案例的明显简单性,一个显而易见的问题出现了:为什么传统的模糊测试没有更早发现这个漏洞?“简单”的答案在于模糊测试工具的配置。OSS-Fuzz 使用的工具未启用 generate_series 扩展,而替代的 fuzzingshell.c 工具则包含了一个未受该漏洞影响的旧版本 seriesBestIndex 函数。尽管 SQLite AFL 仓库 包含了对我们提供给 Big Sleep 代理的相同 CLI 二进制文件进行模糊测试的配置,但似乎并未被广泛使用。
为了验证该漏洞是否确实“浅显”,我们尝试通过模糊测试重新发现它。我们遵循了 SQLite 文档 中的模糊测试说明,并使用了 CLI 目标。我们还确认模糊测试语料库包含了所需的 generate_series 和 rowid 关键字,然后才启动 AFL 运行。然而,在 150 个 CPU 小时的模糊测试后,该问题仍未被发现。
随后,我们尝试通过将必要的关键字添加到 AFL 的 SQL 字典来简化模糊测试的任务。然而,似乎只有当语料库包含一个非常接近崩溃输入的示例时,才能快速找到该漏洞,因为代码覆盖率似乎并不是这个特定问题的可靠指南。
诚然,AFL 并不是最适合像 SQL 这样的基于文本格式的工具,因为大多数输入在语法上是无效的,并会被解析器拒绝。尽管如此,将这个结果与 Michal Zalewski 在 2015 年关于模糊测试 SQLite 的博客文章 进行比较是有趣的。那时,AFL 在发现 SQLite 中的漏洞方面非常有效;经过多年的模糊测试,这个工具似乎达到了自然饱和点。虽然到目前为止我们的结果与 AFL 发布时的显著有效性变化相比似乎微不足道,但有趣的是,它有自己的优势,可能能够有效地发现一组独特的漏洞。
结论
对于团队来说,这是一个验证和成功的时刻——在一个广泛使用且经过良好模糊测试的开源项目中发现漏洞是一个令人兴奋的结果!在提供正确工具的情况下,当前的 LLM 可以进行漏洞研究。
然而,我们想重申,这些结果是高度实验性的。Big Sleep 团队的立场是,目前,特定目标的模糊测试工具在发现漏洞方面可能至少同样有效。
我们希望未来这一努力将为防御者带来显著优势——不仅能够找到崩溃的测试用例,还能够提供高质量的根本原因分析、分类和修复问题的成本可能会在未来变得更便宜、更有效。我们旨在继续分享我们在这一领域的研究,尽量缩小公共前沿技术与私人前沿技术之间的差距。Big Sleep 团队将继续在这一领域工作,推进 Project Zero 使 0-day 漏洞变得困难的使命。
Big Sleep 团队
这不仅仅是 Project Zero 的努力,所有为这一努力做出贡献的人都在下面列出(按字母顺序排列):
Miltos Allamanis, Martin Arjovsky, Charles Blundell, Lars Buesing, Mark Brand, Sergei Glazunov, Dominik Maier, Petros Maniatis, Guilherme Marinho, Henryk Michalewski, Koushik Sen, Charles Sutton, Vaibhav Tulsyan, Marco Vanotti, Theophane Weber, Dan Zheng
原文始发于微信公众号(securitainment):Project Zero:从午睡到大睡:利用大型语言模型捕捉现实世界代码中的漏洞
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论