LibFuzzer workshop学习之路(final)

libfuzzerworkshop学习之路finalfuzzingpcre2

pcre2:PerlCompatibleRegularExpressionsVersion2(Perl兼容的正则表达式)即是一个C语言编写的正则表达式函数库,被很多开源软件所使用比如PHP,Apache,Nmap等。
workshop提供的pcre2版本是10.00,先进行源码编译工作。

/_CXXFLAGS="-O2-fno-omit-frame-pointer-gline-tables-only-fsanitize=address,fuzzer-no-link-fsanitize-address-use-after-scope"CXX="clang++$FUZZ_CXXFLAGS"CC="clang$FUZZ_CXXFLAGS"\CCLD="clang++$FUZZ_CXXFLAGS"./configure--enable-never-backslash-C\--with-match-limit=1000--with-match-limit-recursion=1000make-j

这里的一些插桩的参数和进阶篇的差不多,要注意的编译选项是fuzzer-no-link,如果修改大型项目的CFLAGS,它也需要编译自己的主符号的可执行文件,则可能需要在不链接的情况下仅请求检测,即fuzzer-no-link强制在链接阶段不生效。因此当我在插桩编译一个较大的开源库的时候推荐加上这个选项,如果不加的话fuzz效率如下:

3NEWcov:9ft:10corp:2/5blim:4exec/s:0rss:27MbL:4/4MS:1CrossOver-35REDUCEcov:10ft:11corp:3/5blim:4exec/s:0rss:28MbL:2/2MS:3CopyPart-ChangeByte-EraseBytes-1491REDUCEcov:16ft:17corp:4/21blim:17exec/s:0rss:28MbL:17/17MS:5ChangeBit-ShuffleBytes-InsertRepeatedBytes-ChangeBit-CrossOver-524288pulsecov:16ft:17corp:4/20blim:4096exec/s:87381rss:830Mb2097152pulsecov:16ft:17corp:4/20blim:4096exec/s:123361rss:830Mb8388608pulsecov:16ft:17corp:4/20blim:4096exec/s:131072rss:830Mb

另外,在执行configure生成makefile时针对pcre2添加了一些参数:
--with-match-limit=1000:限制一次匹配时使用的资源数为1000,默认值为10000000
--with-match-limit-recursion=1000:限制一次匹配时的递归深度为1000,默认为10000000(几乎可以说是无限)
--enable-never-backslash-C:禁用在字符串中,将反斜线作为转义序列接受。

编译好开源库后就要研究harness了,workshop提供的如下:

////LicensedundertheApacheLicense,(the"License");""usingstd::string;extern"C"intLLVMFuzzerTestOneInput(constunsignedchar*data,size_tsize){if(size1)return0;regex_tpreg;stringstr(reinterpret_castconstchar*(data),size);stringpat(str);intflags=data[size/2]-'a';//Makeit0whenthebyteis'a'.if(0==regcomp(preg,_str(),flags)){regmatch_tpmatch[5];regexec(preg,_str(),5,pmatch,0);regfree(preg);}return0;}

解释一下逻辑:首先将样本输入中的’a’置0,之后通过regcomp()函数编译正则表达式,即将指定的正则表达式_str()编译为特定数据格式preg,使得匹配更加有效。函数regexec()会使用这个数据在目标文本串中进行模式匹配,之后regfree()释放正则表达式。
这个harness通过include库””,将pcre2主要的函数包含在了里面,同时这些函数涉及到的一些内存相关的操作也常常是触发crash的点。
之后进行编译链接:

clang++-O2-fno-omit-frame-pointer-gline-tables-only-fsanitize=address,fuzzer-no-link-fsanitize-address-use-after-scopepcre2_/src-Wl,--/.libs//.libs/,-no-whole-archive-fsanitize=_fuzzer

和之前不同,这次多了一些参数:--whole-archive和--no-whole-archive是ld专有的命令行参数,clang++并不认识,要通过clang++传递到ld,需要在他们前面加-Wl。--whole-archive可以把在其后面出现的静态库包含的函数和变量输出到动态库,--no-whole-archive则关掉这个特性,因此这里将两个静态库和里的符号输出到动态库里,使得程序可以在运行时动态链接使用到的函数,也使得fuzz效率得到了提升。执行一下很快得到了crash:

