当您首次开始对新目标进行模糊测试时,模糊测试工具可能会非常快速地崩溃。通常进行模糊测试 最初发现的缺陷激增,然后是长尾。缺陷出现频率 的下降原因可能有多种,其中包括: * 被测代码中的缺陷较少。 * 代码的某些部分不会经过模糊测试工具测试。
为了区分这几种情况,您需要能够评估和改进模糊测试工具。
提高代码覆盖率
改进模糊测试工具的第一步是了解其当前性能。一个
对于覆盖率引导型模糊测试工具,一个显而易见的关键指标是代码覆盖率。您可以收集这些信息
使用 fx fuzz
。
例如:
fx fuzz analyze package/fuzzer
这会运行模糊测试工具 60 秒,并报告设备上语料库的代码覆盖率。
如果您指定 --staging
选项,系统会先将该目录中的文件添加到语料库。
添加或改进种子语料库
如果发现代码覆盖率存在缺口,您可以将单个输入添加到种子语料库中:
- 将目录添加到模糊测试工具附近的源代码树。
- 将一个或多个文件添加到此目录,每个文件都包含测试输入的原始字节, 会导致模糊测试工具访问之前未发现的代码。
- 为此目录添加一个
resource
GN 目标,并将其添加到模糊测试工具的deps
中。 - 添加一个参数,其中包含模糊测试工具的 组件清单源。
例如:
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;
}