文章首发于安全客,由安全客原创发布:https://www.anquanke.com/post/id/224823
LibFuzzer workshop学习之路(一)
最近做项目开始上手学习libfuzzer,是跟着libfuzzer-workshop学的。写下自己的心得和收获。
官方给出的定义
1 | LibFuzzer is in-process, coverage-guided, evolutionary fuzzing engine. |
简单来说就是通过与要进行fuzz的库连接,并将libfuzzer生成的输入通过模糊测试进入点(fuzz target)喂给要fuzz的库进行fuzz testing。同时fuzzer会跟踪哪些区域的代码已经被测试过的,并且根据种料库的输入进行变异来使得代码覆盖率最大化。代码覆盖率的信息是由LLVM’s SanitizerCoverage插桩提供的
需要注意的是这几个libfuzzer的特性:in-process指进程内。即libfuzzer在fuzz时并不是产生出多个进程来分别处理不同的输入,而是将所有的测试数据放入进程的内存空间中。coverage-guided指覆盖率指导的。即会进行代码覆盖率的计算,正如定义所说的使得不断增大代码覆盖率。evolutionary是指libfuzzer是进化型的fuzz,结合了产生和变异两种形式。
环境搭建:
跟着https://github.com/Dor1s/libfuzzer-workshop 搭建就好了,主要是build llvm的环境可能要make一会儿。编译好后拿到libfuzzer.a(静态链接库文件),就可以开始上手实践了。
fuzz testing
libfuzzer已经提供了数据样本生成和异常检测功能,我们要做的就是要实现模糊测试进入点(fuzz target),将libfuzzer生成的数据交给目标程序处理。
fuzz target编写模板:
1 | // fuzz_target.cc |
需要注意的是LLVMFuzzerTestOneInput
函数即使我们要实现的接口函数,他的两个参数Data(libfuzzer的测试样本数据),size(样本数据的大小)。DoSomethingInterestingWithMyAPI
函数即我们实际要进行fuzz的函数。
编译.cc文件:
1 | clang++ -g -O1 -fsanitize=fuzzer,address \ |
几个参数:
-g 可选参数,保留调试符号。
-O1 指定优化等级为1
-fsanitize 指定sanitize。
fuzzer是必须的,用来启用libfuzzer。还可以附加的其他sanitize有:address(用来检测内存访问相关的错误,如stack_overflow,heap_overflow,uaf,可以与fuzzer一起使用);memory(检测未初始化内存的访问,应单独使用);undefined(检测其他的漏洞,如整数溢出,类型混淆等未定义的漏洞)
注:-fsanitize-coverage=trace-pc-guard选项在高版本的clang中已不再适用,代码的覆盖率情况默认自动开启。
这一步骤整体过程就是通过clang的-fsanitize=fuzzer选项可以启用libFuzzer,这个选项在编译和链接过程中生效,实现了条件判断语句和分支执行的记录,并且辅以libFuzzer中的库函数(libfuzzer.a),通过生成不同的测试样例然后能够获得代码的覆盖率情况,最终实现所谓的fuzz testing。
开始fuzz
先来lesson 04,要测试的库是vulnerable_functions.h:
1 | // Copyright 2016 Google Inc. All Rights Reserved. |
首先看VulnerableFunction1(),有两个参数data/size,当size>3时会产生数组越界。接下来编写测试接口:
1 | //first_fuzzer.cc |
可以看到,直接将Libfuzzer生成的测试样例给到VulnerableFunction1就好。
接下来编译:clang++ -g -std=c++11 -fsanitize=fuzzer,address first_fuzzer.cc ../../libFuzzer/Fuzzer/libFuzzer.a -o first_fuzzer
生成可执行程序first_fuzzer。
1 | mkdir corpus1 |
corpus1是我们提供的语料库。理想情况下,该语料库应该为被测代码提供各种有效和无效的输入,模糊器基于当前语料库中的样本输入生成随机突变。如果突变触发了测试代码中先前未覆盖的路径的执行,则该突变将保存到语料库中以供将来变更。当然LibFuzzer也可以没有任何初始种子的情况下工作(因为上面提到他是evolutionary型的fuzzer),但如果受测试的库接受复杂的结构化输入,则会因为随机产生的样例不易符合导致效率降低。
另外,如果我们有太多的样例并希望能够精简一下,则可以:
1 | mkdir corpus1_min |
这样,corpus1_min将会存放精简后的输入样例。
运行后得到crash,很快啊!!!
1 | ➜ 04 git:(master) ✗ ./first_fuzzer corpus1 |
需要注意的地方有点多:
前面的几行输出fuzzer相关的选项和配置信息。seed=2222548757是生成的随机数种子,可以利用./first_fuzzer -seed=2222548757
指定随机种子。-max_len为测试输入的最大长度
以#开头的表示在fuzz的过程中覆盖的路径信息。INITED
fuzzer已完成初始化,其中包括通过被测代码运行每个初始输入样本。READ
fuzzer已从语料库目录中读取了所有提供的输入样本。NEW
fuzzer创建了一个测试输入,该输入涵盖了被测代码的新区域。此输入将保存到主要语料库目录。pulse
fuzzer已生成 2的n次方个输入(定期生成以使用户确信fuzzer仍在工作)。REDUCE
fuzzer发现了一个更好(更小)的输入,可以触发先前发现的特征(设置-reduce_inputs=0以禁用)。cov
执行当前语料库所覆盖的代码块或边的总数。ft
libFuzzer使用不同的信号来评估代码覆盖率:边缘覆盖率,边缘计数器,值配置文件,间接调用方/被调用方对等。这些组合的信号称为功能(ft:)。corp
当前内存中测试语料库中的条目数及其大小(以字节为单位)。exec/s
每秒模糊器迭代的次数。rss
当前的内存消耗。L
新输入的大小(以字节为单位)。MS:<n> <操作>
计数和用于生成输入的变异操作列表
#17458 REDUCE cov: 7 ft: 7 corp: 5/14b lim: 170 exec/s: 0 rss: 29Mb L: 4/4 MS: 1 EraseBytes-
指尝试了17458个输入,成功发现了5个样本(放入语料库)大小为14b,共覆盖了7个代码块,占用内存29mb,变异操作为EraseBytes-。
接下来就是异常检测相关的信息:
1 | ==9875==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200005a1d3 at pc 0x00000059b461 bp 0x7ffd79c84880 sp 0x7ffd79c84878 |
可以看到AddressSanitizer检测到其中的一个输入触发了堆溢出(heap-buffer-overflow)的漏洞。
更据错误信息中的 #0 0x59b460 in VulnerableFunction1(unsigned char const*, unsigned long) /home/admin/libfuzzer-workshop/lessons/04/./vulnerable_functions.h:22:14
可以看到错误点在vulnerable_functions.h:22:14
,对应 data[3] == 'Z';
即数组越界的位置。下面的SUMMARY也与之对应。
倒数第二行给出了造成crash的输入,并将其写入了crash-0eb8e4ed029b774d80f2b66408203801cb982a60。
复现crash可执行./first_fuzzer ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60
。
这样fuzz tesing基本上已经完成了,我们得到了一个造成程序crash的输入,并得知存在堆溢出的漏洞。这样我们就可以有针对性的对程序进行动态调试,利用造成crash的输入回溯出漏洞的细节。
继续fuzz函数VulnerableFunction2
1 | constexpr auto kMagicHeader = "ZN_2016"; |
该函数多了一个bool参数,因此我们的的接口函数要有所改动:
1 |
|
为了提高fuzz出crash的概率,我们要分别fuzz flag为true和false的请况,而不应该把flag写死。
接着编译并执行得到:
1 | INFO: Seed: 296692635 |
相信大家已经不再陌生了,定位错误位于#5 0x59b8b5 in VulnerableFunction2(unsigned char const*, unsigned long, bool) /home/admin/libfuzzer-workshop/lessons/04/./vulnerable_functions.h:61:3
即 std::copy(data, data + size, body.data());
,该句造成了stack_overflow,原因在于vector类型的body的大小为1024 - sizeof(kMagicHeader),而在copy时的data的size的限制条件是size > kMaxPacketLen(1024)
,从而造成了缓冲区溢出。
如果我们写死flag为false的话就可能会跑很久,也跑不出来crash。因此,我们在套用模板时要结合函数的逻辑,使得fuzz的接口设计的更加合理,从而增加fuzz的效率。
继续继续VulnerableFunction3:
1 | constexpr std::size_t kZn2016VerifyHashFlag = 0x0001000; |
可以看到,与2不同的地方就是对flag进行了& kZn2016VerifyHashFlag的计算,这种其实我们可以计算出使得flags & kZn2016VerifyHashFlag
为true/false的输入,并模仿second_fuzzer那样写个循环,这样就和2没差了。
但这里workshop提供了一个不同的方法:In this case, we can get some randomization of flags values using data provided by libFuzzer:
1 |
|
采用了libfuzzer提供的随机化方法使得我们的输入flag为随机数,从而flags & kZn2016VerifyHashFlag
的结果也随机化。在次数足够多的情况下false和true的情况将趋于同为50%。
接下来看lesson05:
这次的目标和lesson04有所不同,lesson04中我们针对.h函数库中的函数进行fuzz,而libfuzzer的威力远不止于此,它还可以对大型开源库进行模糊测试。在源码编译开源库时选择合适的选项以及将libfuzzer与开源库链接在一起以进行fuzz,这些细节将在该lesson05体现。
首先解包tar xzf openssl1.0.1f.tgz
。接着执行./config
生成makefile。之后:
1 | make clean |
第二条指令其实并不太规范,将编辑器以外的其他参数也一股脑写到CC变量里了。clang后的参数本应该是由CFLAGS或CXXFLAGS指定的。解释一下选项:
1 | -02 指定优先级别 |
这些参数的指定是至关重要的,它会影响到之后开源库与libfuzzer的链接以及fuzz的效率。如果不设置这些编译选项直接make的话fuzz的效率如下:
1 | ➜ 05 git:(master) ✗ ./openssl_fuzzer2 |
这路径覆盖率太感人,其中的重要原因就在于编译开源库时的选项设置。
设置编译选项后的fuzz效果:
1 | INFO: Seed: 2565779026 |
选择合适的编译器和编译选项,完成对该库的源码编译,生成.a文件。接下来就要研究编写fuzzer接口函数了。
workshop提供了openssl_fuzzer.cc:
1 |
|
这里就涉及到openssl库提供的相关方法了,本篇主要讲解fuzz相关,就不细讲openssl了。总之就是要先搞清楚openssl的用法,再通过include openssl提供的函数来对openssl进行fuzz。编译如下:
1 | clang++ -g openssl_fuzzer.cc -O2 -fno-omit-frame-pointer -fsanitize=address,fuzzer \ |
-I指定inlcude的搜索路径,同时链接静态库libcrypto.a和libFuzzer.a以使用库中的函数。
运行跑出crash:
1 | #24646 REDUCE cov: 611 ft: 889 corp: 50/1105b lim: 116 exec/s: 24646 rss: 382Mb L: 64/77 MS: 5 InsertRepeatedBytes-PersAutoDict-InsertByte-ShuffleBytes-ChangeBit- DE: "\xff\xff\xff\xff\xff\xff\xff\x04"- |
通过SUMMARY容易找到造成heap_overflow的漏洞点在于:/local/mnt/workspace/bcain_clang_bcain-ubuntu_23113/llvm/utils/release/final/llvm.src/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cc:22:3 in __asan_memcpy
这种目录一看就是很底层的目录,不易定位。再往找一层:#1 0x55e323 in tls1_process_heartbeat /home/admin/libfuzzer-workshop/lessons/05/openssl1.0.1f/ssl/t1_lib.c:2586:3
这就很容易定位了,接下来就是漏洞溯源了。
定位到了代码:
1 | tls1_process_heartbeat(SSL *s) |
漏洞点再memcpy(bp, pl, payload);
根据crash原因为over_flow说明payload的长度有问题。往上分析发现payload是通过n2s函数从p指向的结构体(用户数据包)内容得到的。该结构体为:
1 | typedef struct ssl3_record_st |
而程序并没有对用户可控的length做检查,从而导致memcpy溢出(有可能将server端的数据写入到返回数据包中返回给用户)。
初学libfuzzer,如有纰漏错误还烦请师傅们指正。
lesson06 gogogo!
06给出的是CVE-2016-5180漏洞,该漏洞可以实现在ChromeOS下的远程代码执行,无疑是高危漏洞。
PS:我觉得在学习libfuzzer的过程中,我们不能仅仅局限局限于获得了一个crash,还要进一步的去定位漏洞之所在,分析漏洞产生的原因,思考漏洞的利用方法和修补方式,这才是正确对待fuzz的态度以及漏洞挖掘的魅力所在。
如同05所作的步骤,我们要先对开源库进行编译
如果我们单纯直接编译而不进行编译插桩的话:
1 | tar xzvf c-ares.tgz |
得到的fuzz效果为:
1 | ➜ 06 git:(master) ✗ ./c_ares_fuzzer |
出现如此boring的结果的原因再与缺少了编译时的选项设置,即没有在编译时进行插桩
插桩操作:在其中特定的的位置插入汇编代码,实现在程序执行到该处时能够通过此处的插桩掌握程序的执行路径
1 | tar xzvf c-ares.tgz |
这里的-fsanitize=address
即开启ASAN(Address Sanitizer),编译时则会在目标代码的关键位置添加检查代码,例如:malloc(),free()等,一旦发现了内存访问错误,便可以SIGABRT中止程序。(即完成了编译插桩)
这里指定CC选项也是不规范的,提倡CC = "clang" && CFLAGS = "-O2 -fno-omit-frame-pointer -g -fsanitize=address"
顺利编译生成.a静态链接库文件,接着是编写fuzzer接口函数。
workshop提供给我们的harness如下:
1 | // Copyright 2016 Google Inc. All Rights Reserved. |
可以看到harness将输入的测试样本data类型转化为strings s,之后选择了ares_creat_query
和ares_free_strings
作为入口函数来进行fuzz。
编译:clang++ -g c_ares_fuzzer.cc -O2 -fno-omit-frame-pointer -fsanitize=address,fuzzer -Ic-ares c-ares/.libs/libcares.a
运行后很快得到了crash说明提供的harness是很有效的,接口函数的选择合适。
1 | ➜ 06 git:(master) ✗ ./c_ares_fuzzer |
SUMMARY中:SUMMARY: AddressSanitizer: heap-buffer-overflow /home/admin/libfuzzer-workshop/lessons/06/c-ares/ares_create_query.c:196:3 in ares_create_query
,了解到漏洞正位于ares_create_query
函数中。定位一下漏洞点:
1 | //ares_create_query.c:196:3 |
但对于漏洞的具体产生方式进行溯源的话就要对代码进行审计,这里就不展开了。
到这里对libfuzzer的基本操作已经有了一个初步的认识,但fuzz的对象都是规模较小的开源库。当我们面对大规模的开源项目以及需要进行长时间的测试工作时,如何继续保持fuzz的高效进行以得到crash便是我们要继续研究的课题,这就涉及到一些对编译选项的设置以及对fuzz的一些额外条件的设定,这些都会可能会影响到fuzz的效率,等待这我们进一步的研究。