引言

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

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

我们相信这是 AI 代理在广泛使用的实际应用中发现先前未知的可利用内存安全问题的第一个公开示例。在今年早些时候的 DARPA AIxCC 活动中,Atlanta 团队在 SQLite 中发现了一个空指针取消引用,这激发了我们使用它来进行测试,看看我们是否可以找到更严重的漏洞。

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

该漏洞本身非常有趣,再加上 SQLite 现有的测试基础设施(通过 OSS-Fuzz 和项目自己的基础设施)均未发现该问题,因此我们进行了进一步调查。

方法论

Naptime 和现在 Big Sleep 的一个关键激励因素是人们不断发现未识别的针对之前发现和修补的漏洞变体的攻击。随着这一趋势的持续发展,很明显,模糊测试无法成功捕捉此类变体,而对于攻击者来说,手动变体分析是一种经济高效的方法。

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

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

发现漏洞

该漏洞很有趣,在(otherwise)索引类型字段 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 列的约束是一个很好的起点,但还需要更详细地阅读代码。AI代理似乎已经比我们更了解 SQLite,所以它可以走捷径!

一种常见的情况是,模型在生成测试用例时会立即使用 generate_series 虚拟表。(我们还看到过模型首先研究可用的虚拟表的情况,但显然它能够在这里应用预先存在的知识)。

轨迹亮点

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

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

AI 代理对 diff 中的更改进行合理的评估,并使用它来形成研究的起点:

ASSISTANT

CL 通过引入 termFromWhereClause 函数改进了虚拟表查询中 OR 分支之间共享的 WHERE 术语的处理。此函数从 WhereClause 中检索特定术语,同时考虑嵌套子句。在此更改之前,由于术语索引映射不正确,常见术语可能未得到正确处理,从而可能导致查询计划效率低下或结果不正确。最可能的漏洞涉及 allocateIndexInfowhereLoopAddVirtualOne 函数中 WHERE 子句约束的错误处理,其中 CL 已修改了如何将约束偏移量与新的辅助函数一起使用。我将首先检查 allocateIndexInfo 函数。

在对相关代码进行了几个(已省略)步骤探索之后,AI 代理形成了一个更具体的假设,并尝试通过运行补丁中修改后的测试用例来检查它,但这不起作用,因为补丁中包含的测试用例依赖于我们的配置中不可用的扩展:

工具(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 中设置断点。

AI 代理能够很好地适应这一失败,识别出修复测试用例所需采取的步骤,并在几步之后检索到构建达到 whereLoopAddVirtualOne 的测试用例所需的所有信息:

ASSISTANT

sqlite3_create_module 函数注册虚拟表模块。shell.c 文件包含此函数的多个实例,包括 generate_seriesfsdircompletion expert 模块。json 表看起来也很有趣。我现在将重点关注 generate_series 作为起点。我将尝试编写一个使用此模块的查询,并且可能触发不正确的约束处理。

请注意,在最初发现和重现漏洞的过程中,AI 代理对于它试图触发的“不正确的约束处理”含糊其辞;在这种情况下,完整的轨迹读起来有点像人类研究人员从一个想法开始,在调查时想到其他几个想法,最后生成一个重现略有不同的漏洞的测试用例。在这种情况下,当在 generate_series 表上调用 whereLoopAddVirtualOne 时,我们最终得到 seriesBestInde,其中约束确实被漏洞处理。

然而,特别有趣的是,一旦程序崩溃,模型就能更好地解释事情——在生成根本原因分析和完整崩溃报告的同时,我们可以看到 AI 代理清楚地制作了一个测试用例来触发特定的、已识别的边缘情况:

ASSISTANT

相关字段是 sqlite3_index_constraint 结构中的 iColumn。此字段表示约束的列号。为了导致断言失败,我们需要对索引大于 3 或小于 1 的列进行约束。

让我们编写一个涉及这些列约束的 SQL 查询。generate_series 表包含名为 valuestartstep stop 的列。索引为:

       - value: 0

       - start:  1

       - step:  2

       - stop:  3

为了触发该漏洞,我们可以对 ROWID 进行约束。ROWID 的约束使用 iColumn = -1

以下是一个示例查询:

       SELECT * FROM generate_series(1,10,1) WHERE ROWID = 1;

该查询会导致 seriesBestIndex 函数崩溃。

最后,我们得到了一个清晰准确的问题摘要,几乎可以直接用于报告了!

工具(debugger_run)

The program crashed: SIGABRT at :0.

The output on STDERR was:

       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,从而导致断言失败。

那么模糊测试呢?

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

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

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

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

结论

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

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

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

Big Sleep 团队将继续在这个领域努力,以不断推进 Project Zero 让 0day 变得困难的使命。

Big Sleep Team 团队

这不仅是 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

相关推荐