Fuzz4All:使用大语言模型进行通用模糊测试

互联不一般哥 2024-05-08 21:13:05

Fuzz4All: Universal Fuzzing with Large Language Model

Chunqiu Steven Xia1, Matteo Paltenghi2, Jia Le Tian1, Michael Pradel2, Lingming Zhang1

1University of Illinois Urbana-Champaign

2University of Stuttgart

引用

Chunqiu Steven Xia, Matteo Paltenghi, Jia Le Tian, Michael Pradel, and Lingming Zhang. 2024. Fuzz4All: Universal Fuzzing with Large Language Models. In 2024 IEEE/ACM 46th International Conference on Software Engineering (ICSE ’24), April 14–20, 2024, Lisbon, Portugal.

论文:https://arxiv.org/abs/2308.04748

代码仓库:https://github.com/fuzz4all/fuzz4all

摘要

模糊测试在挖掘多种软件系统中的缺陷和安全漏洞方面表现优异。然而,现有的模糊测试工具大多针对特定语言设计,难以直接应用于其他语言或同一语言的不同版本。此外,这些工具生成的输入往往局限于输入语言的特定功能,因此难以揭示与其它功能相关的错误。本文提出了Fuzz4All,它是首个通用型模糊测试工具,能够应对多种不同的输入语言及其不同特性。Fuzz4All的核心思想是利用大语言模型作为输入生成器和变异器,从而使得该方法能为任何语言产生多样化和现实的输入。作者在多个测试系统上评估了Fuzz4All。评估结果显示,通用模糊测试的覆盖率高于现有的、特定语言的模糊测试工具。此外,Fuzz4All已在诸如GCC、Clang、Z3、CVC5、OpenJDK和Qiskit量子计算平台等广泛使用的系统上发现了98个错误,其中64个错误已被开发人员确认为之前未知的错误。

1 引言

模糊测试(Fuzzing)是一种自动化测试方法,旨在通过生成特定输入来揭示测试系统的未预期行为。研究人员和从业者已经成功开发了一系列实用的模糊测试工具,这些工具在现实世界系统中成功地发现了大量的缺陷和安全漏洞。特别是那些处理编程语言或形式化预言输入的系统,例如编译器、运行时引擎和约束求解器等,它们经常被作为模糊测试的待测对象。这些系统时软件开发的基石,如果它们内部存在缺陷,就很有可能会被波及所有相关的下游应用程序。

传统的模糊测试工具基本可以分为基于生成方法(generation-based)和基于变异方法(mutation-based)两类。基于生成方法的模糊测试工具旨在直接构建完整的代码片段,例如,通过使用目标语言的自定义语法。基于变异方法的模糊测试工具则是使用一组种子输入并对其变异操作来进行测试。然而,这两类方法都存在以下局限和挑战:

C1:与特定系统和语言的紧密耦合。传统模糊测试工具通常为特定语言或特定待测系统量身定制。设计和实现一个模糊测试工具是一个耗时的工作。另外将一个语言的模糊测试工具的实现迁移到另一种语言是困难的。甚至适用于一个待测系统的模糊测试策略可能完全不适用于另一个待测系统。

C2:跟不上系统的变化。现实世界中的系统持续改进优化,针对特定语言版本或待测系统设计的传统模糊测试器在面对新版本时可能失效,难以用于测试新增的功能。

C3:生成能力存在限制。在特定目标语言的范围内,基于生成和基于变异的模糊测试只能覆盖输入空间的部分测试输入。基于生成的模糊测试工具很大程度上依赖于输入语法来合成有效的代码。为了生成大量的有效模糊测试输入或避开难以建模的语言特性,基于生成的模糊测试工具往往使用完整语法的一个子集,这限制了它们的测试空间。类似地,基于变异的模糊测试工具也受到其变异运算符的限制,并且需要高质量的基础种子,这些种子往往不易获得。

本文提出设计了Fuzz4All,这是首个具有广泛适用性的模糊测试工具,能够针对多种不同的输入语言及其不同的特性。与现有的主流模糊测试器(例如AFL和libFuzzer)相比,该方法在本质上有所创新。这些主流工具使用简单的变异策略,对目标语言的理解有限,因此在生成有意义的编程语言模糊测试输入方面遇到挑战。Fuzz4All的核心思想是利用大语言模型作为输入生成和变异的引擎。由于大语言模型在多种编程语言和其他形式化语言的大量数据上进行过预训练,它们对这些语言的语法和语义有深入的理解。Fuzz4All利用大语言模型的这一特性,将其作为通用的输入生成器和变异器。

