文章首发于安全客,由安全客原创发布:https://www.anquanke.com/post/id/227394
libfuzzer workshop学习之路(三)
workshop一共给出了11个lesson,每一个lesson都会涉及到一些新的东西,这篇以最后的两个案例(对re2和pcre2的fuzz)为例,会涉及到一些链接库的选择以及插桩编译时的一些参数的设置,还有max_len的设置对我们最后fuzz结果的影响。
fuzzing pcre2
pcre2:Perl Compatible Regular Expressions Version 2
(Perl兼容的正则表达式)即是一个C语言编写的正则表达式函数库,被很多开源软件所使用比如PHP,Apache,Nmap等。
workshop提供的pcre2版本是10.00,先进行源码编译工作。
1 | tar xzf pcre2-10.00.tgz |
这里的一些插桩的参数和进阶篇的差不多,要注意的编译选项是fuzzer-no-link
,如果修改大型项目的CFLAGS,它也需要编译自己的主符号的可执行文件,则可能需要在不链接的情况下仅请求检测,即fuzzer-no-link
强制在链接阶段不生效。因此当我在插桩编译一个较大的开源库的时候推荐加上这个选项,如果不加的话fuzz效率如下:
1 | #2 INITED cov: 7 ft: 8 corp: 1/1b exec/s: 0 rss: 27Mb |
另外,在执行configure生成makefile时针对pcre2添加了一些参数:--with-match-limit=1000
:限制一次匹配时使用的资源数为1000,默认值为10000000--with-match-limit-recursion=1000
:限制一次匹配时的递归深度为1000,默认为10000000(几乎可以说是无限)--enable-never-backslash-C
:禁用在字符串中,将反斜线作为转义序列接受。
编译好开源库后就要研究harness了,workshop提供的如下:
1 | // Copyright 2016 Google Inc. All Rights Reserved. |
解释一下逻辑:首先将样本输入中的’a’置0,之后通过regcomp()函数编译正则表达式,即将指定的正则表达式pat.c_str()编译为特定数据格式preg,使得匹配更加有效。函数regexec()会使用这个数据在目标文本串中进行模式匹配,之后regfree()释放正则表达式。
这个harness通过include库”pcre2posix.h”,将pcre2主要的函数包含在了里面,同时这些函数涉及到的一些内存相关的操作也常常是触发crash的点。
之后进行编译链接:
1 | clang++ -O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -fsanitize-address-use-after-scope pcre2_fuzzer.cc -I pcre2-10.00/src -Wl,--whole-archive pcre2-10.00/.libs/libpcre2-8.a pcre2-10.00/.libs/libpcre2-posix.a -Wl,-no-whole-archive -fsanitize=fuzzer -o pcre2-10.00-fsanitize_fuzzer |
和之前不同,这次多了一些参数:--whole-archive
和--no-whole-archive
是ld专有的命令行参数,clang++并不认识,要通过clang++传递到ld,需要在他们前面加-Wl
。--whole-archive
可以把 在其后面出现的静态库包含的函数和变量输出到动态库,--no-whole-archive
则关掉这个特性,因此这里将两个静态库libpcre2-8.a和libpcre2-posix.a里的符号输出到动态库里,使得程序可以在运行时动态链接使用到的函数,也使得fuzz效率得到了提升。执行一下很快得到了crash:
1 | #538040 NEW cov: 3286 ft: 15824 corp: 6803/133Kb lim: 74 exec/s: 1775 rss: 775Mb L: 24/74 MS: 3 ChangeASCIIInt-ChangeASCIIInt-EraseBytes- |
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/admin/libfuzzer-workshop/lessons/11/pcre2-10.00/src/pcre2_match.c:5968:11 in match
指出在pcre2_match.c里存在stackoverflow。对漏洞进行定位:
在pcre2posix.c中调用了pcre2_match
1 |
|
pcre2_match定义在pcre2_match.c中,在pcre2_match中调用了match函数:
1 |
|
在执行match的过程中出现栈溢出的位置在于:
1 | for(;;) |
当我以为fuzz的工作已经完成的时候,只是尝试着修改了一下编译链接harness时的静态库为全部库:
1 | clang++ -O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -fsanitize-address-use-after-scope pcre2_fuzzer.cc -I pcre2-10.00/src -Wl,--whole-archive pcre2-10.00/.libs/*.a -Wl,-no-whole-archive -fsanitize=fuzzer -o pcre2-10.00-fsanitize_fuzzer |
再次fuzz的结果令我惊讶:
1 | #605510 REDUCE cov: 3273 ft: 15706 corp: 6963/139Kb lim: 86 exec/s: 255 rss: 597Mb L: 18/86 MS: 1 EraseBytes- |
得到了一个不一样的crash。但这也在情理之中,通过链接不同或更多的静态库。只要harness程序逻辑所能涉及到,就有机会得到不同静态库里的crash。
通过SUMMARY: AddressSanitizer: heap-buffer-overflow /home/admin/libfuzzer-workshop/lessons/11/pcre2-10.00/src/pcre2_ord2utf.c:92:12 in _pcre2_ord2utf_8
我们了解到在pcre2_ord2utf.c中存在heapoverflow的漏洞。同样对漏洞进行定位:
这次的函数调用有点多,一层一层的找:
首先在pcre2posix.c
中调用pcre2_compile
:
1 | preg->re_pcre2_code = pcre2_compile((PCRE2_SPTR)pattern, -1, options, |
该函数定义在pcre2_compile.c
中,然后又调用了compile_regex
:
1 | (void)compile_regex(re->overall_options, &code, &ptr, &errorcode, FALSE, FALSE, |
之后在函数compile_regex
中又调用了compile_branch
:
1 | if (!compile_branch(&options, &code, &ptr, errorcodeptr, &branchfirstcu, |
compile_branch
中又调用了add_to_class
:
1 | class_has_8bitchar += |
接着add_to_class
调用PRIV
:
1 | else if (start == end) |
PRIV
定义在pcre2_ord2utf.c
中:
1 | unsigned int |
总结下这两个crash:
第一个crash由harness中的regexech
函数的匹配逻辑触发stack_overflow
,位于pcre2_match.c:5968:11
;第二个crash由regcomp
函数的编译逻辑触发heap_overflow
,位于pcre2_ord2utf.c:92:12
。
一层层的函数调用关系分析得让人头大,但这也正体现了漏洞挖掘中的“挖掘”二字的含义。
fuzzing re2
这一个例子将让我们意识到max_len
的选择对于fuzz效率的影响。
re2是一个高效的、原则性的正则表达式库。是由两位来在Google的大神用C++实现的。Go中的regexp正则表达式包也是由re2实现的。workshop提供的是re2-2014-12-09的版本。
先源码编译:
1 | tar xzf re2.tgz |
接着研究harness:
1 | // Copyright (c) 2016 The Chromium Authors. All rights reserved. |
可以看到harness用到了很多re2里的方法,最后使用FullMatch和PartialMatch接口进行匹配buffer和re。其中buffer是由data_input
和size
初始化得到(data_input由输入的data经无关类型转换得到),re是由pattern和options建立的RE2对象。
注意到harness里有几个条件分支语句,首先是size<1是直接返回,还有就是当size>=3时,初始化pattn和buffer用的是size/3和size-size/3说明它对我们的输入的size进行了切割,初始化pattern用到的是data_input + size / 3
,而初始化buffer是用的之后的data_input。这样使得我们样例的size会对fuzz的过程产生影响。如果size很短,可能无法触发crash,而如果size很大,对harness的执行匹配过程就会更加耗时,影响fuzz寻找覆盖点的效率。下面做几个测试,比较一下max_len对fuzz过程的影响:
编译链接harness:
1 | clang++ -O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -fsanitize-address-use-after-scope -std=gnu++98 target.cc -I re2/ re2/obj/libre2.a -fsanitize=fuzzer -o re2_fuzzer |
由于使用的re2版本较老了,编译的时候使用了c++98标准。
首先我们设置max_len为10,执行时间为100秒,-print_final_stats=1打印最后的结果,corpus1作为语料库的存放处:
1 | ➜ 10 git:(master) ✗ ./re2_fuzzer ./corpus1 -print_final_stats=1 -max_len=10 -max_total_time=100 |
只探测到了36个代码单元。
接着设置max_len为100,执行时间为100秒,-print_final_stats=1打印最后的结果,corpus2作为语料库的存放处:
1 | ./re2_fuzzer ./corpus2 -print_final_stats=1 -max_len=100 -max_total_time=100 |
探测到了50个代码单元,感觉差别不大。
然年设置max_len为1000,执行时间为100秒,-print_final_stats=1打印最后的结果,corpus3作为语料库的存放处:
1 | ./re2_fuzzer ./corpus3 -print_final_stats=1 -max_len=1000 -max_total_time=100 |
这次探测到了97个代码单元,是第二个的2倍,第一个的3倍左右。
最后再设置max_len为500,执行时间为100秒,-print_final_stats=1打印最后的结果,corpus4作为语料库的存放处
1 | ./re2_fuzzer ./corpus4 -print_final_stats=1 -max_len=500 -max_total_time=100 |
结果也比较明显,不同的max_len对fuzz的效率有着不同的影响,当然这也和你写的harness有关。因此在执行fuzzer的时候选择合适的max_len(如本例中的max_len在100~1000比较合适)会使得我们fuzzer探测到更多的代码块,得到crash的效率也就越大。
总结
libfuzzer workshop到此就全部学习完了。libfuzzer作为最常用的fuzz工具,它所涉及到的一些使用方法在workshop里都有相应的lesson。就我个人而言,在逐步学习libfuzzer的过程中感觉到libfuzzer对于开源库提供的接口函数的fuzz是十分强力的,而这也是我们在学习libfuzzer中的难点:如何能够设计出合理的harness,这需要我们对要fuzz的开源库提供的方法有一定的了解,经过攻击面分析等去逐步改善我们的harness,使得我们与获得crash更近一步。
初学libfuzzer,有错误疏忽之处烦请各位师傅指正。