改进模糊测试工具

当您首次开始对新目标进行模糊测试时,模糊测试工具可能会非常快速地崩溃。通常进行模糊测试 最初发现的缺陷激增,然后是长尾。缺陷出现频率 的下降原因可能有多种,其中包括: * 被测代码中的缺陷较少。 * 代码的某些部分不会经过模糊测试工具测试。

为了区分这几种情况,您需要能够评估和改进模糊测试工具。

提高代码覆盖率

改进模糊测试工具的第一步是了解其当前性能。一个 对于覆盖率引导型模糊测试工具,一个显而易见的关键指标是代码覆盖率。您可以收集这些信息 使用 fx fuzz

例如:

fx fuzz analyze package/fuzzer

这会运行模糊测试工具 60 秒,并报告设备上语料库的代码覆盖率。 如果您指定 --staging 选项,系统会先将该目录中的文件添加到语料库。

添加或改进种子语料库

如果发现代码覆盖率存在缺口,您可以将单个输入添加到种子语料库中:

  1. 将目录添加到模糊测试工具附近的源代码树。
  2. 将一个或多个文件添加到此目录,每个文件都包含测试输入的原始字节, 会导致模糊测试工具访问之前未发现的代码。
  3. 为此目录添加一个 resource GN 目标,并将其添加到模糊测试工具的 deps 中。
  4. 添加一个参数,其中包含模糊测试工具的 组件清单源。

例如:

cd $FUCHSIA_DIR
mkdir path-to-library/my-fuzzer-corpus
cp handcrafted-input path-to-library/corpus

//path/to/library/BUILD.gn 中:


import("//build/fuzz.gni")
import("//build/dist/resource.gni")

resource("my-library-corpus") {
  sources = [
    ...
    "corpus/handcrafted-input",
    ...
  ]
  outputs = [ "data/my-library-corpus/{{source_file_part}}" ]
}

cpp_fuzzer("my_fuzzer"){
  sources = [ "my-fuzzer.cc" ]
  deps = [
    ":my-library",
    ":my-library-corpus"
  ]
}

在模糊测试工具的组件清单来源中:

{
  ...
  program: {
    args: [
      ...
      "data/my-library-corpus",
      ...
    ]
  }
}

让代码更易于模糊测试

一般来说,libFuzzer 在查找用于探索新条件分支的输入方面相当有效 就会出现这种问题。例如,它可以使用 比较说明(例如 CMP)来确定与某些 部分输入。

但是,当模糊测试工具遇到“模糊测试工具”时,这种方法可能会失败条件。示例包括:

C/C++

  • 使用外部来源数据的条件。例如:
zx_cprng_draw(&val, sizeof(val));
if (val == 0) { ... }
  • 用于检查可以构造但难以猜测的值的条件。例如:
uint32_t actual = header.checksum;
header.checksum = 0;
uint32_t expected =  crc32(0, reinterpret_cast<const uint8_t*>(&header), sizeof(header));
if (actual == expected) { ... }
int result = ECDSA_verify(0, data, data_len, signature, signature_len, ec_key);
if (result == 0) { ... }

Rust

  • 使用外部来源数据的条件。例如:
let mut randbuf = [0; 8];
zx::cprng_draw(&mut randbuf)?;
let val = u64::from_le_bytes(randbuf);
if val == 0 { ... }
  • 用于检查可以构造但难以猜测的值的条件。例如:
let mut c = Checksum::new();
c.add_bytes(&buf);
c.checksum()
if c == expected { ... }
let digest = H::hash(message);
if boringssl::ecdsa_verify(digest.as_ref(), self.bytes(), &key.inner.key) { ... }

Go

  • 使用外部来源数据的条件。例如:golang if rand.Intn(100) == 0 { ... }

  • 用于检查可以构造但难以猜测的值的条件。例如:

iCksum := ipv4.CalculateChecksum()
if iCksum != want { ... }
ecdsaKey, ok := key.(*ecdsa.PublicKey)
h := e.hash.New()
h.Write(msg)
if ecdsa.Verify(ecdsaKey, h.Sum(nil), ecdsaSignature.R, ecdsaSignature.S) { ... }

作为代码维护人员,您可以使用条件编译在 所测试的代码libFuzzer 将其称为使用适合模糊测试的构建模式。

C/C++

请使用通用 build 宏 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION

例如:

#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
  // Use hard-coded value when fuzzing.
  memset(&val, 0, sizeof(val));
#else
  zx_cprng_draw(&val, sizeof(val));
#endif

在此示例中,我们已将 val 的所有字节设为始终为零。根据代码的不同, 如果 val 是其他确定性值,或者可能是 直接依赖于模糊测试工具输入。

Rust

使用 fuzz cfg 属性

例如:

#[cfg(not(fuzz))]
fn is_valid(&self, key: &EcPubKey<C>, message: &[u8]) -> bool {
    let digest = H::hash(message);
    boringssl::ecdsa_verify(digest.as_ref(), self.bytes(), &key.inner.key)
}

