libFuzzer

1.概述

LibFuzzer是单进程的,覆盖引导的,进化的模糊引擎。

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

2.版本
LibFuzzer正在积极开发中,因此您将需要Clang编译器的当前(或至少是最新版本)(请参阅从主干构建Clang)3333

有关旧版本的文档,请参阅https://releases.llvm.org/5.0.0/docs/LibFuzzer.html

3.Fuzz Target

在库上使用libFuzzer的第一步是实现一个模糊目标 - 一个接受字节数组的函数,并使用被测API对这些字节做一些有趣的事情。 像这样:

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  DoSomethingInterestingWithMyAPI(Data, Size);
  return 0;  // Non-zero return values are reserved for future use.
}

请注意,这种fuzz目标不以任何方式依赖于libfuzzer,因此可能甚至需要将其与其他fuzz引擎(如AFL和/或RADAMSA)一起使用。              

关于fuzz目标,需要记住的一些重要事项:              

  • 在同一过程中,fuzz引擎会多次执行不同输入的fuzz目标。              
  • 它必须允许任何类型的输入(空、大、畸形等)。              
  • 它不能在任何输入上exit()。              
  • 它可以使用线程,但理想情况下,所有线程都应该在函数的末尾连接。              
  • 它必须尽可能具有确定性。不确定性(例如不基于输入字节的随机决策)会使模糊效率低下。              
  • 一定很快。尝试避免立方或更大的复杂性、日志记录或过多的内存消耗。              
  • 理想情况下,它不应该修改任何全局状态(尽管这并不严格)。              
  • 通常,目标越窄越好。 例如。 如果您的目标可以解析多种数据格式,请将其拆分为多个目标,每种格式一个。

 4.Fuzzer Usage

最新版本的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

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

如果修改大型项目的CFLAGS,它也编译需要自己的主符号的可执行文件,则可能需要在不链接的情况下仅请求检测:

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

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

5.Corpus

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

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

语料库还可以充当sanity/regression检查,以确认模糊测试入口点仍然有效,并且所有样本输入都通过测试中的代码运行而没有问题。

如果您有大型语料库(通过模糊测试生成或通过其他方式获取),您可能希望在保留完整覆盖范围的同时将其最小化。一种方法是使用-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

6.Running

To run the fuzzer, first create a Corpus directory that holds the initial “seed” sample inputs:

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

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

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

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

默认情况下,模糊测试过程将无限期地继续 - 至少在发现错误之前。 任何crashes或sanitizer故障都将照常报告,停止模糊测试过程,触发错误的特定输入将写入磁盘(通常为crash- <sha1>,leak- <sha1>或timeout- <sha1>)。

7.Parallel Fuzzing

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

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

8. Fork mode

实验模式-fork = N(其中N是并行作业的数量)使用单独的进程(使用fork-exec,而不仅仅是fork)启用oom-,timeout-和crash-resistant-fuzzing。

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

相关标志:

-ignore_ooms
默认为True。 如果在其中一个子进程的模糊测试期间发生OOM,则复制器将保存在磁盘上,并继续进行模糊测试。
-ignore_timeouts
默认情况下为True,与-ignore_ooms相同,但是超时。
-ignore_crashes
默认情况下为False,与-ignore_ooms相同,但对于所有其他崩溃。
计划是最终用-fork = N替换-jobs = N和-workers = N.

9.Resuming merge 

合并大型语料库可能非常耗时,并且通常希望在可抢占的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
...

 10.Options

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

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

最重要的命令行选项是:

-help
打印帮助信息。
-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本身(不是清理程序)报告错误(泄漏,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,则在并行的单独工作进程的集合中运行执行模糊测试的此数量的作业;每个这样的工作进程都将其stdout / stderr重定向到fuzz- <JOB> .log。
-workers
运行模糊测试作业的同时工作进程数。如果为0(默认值),则使用min(jobs,NumberOfCpuCores()/ 2)。
-dict
提供输入关键字的字典;看字典。
-use_counters
使用覆盖计数器生成代码块被击中频率的近似计数;默认为1。
-reduce_inputs
尽量减少输入的大小,同时保留其完整的功能集;默认为1。
-use_value_profile
使用价值观来指导语料库的扩展;默认为0。
-only_ascii
如果为1,则仅生成ASCII(isprint`` +``isspace)输入。默认为0。
-artifact_prefix
提供在将fuzzing工件(崩溃,超时或慢速输入)保存为$(artifact_prefix)文件时使用的前缀。默认为空。

-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:关闭stdout和stderr。

对于完整的标志列表,使用-help = 1运行fuzzer二进制文件。

11.Output

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

INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0    READ units: 1
#1    INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW    cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW    cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW    cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW    cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
...

输出的早期部分包括有关fuzzer选项和配置的信息,包括当前随机种子(在seed:line中;这可以用-seed=n标志覆盖)。

其他输出行具有事件代码和统计信息的形式。可能的事件代码为:

READ

Fuzzer已经从语料库目录中读取了所有提供的输入样本。              

INITED

fuzzer已经完成初始化,包括通过被测代码运行每个初始输入样本。

NEW

fuzzer已经创建了一个测试输入,它覆盖了被测代码的新领域。此输入将保存到主语料库目录。

REDUCE

fuzzer发现了一个更好(更小)的输入,可以触发先前发现的特征(设置-reduce_inputs=0以禁用)。

pulse

fuzzer已产生2N输入(定期产生,以保证用户fuzzer仍在工作)。                                     

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

RELOAD

fuzzer正在定期从corpus目录重新加载输入;这允许它发现由其他fuzzer进程发现的任何输入(请参见并行模糊)。 

每个输出行还报告以下统计信息(非零时):
COV:
执行当前语料库所涵盖的代码块或边的总数。
FT:
libFuzzer使用不同的信号来评估代码覆盖率:边缘覆盖,边缘计数器,值配置文件,间接调用者/被调用者对等。这些组合的信号称为特征(ft :)。
CORP:
当前内存中测试语料库中的条目数及其大小(以字节为单位)。
LIM:
语料库中新条目长度的当前限制。 随着时间的推移逐渐增加,直到达到最大长度(-max_len)。
EXEC/ S:
每秒的模糊器迭代次数。
RSS:
当前的内存消耗。
对于NEW事件,输出行还包括有关生成新输入的变异操作的信息:

L:
新输入的大小(以字节为单位)。
MS:<n> <操作>
计数和用于生成输入的变异操作列表。

12.Toy example

一个简单的函数,如果收到输入“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: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0    READ units: 1
#1    INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW    cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW    cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW    cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW    cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
==31511== ERROR: libFuzzer: deadly signal
...
artifact_prefix='./'; Test unit written to ./crash-b13e8756b13a00cf168300179061fb4b91fefbed

13.More examples

http://tutorial.libfuzzer.info上可以找到真实的模糊目标和它们发现的错误的例子。除此之外,你还可以在一秒钟内学会如何检测心血。 

14.Dictionaries

libfuzzer支持用户提供的带有输入语言关键字或其他有趣的字节序列(例如多字节magic值)的字典。使用-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"

15.Tracing CMP instructions

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

16.Value Profile

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

目前的工作大致如下:             

编译器使用接收两个CMP参数的回调来检测所有CMP指令。              

回调计算(caller_pc&4095)/(popcnt(arg1^arg2)<<12)并使用该值在位集中设置一个位。              

位集中的每个新观测位都被视为新的覆盖范围。              

这个特性有可能发现许多有趣的输入,但有两个缺点。首先,额外的方法可能会带来2倍的额外减速。第二,语料库可能会增长几倍。 

17.Fuzzer-friendly build mode

有时,测试中的代码不是模糊测试的。 例子:

目标代码使用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
}

18.AFL compatibility

libfuzzer可以与afl一起在同一个测试语料库上使用。两个模糊器都期望测试语料库驻留在一个目录中,每个输入一个文件。你可以在同一个语料库上运行两个fuzzer,一个接一个:

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

定期重新启动两个fuzzer,以便他们可以使用彼此的发现。目前,在共享同一个corpus dir时,没有简单的方法并行运行两个模糊引擎。

您也可以在目标函数llvmFuzzerteStoneInput上使用AFL:请参见下面的示例。 

19.How good is my fuzzer?

一旦实现了目标函数llvmfuzzertestoneinput并将其模糊到死亡,您将希望知道该函数或语料库是否可以进一步改进。当然,一个易于使用的度量标准是代码覆盖率。

我们建议使用clang覆盖率来可视化和研究代码覆盖率(示例)。 

20.User-supplied mu2tators

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

21.Startup initialization

如果需要初始化正在测试的库,则有几个选项。

最简单的方法是在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;
}

22.Leaks

使用AddressSanitizer或LeakSanitizer构建的二进制文件将尝试在进程关闭时检测内存泄漏。 对于过程中模糊测试,这是不方便的,因为一旦发现泄漏突变,模糊器需要用再现器报告泄漏。 但是,在每次突变后运行完全泄漏检测是昂贵的。

默认情况下(-detect_leaks = 1),libFuzzer将在执行每个突变时计算malloc和free调用的次数。 如果数字不匹配(这本身并不意味着存在泄漏),libFuzzer将调用更昂贵的LeakSanitizer传递,如果发现实际泄漏,它将与再现器一起报告,并且进程将退出。

如果您的目标有大量泄漏并且泄漏检测被禁用,则最终会耗尽RAM(请参阅-rss_limit_mb标志)。

23.Developing libFuzzer

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

猜你喜欢

转载自blog.csdn.net/zhang14916/article/details/89208540