538092REDUCEcov:3286ft:15824corp:6803/133Kblim:74exec/s:1775rss:775MbL:23/74MS:2CopyPart-EraseBytes-538204REDUCEcov:3286ft:15824corp:6803/133Kblim:74exec/s:1758rss:775MbL:16/74MS:1EraseBytes-00x5e1517inmatch/home/admin/libfuzzer-workshop/lessons/11//src/pcre2_:5968:1120x5f5e64inregexec/home/admin/libfuzzer-workshop/lessons/11//src/:291:640x459661infuzzer::Fuzzer::ExecuteCallback(unsignedcharconst*,unsignedlong)/local/mnt/workspace/bcain_clang_bcain-ubuntu_23113/llvm/utils/release/final//projects/compiler-rt/lib/fuzzer/:553:1560x45b147infuzzer::Fuzzer::MutateAndTestOne()/local/mnt/workspace/bcain_clang_bcain-ubuntu_23113/llvm/utils/release/final//projects/compiler-rt/lib/fuzzer/:695:1980x449c28infuzzer::FuzzerDriver(int*,char***,int(*)(unsignedcharconst*,unsignedlong))/local/mnt/workspace/bcain_clang_bcain-ubuntu_23113/llvm/utils/release/final//projects/compiler-rt/lib/fuzzer/:825:6100x7f0d3f5c3bf6in__libc_start_main(/lib/x86_64-linux-gnu/+0x21bf6)00x55136finLLVMFuzzerTestOneInput/home/admin/libfuzzer-workshop/lessons/11/pcre2_:13Thisframehas6object(s):[32,40)'__'[64,72)'__'[96,128)'preg'(line15)[160,192)'str'(line16)==Memoryaccessatoffset159underflowsthisvariable[224,256)'pat'(line17)[288,328)'pmatch'(line20)HINT:thismaybeafalsepositiveifyourprogramusessomecustomstackunwindmechanism,swapcontextorvfork(longjmpandC++exceptions*are*supported)SUMMARY:AddressSanitizer:stack-buffer-overflow/home/admin/libfuzzer-workshop/lessons/11//src/pcre2_:5968:11inmatchShadowbytesaroundthebuggyaddress:0x100050133c30:000000000000000000000000000000000x100050133c40:000000000000000000000000000000000x100050133c50:000000000000000000000000000000000x100050133c60:000000000000000000000000000000000x100050133c70:0000000000000000f1f1f1f1f8f2f2f2=0x100050133c80:f8f2f2f200000000f2f2f2[f2]000000000x100050133c90:f2f2f2f200000000f2f2f2f2000000000x100050133ca0:00f3f3f3f3f3f3f300000000000000000x100050133cb0:000000000000000000000000000000000x100050133cc0:000000000000000000000000000000000x100050133cd0:00000000000000000000000000000000Shadowbyteleg(oneshadowbyterepresents8applicationbytes):Addressable:00Partiallyaddressable:01020304050607Heapleftredzone:faFreedheapregion:fdStackleftredzone:f1Stackmidredzone:f2Stackrightredzone:f3Stackafterreturn:f5Stackuseafterscope:f8Globalredzone:f9Globalinitorder:f6Poisonedbyuser:f7Containeroverflow:fcArraycookie:acIntraobjectredzone:bbASaninternal:feLeftallocaredzone:caRightallocaredzone:cbShadowgap:cc==17319==ABORTINGMS:1ChangeBit-;baseunit:7a9e5264e8896a1d996088a56a315765c53c7b330x5c,0x43,0x2b,0x5c,0x53,0x2b,0xde,0xac,0xd4,0xa3,0x53,0x2b,0x21,0x21,0x68,\\C+\\S+\xde\xac\xd4\xa3S+!!hartifact_prefix='./';Testunitwrittento./crash-5ae911f7e958e646e05ebe28421183f6efc0bc88Base64:XEMrXFMr3qzUo1MrISFo

SUMMARY:AddressSanitizer:stack-buffer-overflow/home/admin/libfuzzer-workshop/lessons/11//src/pcre2_:5968:11inmatch指出在pcre2_里存在stackoverflow。对漏洞进行定位:
在中调用了pcre2_match

inpcre2_=match(start_match,mb-start_code,start_match,2,mb,NULL,0);

在执行match的过程中出现栈溢出的位置在于:

