这篇文章将指出如何将单个基于fuzzer变异生成的输入分成多个部分,即所谓的sub-inputs。
为什么要将单一输入分成多个输入
fuzz中将单一输入分成多个独立的部分非常有用且非常常见,有一些例子:
- fuzz正则表达式库需要
正则表达式
用于正则表达式重新编译和匹配的标记
搜索正则表达式所需要的字符串 - fuzz 音频/视频格式解析器需要
解码标志
一些数据帧 - fuzz XSLT/CSS库需要:
样式表
XML/HTML输入
Common Data Format
当试图将fuzz生成的输入分割成几个部分时,我们需要问的第一个问题是输入格式是否常见,即它是否被其他fuzz目标的库、api使用或处理。
如果数据格式是常见的(例如广泛使用的媒体格式或网络数据包格式),那么fuzz目标非常希望使用这种数据格式,而不是一些自定义修改的输入。这样就更容易为这个模糊目标获取种子语料库,并使用生成的语料库来测试/模糊其他目标。
Multiple Options
当通用数据格式的fuzz目标需要一些标志、选项或额外的辅助子输入(sub-input)时,有时可能将额外的输入嵌入到主数据格式的自定义部分或注释中。
例如:
- PNG允许自定义“块”(chunk),因此fuzzPNG解析器时,可以将PNG处理过程中使用的flags隐藏在另一个PNG块中。
- 当fuzz C/ c++ /Java/JavaScript输入时,可以将子输入隐藏在一行//注释中。
Hash
当我们只需要一个固定大小的子输入时,我们的fuzz目标可能会对该输入进行hash并将结果作为flag,(例如标志flag/选项options)时,这种操作很容易实现,但是它的适用性仅限于相对简单的情况。主要问题是输入的局部突变会导致子输入的巨大变化,这往往会使fuzz的效率降低。如果flag是单独的位,并且输入类型允许在输入中进行一些位翻转(例如纯文本),请尝试这种方法。
自定义序列化格式
如果你不希望和其他API或者fuzz对象共享语料库(corpus),那么对于多输入的fuzz对象,可以采用自定义序列化格式。
First/Last Bytes
fuzz时只需要一个固定大小的子输入(例如标志/选项)时,可以将输入的第一个(或最后一个)K个字节作为子输入,其余字节作为主输入。
只需记住将主输入复制到一个大小为SIZE-K字节的单独堆缓冲区中,以便检测主输入溢出的缓冲区。
Magic separator
选择一个4字节(或8字节)的magic常量作为输入之间的分隔符。在fuzz目标中,使用此分隔符分割输入。使用memmem在输入中找到分隔符——memmem已知对fuzzing引擎很友好,至少对libFuzzer很友好。
例如
// Splits [data,data+size) into a vector of strings using a "magic" Separator.
std::vector<std::vector<uint8_t>> SplitInput(const uint8_t *Data, size_t Size,
const uint8_t *Separator,
size_t SeparatorSize) { ... }
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
const uint8_t Separator[] = {0xDE, 0xAD, 0xBE, 0xEF};
auto Inputs = SplitInput(Data, Size, Separator, sizeof(Separator));
// Use Inputs.size(), Inputs[0], Inputs[1], ...
}
对于一个现代的fuzz引擎来说,发分隔符是相对容易的,但我们任然建议在几个种子输入中嵌入所需的分隔符。
Fuzzed Data Provider
FuzzedDataProvider (FDP)是一个单头c++库,它有助于将fuzz输入拆分为类型不同的多个部分。它是LLVM的一部分,可以通过
#include <fuzzer/ fuzzeddataprovider>
包含到程序中。如果你的编译器没有这个头文件(Clang版本过旧或其他原因),你可以从这里复制头文件并手动添加到你的项目中。它应该可以工作,因为头文件不依赖于LLVM。
使用这个库的优点和缺点是,输入分割是动态发生的,也就是说,你不需要定义输入的任何结构。这在某些情况下可能非常有用,但也会使语料库不再是特定的格式。例如,如果fuzz图像解析器并将fuzz输入拆分为几个部分,那么语料库元素将不再是有效的图像文件,您也不能简单地向语料库添加图像文件。
Main concepts
- FuzzedDataProvider是一个类,它的构造函数接受const uint8_t*,size_t参数。通常,您会在LLVMFuzzerTestOneInput开头调用它,并传递fuzzing引擎提供的数据和大小参数。
- 一旦使用fuzz输入构造了FDP对象,您就可以通过调用下面列出的FDP方法来使用输入中的数据
- 如果没有数据,FDP将返回所请求类型的默认值或一个空容器(当消耗一个字节序列时)。
- 如果在循环中使用FDP的数据,请确保在循环迭代之间检查remaining_bytes()返回的值。
- 不要使用返回std::string的方法,除非你的API需要一个字符串对象或一个尾部为空字节的c风格字符串。这是一个常见的错误,它会导致AddressSanitizer无法检测off-by-one缓冲区溢出。
Methods for extracting individual values
- ConsumeBool、ConsumeIntegral、ConsumeIntegralInRange方法有利于提取一个布尔或整数值(确切的类型被定义为一个模板参数),例如一些目标API的flag,或为一个循环的迭代次数,或fuzz输入的部分长度
- consumerprobability、consumerfloatingpoint、consumerfloatingpointinrange方法与上面提到的非常相似。不同之处在于这些方法返回一个浮点值。
- 当需要从预定义的值集(如enum或数组)中选择模糊输入时,ConsumeEnum和PickValueInArray方法非常方便。
上述方法使用fuzz输入的最后一个字节来派生所请求的值。这允许在某些情况下使用有效/测试文件作为种子语料库。
Methods for extracting sequences of bytes
这些方法中的许多都有一个length参数。通过调用remaining_bytes()方法,您总是可以知道provider对象中还剩下多少字节。
Type-length-value
定制的Type-length-value (TLV)听起来可能是一个不错的解决方案。然而,我们通常不推荐使用定制的TLV来分割fuzz生成的输入,原因如下:
- 这是您需要维护的仅用于测试的代码,而且很容易出错
- fuzzing引擎执行的典型突变,如插入一个字节,将过于频繁地破坏TLV结构,从而降低fuzzing的效率
但是,将TLV输入与自定义变值器结合使用可能是一个不错的选择。