摘要
problem 1:虽然目前最先进的fuzzers能够有效地生成输入,但是现有的模糊驱动程序仍然不能全面覆盖库的入口。(entries in libraries,库中的不同条目或入口点。包括调用库中的函数、使用库中的类等)
problem 2:大多数模糊驱动程序都是开发人员手工制作的,它们的质量取决于开发人员对代码的理解。虽然现有工作尝试通过从代码和执行轨迹学习API使用来自动生成模糊驱动程序,但生成的模糊驱动程序被学习的代码限制在几个特定的调用序列中。
Hopper:为了解决上述挑战,作者提出了Hopper。它可以模糊库,而不需要任何领域知识来制作模糊驱动程序。它将库模糊化问题转化为解释器模糊化问题。与被测库链接的解释器可以描述任意API用法的输入。为了为解释器生成语义正确的输入,Hopper学习库中API内(Intra-API)和API间(Inter-API)的约束,并通过语法意识改变程序。
成果:实现了Hopper,并在11个现实世界的库上评估了它对手工制作的模糊器和其他自动解决方案的有效性。研究结果表明,Hopper在代码覆盖率的漏洞挖掘方面远远优于其他模糊器,发现了之前其他模糊器未能发现的25个漏洞。此外,还证明所提出的API内和API间约束学习方法可以正确地学习库中隐含的约束,因此,显著提高模糊效率,Hopper能够探索广泛的API用法。
一、介绍
fuzzing是用于发现软件漏洞的最流行的技术之一。它通过给予软件大量的随机输入来观察是否发生意外行为(unexpected behaviors)。
基于覆盖率的灰盒模糊器(Coverage-based grey-box fuzzer),例如AFL和LibFuzzer,它可以改变输入以探索更深层次的程序状态,而无需了解输入格式或程序规范。
基于约束的灰盒模糊器(Constraint-based grey-box fuzzers),采用约束求解技术来达到复杂约束保护代码分支。
在库模糊测试中,模糊驱动程序在调用API时需要传入正确的参数类型,并满足API内和API间的约束,否则可能发生意外崩溃。
举个例子:测试c-ares库中的ares_send(ares_channel channel, char *qbuf, int qlen, ares_callback callback, void *arg)函数
约束一,为了满足外部API的约束,需要调用ares_init()函数初始化参数channel。
约束二,内部API约束要求第三个参数 qlen 应该设置为第二个参数 qbuf 的长度,同时第四个参数 callback 应该是一个非空函数指针,并且具有 ares_callback 类型。所以需要按这些要求对这些参数进行一个设置。
# 测试打印字符数组的函数
# 该函数的外部约束就是第一个参数需要时char类型的数组,
# 内部约束就是length为数组a的长度
void print(char a[], int length) {
for (int i = 0; i < length; i++) {
printf("%c", a[i]);
}
}
任何一个违反上述约束的行为,都可能会导致程序崩溃。
problem 1:在实践中,关于这些约束的信息要么缺失,要么分散在库文档或注释中,使得很难以全自动的方式收集它们。
-
自动生成模糊驱动程序的方法:
- 基于学习的方法(Learning-based methods):例如FuzzGen试图从现有的消费者代码(consumer code)中学习API的正确用法,但当消费者代码不可用(Consumer code is unavailable)(例如对新的或者正在进行的库),该方法无效。
- 基于模型的方法(Model-based methods):例如GraphFuzz,要求用户提供测试API的规范,这需要特定领域的知识和大量人工参与。
- 此外,用这两种方法生成的模糊驱动程序的质量在很大程度上受外部输入的影响,这些输入可能是不完整或不准确的,模糊驱动程序可能会因为这些输入发生虚假崩溃(spurious crashes)。
-
概念补充 \red{概念补充} 概念补充:
- 生产者代码:生产者代码是实现和提供某个功能或服务的代码。这可以是一个库、框架、服务等。
- 消费者代码:消费者代码是使用生产者代码提供的功能的代码。这些代码调用、调用或“消费”生产者代码,以在自己的应用程序中实现特定的功能或服务。
solution:作者提出了Hopper在不需要外部知识的条件下模糊测试API。受基于覆盖率的模糊测试的启发,它从变异的随机种子中学习有效的格式。
Hopper从变异API的调用及其参数的组成中学习API的潜在用法。如果执行变异的程序触发了新的路径或崩溃,Hopper会根据动态反馈推断API内和API间的约束。
为了实现这一点,引入了一个特定领域的语言(Domain-Specific Language,DSL)用于描述任意的API用法。DSL输入可以通过一个轻量级解释器与正在测试的库链接并解释。通过这种方式,将库模糊测试转化为解释器模糊测试。模糊器负责生成以DSL格式编码的程序,将其提供给解释器。然后,解释器执行这些程序,以查看是否发生了意外的行为。
二、背景
2.1 库的模糊测试
problem:由于库在各种程序中被广泛使用,所以保护库的安全也十分重要。但仅对程序进行模糊测试来测试库是不完全的。因为程序可能在复杂的路径约束条件下调用库的API,或者使用特定的参数调用库的API,这使得这些API很难被完全测试到。
- 为了解决这个问题,已经出现了专门的模糊测试工具来测试库,例如,LibFuzzer。LibFuzzer测试库的必要步骤如下:
-
制作模糊驱动程序(Craft a fuzz drivers):一个高质量的模糊驱动程序应该提供一个入口,以探索库中尽可能多的代码进行彻底的测试。
具体的执行路径不仅由API调用中的参数决定,还由调用它们相关的API决定。这些相关的API可以返回值作为参数,或者影响依赖于它们的其他API的上下文(例如,全局值)。
显然枚举所有有效的API是耗时且具有挑战性的。所以模糊驱动程序只包含一些常见的API用法。
此外,由于不同的API使用具有不同的模糊搜索空间,因此将它们全部按顺序放置将是低效的。但将它们写入多个模糊驱动程序或在单个模糊驱动程序中有条件地执行它们会更有效。- 例如下图:虽然这些库被广泛的使用,但是开发者只将其中少部分API写入了模糊驱动程序。
-
指定输入格式(Specify the format of input):由于LibFuzzer生成的字节流是盲目的,这使得创建满足API内的约束的结构化输入变得困难。所以我们需要指定输入格式,引导模糊测试工具生成超出字节数组的参数。
FuzzedDataProvider 能够将模糊输入分成多个不同类型的部分,而 libprotobuf-mutator可以基于提供的语法生成结构化的输入。然而,内部 API 约束的存在使得定义用于模糊测试的潜在参数范围变得非常庞大的任务。因此,开发者可能会选择将参数直接编码为字面常量,嵌入到模糊测试驱动程序中。
例如下图cJSON_ParseWithOpts的第二个参数,但方法可能导致对API函数的测试不充分。
-
制作模糊驱动程序(Craft a fuzz drivers):一个高质量的模糊驱动程序应该提供一个入口,以探索库中尽可能多的代码进行彻底的测试。
2.2 模糊测试解释器
- 语法感知的灰盒模糊测试在解析输入的程序中取得的成功,尤其是在解释器中。模糊测试解释器的成功归纳于以下两个关键技术。
- 语法感知输入变异(Grammar-aware input mutation):盲目变异的输入可能被解析过程(parsing procedure)拒绝,而结构化的输入可以达到更深的路径。语法感知的变异将输入解析为基于其编码语法的中间表示(IRs),并在遵循约束的同时对 IRs 进行变异。
- 覆盖率反馈引导模糊测试(Coverage guided fuzzing):在覆盖反馈的指引下,模糊器保留触发新路径的输入,并对其突变,以进入更深的分支或发现新的bug。
- 我么可以观察到,为库构造的模糊驱动程序类似于为输入实现一个解释器,对解释器进行模糊测试等同于等同于在底层对库进行模糊测试。
三、设计
3.1 概述
Hopper将库的模糊测试问题转化为解释器模糊测试问题。Hopper主要由两个主要组件组成:
语法感知模糊器(grammer-aware fuzzer):生成DSL格式的输入,或对解释器的反馈进行变异生成DSL格式的输入,然后送入解释器。
轻量级的解释器(lightweight interpreter):输入DSL格式的输入,执行,然后输出一个反馈。
模糊器为了调用库的API生成高质量的输入。它首先从库的头文件中提取函数声明和类型定义,通过利用这些信息,Hopper通过API函数和参数的随机组合生成一系列API调用。
解释器使用输入并调用库API。在编译阶段,被测库与解释器相链接,在链接之前,Hopper对二进制文件进行检测,以捕获执行期间的内部状态。
3.2 DSL和输入解释
为什么要在Hopper中引入DSL和轻量级解释器?
开发模糊驱动程序所使用的编程语言通常和被测库的编程语言一致。因此,对于像C/C++这样的编译语言,模糊驱动程序在模糊测试前就需要被编写和编译。在每一轮模糊测试的过程中,只有输入字节会被改变。为了在模糊测试过程中替换API调用序列,同时避免编译开销,所以在Hopper中引入了DSL和一个轻量级解释器,来加速整个过程。
3.2.1 DSL
为了通用性,Hopper中的解释器将DSL程序作为输入。每个DSL程序都包含语句作为其最基本的组件,每个语句都有一个唯一的索引,后继的语句可以引用它。
- 作者在DSL中将MCF中发现的常见模糊行为分为五种语句类型:
- Load Statement:load语句定义值的类型和文本表现形式。强类型允许直接以特定类型生成输入数据,这消除了从未知类型的字节流输入转换为输入数据类型的要求。
- Call Statement:call语句通过提供函数名和参数列表来调用库中的特定API函数,其中每个参数引用了load语句定义的变量。
- Update Statement:update语句在运行时重写了call语句的返回值。
- Assert Statement:assert语句在运行时检查call的返回值。如果断言失败,程序立即退出。(例如,调用者需要简介引用返回值,但这个返回值时null)
- File Statment:file语句为I/O操作指定有效的文件资源。如果file语句被用于reading,那么会在运行时在文件中填充一系列随机字节以供读取。
此外,为了保持语法简单,作者的DSL不支持条件语句。某些模糊驱动程序使用条件语句来选择不同的API函数或参数。相比之下,Hopper生成不同的DSL程序来枚举这些API函数或参数。
3.2.2 解释器
解释器解析DSL程序,并在执行每条语句后监视程序的状态。为了而调用库API,Hopper在编译阶段将解释器与测试中的库链接起来。
解释器还构建了一个映射表,根据库的头文件将每个函数的名称与它的调用方(如主函数)关联起来。在程序执行期间,调用方将值转换为被调函数所需的参数类型,然后执行。
在链接解释器之前,Hopper使用计算分支的代码以及挂钩 (hooks) 比较指令和资源管理函数(例如malloc,free和fopen)来对库二进制文件进行插装。
- 然后,解释器在运行时收集以下反馈。
-
可选的分支跟踪(optional branch tracking):Hopper定义了一个全局标志来保护分支跟踪代码。该标志的值由DSL输入确定,当调用以问号 ? 结尾的API函数时(如下图13行),解释器为该调用启动分支跟踪。
-
上下文敏感的代码覆盖率(Context-sensitive CodeCoverage):为了区分不同API访问相同分支的情况,解释器将当前API函数名称的哈希设置为与上下文相关的。这样,每个API函数都有一个唯一的上下文哈希。插装代码读取上下文,并为每个API计算一个独特的代码跟踪(code trace)。
通过为每个API计算不同的代码跟踪,解释器能够跟踪和监视多个API的执行情况,并区分它们之间的代码访问路径。这有助于分析和理解各个API的行为以及它们对程序状态的影响。
- 溢出检测(Overflow Detection):当一个语句加载一个大小可变的值(如数组)时,解释器会将这些值存储在内存区域,并附加一个canary标记来检测肯恩那个泄露的缓冲区。
- 释放后使用检测(Use-after-free Detection):解释器通过插桩维护一个由malloc分配和free释放的内存块。对于函数参数中使用的每个指针,如果该指针指向的内存块被释放,解释器立即退出程序,以避免释放后使用的问题。
- 比较挂钩(Comparison Hooking):解释器收集比较指令和函数中使用的参数,以指导模糊器解决魔法字节 (magic bytes)。在模糊测试中,解决或正确处理这些“魔术字节”通常是一个重要的目标,因为它们可能作为特定条件的标识或触发特定行为的关键。
-
可选的分支跟踪(optional branch tracking):Hopper定义了一个全局标志来保护分支跟踪代码。该标志的值由DSL输入确定,当调用以问号 ? 结尾的API函数时(如下图13行),解释器为该调用启动分支跟踪。
LibFuzzer使用的模糊驱动程序很难生成一个在推出之前重置所有资源的程序,而Hopper解释器通过在单个进程中运行每个输入来解决这个问题。
3.3 语法感知的输入模糊
为了在库而不是解释器中找到错误,Hopper专注于生成各种有效的API调用。该过程包括两个阶段,如下图所示,首先,Hopper根据函数签名中的可用信息生成输入,以初始化种子池。其次,Hopper在种子库中选择输入,并在覆盖反馈的指导下变异它们。
-
试点阶段(Pilot Phase):Hopper绘制输入的概况并推断约束。最初,种子池不包含任何输入。因此,Hopper尝试根据每个API函数的声明为它们生成简单的输入,并从中学习约束。
为了实现这一点,Hopper选择库中的一个API函数作为目标,并尝试为其随机生成一个调用语句,这包括生成参数和插入引用必要上下文的相关调用。这些语句最终形成一个输入,然后由解释器执行。
如果输入触发了库中的新路径而没有崩溃,Hopper会将其保存到种子池中以进行进一步的突变。 -
进化阶段(Evolution Phase):Hopper从种子池中选择一个输入并基于它们的类型随机变异语句。在执行分支覆盖率的指导下,Hopper保留了突变的可以探索更深层次代码和找到更多漏洞的输入。Hopper变异程序语句有以下五个步骤:
-
- Hopper从种子池中选取具有优先级的种子,因为新种子更有可能探索到更深的路径,所以给与新种子更高的选择优先级。
-
- Hopper选择根据种子的权重从从输入种子中选择要变异的语句。断言、文件和更新(assert、file、update)语句的权重被设置为0且不变异,而加载和调用(load、call)语句的权重由他们的复杂度决定。
-
- Hopper通过响应的策略变异加载语句和调用语句,如3.3.1和3.3.2节所述。
-
- Hopper根据在模糊测试的过程中学到的约束优化输入以正确的使用API。如3.4节所述。
-
- Hopper通过移除对探索路径且增加了变异的搜索空间的冗余语句使得输入最小化。正如3.3.3节所述。
-
3.3.1 调用语句的变异策略(mutation strategies for call statements)
1. 将其中一个参数替换为保持相同类型的参数(Line 4 tp Line 24 in Algorithm 1)。
2. 在目标调用前插入一个新call语句(Line 27 to Line 29 in Algorithm 1)。插入的call可以修改目标调用的参数值或更改库的全局状态。Hopper通过分支反馈来确定插入调用的有效性。(详情见3.4.2节)。
3. 更新call的返回值,在call之后插入一个update语句,使用新值覆盖部分旧值。
3.3.2 类型感知的值变异 (type-aware value mutation)
由于库经常使用各种参数类型,包括自定义复合类型,因此Hopper在调用相应API时需要为这些参数生成适当的类型。值变异也是如此,根据值的类型进行变异能够更有效地探索新的状态。为了实现这一点,Hopper解析类型定义(例如,struct、union和enum),并递归地在头文件中输入别名。
- Hopper使用以下规则来生成新类型的值。
-
基本类型(Primitive Types):几乎所有的基本类型都是数值类型。因此Hopper生成的数字在一个小范围内均匀分布。Hopper采用下面四种方法之一实现值突变。
- 设置一个有趣的值(interesting value)。
- 翻转一位或一个字节
- 加减一个小数字
- 如果该值用作比较指令的操作数之一,Hopper将其设置为从该比较中收集的字面值。即,Hopper可能会分析程序的比较指令,并使用其中涉及的字面值进行变异。
有趣的值:在测试或分析过程中,具有特殊或边缘情况行为的特定数值。这些值被选择用于测试程序的极端情况,以确保程序在各种情况下都能正确工作。
- 数组(Array):基于数组的长度和元素类型,Hopper生成一个元素序列。如果长度可变,Hopper首先随机选择一个长度。在编译过程中,Hopper选择一个或多个元素,分别对它们进行变异。此外,如果数组的长度不固定,Hopper可以通过删除或插入来调整数组的大小。
- 结构类型(Structure):具有自定义结构类型的值是通过递归生成其字段来创建的,当改变自定义结构值时,Hopper随机选择结构中的一个字段,并根据其类型突变它。
-
简单指针(Trivial Pointer):简单指针是指那些揭示指针类型布局的指针,即指向基本数据类型和结构类型的指针。Hopper 通过以下方法来变异它们:
-
- 设置为空指针
-
- 指向有相同类型的现有语句的地址
-
- 指向新生成的数组,其元素类型与被指对象相同
-
- 指向新生成的调用语句返回值的地址。call语句返回相同的指针类型。
-
-
非凡指针(Nontrivial Pointer):例如不透明指针,空指针和函数指针,由于其不可预测的性质,不能直接变异。
- 不透明指针(opaque pointers):Hopper通过返回指针或通过引用填充指针来检索初始化指针的API函数。
- 空指针(void pointers):有别名类型,Hopper将他们视为不透明指针,否则,Hopper尝试将任意长度的字节数组强制转换为所需的空指针,以查看它是否有效(详情见3.4.1节)。
- 函数指针(funtion pointers):Hopper在编译阶段合成具有所需签名的空函数,以允许模糊器使用让它们的地址作为指针。
- 此外,字节数组可能包含具有自己编码的数据,这在头文件中没有定义。
-
基本类型(Primitive Types):几乎所有的基本类型都是数值类型。因此Hopper生成的数字在一个小范围内均匀分布。Hopper采用下面四种方法之一实现值突变。
3.3.3 输入最小化(Input Minimization)
冗余的语句和值会降低执行速度,并在变异过程增加了搜索空间,这使得模糊检测效率低下。
- 为了解决这个问题,Hopper采用两个步骤来最小化输入:
- 最小化在变异和细化后的输入(Minimize inputs after mutation and refinement):Hopper反向检查输入中的语句,不包括目标调用语句。如果语句不再被其他语句引用,Hopper就会删除掉它。
- 最小化触发新路径的输入(Minimize inputs that trigger new paths):Hopper删除对执行路径没有影响的调用,以及load语句中的冗余值。它尝试将指针设置为null或在可能的情况下缩小数组的长度。如果执行路径保持不变,则Hopper将保留输入中的突变。
3.4 约束学习
为了正确调用API,Hopper生成的DSL程序必须满足API内和API间的约束。PI内约束规定必须使用适当的参数调用API,而API间约束指定调用API的适当顺序。Hopper通过库的运行时反馈来学习这些约束,而不是依赖于外部资源。
3.4.1 内部API约束(Intra-API Constraint)
作者基于他们的对真实库的观察提出六个通用的API内部约束
- 非空指针(Non-null Pointer(NON-NULL)):不检查空指针的API在使用空指针调用时可能会崩溃。通常不清楚这是否是一个真bug,因为一些开发者认为执行空值检查时用户责任。
- 有效文件资源(Valid File Resource(FILE)):当API函数读取或写入文件时,作为参数提供的文件名必须有效。如果文件名时随机生成的,则API调用可能会提前终止,或者在用作输出流时使磁盘混乱。
- 具体值(Specific Value(EQUAIL)):在参数中使用数字来指定数组指针边界的API,如果数字不正确,可能会出现一处错误。
- ** 有界范围(Bounded Range(RANGE)):如果参数的值超出范围,基于参数值访问或分配有限资源的API可能会遇到资源耗尽或溢出错误。
- 数组长度(Array Length(ARRAY-LEN)):有些API假设指针引用的数组有足够的元素,而不是要求参数指示边界。如果数组没有足够的元素,这可能会导致溢出错误。
- 特定类型转换(Specific Type Cast(CAST)):由于缺失void类型的布局信息,开发人员必须生成具有具体类型的对象,并将其转换为void指针。
Hopper在整个模糊测试过程中学习API内部约束,包括试点和进化阶段。除了推断参数本身的约束,Hopper递归的探索复合结构,以推断其中包含的任何对象的约束,例如指针,结构体中的字段和数组中的元素。
对于没有别名的void指针,可以从随机字节流强制转换过来,Hopper会添加CAST约束将它们视为char类型。
当输入触发新的执行路径时,Hopper会检查是否触发了文件打开函数(如fopen),并比较文件名与被调用API的参数是否匹配。如果存在匹配项,将为相应的参数创建一个FILE约束。
- 如果一个输入出发了一个新的崩溃,Hopper会按以下步骤来推断API内的约束。
-
- 如果因为调用空指针而触发了分段错误,Hopper在参数中定位每一个空指针,将其设置为受保护内存块的地址,并再次运行此变异程序。如果在相同的位置再次崩溃并触发受保护内存块的非法访问,则意味着在API调用中访问指针时,没有进行空检查。在这种情况下,Hopper为这个指针添加了一个NON-NULL约束。
-
- 如果程序崩溃是由于访问数组右侧附加的 canary(哨兵值)引起的(其中 𝑠𝑖_𝑎𝑑𝑑𝑟 在 canary 的范围内),Hopper 尝试确定参数中是否存在变长数组的长度或索引。具体步骤如下:
- 首先,Hopper 定位哪个数组发生了溢出。我们将该数组的长度表示为 𝑁。
- 针对调用的参数中的每个数值,Hopper 尝试分别将其设置为 𝑁−1、𝑁 和 𝑁+1。
- 如果在设置为 𝑁 和 𝑁+1 时都导致通过访问 canary 引起崩溃,Hopper 将添加一个 RANGE 约束,将该值设置在 [0, 𝑁) 的范围内。
- 如果只有在设置为 𝑁+1 时发生崩溃,Hopper 将添加一个 EQUAL 约束,将该值设置为与数组长度相同。
- 如果程序崩溃是由于访问数组右侧附加的 canary(哨兵值)引起的(其中 𝑠𝑖_𝑎𝑑𝑑𝑟 在 canary 的范围内),Hopper 尝试确定参数中是否存在变长数组的长度或索引。具体步骤如下:
-
- 如果上述策略未能修正canary的非法访问,Hopper会尝试将参数中的数组填充至特定长度看看是否能解决崩溃问题。如果是,则添加一个ARRAY-LEN约束以确保此数组最少为100个字节。
-
- 对于其他非法访问,如果参数有CAST约束,Hopper会尝试改变参数指向的字节数组。如果非法地址随变异字节而变化,则空指针可以被解释为包含指针的结构。因此,Hopper删除了char* 类型的CAST约束
- 如果输入导致超时或内存不足,Hopper会搜索参数中的大数值并使其发生变化。如果在将该值设置为较小值后,执行速度明显加快或正常退出,则Hopper会为参数添加一个RANGE约束,以限制其最大值。
-
这些约束用于在变异之后细化输入,并且违反约束的输入被从种子集中排除。
3.4.2 API间的约束
API间约束。一个API调用可以修改它在程序中引用的参数或内部状态,这会影响它后续调用的行为。API调用之间的这些关系称为API间约束。
Hopper没有强加具体的约束,而是通过保存有效的程序以供以后的突变来保留API间的约束。为了学习这些约束,Hopper通过函数签名静态组装库API,然后通过覆盖反馈动态验证其有效性。
- 首先,Hopper通过分析API函数的签名静态的推断它们之间的单纯的约束。通过比较它们的参数和返回值的类型,我们可以推断这两个API函数是否可能相关。
- 对于任何给定的一对API函数F1和F2,如果它们的类型以下列方式重叠,则它们往往是相关的。
-
- 类型T是F1中的一个参数的类型,也是F2的返回值的类型。
-
- F1和F2都使用类型T作为参数。
-
- F1使用类型为T的参数,而F2使用可以修改其指向的值的指针T。
-
- 此外,如果后两种情况下参数的标识符相同,则这两个API函数更有可能相关。
- 对于任何给定的一对API函数F1和F2,如果它们的类型以下列方式重叠,则它们往往是相关的。
- 如果两个API函数可以帮助后者到达一条新的路径,则它们之间存在有效关系。通过精确控制覆盖跟踪,如第3.2.2节所述,Hopper通过以下步骤学习API函数之间的有效关系。
-
- 当在目标API调用之前插入新调用时,将仅跟踪目标调用的覆盖率。
-
- 如果插入触发了一个新的路径,Hopper会从输入中逐个删除新的调用,并检查每次删除后覆盖率是否保持不变。如果覆盖范围发生变化,Hopper会将移除的调用识别为到达新路径的关键调用。换句话说,在程序中找到了两个API函数之间的有效API间约束。
-
除了识别有效的调用序列,Hopper还识别API调用序列中的有效参数。一旦一个程序触发了新的路径,Hopper检查新的路径是否是由某些参数的突变引入的。
在变异过程中,Hopper将原始参数替换为来自该高速缓存的有效参数,以便为API调用引入适当的上下文。
四、 实现
4.1 模糊器
- Hopper使用从C头文件提取的语义生成输入。结合Rust的特性和宏,实现了这个过程。
- 首先,作者采用Rust bindgen [30]从库头文件自动生成Rust FFI绑定,包括类型定义和函数签名。对于C++库,Hopper只接受C风格的API声明。
- 其次,作者绑定中的类型定义了mutate、generate、serialize、malignalize trait,这些trait将自定义的行为应用于每种类型的对象以进行模糊处理。
- 最后,作者生成了一个对象构建器表,Hopper使用它来调用trait实现。
此外,这些自动生成的代码被编译并链接到模糊器。为了将触发bug的输入报告给开发人员进行分析,作者构建了一个将DSL转换为C源代码的工具。
4.2 解释器
Hopper使用E9Patch 的静态二进制重写来检测库二进制文件。作者从E9 AFL中借用了分支跟踪的代码,并将其改进为可控和API敏感的。此外,插装收集比较指令并挂钩资源管理功能(例如,malloc、free和fopen)。
当执行输入时,Hopper的解释器会根据实现malignalize trait的代码来解析它们。为了调用API,Hopper使用Rust FFI绑定和过程宏生成第3.2.2节中描述的调用者映射表的代码。然后将此代码编译到解释器中。与AFL类似,解释器使用分叉服务器技术来减少进程初始化的开销。一旦它接收到一个新的输入进行解释,它就会分叉一个新的进程。
在解释过程中时,Hoppe通过在内存中的数组值之后放置canary来检测内存溢出,这个内存区域由固定地址的mmap连续存储器实现。在竞技场中,数组的最后一个字节的地址与页面的最后一个字节对齐。为了检测溢出,Hopper在数组后保留了一个页面大小的内存,并将页面设置为mprotect不可读和不可写。
五、评估
作者在11个广泛使用的现实世界库上评估了Hopper。所有选定的库通常被各种应用程序使用,并已被OSS-Fuzz评估。为了证明Hopper的有效性,主要回答了以下研究问题:
- RQ1:Hopper在库中的效果如何?
- RQ2:Hopper能否正确推断API约束?
- RQ3:Hopper可以生成与MCF相当的程序吗?
5.1 Hopper在库中的效果如何?
5.1.1 代码覆盖率
为了证明Hopper能够模糊更多的API,作者统计了模糊器生成的有效程序调用的唯一API的数量。表1显示,平均而言,Hopper成功调用了93.52%的API,而MCF、FuzzGen和GraphFuzz分别只覆盖了18.58%、13.93%和41.42%的API。
5.1.2 漏洞查找率
对于Hopper触发的崩溃,作者首先消除了任何违反Hopper学习的API间约束的虚假崩溃,然后删除了在程序崩溃点具有相同堆栈跟踪的重复崩溃。其余的崩溃是通过检查代码、调试程序和阅读官方文档来手动验证的。
尽管Hopper由于其对那些预定义的API调用的低效突变能力而错过了其他模糊器报告的某些错误,但它优先考虑了更有可能触发错误或通过其种子选择探索新状态的突变API使用,其中包括库中的任何API使用。 Hopper发现了更多的bug。
Hopper在代码覆盖率和bug发现方面都优于MCF和其他库模糊方法。具体来说,Hopper检测到25个新的bug,其中17个已经被确认。
5.2 Hopper能否正确推断API约束?
Hopper能够生成正确和有效的API用法的关键之一是它能否学习内部和内部API约束。
Hopper能够以高精度(96.51%)和召回率(97.61%)学习API内约束,同时还能够通过使用API间约束来加速模糊搜索过程。
5.3 Hopper可以生成与MCF相当的程序吗?
通过语法感知的输入模糊,Hopper合成了满足API内和API间约束的程序。与MCF相比,Hopper生成的程序可以探索更广泛的API用法。
六、讨论
6.1 输入生成中的多维搜索空间
传统的库模糊生成一个字节数组作为输入,并将构造参数的工作留给模糊驱动程序。相比之下,在Hopper中,用于输入生成的搜索空间是多维的,因为它涉及API函数和参数,这构成了重大挑战。此外,每个参数都有自己的编码格式,需要特定的变异策略。虽然Hopper通过实现约束学习和类型感知突变等新技术来缓解这个问题,但仍然有很大的改进空间。
6.2 与C++库兼容
目前,Hopper仅支持对具有C格式头文件的库进行模糊测试。C++头文件中使用的模板直到用户实例化时才进行编译,这会延迟模板函数的编译,因此对于Hopper来说生成C++库的API调用和参数是具有挑战性的。此外,确定模板参数的具体类型来实例化模板也很重要。
为了使其与C++兼容,需要更通用的生成和变异方法。文章来源:https://www.toymoban.com/news/detail-782489.html
6.3 误报崩溃(False Positive Crashes)
尽管Hopper在推断常见约束以过滤掉大多数虚假崩溃方面非常有效,但剩余的崩溃仍然可能是误报,因为API需要以特定的方式使用。通过动态反馈学习这些约束可能具有挑战性,因为它们没有通用的标准,如第5.2节所述。然而,在模糊化过程中,Hopper将不再为未能学习约束并且很有可能虚假崩溃的API生成输入。为了使Hopper更加实用和用户友好,作者计划为用户添加关于未学习的约束的警告,并为他们自己添加这些自定义约束提供方便的方法。文章来源地址https://www.toymoban.com/news/detail-782489.html
七、结论
- 这篇文章中,作者提出了Hopper,一个新的Fuzzer,旨在Fuzz库中,而不需要任何领域知识来制作Fuzz驱动程序。
- Hopper将测试中的库与解释器链接在一起,解释器将DSL程序作为输入,并驱动库执行请求的模糊行为。
- 为了生成有效的DSL格式的API调用,Hopper学习库中API内部和API间的约束,并根据语法感知来改变输入。
- 作者评估了Hopper在11个真实库上的效果。Hopper在代码覆盖率和错误查找方面都优于MCFs和其他自动解决方案。
- 具体来说,Hopper发现了25个其他人找不到的新漏洞。实验结果表明,Hopper有效地为库的开箱即用模糊探索了大量的API使用。
到了这里,关于Hopper: Interpretative Fuzzing for Libraries——论文阅读的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!