for(;;){if(eptr==pp)gotoTAIL_RECURSE;RMATCH(eptr,ecode,offset_top,mb,eptrb,RM46);if(rrc!=MATCH_NOMATCH)RRETURN(rrc);eptr--;BACKCHAR(eptr);//overflow处if(ctype==OP_ANYNLeptrppUCHAR21(eptr)==CHAR_NLUCHAR21(eptr-1)==CHAR_CR)eptr--;}

当我以为fuzz的工作已经完成的时候,只是尝试着修改了一下编译链接harness时的静态库为全部库:

clang++-O2-fno-omit-frame-pointer-gline-tables-only-fsanitize=address,fuzzer-no-link-fsanitize-address-use-after-scopepcre2_/src-Wl,--/.libs/*.a-Wl,-no-whole-archive-fsanitize=_fuzzer

再次fuzz的结果令我惊讶:

605733NEWcov:3273ft:15707corp:6964/139Kblim:86exec/s:255rss:597MbL:29/86MS:3ShuffleBytes-CopyPart-CMP-DE:"+n"-606040REDUCEcov:3273ft:15707corp:6964/139Kblim:86exec/s:255rss:597MbL:19/86MS:1EraseBytes-606196NEWcov:3273ft:15709corp:6966/139Kblim:86exec/s:255rss:597MbL:86/86MS:5ChangeASCIIInt-ChangeBit-ChangeBit-ChangeASCIIInt-CrossOver-===================================================================10857==ERROR:AddressSanitizer:heap-buffer-overflowonaddress0x6110001625eaatpc0x00000055d548bp0x7ffccf4098f0sp0x7ffccf4098e8WRITEofsize1at0x6110001625eathreadT010x4f60f4inadd_to_class/home/admin/libfuzzer-workshop/lessons/11//src/pcre2_:2870:2030x4e03e0incompile_branch/home/admin/libfuzzer-workshop/lessons/11//src/pcre2_:3923:1150x4d136cinpcre2_compile_8/home/admin/libfuzzer-workshop/lessons/11//src/pcre2_:7734:770x4c83c9inLLVMFuzzerTestOneInput/home/admin/libfuzzer-workshop/lessons/11/pcre2_:19:1290x584cd5infuzzer::Fuzzer::RunOne(unsignedcharconst*,unsignedlong,bool,fuzzer::InputInfo*,bool*)/home/admin/libfuzzer-workshop/libFuzzer/Fuzzer/./:470:3110x586c75infuzzer::Fuzzer::Loop(std::vectorfuzzer::SizedFile,fuzzer::fuzzer_allocatorfuzzer::SizedFile)/home/admin/libfuzzer-workshop/libFuzzer/Fuzzer/./:830:5130x56cc20inmain/home/admin/libfuzzer-workshop/libFuzzer/Fuzzer/./:19:10150x41deb9in_start(/home/admin/libfuzzer-workshop/lessons/11/pcre2_10.00_fuzzer+0x41deb9)0x6110001625eaislocated0bytestotherightof234-byteregion[0x611000162500,0x6110001625ea)allocatedbythreadT0here:10x4d0953inpcre2_compile_8/home/admin/libfuzzer-workshop/lessons/11//src/pcre2_:7656:330x4c83c9inLLVMFuzzerTestOneInput/home/admin/libfuzzer-workshop/lessons/11/pcre2_:19:1250x584cd5infuzzer::Fuzzer::RunOne(unsignedcharconst*,unsignedlong,bool,fuzzer::InputInfo*,bool*)/home/admin/libfuzzer-workshop/libFuzzer/Fuzzer/./:470:370x586c75infuzzer::Fuzzer::Loop(std::vectorfuzzer::SizedFile,fuzzer::fuzzer_allocatorfuzzer::SizedFile)/home/admin/libfuzzer-workshop/libFuzzer/Fuzzer/./:830:590x56cc20inmain/home/admin/libfuzzer-workshop/libFuzzer/Fuzzer/./:19:10ifPCRE2_CODE_UNIT_WIDTH==8registerinti,j;for(i=0;iPRIV(utf8_table1_size);i++)if((int)cvalue=PRIV(utf8_table1)[i])break;buffer+=i;for(j=i;j0;j--){*buffer--=0x80|(cvalue0x3f);//此处对于内存指针循环操作由于限制条件不当导致出现了heap_overflowcvalue=6;}*buffer=PRIV(utf8_table2)[i]|cvalue;returni+1;/*ConverttoUTF-16*/else*buffer=(PCRE2_UCHAR)cvalue;return1;"util/"usingstd::string;voidTest(conststringbuffer,conststringpattern,constRE2::Optionsoptions){RE2re(pattern,options);if(!())return;stringm1,m2;inti1,i2;doubled1;if(()==0){RE2::FullMatch(buffer,re);RE2::PartialMatch(buffer,re);}elseif(()==1){RE2::FullMatch(buffer,re,m1);RE2::PartialMatch(buffer,re,i1);}elseif(()==2){RE2::FullMatch(buffer,re,i1,i2);RE2::PartialMatch(buffer,re,m1,m2);}re2::StringPieceinput(buffer);RE2::Consume(input,re,m1);RE2::FindAndConsume(input,re,d1);stringtmp1(buffer);RE2::Replace(tmp1,re,"zz");stringtmp2(buffer);RE2::GlobalReplace(tmp2,re,"xx");RE2::QuoteMeta(re2::StringPiece(pattern));}//"C"intLLVMFuzzerTestOneInput(constuint8_t*data,size_tsize){if(size1)return0;RE2::Optionsoptions;size_toptions_randomizer=0;for(size_ti=0;isize;i++)options_randomizer+=data[i];if(options_randomizer1)_encoding(RE2::Options::EncodingLatin1);_posix_syntax(options_randomizer2);_longest_match(options_randomizer4);_literal(options_randomizer8);_never_nl(options_randomizer16);_dot_nl(options_randomizer32);_never_capture(options_randomizer64);_case_sensitive(options_randomizer128);_perl_classes(options_randomizer256);_word_boundary(options_randomizer512);_one_line(options_randomizer1024);_log_errors(false);constchar*data_input=reinterpret_castconstchar*(data);{stringpattern(data_input,size);stringbuffer(data_input,size);Test(buffer,pattern,options);}if(size=3){stringpattern(data_input,size/3);stringbuffer(data_input+size/3,size-size/3);Test(buffer,pattern,options);}return0;}

可以看到harness用到了很多re2里的方法,最后使用FullMatch和PartialMatch接口进行匹配buffer和re。其中buffer是由data_input和size初始化得到(data_input由输入的data经无关类型转换得到),re是由pattern和options建立的RE2对象。
注意到harness里有几个条件分支语句,首先是size1是直接返回,还有就是当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:

clang++-O2-fno-omit-frame-pointer-gline-tables-only-fsanitize=address,fuzzer-no-link-fsanitize-address-use-after-scope-std=gnu++98/re2/obj/=fuzzer-ore2_fuzzer

由于使用的re2版本较老了,编译的时候使用了c++98标准。

首先我们设置max_len为10,执行时间为100秒,-print_final_stats=1打印最后的结果,corpus1作为语料库的存放处:

➜10git:(master)✗./re2_fuzzer./corpus1-print_final_stats=1-max_len=10-max_total_time=100Done643760runsin101second(s)stat::number_of_executed_units:643760stat::average_exec_per_sec:6373stat::new_units_added:36stat::slowest_unit_time_sec:0stat::peak_rss_mb:456

只探测到了36个代码单元。
接着设置max_len为100,执行时间为100秒,-print_final_stats=1打印最后的结果,corpus2作为语料库的存放处:

./re2_fuzzer./corpus2-print_final_stats=1-max_len=100-max_total_time=100Done233437runsin101second(s)stat::number_of_executed_units:233437stat::average_exec_per_sec:2311stat::new_units_added:50stat::slowest_unit_time_sec:0stat::peak_rss_mb:675

探测到了50个代码单元,感觉差别不大。
然年设置max_len为1000,执行时间为100秒,-print_final_stats=1打印最后的结果,corpus3作为语料库的存放处:

./re2_fuzzer./corpus3-print_final_stats=1-max_len=1000-max_total_time=100Done105935runsin101second(s)stat::number_of_executed_units:105935stat::average_exec_per_sec:1048stat::new_units_added:97stat::slowest_unit_time_sec:0stat::peak_rss_mb:830

这次探测到了97个代码单元,是第二个的2倍,第一个的3倍左右。
最后再设置max_len为500,执行时间为100秒,-print_final_stats=1打印最后的结果,corpus4作为语料库的存放处

./re2_fuzzer./corpus4-print_final_stats=1-max_len=500-max_total_time=100Done119361runsin101second(s)stat::number_of_executed_units:119361stat::average_exec_per_sec:1181stat::new_units_added:117stat::slowest_unit_time_sec:0stat::peak_rss_mb:827

结果也比较明显,不同的max_len对fuzz的效率有着不同的影响,当然这也和你写的harness有关。因此在执行fuzzer的时候选择合适的max_len(如本例中的max_len在100~1000比较合适)会使得我们fuzzer探测到更多的代码块,得到crash的概率也就越大。

总结

libfuzzerworkshop到此就全部学习完了。libfuzzer作为最常用的fuzz工具,它所涉及到的一些使用方法在workshop里都有相应的lesson。就我个人而言,在逐步学习libfuzzer的过程中感觉到libfuzzer对于开源库提供的接口函数的fuzz是十分强力的,而这也是我们在学习libfuzzer中的难点:如何能够设计出合理的harness,这需要我们对要fuzz的开源库提供的方法有一定的了解,经过攻击面分析等去逐步改善我们的harness,使得我们与获得crash更近一步。

初学libfuzzer,有错误疏忽之处烦请各位师傅指正。

发布于 2025-05-11
157
目录

    推荐阅读