Fuzz4All的输入由用户提供的文档构成,这些文档详细描述了待测试系统。然而,这些文档类型的输入可能过于详尽,不适合直接用作大语言模型的提示。为了避免用户手动设计提示(耗时且不精确),作者引入了一种自动提示环节,该环节能自动从用户提供的输入中提炼出简洁而有效的模糊测试提示。这个提示是大语言模型生成模糊测试输入的起点。由于连续使用相同的提示会导致生成大量相似的模糊测试输入,作者设计了一个大语言模型驱动的模糊测试循环,该循环能迭代更新提示以生成多样化的模糊测试输入。最后,大语言模型生成的模糊测试输入被传递给待测系统进行测试。

Fuzz4All旨在解决传统模糊测试器所面临的局限性和挑战。与针对特定待测系统(C1)的单一用途模糊测试工具相比,Fuzz4All利用大语言模型作为生成引擎,能够广泛应用于各种待测系统和输入语言。与针对特定系统或者预言版本的现有模糊测试工具相比(C2),Fuzz4All能够轻松适应目标的版本升级。此外,Fuzz4All利用大语言模型在数十亿代码片段上预训练的优势,使得其能够生成遵循输入语言语法和语义约束的广泛示例,从而解决了传统模糊测试工具生成能力受限的问题(C3)。最后,Fuzz4All不需要对待测系统进行任何修改,这使得该方法在实践中易于应用。

作者对六种不同的编程语言(C、C++、SMT、Go、Java和Python)以及九个待测试系统进行了全面评估。针对每一项,作者对比了Fuzz4All与当前最先进的基于生成和基于变异的模糊测试工具。结果表明,Fuzz4All在所有语言中均实现了最高的代码覆盖率,平均提升了之前最高覆盖率的36.8%。此外,作者还证明了Fuzz4All支持针对待测系统特定特性的通用模糊测试和定制模糊测试,用户通过提供相应的输入文档来决定。最后,Fuzz4All在评估过程中发现了待测系统的98个错误,其中64个已被开发人员确认为之前未知的错误。

本文的主要贡献可以总结如下:

通用模糊测试。作者提出了一种新的模糊测试维度,该方法直接利用大语言模型的多语言能力,生成广泛的有意义输入,对多种待测系统进行测试。自动提示生成。作者设计了一个新颖的自动提示阶段,通过自动提炼用户输入,生成一个有效的提示,用于生成待测系统的输入。大语言模型驱动的模糊测试循环。作者介绍了一种算法,通过迭代修改提示,选择示例和生成策略,持续生成新的模糊测试输入。现实世界效果的证据。作者在六种流行的语言和九个真实世界的待测系统上进行了评估,结果显示,作者的方法显著提高了覆盖率(平均提高了36.8%),并检测到98个错误,其中64个已经被确认为之前未知的。

2 本文方法

图1:Fuzz4All的总体框架

作者提出了Fuzz4All,这是一个通用的模糊测试工具,它利用大语言模型支持对任何接受编程语言输入的待测试系统进行通用和定向模糊测试。图1展示了作者的方法概述。Fuzz4All首先接收任意用户输入,这些输入描述了要生成的模糊测试输入,例如待测系统的文档、示例代码片段或规范。由于这些用户输入可能冗长、重复且部分与模糊测试不相关,后续需要将其提炼成简洁而有效的模糊测试提示。为此,Fuzz4All执行了一个自动提示步骤,通过使用大语言模型来生成多个不同的候选提示。每个候选提示都传递给生成大语言模型来生成代码片段(即模糊测试输入)。Fuzz4All最后会选择最高质量模糊测试输入的提示。

Fuzz4All基于两个模型:一个是提炼大语言模型,用于精简用户输入;另一个是生成大语言模型,用于生成模糊测试输入。由于提炼大语言模型需要理解和精简用户输入,作者使用了一个高性能的大型基础模型,具有强大的自然语言理解能力。然而,直接使用这样一个大型模型进行输入生成将是低效的,因为自动回归生成的推断成本很高。相反,为了进行高效的模糊测试,Fuzz4All使用一个较小的模型作为生成大语言模型。虽然作者的方法适用于任何一对提炼和生成大语言模型,但作者使用最先进的GPT4和StarCoder来实现Fuzz4All。

