libFuzzer – 用于覆盖率引导的模糊测试的库。

介绍

LibFuzzer 是一个进程内、覆盖率引导的、进化式的模糊测试引擎。

LibFuzzer 与被测库链接,并通过特定的模糊测试入口点(也称为“目标函数”)向库提供模糊输入;然后,模糊器跟踪代码的哪些区域被覆盖,并对输入数据的语料库生成变异,以最大化代码覆盖率。libFuzzer 的代码覆盖率信息由 LLVM 的 SanitizerCoverage instrumentation 提供。

联系方式: libfuzzer(#)googlegroups.com

状态

libFuzzer 的原始作者已经停止了对其的积极开发,转而开发另一个模糊测试引擎 Centipede。LibFuzzer 仍然得到完全支持,重要的错误将得到修复。但是,请不要期望除了错误修复之外的主要新功能或代码审查。

版本

LibFuzzer 需要匹配版本的 Clang。

入门指南

模糊测试目标

在库上使用 libFuzzer 的第一步是实现一个模糊测试目标——一个接受字节数组并使用被测 API 对这些字节执行一些有趣操作的函数。 像这样

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  DoSomethingInterestingWithMyAPI(Data, Size);
  return 0;  // Values other than 0 and -1 are reserved for future use.
}

请注意,此模糊测试目标不以任何方式依赖于 libFuzzer,因此可以甚至期望将其与其他模糊测试引擎(例如 AFL 和/或 Radamsa)一起使用。

关于模糊测试目标的一些重要事项

  • 模糊测试引擎将在同一进程中使用不同的输入多次执行模糊测试目标。

  • 它必须容忍任何类型的输入(空、巨大、格式错误等)。

  • 它绝不能在任何输入上 exit()

  • 它可以使用线程,但理想情况下所有线程都应在函数末尾连接。

  • 它必须尽可能具有确定性。非确定性(例如,不是基于输入字节的随机决策)将使模糊测试效率低下。

  • 它必须很快。 尽量避免三次或更高的复杂度、日志记录或过多的内存消耗。

  • 理想情况下,它不应修改任何全局状态(尽管这不是严格的)。

  • 通常,目标越窄越好。 例如,如果您的目标可以解析多种数据格式,请将其拆分为多个目标,每种格式一个。

模糊器用法

最近版本的 Clang(从 6.0 开始)包含 libFuzzer,无需额外安装。

为了构建您的模糊器二进制文件,请在编译和链接期间使用 -fsanitize=fuzzer 标志。 在大多数情况下,您可能希望将 libFuzzer 与 AddressSanitizer (ASAN)、UndefinedBehaviorSanitizer (UBSAN) 或两者结合使用。 您也可以使用 MemorySanitizer (MSAN) 构建,但支持是实验性的

clang -g -O1 -fsanitize=fuzzer                         mytarget.c # Builds the fuzz target w/o sanitizers
clang -g -O1 -fsanitize=fuzzer,address                 mytarget.c # Builds the fuzz target with ASAN
clang -g -O1 -fsanitize=fuzzer,signed-integer-overflow mytarget.c # Builds the fuzz target with a part of UBSAN
clang -g -O1 -fsanitize=fuzzer,memory                  mytarget.c # Builds the fuzz target with MSAN

这将执行必要的 instrumentation,以及与 libFuzzer 库的链接。 请注意,-fsanitize=fuzzer 链接到 libFuzzer 的 main() 符号。

如果修改大型项目的 CFLAGS,该项目还会编译需要自己 main 符号的可执行文件,则可能需要仅请求 instrumentation 而不进行链接

clang -fsanitize=fuzzer-no-link mytarget.c

然后,可以通过在链接阶段传入 -fsanitize=fuzzer 将 libFuzzer 链接到所需的驱动程序。

语料库

像 libFuzzer 这样的覆盖率引导的模糊器依赖于被测代码的示例输入语料库。理想情况下,此语料库应使用被测代码的各种有效和无效输入进行播种;例如,对于图形库,初始语料库可能包含各种不同的小型 PNG/JPG/GIF 文件。模糊器基于当前语料库中的示例输入生成随机变异。如果变异触发了被测代码中先前未覆盖路径的执行,则该变异将保存到语料库中以供将来变异。

