用AI Agent捕捉软件漏洞

在我们之前的文章《Naptime 项目:评估大型语言模型的攻击性安全能力》中,我们介绍了用于大型语言模型辅助漏洞研究的框架,并通过提高 Meta 的 Cyber​​SecEval2 基准测试的最新性能展示了其潜力。从那时起,Naptime 演变为 Big Sleep,这是 Google Project Zero 和 Google DeepMind 之间的合作项目。

今天,我们很高兴与大家分享 Big Sleep 代理发现的第一个真实漏洞:SQLite(一种广泛使用的开源数据库引擎)中可利用的堆栈缓冲区下溢。我们发现了该漏洞并于 10 月初将其报告给开发人员,他们于当天对其进行了修复。幸运的是,我们在正式发布之前就发现了这个问题,因此 SQLite 用户没有受到影响。

我们相信这是 AI 代理在广泛使用的现实软件中发现以前未知的可利用内存安全问题的第一个公开示例。今年早些时候,在 DARPA AIxCC 活动上,亚特兰大团队发现了 SQLite 中的空指针取消引用,这启发我们将其用于测试,看看能否发现更严重的漏洞。

我们认为这项工作具有巨大的防御潜力。在软件发布之前就发现漏洞意味着攻击者没有竞争的余地:漏洞在攻击者有机会利用它们之前就被修复了。模糊测试有很大帮助,但我们需要一种方法来帮助防御者找到难以(或不可能)通过模糊测试发现的漏洞,我们希望人工智能可以缩小这一差距。我们认为这是一条有希望的道路,最终扭转局面,为防御者实现不对称优势。

漏洞本身非常有趣,而且 SQLite 的现有测试基础设施(通过 OSS-Fuzz 和项目自己的基础设施)都没有发现这个问题,所以我们做了进一步的调查。

1、方法论

Naptime 和现在 Big Sleep 的一个关键激励因素是继续在开放环境发现针对之前发现和修补的漏洞变体的漏洞利用。随着这种趋势的持续,很明显模糊测试无法成功捕获此类变体,而对于攻击者来说,手动变体分析是一种经济高效的方法。

我们还认为,与更一般的开放式漏洞研究问题相比,这种变体分析任务更适合当前的 LLM。通过提供一个起点(例如之前修复的漏洞的详细信息),我们消除了漏洞研究中的许多歧义,并从一个具体的、有根据的理论开始:“这是一个以前的错误;可能在某个地方还有另一个类似的错误”。

我们的项目仍处于研究阶段,我们目前正在使用具有已知漏洞的小程序来评估进展。最近,我们决定通过在 SQLite 上运行我们的第一个广泛的真实世界变体分析实验来测试我们的模型和工具。我们收集了最近提交到 SQLite 存储库的大量内容,手动删除了琐碎的和仅用于文档的更改。然后,我们调整了提示,为代理提供了提交消息和更改的差异,并要求代理检查当前存储库(在 HEAD 处)中是否存在可能尚未修复的相关问题。

2、发现的漏洞

该漏洞很有趣,其中在(否则)索引类型字段 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 虚拟表。 (我们还看到了模型首先研究可用虚拟表的情况,但它显然能够在这里应用预先存在的知识)。

3、轨迹亮点

在基于 Gemini 1.5 Pro 的这次成功运行中,种子提交是 [1976c3f7];这是一个相当大且不明显的变化。我们的代理发现的错误与种子提交中的更改仅松散相关 - 这在手动变体分析中并不罕见,了解代码库中的一个错误通常会导致研究人员发现其他问题。

以下是精选的亮点,我们的评论以斜体显示 - ASSISTANT 块中的所有文本都直接来自代理。

代理对 diff 中的变化进行合理的评估,并以此作为研究的起点:

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(report_success)

由于函数 seriesBestIndex 中的断言失败,程序在输入 SELECT * FROM generate_series(1,10,1) WHERE ROWID = 1; 时崩溃。

断言 assert(iCol>=0 && iCol<=2) 失败,因为 sqlite3_index_info 结构中的 iColumn 字段可以包含列索引有效范围之外的值,例如当约束涉及 ROWID 时为 -1。

这导致 iCol 被计算为 -2,从而导致断言失败。

4、模糊测试怎么样?

鉴于重现案例看似简单,一个显而易见的问题出现了:为什么传统模糊测试没有更早发现这个错误?“简单”的答案在于模糊测试工具的配置。OSS-Fuzz 使用的工具不是在启用 generate_series 扩展的情况下构建的,而替代的 fuzzingshell.c 工具包含旧版本的 seriesBestIndex 函数,不受该错误的影响。虽然 SQLite AFL repo 包含用于模糊测试我们提供给 Big Sleep 代理的相同 CLI 二进制文件的配置,但它似乎并未得到广泛使用。

为了了解该错误是否真的“浅显”,我们尝试通过模糊测试重新发现它。我们遵循 SQLite 文档中的模糊测试说明并使用 CLI 目标。我们还在启动 AFL 运行之前验证了模糊测试语料库是否包含所需的 generate_seriesrowid 关键字。然而,经过 150 个 CPU 小时的模糊测试后,该问题仍未被发现。

然后,我们尝试简化模糊测试器的任务,例如,将必要的关键字添加到 AFL 的 SQL 字典中。然而,似乎只有当语料库包含非常接近崩溃输入的示例时,才能快速找到该错误,因为代码覆盖率似乎不是这个特定问题的可靠指南。

不可否认,AFL 并不是最适合文本格式(如 SQL)的工具,其中大多数输入在语法上无效,将被解析器拒绝。尽管如此,将这个结果与2015年 Michal Zalewski 关于模糊测试 SQLite 的博客文章进行比较还是很有趣的。当时,AFL 在发现 SQLite 中的错误方面非常有效;经过多年的模糊测试,该工具似乎已经达到了自然饱和点。虽然我们迄今为止的结果与 AFL 发布后效率的显著变化相比似乎微不足道,但有趣的是,它有自己的优势,可能能够有效地发现一组独特的漏洞。

5、结束语

对于团队来说,这是一个验证和成功的时刻——在一个广泛使用且模糊测试良好的开源项目中发现漏洞是一个令人兴奋的结果!当提供正确的工具时,当前的 LLM 可以进行漏洞研究。

但是,我们想重申,这些都是高度实验性的结果。Big Sleep 团队的立场是,目前,目标特定的模糊测试器至少同样有效(在发现漏洞方面)。

我们希望,未来这一努力将为防御者带来显著优势——不仅有可能找到崩溃的测试用例,还能提供高质量的根本原因分析、分类和修复问题在未来可能会更便宜、更有效。我们的目标是继续分享我们在这个领域的研究成果,尽可能缩小公共最新技术和私人最新技术之间的差距。

Big Sleep 团队将继续在这个领域工作,推进 Project Zero 让 0-day 变得困难的使命。


原文链接:From Naptime to Big Sleep: Using Large Language Models To Catch Vulnerabilities In Real-World Code

汇智网翻译整理,转载请标明出处