文章首发于安全客,由安全客原创发布: https://www.anquanke.com/post/id/225957
LibFuzzer workshop学习之路(二)
上一篇对libfuzzer的原理和使用有了基本的了解,接下来就到进阶的内容了,会涉及到字典的使用,语料库精简,错误报告生成以及一些关键的编译选项的选择等内容,希望能对libfuzzer有更深入的学习。
lesson 08(dictionaries are so effective)
对libxml2进行fuzz。
首先对其解压并用clang编译之。
1 | tar xzf libxml2.tgz |
解释下新的编译选项-gline-tables-only
:表示使用采样分析器
clang手册中对采样分析器的解释:Sampling profilers are used to collect runtime information, such as hardware counters, while your application executes. They are typically very efficient and do not incur a large runtime overhead. The sample data collected by the profiler can be used during compilation to determine what the most executed areas of the code are.
用于收集程序执行期间的信息比如硬件计数器,在编译期间使用采样分析器所收集的数据来确定代码中最值得执行的区域。因此,使用样本分析器中的数据需要对程序的构建方式进行一些更改。在编译器可以使用分析信息之前,代码需要在分析器下执行。这也对提高我们fuzz效率很重要。
提供的harness:
1 | // Copyright 2015 The Chromium Authors. All rights reserved. |
将输入的样本类型转换后交给xmlReadMemory
处理。编译如下:clang++ -O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -std=c++11 xml_read_memory_fuzzer.cc -I libxml2/include libxml2/.libs/libxml2.a -fsanitize=fuzzer -lz -o libxml2-v2.9.2-fsanitize_fuzzer1
由于编译时使用了样本分析器,fuzz的执行速率和覆盖率都很可观
1 | #2481433 NEW cov: 2018 ft: 9895 corp: 3523/671Kb lim: 1470 exec/s: 2038 rss: 553Mb L: 484/1470 MS: 1 CopyPart- |
但迟迟没有crash。这可能有很多原因:1.程序很健壮。2.我们选择的接口函数不合适 3.异常检测的设置不当。
这三个可能的原因中程序是否健壮我们不得而知,接口函数是否合适我们通过覆盖率了解到以xmlReadMemory
作为入口函数执行到的代码块还是较高的,但也有可能因为漏洞不在接口函数的部分。第三个可能,由于异常检测的设置不当导致即使产生了异常但因为于设置的异常检测不匹配和没有捕获到。回头看下我们的santize设置为address开启内存错误检测器(AddressSanitizer),该选项较为通用且宽泛(无非stack/heap_overflow),但其实还有一些更具针对行的选项:
1 | -fsanitize-address-field-padding=<value> |
其中有一个-fsanitize-address-use-after-scope
描述为开启use-after-scope检测,将其加入到编译选项中,再次编译。
1 | export FUZZ_CXXFLAGS="-O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -fsanitize-address-use-after-scope" |
跑了一会儿依然没有收获,看来这将会是一个较长时间的过程。
1 | #1823774 REDUCE cov: 2019 ft: 9428 corp: 3417/499Kb lim: 1160 exec/s: 2867 rss: 546Mb L: 229/1150 MS: 4 ChangeBinInt-InsertByte-InsertByte-EraseBytes- |
但我们不能放任其fuzz,要想一些办法去提高我们fuzz的效率,这其中一个办法就是使用字典。
我们知道基本上所有的程序都是处理的数据其格式是不同的,比如 xml文档, png图片等等。这些数据中会有一些特殊字符序列 (或者说关键字), 比如在xml文档中就有CDATA,<!ATTLIST等,png图片就有png 图片头。如果我们事先就把这些字符序列列举出来吗,fuzz直接使用这些关键字去组合,就会就可以减少很多没有意义的尝试,同时还有可能会走到更深的程序分支中去。
这里whorkshop就提供了AFL中所使用的dict:
1 | //xml.dict |
其中关键字就是””里的内容,libfuzzer会使用这些关键字进行组合来生成样本。字典使用方法./libxml2-v2.9.2-fsanitize_fuzzer1 -max_total_time=60 -print_final_stats=1 -dict=./xml.dict corpus1
执行结果:
1 | #468074 REDUCE cov: 2521 ft: 8272 corp: 2493/82Kb lim: 135 exec/s: 7801 rss: 452Mb L: 105/135 MS: 4 InsertRepeatedBytes-CMP-CopyPart-CrossOver- DE: "\x01\x00\x00\x00"- |
可以看到最后还给出了Recommended dictionary
,可以更新到我们的.dict中。stat::new_units_added: 4709
说明最终探测到了5007个代码单元。
不使用字典的话:
1 | Done 402774 runs in 61 second(s) |
可以看到使用字典效率确实提高不少。
此外,当我们长时间fuzz时,会产生和编译出很多样本,这些样本存放在语料库corpus中,例如上面就产生了➜ 08 git:(master) ✗ ls -lR| grep "^-" | wc -l 7217
7217个样本,其中很多是重复的,我们可以通过以下方法进行精简(使用-merge=1标志):
1 | mkdir corpus1_min |
精简到了2313个样本。
workshop还提供了另一个fuzz target:
1 | ➜ 08 git:(master) ✗ cat xml_compile_regexp_fuzzer.cc |
与之前的不同,将输入的数据copy到buffer中,再交给xmlRegexpCompile
处理。编译运行如下:
1 | ➜ 08 git:(master) ✗ clang++ -O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -fsanitize-address-use-after-scope -std=c++11 xml_compile_regexp_fuzzer.cc -I libxml2/include libxml2/.libs/libxml2.a -fsanitize=fuzzer -lz -o libxml2-v2.9.2-fsanitize_fuzzer1 |
好家伙,这个harness几秒抛出了crash,说明对于的入口函数的选择至关重要。但这次的异常有点奇怪==8434==ERROR: AddressSanitizer: allocator is out of memory trying to allocate 0x18 bytes
描述说申请超出了内存,也没有SUMMARY对漏洞进行定位。
因此我们应该意识到问题是出在了harness上,由于在xml_compile_regexp_fuzzer.cc
中使用std::vector<uint8_t> buffer(size + 1, 0);
对data进行转储,在样例不断增加的过程中vector超出了扩容的内存限制,从而抛出了crash,这并不是测试函数xmlRegexpCompile
函数的问题。
在另一个对xmlReadMemory
的fuzz还在进行,学长说它fuzz这个函数花了十几个小时才出crash。
lesson 09(the importance of seed corpus)
这次我们的目标为开源库libpng,首先对源码进行编译
1 | tar xzf libpng.tgz |
workshop给出的是#1的编译策略,没有启用采样分析器,而且 -fsanitize-coverage=trace-pc-guard适用在older version的libfuzzer。因此我用的是#2的编译策略,上一个lesson证明这样的编译插桩能有效提高fuzz的效率。
提供的harness:
1 | // Copyright 2015 The Chromium Authors. All rights reserved. |
对于模糊测试来说,能否写出合适的harness关乎着fuzz最后的结果,我们通常选择涉及内存管理,数据处理等方面的函数作为我们的接口函数去fuzz。
这里给出的harness中我们比较容易看到它会首先去通过png_sig_cmp
函数去判断输入的data是否符合png的格式,符合才能进入到后面的逻辑中,这一方面是确保data的有效性,同时也提高了数据变异的速率。
由于要求输入数据为png的格式,那自然想到使用字典去拼接关键字。这样的想法是正确的,下面比较一下两者的差异:
先编译:clang++ -O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -std=c++11 libpng_read_fuzzer.cc -I libpng libpng/.libs/libpng16.a -fsanitize=fuzzer -lz -o libpng_read_fuzzer
使用的也是AFL给出的png.dict:
1 | # Copyright 2016 Google Inc. |
先不使用字典:
1 | ./libpng_read_fuzzer -max_total_time=60 -print_final_stats=1 |
探测到了512个代码单元
之后使用字典:
1 | ./libpng_read_fuzzer -max_total_time=60 -print_final_stats=1 -dict=./png.dict |
啊这,直接出crash,有点东西。这也再次说明了好的字典使得我们fuzz时的输入数据更具有针对性,当然也提高了触发更多代码单元和获得crash的可能。
我使用workshop的#1编译方法在使用dict的情况下cov只有40多,也未能得到crash,因此上面能得到crash也得益于我们的插桩策略。
在未使用语料库的情况下就得到了crash实属意料之外,如果我们在使用字典的下情况仍然暂时未得到crash,另一个方法可以去寻找一些有效的输入语料库。因为libfuzzer是进化型的fuzz,结合了产生和变异两个发面。如果我们可以提供一些好的seed,虽然它本身没法造成程序crash,但libfuzzer会在此基础上进行变异,就有可能变异出更好的语料,从而增大程序crash的概率。具体的变异策略需要我们去阅读libfuzzer的源码或者些相关的论文。
workshop给我们提供了一些seed:
1 | ➜ 09 git:(master) ✗ ls seed_corpus |
使用seed_corpus去fuzz:
1 | ➜ 09 git:(master) ✗ ./libpng_read_fuzzer seed_corpus |
也顺利得到了crash,这次的crash和上面的crash有所不同,上面造成crash时的cov只有293,而且造成crash的输入为Base64: iVBORw0KGgoAAAANSUhEUgAAABAAAABjAQAAAAAEAEFBYkdLQXNQTFREQUFBQUG51
,而使用seed的话cov达到了626,而且造成crash的数据为Base64: iVBORw0KGgoAAAANSUhEUgAAACcAAADICAIAAAAiOjnJAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGGAAAAdzQ0FMB93tBBQzdEkAAAAAt7pHQmCC
,要长很多。
多数情况下我们同时使用字典和语料库,从产生和变异两个方面去提高样例的威力,双管齐下。
接下来就要分析crash的原因了:ERROR: AddressSanitizer: allocator is out of memory trying to allocate 0x60000008 bytes
,怎么有点眼熟,好像和lesson 09的报错一样。。但也有所不同,它对错误定位在了in malloc /local/mnt/workspace/bcain_clang_bcain-ubuntu_23113/llvm/utils/release/final/llvm.src/projects/compiler-rt/lib/asan/asan_malloc_linux.cc:145:3
,这个是底层malloc的位置,同时有个hint:if you don't care about these errors you may set allocator_may_return_null=1
,提示我们这个crash是由于malloc申请失败造成的,也就是/home/admin/libfuzzer-workshop/lessons/09/libpng/pngrutil.c:310:16
处的malloc:
1 | if (buffer == NULL) |
定位到问题出在png_malloc_base(png_ptr, new_size)处,由于没有对new_size的大小进行严格限制岛主在malloc时trying to allocate 0x60000008 bytes
导致异常崩溃。
总结
这一篇操作下来我感觉到对于提高libfuzzer的效率包括在编译插桩、字典使用、语料库选择方面有了更清楚的认识。模糊测试fuzz在软件诞生时就应运而生了,经过了如此长时间的发展,对人们它的研究也在不断深入,并且根据不同的需求开发出了很多个性化的fuzz工具。正所谓理论结合实践,要想对libfuzzer有更深入的了解,我们还是要去分析它的源码,参考各种研究paper。
初学libfuzzer,有错误疏忽之处烦请各位师傅指正。