LibFuzzer 可以在没有任何初始种子的情况下工作,但如果被测库接受复杂、结构化的输入,则效率会降低。

语料库还可以充当健全性/回归检查,以确认模糊测试入口点仍然有效,并且所有示例输入都可以在被测代码中无问题地运行。

如果您有一个大型语料库(通过模糊测试生成或通过其他方式获得),您可能希望在仍然保留完整覆盖率的情况下将其最小化。 一种方法是使用 -merge=1 标志

mkdir NEW_CORPUS_DIR  # Store minimized corpus here.
./my_fuzzer -merge=1 NEW_CORPUS_DIR FULL_CORPUS_DIR

您可以使用相同的标志向现有语料库添加更多有趣的条目。 只有触发新覆盖率的输入才会被添加到第一个语料库。

./my_fuzzer -merge=1 CURRENT_CORPUS_DIR NEW_POTENTIALLY_INTERESTING_INPUTS_DIR

运行

要运行模糊器,首先创建一个 Corpus 目录,其中包含初始“种子”示例输入

mkdir CORPUS_DIR
cp /some/input/samples/* CORPUS_DIR

然后在语料库目录上运行模糊器

./my_fuzzer CORPUS_DIR  # -max_len=1000 -jobs=20 ...

当模糊器发现新的有趣的测试用例(即触发被测代码中新路径覆盖率的测试用例)时,这些测试用例将被添加到语料库目录。

默认情况下,模糊测试过程将无限期地继续——至少直到发现错误为止。任何崩溃或 sanitizer 失败都将照常报告,停止模糊测试过程,并且触发该错误的特定输入将被写入磁盘(通常为 crash-<sha1>leak-<sha1>timeout-<sha1>)。

并行模糊测试

每个 libFuzzer 进程都是单线程的,除非被测库启动了自己的线程。 但是,可以使用共享语料库目录并行运行多个 libFuzzer 进程;这样做的好处是,一个模糊器进程找到的任何新输入都将可供其他模糊器进程使用(除非您使用 -reload=0 选项禁用此功能)。

这主要由 -jobs=N 选项控制,该选项指示应运行 N 个模糊测试作业直至完成(即,直到发现错误或达到时间/迭代限制)。 这些作业将在 一组工作进程中运行,默认情况下使用可用 CPU 核心的一半;工作进程的计数可以被 -workers=N 选项覆盖。 例如,在 12 核机器上使用 -jobs=30 运行将默认运行 6 个工作进程,每个工作进程在整个过程完成时平均发现 5 个错误。

Fork 模式

实验性 模式 -fork=N(其中 N 是并行作业数)启用使用单独进程(使用 fork-exec,而不仅仅是 fork)的 oom、超时和抗崩溃模糊测试。

顶层 libFuzzer 进程本身不会执行任何模糊测试,但会生成最多 N 个并发子进程,为它们提供语料库的小型随机子集。 子进程退出后,顶层进程会将子进程生成的语料库合并回主语料库。

相关标志

-ignore_ooms

默认情况下为 True。 如果在其中一个子进程的模糊测试期间发生 OOM,则重现器将保存在磁盘上,并且模糊测试继续。

-ignore_timeouts

默认情况下为 True,与 -ignore_ooms 相同,但用于超时。

-ignore_crashes

默认情况下为 False,与 -ignore_ooms 相同,但用于所有其他崩溃。

计划最终用 -fork=N 替换 -jobs=N-workers=N

恢复合并

合并大型语料库可能很耗时,并且通常希望在可抢占的 VM 上执行,在这些 VM 上,进程可能随时被终止。 为了无缝恢复合并,请使用 -merge_control_file 标志,并使用 killall -SIGUSR1 /path/to/fuzzer/binary 来优雅地停止合并。 例子

% rm -f SomeLocalPath
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-INNER: using the control file 'SomeLocalPath'
...
# While this is running, do `killall -SIGUSR1 my_fuzzer` in another console
==9015== INFO: libFuzzer: exiting as requested

# This will leave the file SomeLocalPath with the partial state of the merge.
# Now, you can continue the merge by executing the same command. The merge
# will continue from where it has been interrupted.
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-OUTER: non-empty control file provided: 'SomeLocalPath'
MERGE-OUTER: control file ok, 32 files total, first not processed file 20
...

选项

要运行模糊器,请将零个或多个语料库目录作为命令行参数传递。 模糊器将从这些语料库目录中的每一个读取测试输入,并且生成的任何新测试输入都将写回第一个语料库目录

./fuzzer [-flag1=val1 [-flag2=val2 ...] ] [dir1 [dir2 ...] ]

如果将文件列表(而不是目录)传递给模糊器程序,则它将重新运行这些文件作为测试输入,但不会执行任何模糊测试。 在此模式下,模糊器二进制文件可以用作回归测试(例如,在持续集成系统上)以检查目标函数和保存的输入是否仍然有效。

最重要的命令行选项是

-help

打印帮助消息 (-help=1)。

-seed

随机种子。 如果为 0(默认值),则生成种子。

-runs

单个测试运行的次数,-1(默认值)表示无限期运行。

-max_len

测试输入的最大长度。 如果为 0(默认值),则 libFuzzer 尝试根据语料库猜测一个好值(并报告它)。

-len_control

首先尝试生成小输入,然后随着时间的推移尝试更大的输入。 指定长度限制增加的速率(越小 == 越快)。 默认值为 100。如果为 0,则立即尝试大小最大为 max_len 的输入。

-timeout

超时时间(秒),默认值为 1200。如果输入花费的时间超过此超时时间,则该过程被视为失败案例。

-rss_limit_mb

内存使用量限制(Mb),默认值为 2048。 使用 0 禁用限制。 如果输入需要超过此 RSS 内存量才能执行,则该过程被视为失败案例。 该限制每秒在单独的线程中检查一次。 如果在没有 ASAN/MSAN 的情况下运行,则可以使用 ‘ulimit -v’ 代替。

-malloc_limit_mb

如果非零,则如果目标尝试使用一个 malloc 调用分配此 Mb 数,模糊器将退出。 如果为零(默认值),则应用与 rss_limit_mb 相同的限制。

-timeout_exitcode

如果 libFuzzer 报告超时,则使用的退出代码(默认值为 77)。

-error_exitcode

如果 libFuzzer 本身(而不是 sanitizer)报告错误(泄漏、OOM 等),则使用的退出代码(默认值为 77)。

-max_total_time

如果为正数,则表示运行模糊器的最大总时间(秒)。 如果为 0(默认值),则无限期运行。

-merge

如果设置为 1,则来自第 2、第 3 等语料库目录的任何触发新代码覆盖率的语料库输入都将合并到第一个语料库目录中。 默认为 0。此标志可用于最小化语料库。

-merge_control_file

指定用于合并过程的控制文件。 如果合并过程被终止,它会尝试将此文件置于适合恢复合并的状态。 默认情况下,将使用临时文件。

-minimize_crash

如果为 1,则最小化提供的崩溃输入。 与 -runs=N 或 -max_total_time=N 一起使用以限制尝试次数。

-reload

如果设置为 1(默认值),则定期重新读取语料库目录以检查新输入; 这允许检测其他模糊测试过程发现的新输入。

-jobs

要运行完成的模糊测试作业数。 默认值为 0,这会运行单个模糊测试过程直到完成。 如果该值 >= 1,则会运行此数量的执行模糊测试的作业,这些作业位于一组并行独立的worker进程中; 每个这样的 worker 进程的 stdout/stderr 都重定向到 fuzz-<JOB>.log

-workers

同时运行 worker 进程以完成模糊测试作业的数量。 如果为 0(默认值),则使用 min(jobs, NumberOfCpuCores()/2)

-dict

提供输入关键字字典; 请参阅 字典

-use_counters

使用 覆盖率计数器生成代码块被命中的频率的近似计数; 默认为 1。

-reduce_inputs

尝试在保留输入完整功能集的同时减小输入的大小; 默认为 1。

-use_value_profile

使用值 profile 来指导语料库扩展; 默认为 0。

-only_ascii

如果为 1,则仅生成 ASCII (isprint``+``isspace) 输入。 默认为 0。

-artifact_prefix

提供一个前缀,用于在将模糊测试工件(崩溃、超时或慢速输入)另存为 $(artifact_prefix)file 时使用。 默认为空。

-exact_artifact_path

如果为空(默认值)则忽略。 如果非空,则在失败(崩溃、超时)时将单个工件写入为 $(exact_artifact_path)。 这会覆盖 -artifact_prefix,并且不会在文件名中使用校验和。 请勿对多个并行进程使用相同的路径。

-print_pcs

如果为 1,则打印出新覆盖的 PC。 默认为 0。

-print_final_stats

如果为 1,则在退出时打印统计信息。 默认为 0。

-detect_leaks

如果为 1(默认值)并且如果启用了 LeakSanitizer,则尝试在模糊测试期间检测内存泄漏(即,不仅在关闭时)。

-close_fd_mask

指示启动时要关闭的输出流。 请注意,这将删除来自目标代码的诊断输出(例如,关于断言失败的消息)。

  • 0(默认值):既不关闭 stdout 也不关闭 stderr

  • 1:关闭 stdout

  • 2:关闭 stderr

  • 3:同时关闭 stdoutstderr

有关标志的完整列表,请使用 -help=1 运行模糊器二进制文件。

输出

在操作期间,模糊器会将信息打印到 stderr,例如

INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 1434179311
INFO: Loaded 1 modules   (8 inline 8-bit counters): 8 [0x5f03d189be90, 0x5f03d189be98),
INFO: Loaded 1 PC tables (8 PCs): 8 [0x5f03d189be98,0x5f03d189bf18),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2      INITED cov: 2 ft: 2 corp: 1/1b exec/s: 0 rss: 31Mb
#144    NEW    cov: 3 ft: 3 corp: 2/2b lim: 4 exec/s: 0 rss: 31Mb L: 1/1 MS: 2 ChangeByte-ChangeByte-
#157    NEW    cov: 4 ft: 4 corp: 3/4b lim: 4 exec/s: 0 rss: 31Mb L: 2/2 MS: 3 CrossOver-ChangeBit-CrossOver-
#1345   NEW    cov: 5 ft: 5 corp: 4/8b lim: 14 exec/s: 0 rss: 32Mb L: 4/4 MS: 3 InsertByte-ChangeBit-CrossOver-
#1696   NEW    cov: 6 ft: 6 corp: 5/10b lim: 17 exec/s: 0 rss: 32Mb L: 2/4 MS: 1 EraseBytes-
#1832   REDUCE cov: 6 ft: 6 corp: 5/9b lim: 17 exec/s: 0 rss: 32Mb L: 3/3 MS: 1 EraseBytes-
...

输出的早期部分包括有关模糊器选项和配置的信息,包括当前随机种子(在 Seed: 行中;可以使用 -seed=N 标志覆盖此种子)。

进一步的输出行具有事件代码和统计信息的格式。 可能的事件代码是

READ

模糊器已从语料库目录中读取所有提供的输入样本。

INITED

模糊器已完成初始化,其中包括运行每个初始输入样本通过被测代码。

NEW

模糊器已创建一个测试输入,该输入覆盖了被测代码的新区域。 此输入将保存到主语料库目录。

REDUCE

模糊器找到了一个更好(更小)的输入,该输入触发了先前发现的功能(设置 -reduce_inputs=0 以禁用)。

pulse

模糊器已生成 2n 个输入(定期生成以向用户保证模糊器仍在工作)。

DONE

模糊器已完成操作,因为它已达到指定的迭代限制 (-runs) 或时间限制 (-max_total_time)。

RELOAD

模糊器正在执行从语料库目录定期重新加载输入; 这使其能够发现其他模糊器进程发现的任何输入(请参阅 并行模糊测试)。

每行输出还报告以下统计信息(当非零时)

cov

执行当前语料库覆盖的代码块或边总数。

ft

libFuzzer 使用不同的信号来评估代码覆盖率:边覆盖率、边计数器、值 profile、间接调用者/被调用者对等。 这些信号组合在一起称为 features (ft:)。

corp

当前内存中测试语料库中的条目数及其大小(以字节为单位)。

lim

当前对语料库中新条目长度的限制。 随着时间的推移而增加,直到达到最大长度 (-max_len)。

exec/s

每秒模糊器迭代次数。

rss

当前内存消耗。

对于 NEWREDUCE 事件,输出行还包括有关生成新输入的变异操作的信息

L

新/减少的输入的大小(以字节为单位)以及当前内存中测试语料库中最大输入的大小。

MS: <n> <operations>

用于生成输入的变异操作的计数和列表。

示例

玩具示例

一个简单的函数,如果收到输入 “HI!”,它会做一些有趣的事情

cat << EOF > test_fuzzer.cc
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  if (size > 0 && data[0] == 'H')
    if (size > 1 && data[1] == 'I')
       if (size > 2 && data[2] == '!')
       __builtin_trap();
  return 0;
}
EOF
# Build test_fuzzer.cc with asan and link against libFuzzer.
clang++ -fsanitize=address,fuzzer test_fuzzer.cc
# Run the fuzzer with no corpus.
./a.out

您应该很快收到错误

INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 1434179311
INFO: Loaded 1 modules   (8 inline 8-bit counters): 8 [0x5f03d189be90, 0x5f03d189be98),
INFO: Loaded 1 PC tables (8 PCs): 8 [0x5f03d189be98,0x5f03d189bf18),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2      INITED cov: 2 ft: 2 corp: 1/1b exec/s: 0 rss: 31Mb
#144    NEW    cov: 3 ft: 3 corp: 2/2b lim: 4 exec/s: 0 rss: 31Mb L: 1/1 MS: 2 ChangeByte-ChangeByte-
#157    NEW    cov: 4 ft: 4 corp: 3/4b lim: 4 exec/s: 0 rss: 31Mb L: 2/2 MS: 3 CrossOver-ChangeBit-CrossOver-
#1345   NEW    cov: 5 ft: 5 corp: 4/8b lim: 14 exec/s: 0 rss: 32Mb L: 4/4 MS: 3 InsertByte-ChangeBit-CrossOver-
#1696   NEW    cov: 6 ft: 6 corp: 5/10b lim: 17 exec/s: 0 rss: 32Mb L: 2/4 MS: 1 EraseBytes-
#1832   REDUCE cov: 6 ft: 6 corp: 5/9b lim: 17 exec/s: 0 rss: 32Mb L: 3/3 MS: 1 EraseBytes-
==840148== ERROR: libFuzzer: deadly signal
...
SUMMARY: libFuzzer: deadly signal
MS: 2 CopyPart-ChangeByte-; base unit: dbee5f8c7a5da845446e75b4a5708e74428b520a
0x48,0x49,0x21,
HI!
artifact_prefix='./'; Test unit written to ./crash-7a8dc3985d2a90fb6e62e94910fc11d31949c348
Base64: SEkh

更多示例

真实模糊测试目标的示例以及它们发现的错误可以在 http://tutorial.libfuzzer.info 上找到。 除此之外,您还可以学习如何在 1 秒内检测到 Heartbleed

高级功能

字典

LibFuzzer 支持用户提供的字典,其中包含输入语言关键字或其他有趣的字节序列(例如,多字节幻数)。 使用 -dict=DICTIONARY_FILE。 对于某些输入语言,使用字典可以显着提高搜索速度。 字典语法类似于 AFL 为其 -x 选项使用的语法

# Lines starting with '#' and empty lines are ignored.

# Adds "blah" (w/o quotes) to the dictionary.
kw1="blah"
# Use \\ for backslash and \" for quotes.
kw2="\"ac\\dc\""
# Use \xAB for hex values
kw3="\xF7\xF8"
# the name of the keyword followed by '=' may be omitted:
"foo\x0Abar"

跟踪 CMP 指令

使用额外的编译器标志 -fsanitize-coverage=trace-cmp(默认情况下作为 -fsanitize=fuzzer 的一部分启用,请参阅 SanitizerCoverageTraceDataFlow),libFuzzer 将拦截 CMP 指令并根据拦截的 CMP 指令的参数指导变异。 这可能会减慢模糊测试速度,但很可能会提高结果。

值 Profile

使用 -fsanitize-coverage=trace-cmp-fsanitize=fuzzer 的默认值)和额外的运行时标志 -use_value_profile=1,模糊器将收集比较指令参数的值 profile,并将一些新值视为新覆盖率。

当前的实现大致执行以下操作

  • 编译器 instrumentation 所有 CMP 指令,并使用一个回调来接收两个 CMP 参数。

  • 回调计算 (caller_pc&4095) | (popcnt(Arg1 ^ Arg2) << 12) 并使用此值在位集中设置一位。

  • 位集中每个新观察到的位都被视为新覆盖率。

此功能有可能发现许多有趣的输入,但有两个缺点。 首先,额外的 instrumentation 可能会带来高达 2 倍的额外减速。 其次,语料库可能会增长数倍。

模糊器友好的构建模式

有时,被测代码对模糊测试不友好。 例子

  • 目标代码使用 PRNG 进行播种,例如通过系统时间,因此即使最终结果相同,两个连续的调用也可能执行不同的代码路径。 这将导致模糊器将两个相似的输入视为显着不同,并且它会炸毁测试语料库。 例如,libxml 在其哈希表内部使用 rand()

  • 目标代码使用校验和来防止无效输入。 例如,png 检查每个块的 CRC。

在许多情况下,构建一个特殊的模糊器友好构建版本,其中禁用某些模糊器不友好的功能是有意义的。 我们建议对所有此类情况使用通用的构建宏以保持一致性:FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION

void MyInitPRNG() {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
  // In fuzzing mode the behavior of the code should be deterministic.
  srand(0);
#else
  srand(time(0));
#endif
}

AFL 兼容性

LibFuzzer 可以与 AFL 一起在同一测试语料库上使用。 两个模糊器都希望测试语料库驻留在目录中,每个输入一个文件。 您可以在同一语料库上一个接一个地运行两个模糊器

./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
./llvm-fuzz testcase_dir findings_dir  # Will write new tests to testcase_dir

定期重启两个模糊器,以便它们可以使用彼此的发现。 目前,没有简单的方法可以并行运行两个模糊测试引擎,同时共享相同的语料库目录。

您也可以在目标函数 LLVMFuzzerTestOneInput 上使用 AFL:请在此处查看示例 here

我的模糊器有多好?

一旦您实现了目标函数 LLVMFuzzerTestOneInput 并对其进行了彻底的模糊测试,您将想知道该函数或语料库是否可以进一步改进。 当然,一个易于使用的指标是代码覆盖率。

我们建议使用 Clang Coverage 来可视化和研究您的代码覆盖率 (example)。

用户提供的 mutator

LibFuzzer 允许使用自定义(用户提供)的 mutator,有关更多详细信息,请参阅 Structure-Aware Fuzzing

启动初始化

如果被测库需要初始化,则有几种选择。

最简单的方法是在 LLVMFuzzerTestOneInput 内部(或者在全局范围内,如果对您有效)拥有一个静态初始化的全局对象

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  static bool Initialized = DoInitialization();
  ...

或者,您可以定义一个可选的 init 函数,它将接收您可以读取和修改的程序参数。 当您确实需要访问 argv/argc 时才这样做。

extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) {
 ReadAndMaybeModify(argc, argv);
 return 0;
}

将 libFuzzer 用作库

如果正在模糊测试的代码必须提供自己的 main,则可以将 libFuzzer 作为库调用。 确保在编译期间传递 -fsanitize=fuzzer-no-link,并将您的二进制文件链接到 libFuzzer 的 no-main 版本。 在 Linux 安装上,这通常位于

/usr/lib/<llvm-version>/lib/clang/<clang-version>/lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

如果从源代码构建 libFuzzer,则它位于构建输出目录中的以下路径

lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

从这里,代码可以执行它需要的任何设置,当它准备好开始模糊测试时,它可以调用 LLVMFuzzerRunDriver,传入程序参数和回调。 此回调的调用方式与 LLVMFuzzerTestOneInput 相同,并且具有相同的签名。

extern "C" int LLVMFuzzerRunDriver(int *argc, char ***argv,
                  int (*UserCb)(const uint8_t *Data, size_t Size));

拒绝不需要的输入

可能需要拒绝某些输入,即不将它们添加到语料库中。

例如,当模糊测试由解析和其他逻辑组成的 API 时,可能希望仅允许将那些成功解析的输入添加到语料库中。

如果模糊测试目标在给定输入时返回 -1,则无论它触发什么覆盖率,libFuzzer 都不会将该输入添加到语料库顶部。

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  if (auto *Obj = ParseMe(Data, Size)) {
    Obj->DoSomethingInteresting();
    return 0;  // Accept. The input may be added to the corpus.
  }
  return -1;  // Reject; The input will not be added to the corpus.
}

泄漏

使用 AddressSanitizerLeakSanitizer 构建的二进制文件将尝试在进程关闭时检测内存泄漏。 对于进程内模糊测试,这很不方便,因为模糊器需要在发现泄漏变异后立即报告带有重现器的泄漏。 但是,在每次变异后运行完整的泄漏检测成本很高。

默认情况下 (-detect_leaks=1),libFuzzer 将在执行每次变异时计算 mallocfree 调用的次数。 如果数字不匹配(这本身并不意味着存在泄漏),libFuzzer 将调用成本更高的 LeakSanitizer 传递,如果找到实际泄漏,它将与重现器一起报告,并且该进程将退出。

如果您的目标存在大量泄漏并且禁用了泄漏检测,您最终将耗尽 RAM(请参阅 -rss_limit_mb 标志)。

开发 libFuzzer

默认情况下,LibFuzzer 作为 LLVM 项目的一部分在 macos 和 Linux 上构建。 其他操作系统的用户可以显式请求使用 -DCOMPILER_RT_BUILD_LIBFUZZER=ON 标志进行编译。 测试使用从构建目录中的 check-fuzzer 目标运行,该目录使用 -DCOMPILER_RT_INCLUDE_TESTS=ON 标志配置。

ninja check-fuzzer

FAQ

问:为什么 libFuzzer 不使用任何 LLVM 支持?

有两个原因。

首先,我们希望这个库在 LLVM 之外使用,而用户无需构建 LLVM 的其余部分。 这对于许多 LLVM 人员来说可能听起来没有说服力,但在实践中,构建整个 LLVM 的需求吓坏了许多潜在用户——我们希望更多用户使用此代码。

其次,有一个微妙的技术原因不依赖于 LLVM 的其余部分,或任何其他大型代码体(甚至可能不是 STL)。 启用覆盖率 instrumentation 后,它还将 instrumentation LLVM 支持代码,这将扩大进程的覆盖率集(因为模糊器是进程内的)。 换句话说,通过使用更多的外部依赖项,我们将减慢模糊器的速度,而模糊器存在的主要原因是极快的速度。

问:libFuzzer 支持 Windows 吗?

是的,libFuzzer 现在支持 Windows。 初始支持在 r341082 中添加。 任何 Clang 9 的构建都支持它。 您可以从 LLVM Snapshot Builds 下载具有 libFuzzer 的 Windows 版 Clang 构建。

不支持在没有 ASAN 的 Windows 上使用 libFuzzer。 不支持使用 /MD(动态运行时库)编译选项构建模糊器。 将来可能会添加对这些的支持。 也不支持使用 /INCREMENTAL 链接选项(或暗示它的 /DEBUG 选项)链接模糊器。

将任何问题或评论发送到邮件列表:libfuzzer(#)googlegroups.com

问:何时 libFuzzer 不是解决问题的好方案?

  • 如果测试输入由目标库验证,并且验证器在无效输入上断言/崩溃,则进程内模糊测试不适用。

  • 目标库中的错误可能会在未被检测到的情况下累积。 例如,内存损坏最初未被检测到,然后在测试另一个输入时导致崩溃。 这就是为什么强烈建议在所有 sanitizer 的情况下运行此进程内模糊器,以便当场检测到大多数错误。

  • 更难保护进程内模糊器免受目标库中过度的内存消耗和无限循环的影响(仍然可能)。

  • 目标库不应具有在运行之间未重置的重大全局状态。

  • 许多有趣的目标库的设计方式不支持进程内模糊器接口(例如,需要文件路径而不是字节数组)。

  • 如果单次测试运行耗时较长(或更长),那么进程内模糊测试器的速度优势可以忽略不计。

  • 如果目标库运行持久线程(其生命周期超过一次测试的执行),则模糊测试结果将不可靠。

问:那么,这个模糊测试器究竟有什么用呢?

对于测试输入相对较小、每次输入运行时间少于 10 毫秒且库代码在无效输入下预计不会崩溃的库,此模糊测试器可能是一个不错的选择。示例:正则表达式匹配器、文本或二进制格式解析器、压缩、网络、加密。

问:LibFuzzer 在我复杂的模糊测试目标上崩溃了(但在较小的目标上运行良好)。

检查您的模糊测试目标是否使用了 dlclose。目前,libFuzzer 不支持调用 dlclose 的目标,这可能会在未来修复。

奖杯