- 【参考文献】Zhan Q, Fang R, Bindu R, et al. Removing RLHF Protections in GPT-4 via Fine-Tuning[J]. arXiv preprint arXiv:2311.05553, 2023.
- 【注】本文仅为作者个人学习笔记,如有冒犯,请联系作者删除。
目录
摘要
一、介绍
二、背景
1、库的模糊测试
2、模糊测试解释器
三、设计
1、概述
2、DSL和输入解释
2.1、 DSL
2.2、解释器
3、语法感知输入模糊测试 (Grammar-aware Input Fuzzing)
3.1、针对调用语句的突变策略 (call statement mutation)
3.2、基于类型感知的值突变策略 (type-aware value mutation)
3.3、输入最小化 (Input Minimization)
4、约束学习
4.1、API内部约束 (Intra-API Constraint)
4.2、API之间约束 (Inter-API Constraint)
四、实现
1、模糊器
2、解释器
五、评估
六、讨论
1、输入生成中的多维搜索空间
2、与c++库的兼容性
3、误报崩溃
七、结论
摘要
- 模糊测试技术在最近几年已经证明其漏洞挖掘的有效性,但由于模糊测试需要手动为测试对象构造模糊驱动程序 (Fuzz Dirvers) ,导致仍有非常多的代码(如API)不能被测试到。
- 【注】模糊驱动程序 (Fuzz Dirvers):用于执行模糊测试的工具或程序。模糊驱动程序负责将模糊器生成的测试用例输入到被测试的软件、系统或组件中。
- 尽管最先进的模糊器可以有效地生成输入,但现有的模糊驱动程序仍然不能充分地覆盖库 (Library)。(库提供API给用户使用)
- 【注】模糊驱动程序将模糊器生成的输入转换为API的参数,然后执行API对库进行测试。故模糊驱动程序需要针对API进行编写,并且可以是多个顺序不同的API,而手动编写各种各样的模糊驱动程序是不切实际的。
- 模糊驱动程序大多数都是由开发人员手动编写的,它们的质量取决于开发人员对代码的理解。现有的工作已经尝试通过从代码和执行跟踪中学习API的使用方法来自动生成模糊驱动程序。但这样生成的模糊驱动程序只能是一些特定的调用序列,即局限于已知的使用模式,无法覆盖未知或不常见的调用序列。
- 为了解决这些挑战,本文提出了Hopper,它可以对库进行模糊测试,而不需要任何专业知识来制作模糊驱动程序。它将库的模糊问题转化为解释器的模糊问题。链接到被测库的解释器可以解释并执行任意API的调用。
- 为了生成解释器能使用的输入,Hopper学习了库的API内约束和API间约束,并用语法感知突变程序。经过实验证明,Hopper能够直接对库进行模糊测试,以此来探索API的广泛用法。
一、介绍
- 模糊测试 (Fuzzing)作为发现软件漏洞最流行的技术之一,通过向软件中输入大量的随机字节流,以此来发现其中潜在的Bug。基于约束的灰盒模糊器 (Constraint-based grey-box fuzzers)作为最先进的模糊器,使用约束求解技术来到达由复杂约束保护的代码分支。
- 虽然灰盒模糊测试技术极大地促进了程序模糊测试的泛化和自动化(以现成的二进制程序为测试对象),但要将这种方法运用到库中(以API为测试对象)是具有挑战性的。
- 要为库使用最先进的模糊器,用户必须手动创建一个模糊驱动程序。该驱动程序通过将模糊器提供的随机字节流转换为API的输入。这些输入不仅需要满足正确的参数类型,而且需要满足API内部和API之间的约束。之后再由模糊驱动程序去调用API进行测试。
- 但是编写高质量的模糊驱动程序十分困难,不仅耗时,而且需要对库有深刻的理解。因此,大多数现有的模糊驱动程序只覆盖了少部分库的API,而那些不常使用的API就会缺乏充分的测试。
- 最近,研究人员提出了基于学习和基于模型自动生成模糊驱动程序的方法。
- 基于学习:例如,FuzzGen试图从现有的消费者代码中学习API的使用方法。但是,如果没有消费者代码可用时,如新的或正在创建的库,该方法就会失效。
- 【注】消费者代码:指的是使用特定软件、库或API的程序或代码。在软件开发中,这些代码通常被称为消费者,因为它们使用或消费提供的功能、库或服务。
- 基于模型:例如,GraphFuzz要求用户提供被测API的使用规范,并且需要专业知识和大量的人工参与。
- 上述两个方法生成的模糊驱动程序的质量很大程度上受到外部输入(消费者代码或用户提供的专业知识)的影响,而这些输入可能是不准确或不完整的。
- 基于学习:例如,FuzzGen试图从现有的消费者代码中学习API的使用方法。但是,如果没有消费者代码可用时,如新的或正在创建的库,该方法就会失效。
- 是否可以完全舍弃模糊驱动程序,直接生成一个待执行的程序,从而达到自动测试任意库API的目的呢?
- 为此,本文提出了Hopper来完全零人工地学习和测试库的API。Hopper主要由两个部分组成:轻量级的解释器和基于语法感知 (grammar-aware)的模糊器。二者通过一个自定义的领域特定语言 (DSL)交流连接。模糊器生成具有DSL语法的程序,包含调用的API及其参数。解释器和被测库链接在一起,可以解释DSL程序,并执行解析后的API。根据解释器的执行反馈,模糊器可以推断约束、持续优化下一轮生成的DSL程序。
- 【注】领域特定语言 (DSL):Domain-Specific Language,是针对特定领域、特定问题或特定任务而设计的计算机编程语言。与通用编程语言(如C、Python、Java等)相比,DSL更专注于解决某一特定领域的问题,并提供了更高的抽象级别和更直观的表达能力。
二、背景
1、库的模糊测试
- 由于库在各种程序中被广泛应用,所以保障库的安全十分重要。然而,仅仅对程序进行模糊测试来测试库往往是不够的。程序可能在复杂的路径约束条件下调用库的API,或者使用特定的参数调用库的API,这使得这些API很难被完全测试到。为了解决这个问题,已经出现了专门的模糊测试工具来测试库,例如,LibFuzzer。下面是使用LibFuzzer测试库的必要步骤:
- 制作模糊驱动程序 (Craft a fuzz drivers)
- 模糊驱动程序是一个描述了库API用法的程序,包括API调用序列及其参数。一个高质量的模糊驱动程序应该提供一个入口,以便在库中探索尽可能多的代码,进行彻底的测试。
- 然而,要列举出所有有效的API用法费时又费力。因此,模糊驱动程序通常只包含一些常用的API。
- 例如,下图给出了11个C/C++库中API的模糊测试覆盖情况。可以看到,尽管这些库在各种软件和系统中作为基础设施被广泛地应用,然而他们的开发者只将少部分的API编写进了模糊驱动程序中。
- 此外,由于不同的API使用可能涉及到不同的输入类型、参数组合和代码执行路径,因此将它们简单地按顺序串联起来进行模糊测试并不是最有效的方法。更好的策略是针对不同的API使用情况编写多个模糊测试驱动程序,或者在单个模糊测试驱动程序中根据条件有选择地执行特定的API进行测试。
- 例如,下图模糊驱动程序在调用解析函数时,会根据前4个字节的数据调用不同的打印函数。
- 指定输入格式 (Specify the format of input)
- LibFuzzer生成的随机字节流使得创建满足API内部约束的结构化输入变得困难。为了解决这个问题,我们需要指定输入格式,并引导模糊器生成更多复杂的输入。然而,API内部约束的存在使得定义输入的参数范围成为了一项艰巨的任务。开发人员可能将参数直接编码为模糊驱动程序中的常量。
- 例如下图中的 cJSON_ParseWithOpts 的第一个参数。但使用这种方法可能导致对API的测试不充分。
- LibFuzzer生成的随机字节流使得创建满足API内部约束的结构化输入变得困难。为了解决这个问题,我们需要指定输入格式,并引导模糊器生成更多复杂的输入。然而,API内部约束的存在使得定义输入的参数范围成为了一项艰巨的任务。开发人员可能将参数直接编码为模糊驱动程序中的常量。
2、模糊测试解释器
- 解释器能够解析DSL程序归功于以下两个关键技术:
- Grammar-aware input mutation
- 随意突变输入可能会被解释器拒绝解析。语法感知突变根据编码语法将输入解析为中间表示IR,并在遵守约束的同时对IR进行突变。
- Coverage guided fuzzing
- 在覆盖反馈的指导下,模糊器会保留触发新路径的输入,并进一步对它们进行突变,以进入更深的分支或发现新的错误。
- Grammar-aware input mutation
- 我们观察到,构建库的模糊驱动程序类似于为输入实现一个解释器,而对解释器进行模糊测试等同于在底层对库进行模糊测试。
- 【注】原本是模糊器生成随机字节流,模糊驱动程序将随机字节流转换为API的参数,再调用API进行测试(模糊驱动程序包含API的调用)。现在是模糊器生成具有DSL语法的程序,其中包含调用的API及其参数。解释器和被测库链接在一起,解释DSL程序后再执行对应的API。
三、设计
1、概述
- Hopper将库的模糊问题转化为解释器的模糊问题。它主要由主要由两个部分组成:生成DSL程序的基于语法感知的模糊器和执行DSL程序的轻量级解释器。
- 模糊器为调用库API生成高质量的DSL程序。它首先需要从库的头文件中提取API函数声明以及参数类型的详细定义,然后利用这些信息生成一系列API调用。
- 解释器解析生成的DSL程序。它在编译阶段与被测库相链接,当输入程序到达时,解释器根据DSL语法解析程序,并按顺序执行语句。
2、DSL和输入解释
- 通常,为了模糊测试一个库,开发模糊驱动程序所使用的编程语言会和该库的编程语言一致。对于像C/C++这样的编译语言,模糊驱动程序需要先进行编写和编译。然后在模糊测试期间,每一轮测试只会修改模糊驱动程序的输入,而不会对模型驱动程序重新进行编译。(API调用序列包含在模糊驱动程序中)
- 但是这样的测试是不充分的,因为API的调用顺序是固定的。如果需要调整API的调用顺序,就需要重新对模糊驱动程序进行编写和编译。
- 为了避免在模糊测试期间频繁地编写和编译模糊驱动程序,Hopper引入了DSL和一个轻量级的解释器。这允许在模糊测试过程中动态地更改API调用序列,而无需重新编译模糊驱动程序,从而加速整个模糊测试过程。(DSL程序包含了API的调用序列)
2.1、 DSL
- Hopper中的解释器接收DSL程序作为输入。每个程序由一系列语句构成,每个语句都具有一个唯一的索引,使得后续的语句可以引用到它。
- DSL常见的模糊测试语句可以归类为下面五种:
- Load语句
- 用于定义了变量的类型和值。强类型 (Strong typing)使得在模糊测试中可以直接使用特定类型的值来生成输入数据,而无需先将其从随机字节流转换为特定类型的值。即DSL中的Load语句允许直接创建特定类型的值,而不用从一个无法确定其类型的字节流中解析出值。
- Call语句
- 用于调用库中的API,需要提供函数名称和参数列表,其中每个参数引用了Load语句定义的变量。
- Update语句
- 在运行时重写了Call语句的返回值。
- Assert语句
- 在运行时对Call语句的返回值进行检查。
- File语句
- 指定了用于I/O操作的文件资源。
- Load语句
2.2、解释器
- 解释器负责解析DSL程序,并在执行每条语句后,监视程序的状态。在编译阶段,Hopper将解释器与被测库进行链接,以便在执行期间能够调用库中的API函数。
- 解释器还构建了一个表(映射表),根据库的头文件将每个函数的名称与其具体实现的代码块关联起来。在程序执行期间,代码块将值转换为其所需的参数类型,然后使用这些参数调用函数。
- 在链接解释器之前,Hopper用计算分支的代码对库的二进制文件进行检测,并且会挂钩 (hooks)比较指令和资源管理函数。
- 【注】挂钩 (hook):挂钩允许开发者在程序执行的特定位置插入自定义代码,以便在该位置执行额外的操作或监控程序的行为。
- 解释器在运行时可以获取以下反馈信息:
- 可选的分支跟踪 (Optional Branch Tracking)
- Hopper定义了一个全局标志来决定是否运行分支跟踪代码,该标志的值由DSL程序决定。当调用API以问好结尾时(如下图),解释器为该调用启用分支跟踪。
- 上下文敏感的代码覆盖率 (Context-sensitive Code Coverage)
- 解释器在执行过程中会根据当前正在执行的API函数的名称生成哈希值,并将其视为执行上下文的一部分。当涉及到不同的API调用时,即使它们访问了相同的代码分支,由于上下文信息(即不同API函数名称的哈希值)不同,每个API调用的执行路径将被视为不同的路径。
- 溢出检测 (Overflow Detection)
- 当一个语句加载一个大小可变的值(如数组)时,解释器会将这些值存储在内存区域,并在最后附加一个canary标记。如果发生了尝试写入或修改canary之后内容的行为,就可能发生了溢出。
- 使用已释放内存检测 (Use-after-free Detection)
- 解释器在运行过程中维护一组由malloc分配、由free释放的内存块。针对函数参数中使用的每个指针,如果指针所指向的内存块已经被释放,解释器会立即终止程序,以避免出现使用已释放内存的问题。
- 比较挂钩 (Comparison Hooking)
- 对比较指令和函数行进行挂钩。在这个过程中,解释器会收集比较指令和函数中使用的参数,以指导模糊器解决魔术字节 (magic bytes)。
- 【注】魔术字节 (magic bytes):指在编程或计算机系统中用作特殊标识或特定用途的特殊字节序列。在模糊测试中,解决或正确处理这些“魔术字节”通常是一个重要的目标,因为它们可能作为特定条件的标识或触发特定行为的关键。
- 对比较指令和函数行进行挂钩。在这个过程中,解释器会收集比较指令和函数中使用的参数,以指导模糊器解决魔术字节 (magic bytes)。
- 可选的分支跟踪 (Optional Branch Tracking)
3、语法感知输入模糊测试 (Grammar-aware Input Fuzzing)
- 为了让模糊器可以生成足够多复杂的程序来触发隐藏在待测库中的程序缺陷,Hopper中的模糊器采用两个步骤来生成程序。首先,Hopper根据API描述生成最简单的输入(DSL程序),以初始化种子池;其次,Hopper在种子池中选择输入,并在覆盖度反馈的指导下对其进行突变,来生成更多不同的输入。
- pilot phase
- 在这一阶段,模糊器会尝试根据头文件中的函数声明信息来为待测API组装出简单的测试程序。根据API参数类型的不同,模糊器会选择直接生成(基本类型或者定义完整的复合类型)或是从其他API的返回值中获取(不透明指针或空类型指针)参数。
- 下图是一个简单的例子,测试的目标是void foo (Bar bar)。如果生成的程序触发了新的代码路径,那么就把这个程序添加到Hopper的种子池中。
- 【注】为了方便大家理解,该案例使用C的语法来表示fuzzer生成的程序,而不是Hopper的DSL。
- Evolution
- 在这一阶段,Hopper会依次从种子池中取出先前保存的程序,并根据语句的类型随机对程序进行变异操作。如果变异后的程序触发了新的代码路径,则将突变后的程序保存到种子池中。
- Hopper变异程序语句具体有以下五个步骤:
- 按优先级从种子池中选择更有可能到达更深路径的程序。
- 根据语句的权重选择程序中的语句进行变异。assert、file和update语句的权重为零,不发生变化,而load和call语句的权重由其复杂性决定。
- 根据对应的策略对load语句和call语句进行变异。(详见3.1和3.2)
- 根据在模糊测试过程中学习到的约束,对程序进行过滤,保证能正常测试库的API。(详见4)
- 对程序进行优化,去除对到达路径没有影响的冗余语句。(详见3.3)
- 在下图的例子中,Hopper为测试的目标函数引入了一个与它共享同一参数的函数调用语句来完成变异。
3.1、针对调用语句的突变策略 (call statement mutation)
- Hopper对call语句采用了下列的突变策略:
- 将传递给函数的一个参数替换为一个同类型的新参数。
- 在原始目标调用之前插入一个新的函数调用或方法调用。这个新的调用可能会改变传递给目标调用的参数值,或修改程序所依赖的库的全局状态。
- 更新调用的返回值。在原始调用之后插入一个更新语句,旨在修改调用生成的返回值。
3.2、基于类型感知的值突变策略 (type-aware value mutation)
- 由于库会使用各种参数类型,包括自定义复合类型,Hopper需要在调用API时生成适当类型的参数传入。对于值突变也是如此,根据值的类型进行突变可以更有效地探索新状态。
- Hopper使用下列策略生成新的类型值:
- 原始类型 (Primitive Types)
- 几乎所有的原始类型都是数值类型。因此,Hopper在一个小范围内生成符合均匀分布的数字。
- Hopper使用下面四种方法中的一种对原始值进行突变:
- 设置一个有趣的值(例如,对于int类型,设置为0x80000000)。
- 翻转一个位或一个字节。
- 加或减一个小的数字。
- 如果该值用作比较指令的操作数之一,则设置为之前比较指令中使用过的值。
- 数组 (Array)
- Hopper会根据数组的长度和元素类型生成一个序列。
- 如果数组长度是可变的,Hopper首先随机选择一个长度。在突变过程中,Hopper从数组中选择一个或多个元素,分别对它们进行突变。还可以通过插入或删除元素来调整数组的大小。
- 结构 (Structure)
- 在Hopper生成自定义结构类型的值时,它会通过递归地生成其字段来创建这些值。
- 在突变自定义结构类型的值时,Hopper会随机选择该结构中的一个字段,并根据该字段的类型进行变异。
- 简单的指针 (Trivial Pointer)
- Trivial指针(如简单数据类型和结构类型的指针)。Hopper通过以下操作对其进行突变:
- 将该指针值设置为 null,表示它不指向任何有效的内存位置。
- 指向具有相同类型的另一个指针所指向的内存位置。
- 创建一个新的数组,其元素类型与该指针指向的对象类型相同,并将该指针指向这个新数组的位置。
- 创建一个新的调用语句,该语句返回值与该指针类型相同,并将该指针指向这个新生成调用语句的返回值所在的内存位置。
- Trivial指针(如简单数据类型和结构类型的指针)。Hopper通过以下操作对其进行突变:
- 非凡的指针 (Nontrivial Pointer)
- Nontrivia指针(如不透明指针、void指针和函数指针),由于其不可预测的性质,Hopper不能直接进行突变。因此,Hopper会对它们进行单独处理。
- 不透明指针 (opaque pointers)
- Hopper会检索用于初始化不透明指针的API函数。这些API函数可能通过返回指针值或通过引用传递来对指针进行赋值。Hopper会获取这些API函数,并通过它们来初始化不透明指针。
- 【注】不透明指针 (opaque pointers):隐藏了指针所指向的具体数据结构或对象的信息,仅允许通过特定的函数或接口来操作和访问其所指向的数据。
- void指针
- 如果void指针有别名,如“typedef void* MyVoidPointer;”,Hopper也会将它们视为不透明指针处理。
- 否则,Hopper会尝试将随机字节数组强制转换为所需的void指针类型,再查看它的有效性。(详见4.1)
- 【注】void指针是一种通用的指针类型,可以指向任意类型的数据。
- 函数指针
- Hopper在编译阶段合成具有所需签名的空函数,以便模糊器可以使用它们的地址作为指针。
- 原始类型 (Primitive Types)
3.3、输入最小化 (Input Minimization)
- 冗余的语句和数值会降低执行速度,并增加变异过程中的搜索空间,导致模糊测试效率低下。为了解决这个问题,Hopper采用了两个步骤来最小化输入:
- 在变异和优化后最小化输入: Hopper在变异和优化后检查输入中的语句,从后向前排除目标调用语句。如果一个语句不再被其他语句引用,就将它删除。
- 例如,下图中(c)中的第0行在变异后变得多余,因此在(d)中将其删除。
- 最小化触发新路径的输入
- 删除对执行路径没有影响的函数或方法调用。(详见4.2)
- 在加载数据的语句中,Hopper会识别和删除冗余的数值。
- 设置指针值为null或缩小数组长度。
- 在变异和优化后最小化输入: Hopper在变异和优化后检查输入中的语句,从后向前排除目标调用语句。如果一个语句不再被其他语句引用,就将它删除。
4、约束学习
- 为了正确调用API,Hopper生成的DSL程序必须同时满足API内部和API之间的约束。API内部的约束规定必须使用适当的参数调用API,而API之间的约束则指定了调用API的适当顺序。
- Hopper通过库的运行时反馈来学习这些约束。概括地来讲,一旦解释器遇到了一个crash,那么会将和crash相关的信息发回给模糊器,模糊器内的约束学习模块会对这些信息进行分析并推导出对应的约束,这些约束将用于纠正模糊器生成的错误的输入程序。
4.1、API内部约束 (Intra-API Constraint)
- 在Hopper中,API内约束包括:
- 指针非空 (NON-NULL)
- API函数如果不检查空指针,在使用空指针调用时可能导致程序崩溃。
- 有效文件资源 (FILE)
- 当API函数涉及到对文件进行读取或写入操作时,作为参数提供的文件名必须是有效的。
- 特定值 (EQUAL)
- 一些API函数使用数值作为参数来指定数组指针的边界,如果数值不正确可能会导致溢出错误。
- 受限区间 (RANGE)
- 参数为数字或分配资源有限的API,如果数字超出范围可能会遇到资源耗尽或溢出错误。
- 数组长度 (ARRAY-LEN)
- 有些API函数在使用指针引用的数组时,不要求参数指示边界。当实际传递给函数的数组元素数量少于函数所期望的数量时,可能会导致溢出错误。
- 指定类型转换 (CAST)
- 由于void类型缺少初始信息,开发者需要生成具体类型的对象并将其引用强制转换为void指针。
- 指针非空 (NON-NULL)
- 对于没有别名的void指针,它们可以从随机字节数组中强制转换过来,Hopper会添加CAST约束将它们视为char类型。
- 当输入触发新的执行路径时,Hopper会检查是否触发了文件打开函数(如fopen),并比较文件名与用于调用API的参数是否匹配。如果存在匹配项,将为相应的参数创建一个FILE约束。
- 如果一个输入触发了一个新的crash,Hopper将按照以下步骤推断API内部约束(以访问空指针为例):
- 如果程序因访问空指针而导致分段错误,Hopper会收集崩溃程序的反馈信息,如果程序抛出了SIGSEGV的信号,并且导致崩溃的内存地址是一个接近0的值,那么Hopper就认为这一崩溃是由一次空指针访问引起的。而如果此空指针是Hopper自身生成的空指针,那么我们就为这个空指针对应的参数添加一个NON-NULL约束,避免对此参数生成空指针。
- 这些约束用于对突变后的输入进行过滤,违反约束的输入将从种子池中删除。
4.2、API之间约束 (Inter-API Constraint)
- API调用可以修改其引用的参数或程序中的内部状态,从而影响后续的调用过程。API调用之间存在的各种关系和依赖被称为API之间的约束。
- Hopper通过以下步骤学习API函数之间的有效关系。
- 当Hopper在目标API调用之前插入新的调用时,它仅跟踪目标调用的覆盖率,即只有目标调用的执行路径和代码覆盖率会被记录和分析。
- 如果插入的新调用引发了程序执行新的路径,Hopper会逐个删除这些新调用,并在每次删除后检查覆盖率是否保持不变。如果覆盖率发生变化,那么被移除的调用被认为是到达新路径的关键步骤。
- 如果生成或是变异后的程序恰巧捕捉到了某一种API间的特殊关系(触发了新的代码覆盖),那么就将这个程序保存到种子池中。此外,Hopper还会分析出引入新的代码覆盖的具体API参数,然后针对该参数对程序进行切片,将与该参数相关的所有语句保存,以供下一次需要为API生成对应类型的参数时复用,这有助于确保API调用在新的上下文环境下能够触发类似的有效路径,从而提高模糊测试的效率和覆盖率。
四、实现
1、模糊器
- Hopper的模糊器从C头文件中提取语义信息,以生成有效的输入。
- 首先,使用Rust的bindgen工具自动生成Rust FFI (Foreign Function Interface)绑定。这些绑定包括类型定义和函数签名,是从库的头文件自动生成的。对于C++库,Hopper仅接受C的API声明,因为bindgen还不支持像模板这样的特性。
- 接下来,为绑定中的类型定义了mutate、generate、serialize和deserialize等特性。这些特性将应用于不同类型的对象,以用于模糊测试。对于原始类型,由于它们在库中具有通用的内存布局,所以需要手动实现了这些特性。至于自定义结构,使用了Rust的过程宏自动生成了实现这些特性的代码。
- Hopper还为函数指针类型合成了空函数,根据其函数签名自动生成这些函数。当作为回调函数调用时,这些函数不执行任何操作,仅返回一个零初始化的对象。
- 最后,生成了对象构建器表,Hopper用它来调用特性实现。这些自动生成的代码会被编译并链接到模糊器中。
- 此外,为了将触发漏洞的输入报告给开发人员进行分析,还建立了一个将DSL转换为C源代码的工具,以便开发人员进行更深入的分析。
2、解释器
- Hopper使用E9Patch对库二进制文件进行静态二进制重写。还借鉴了E9AFL的代码,用于分支跟踪,并对其进行改进,使其可控制并对API敏感。此外,该工具收集比较指令和钩取资源管理函数(例如,malloc、free和fopen)。
- 在执行输入时,Hopper的解释器根据实现反序列化特性的代码对其进行解析。为了调用API,Hopper使用了调用者映射表(详见三2.2)的代码,该代码使用Rust FFI绑定和过程宏生成。然后将此代码编译到解释器中。类似于AFL,解释器使用fork服务器技术来减少进程启动的开销。一旦接收到新的输入,解释器就会为其派生一个新进程以进行解释。
- 在解释过程中,Hopper会在内存区域的数组值之后放置一个页面大小的canary标记来检测内存溢出,并通过mprotect将页面设置为不可读、写。这个内存区域通过在固定地址上映射连续的内存来实现。在该区域内,数组的最后一个字节的地址与页面的最后一个字节对齐。
五、评估
- 我们在11个广泛使用的真实C/C++库上测试了Hopper,测试的对象包括cJSON, c-ares, libpng, lcms, libmagic, libpcap, zlib, cre2(re2的C程序封装),sqlite3, libvpx以及libaom。每一组测试设定为24小时,重复实验5次并对结果取平均值。所有的测试都绑定在单核上运行。我们将Hopper的测试结果与相同条件下,MCFs,FuzzGen和GraphFuzz的结果进行了比较。评估的标准主要包含3个维度:代码覆盖率,bug发现和习得约束的正确性和有效性。
- 代码覆盖率:
- Bug发现:
- 约束有效性:
- 约束正确性:
- 实验结果表明:
- Hopper在代码覆盖率和Bug发现方面都优于mcf和其他库模糊测试方法。
- Hopper能够以较高的准确率(96.51%)和召回率(97.61%)学习API内部约束,同时还能够通过使用API之间约束来加速模糊化过程中的搜索过程。
- 通过语法感知输入模糊,Hopper合成了既满足API内部约束又满足API之间约束的程序。与mcf相比,Hopper生成的程序可以探索更广泛的API用法。
六、讨论
1、输入生成中的多维搜索空间
- 传统的库模糊测试生成一个随机字节流作为输入,并将构建参数的任务留给模糊驱动程序。与之相反,在Hopper中,输入生成的搜索空间是多维的,因为它涉及到API调用和参数,这带来了重大挑战。此外,每个参数都有其自己的编码格式,并且需要使用特定的变异策略。尽管Hopper通过实现诸如约束学习和类型感知变异等技术来缓解这个问题,但仍有很大的改进空间。
2、与c++库的兼容性
- 目前,Hopper仅支持对具有C格式头文件的库进行模糊测试。C++头文件中使用的模板直到用户实例化时才进行编译,这会延迟模板函数的编译,因此对于Hopper来说生成C++库的API调用和参数是具有挑战性的。
- 此外,确定模板参数的具体类型来实例化模板也很重要。为了使其与C++兼容,需要更通用的生成和变异方法。
3、误报崩溃
- 尽管Hopper推断常见的约束非常有效,能过滤掉大多数虚假崩溃,但由于API需要以特定的方式使用,因此剩余的崩溃可能仍然是误报的。通过动态反馈学习这些约束可能具有挑战性,因为它们没有固定的标准。
- 然而,在模糊测试过程中,Hopper将不再为那些未能学习约束并具有高概率虚假崩溃的API生成输入。
- 为了使Hopper更加实用和友好,我们计划向用户发出关于未学习约束的警告,并为用户提供方便的方式来添加这些自定义约束。
七、结论
- 本文提出了一种新的模糊器Hopper,旨在无需制作模糊驱动程序所需的任何领域知识来模糊测试库。被测库与Hopper的解释器链接在一起,解释器以DSL程序作为输入,并驱动库执行请求的模糊测试。
- 为了以DSL格式生成有效的API调用,Hopper中的模糊器学习了库中的API内部和API之间的约束,并用语法感知改变了输入。
- 本文评估了Hopper在11个库中的有效性。Hopper在代码覆盖率和Bug发现方面都优于mcf和其他测试方案。实验结果表明,Hopper能有效地探索库API的广泛使用方式,并用于模糊测试。