利用自动提示选择的最佳提示作为生成大语言模型的初始输入提示,随后Fuzz4 All进入模糊测试循环。在此循环中,Fuzz4 All持续生成模糊测试输入。为了避免生成大量相似的模糊测试输入,Fuzz4 All在每个迭代中持续更新输入提示。该方法选择一个之前生成的输入作为示例,并向大模型展示了未来期望的输入类型。除了示例外,Fuzz4 All还将一个生成指令附加到初始提示中,引导模型生成新的模糊测试输入。

2.1 自动化提示生成

本节将详细介绍Fuzz4 All 的第一个主要步骤,该步骤自动将用户输入提炼成一个适合模糊测试的提示。用户输入可能包括技术文档、示例代码、规范,甚至不同模态的组合。与传统模糊测试工具要求输入遵循特定格式(例如,代码片段作为种子或良好形成的规范)不同,Fuzz4All可以直接理解用户输入中的自然语言描述或代码示例。然而,用户输入中的一些信息可能是冗余或不相关的,因此,直接将用户输入作为生成大语言模型的提示可能效果不佳。因此,自动提示的目标是生成一个提炼的输入提示,以便有效地进行基于大语言模型的模糊测试。

算法1详细阐述了Fuzz4All的自动提示步骤。输入包括用户输入和需要生成的候选提示的数量。最终输出是被选中用于模糊测试活动的输入提示。该算法的目标是使用提炼大语言模型来生成能够提炼用户提供的信息的提示,因此作者向提炼大语言模型给出了以下自动提示指令:“Please summarize the above information in a concise manner to describe the usage and functionality of the target”。假设MD是提炼大语言模型,userInput是用户输入,APInstruction是自动提示指令。生成的提示可以被形式化为条件概率:MD(prompt | userInput, APInstruction)。

为了选出在模糊测试步骤中使用的最佳输入提示,算法通过进行小规模模糊测试实验来评估每个候选提示。具体而言,该方法使用每个提示作为生成大语言模型的输入,为每个提示生成多个代码片段。Fuzz4All然后根据一个评分函数对每个提示生成的代码片段进行评分。虽然评分函数可以根据各种不同的指标,例如覆盖率、错误发现或生成的模糊测试输入的复杂性,但为了使方法轻量级且通用,Fuzz4All的评分函数是生成的唯一有效代码片段的数量,即被目标待测系统接受的代码片段的数量。

2.2 模糊测试循环

在Fuzz4All的第一步中创建了输入提示之后,模糊测试循环的目标是使用生成大语言模型生成多样化的模糊测试输入。由于大语言模型的随机性,使用相同的输入进行多次采样可能会产生相同或类似的代码片段。在模糊测试中,作者希望避免产生重复输入,而是生成覆盖新代码并发现新错误的多样化的模糊测试输入。为了实现这一目标,作者利用大语言模型的能力,结合示例和自然语言指令来引导生成过程。

模糊测试循环的核心思想是通过选择之前的迭代中的一个示例模糊输入和指定一个生成策略来持续地增强原始输入提示。使用示例的目的是向生成大语言模型展示期望它产生的代码片段类型。生成策略被设计为关于如何处理提供的代码示例的指令,这些策略借鉴了传统模糊测试器的经验,模仿了它们生成新模糊输入(如基于生成的模糊测试器)和产生之前生成输入的变体(如基于变异的模糊测试器)的能力。在每次新的模糊测试循环迭代之前,Fuzz4All都会在输入提示中添加示例和生成策略,以实现持续生成新的模糊输入的目标。

算法2描述了模糊测试循环。输入包括初始输入提示和模糊测试预算。最终输出是由用户定义的判据识别的错误集。首先,算法初始化生成策略(生成新输入、变异现有输入和语义等价),这些策略将在模糊测试循环中用于修改输入提示(第2行)。图2列出了Fuzz4All的三种生成策略及其相应的指令。对于生成大语言模型的首次调用,表示为MG,算法尚未有任何模糊输入的示例。因此,它将生成新输入的生成指令附加到输入提示中,指导模型生成第一批模糊输入(第3行)。

图2:模糊策略和模糊循环的例子

