How To Split A Fuzzer-Generated Input Into Several

这篇文章将指出如何将单个基于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生成的输入,原因如下:

  1. 这是您需要维护的仅用于测试的代码,而且很容易出错
  2. fuzzing引擎执行的典型突变,如插入一个字节,将过于频繁地破坏TLV结构,从而降低fuzzing的效率

但是,将TLV输入与自定义变值器结合使用可能是一个不错的选择。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值