#[cfg(fuzz)]
fn is_valid(&self, key: &EcPubKey<C>, message: &[u8]) -> bool {
    // Skip validation when fuzzing.
    return true;
}

Go

使用 fuzz 软件包。由于 Go 只在 级别,此软件包包含两个定义 const Enabled <bool> 的文件。哪个文件 因此 Enabled 的值取决于是否内置了代码 是否属于模糊测试工具变体。

例如:

import "fuzz"

func (b IPv4) CalculateChecksum() uint16 {
  if fuzz.Enabled {
    // Return hard-coded value when fuzzing.
    return uint16(0xffff)
  }
  return Checksum(b[:b.HeaderLength()], 0)
}

添加自定义更改器

在某些情况下,模糊测试工具提供的输入可能会在对其执行操作之前进行转换。 这可以大大降低模糊测试工具将输入与其产生的行为相关联的能力。 例如,我对某个库的输入进行了模糊测试,在处理之前,先解压缩该库的输入。最 对这个库进行模糊测试的有效方法是对未压缩的输入执行更改, 然后再调用该库

实现此目的的方法之一是添加自定义赋值。这些是用户提供的数据实现 由 LLVM 的模糊测试接口定义的可选函数:

extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
                                          size_t MaxSize, unsigned int Seed);

如果提供,libFuzzer 最初将使用大小为 MaxSize 的缓冲区 Data 调用此函数 用来自语料库的有效输入的 Size 个字节填充。此函数可以转换此数据, 在调用另一个 LLVM 模糊测试接口函数之前:

size_t LLVMFuzzerMutate(uint8_t *Data, size_t Size, size_t MaxSize);

此函数会执行实际变更以创建新输入。

在需要压缩输入的库示例中,自定义赋值函数可将 输入文本,对其调用 LLVMFuzzerMutate 以创建新输入,并压缩结果。

Google 的公开模糊测试文档对此情况及其他情况提供了详细示例。请参阅 使用 libFuzzer 进行结构感知模糊测试

提供字典

字典是接口有效输入中常见的一组令牌。如果提供 模糊测试工具将使用字典构造更可能有效的输入, 以便提供更深入的覆盖率

如果您知道代码需要的令牌类型,则可以将其添加到字典文件中,每个 行。然后,以与种子相同的方式,以 resource 的形式将文件提供给模糊测试工具 语料库,并将其添加到模糊测试工具的组件清单源中。

例如:

{
  ...
  program: {
    args: [
      ...
      "-dict=data/my-dictionary.txt",
      ...
    ]
  }
}

改进模糊测试工具的性能

模糊测试工具达到良好的覆盖率后,另一个关键指标就是它的迭代次数 可以针对一组给定的有限计算资源执行。模糊测试工具及其测试的代码 复杂性相当大,因此模糊测试工具每秒不会 或单个内存限制,则模糊测试工具应该低于该限值。但如果其他一切都是 同样,使用更快速、更精简的模糊测试工具, 这个过程要占用大量内存

启动初始化

通常,被测代码需要先进行一些昂贵的设置,然后才能进行测试。表演 每次迭代时进行这种初始化可能会拖慢模糊测试工具的速度。在这些情况下, 最好使用等于进程的生命周期来延迟初始化变量。

例如:

bool SetUp() {
   DoSomeExpensiveWork();
   return true;
}

extern "C" LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  static bool ready = SetUp();
  ...
}

然而,要非常小心地注意从一次迭代到迭代中传递的程序状态。 继续。如果某个缺陷不仅取决于当前的测试输入,还取决于 因此,如果不重播整个模糊测试工具运行作业,就很难再重现问题。

预先分配存储空间

一些代码会针对大量内存执行操作,例如压缩算法。尝试 每次迭代分配和释放多少兆字节的内存都会导致性能下降。一个 另一种方法是预分配最大大小的缓冲区。

例如:

static const size_t kMaxOutSize = 0x10000000; // 256 MB

extern "C" LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  static uint8_t out_buf[kMaxOutSize];
  size_t max = Decompressor::GetMaxUncompressedSize(size);
  if (sizeof(out_buf) < max) {
    return 0;
  }
  Decompressor::Decompress(data, size, out_buf, max);
  return 0;
}

如上所示,这会在性能和排错程序准确性之间权衡取舍。如果代码 改为分配了长度为 max 的内存区域,AddressSanitizer 为 能够检测 Decompress 是否溢出了任何金额。对于预先分配的区域 静默地取得成功。幸运的是,AddressSanitizer 提供了一种 手动给内存中毒

例如:

#include <sanitizer/asan_interface.h>

static const size_t kMaxOutSize = 0x10000000; // 256 MB

extern "C" LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  static uint8_t out_buf[kMaxOutSize];
  size_t max = Decompressor::GetMaxUncompressedSize(size);
  if (sizeof(out_buf) < max) {
    return 0;
  }
  ASAN_POISON_MEMORY_REGION(&data[max], sizeof(out_buf) - max);
  Decompressor::Decompress(data, size, out_buf, max);
  ASAN_UNPOISON_MEMORY_REGION(&data[max], sizeof(out_buf) - max);
  return 0;
}