接着,算法进入主模糊测试循环(第5-9行),该循环持续更新提示以创建新的模糊测试输入。算法从之前生成的模糊测试输入批次中随机选择一个示例,该示例必须适用于待测试系统(第6行)。除了示例外,算法还随机选择三种生成策略中的一种(第7行)。生成策略指导模型对选定的示例进行变异、产生与示例语义等价的模糊测试输入或生成一个新的模糊测试输入。算法将初始输入提示、选定的示例和选定的生成策略拼接成一个新的提示,然后用这个提示查询生成大语言模型以生成另一批模糊测试输入(第8行)。主模糊测试循环重复进行,直到算法耗尽模糊测试预算。对于创建的模糊测试输入,Fuzz4All将其传递给待测系统。如果用户定义的判据识别出意外的行为,例如崩溃,那么算法将添加一个报告到检测到的错误集(第4和9行)。

Fuzz4All在模糊测试循环中生成的模糊测试输入可用于检查待测系统的行为。每个待测系统的判据都是定制的,用户可以完全定义和自定义。例如,当模糊测试C编译器时,用户可以定义一个差分测试判据,该判据比较在不同优化级别下编译器的输出。在本论文中,作者关注简单且易于定义的判据,例如由于段错误和内部断言失败导致的崩溃。

3 实验评估

在本文中,作者研究以下研究问题:

RQ1:Fuzz4All与现有模糊测试工具相比表现如何?

RQ2:Fuzz4All在执行定向模糊测试方面的效率如何?

RQ3:Fuzz4All的不同组成部分如何对其有效性产生贡献?

RQ4:Fuzz4All发现了哪些现实世界的错误?

3.1 待测系统和基线

表 1 待测系统和对应的基线工具

为了证明Fuzz4All的通用性,作者在6种输入语言和9个待测系统上对其进行了评估。表1列出了每种语言、待测系统及其相应的基线工具。请注意,作者针对每种语言的待测系统进行了覆盖率实验,覆盖率测量的待测系统版本显示在表1的最后一列。除了覆盖率实验之外,作者对还每个目标的每日发布版进行了模糊测试。除非另有说明,作者使用意外的编译器崩溃作为判据,并将能够成功编译的模糊测试输入视为有效。每个基线模糊测试工具都使用其默认设置运行。对于需要输入种子的基线模糊器,作者选择使用它们仓库中提供的默认种子语料库。

3.2 实验设置和度量指标

模糊测试实施方案。为了解答RQ1,作者设定了24小时的模糊测试预算(包括自动提示),这是之前的类似研究中普遍采用的方案。为了减小实验结果的波动性,作者对Fuzz4All和基线进行了五次重复实验。由于实验成本较高,对于后续的RQ,作者设定了10,000个生成的模糊测试输入的模糊测试预算,并对消融研究进行了四次重复实验。

实验环境。实验在一个拥有64个核心处理器、256GB内存的工作站上进行,该工作站运行Ubuntu 20.04.5 LTS操作系统,并配备有4个NVIDIA RTX A6000 GPU(每个模糊测试运行仅使用一个GPU)。

度量指标。作者采用广泛接受的代码覆盖率指标来评估模糊测试工具。为了保持一致性,作者报告了评估中研究的每个目标的行覆盖率。与先前的研究保持一致,作者使用曼-惠特尼U测试(Mann-Whitney U-test)来计算统计显著性,并在适用的表中用*表示显著的(p < 0.05)覆盖结果。此外,作者还测量了输入的有效性率,即生成的模糊测试输入中有效且唯一的百分比。由于Fuzz4All支持通用和定向模糊测试,为了评估定向模糊测试的有效性,作者报告了命中率,即使用特定目标特性的模糊测试输入的百分比(使用简单的正则表达式检查)。最后,作者也报告了模糊测试最重要的指标和目标:Fuzz4All在9个待测系统上检测到的错误数量。

RQ1 与现有的模糊测试工具比较

图3:覆盖率趋势对比

图3显示了Fuzz4All与基线在24小时内的覆盖率趋势对比,实线表示平均覆盖率,区域表示五次运行中的最小值和最大值。实验结果表明,Fuzz4All在所有目标上都实现了最高的覆盖率,与表现最佳的基线相比提高了36.8%。与基于生成的模糊测试器相比,Fuzz4All能够几乎立即达到更高的覆盖率,这证明了大语言模型在生成多样性代码片段方面的强大能力。虽然基于变异的模糊测试器通过使用高质量的种子在开始时能够实现更高的覆盖率,但通过变异获得的覆盖率迅速下降,而Fuzz4All能够稳步地覆盖更多的代码。

基线模糊测试工具往往会在24小时实验的后段时间趋于平稳状态,而Fuzz4All即使在模糊测试的尾声仍能不断发现新代码的覆盖输入。

RQ2 定向模糊测试的有效性

作者还评估了Fuzz4All进行定向模糊测试的能力,即生成针对特定特性的模糊测试输入。针对每个待测系统和语言,作者选取了三个不同的示例特性,并将它们与RQ1中使用的通用用户输入设置进行比较。这些特性包括内置库或函数/API(Go、C++和Qiskit)、语言关键字(C和Java)以及理论(SMT)。

定向模糊测试运行的用户输入是作者关注的具体特性的文档。作者观察到,针对特定特性生成的模糊测试输入量很大,这些输入直接使用该特性,平均命中率为83.0%。这一结果表明,Fuzz4All确实通过描述特定特性的输入提示提示生成大语言模型,从而执行定向模糊测试。此外,对相关特性进行模糊测试可以导致适度的跨特性命中率(即特性X在针对特性Y的模糊测试运行中的命中率)。例如,C的关键字typedef和union都与类型操作相关,因此,它们的跨特性命中率比不相关的特性,如goto,要高得多。虽然通用模糊测试方法在整体代码覆盖率方面表现最佳,但在针对特定特性的目标上可能极其低效(与Fuzz4All的定向模糊测试相比,命中率平均降低了96.0%)。

RQ3 消融实验

为了探究Fuzz4All的各个组件如何共同作用于其整体模糊测试效果,作者基于Fuzz4All的两个核心组件进行了消融研究:(a) 自动提示,指提供给生成大语言模型的初始输入提示的类型;(b) 模糊测试循环,涉及使用选定的示例和生成策略。针对这两个核心组件,作者分别研究了三种不同的变体。

自动提示。首先,作者考察了提供给生成大语言模型不同初始输入的效果。为了减少其他因素的影响,作者将生成策略固定为仅使用生成新输入,并研究了三种变体:1) 无输入,不使用任何初始提示;2) 原始提示,直接使用原始用户输入作为初始提示;3) 自动提示,应用自动提示来生成初始提示。作者观察到,在所有研究过的语言中,无输入变体达到了最低的覆盖率。另外,使用原始提示变体时覆盖率有所提高,这些提示中包括原始文档作为初始提示。最后,还可以通过使用自动提示阶段将用户输入提炼成简洁但信息丰富的提示(自动提示),而不是使用原始用户输入,进一步改善代码覆盖率和有效性。直接使用用户提供的输入可能包含与模糊测试无关的信息,导致有效率降低(因为大语言模型可能难以理解原始文档)和覆盖率降低(原始文档不是为大语言模型生成而设计的)。

模糊测试循环。接着,作者通过保持初始提示不变(使用默认的自动提示)来检查模糊测试循环设置的不同变体:1) 无示例在模糊测试循环中不选择示例(即,它持续从相同的初始提示中采样);2) 有示例选择示例但只使用生成新指令;3) Fuzz4All是使用所有生成策略的完整方法。可以观察到,仅从相同的输入中采样(无示例),大语言模型往往会重复生成相同的或类似的模糊测试输入。平均而言,与使用完整的Fuzz4All方法相比,无示例生成的模糊测试输入中有8.0%是重复的,而使用完整的Fuzz4All方法时,这一比例仅为4.7%。在输入提示中添加示例(有示例)可以避免从相同的分布中采样,并提高覆盖率和有效性率。最后,完整的Fuzz4All方法在所有待测系统中实现了最高的覆盖率。

RQ4 缺陷检测

表2:检测到的错误统计

表2总结了Fuzz4All在9个待测系统上发现的错误。Fuzz4All总共检测到了98个错误,其中64个错误已被开发人员确认为之前未知的。这些结果不仅表明了Fuzz4All在发现大量错误方面的实用性,也验证了Fuzz4All在多种语言和SUT上的通用性。

转述:杨鼎

0 阅读:0

互联不一般哥

简介:感谢大家的关注