LLVM LibFuzzer简介

2020-09-18 01:19:41

模糊是一种软件测试方法,它将错误的数据作为输入传递给程序,并监控其是否有错误行为。如今,模糊是发现软件安全问题的最有效的方法之一。2014年,MichałZalewski提出了美国模糊LOP,这是第一个有封面的引导模糊。这开启了市场上模糊解决方案和技术的现代世界。

在本文中,我们将讨论libFuzzer,这是一个允许您将模糊方法集成到库中的LLVM实用程序,并简要介绍了使捕获问题的效率最大化的技术。

LibFuzzeris是LLVM包的一部分。它允许您将覆盖引导的模糊逻辑集成到C/C++应用程序中。LibFuzzer的一个重要功能是它与杀菌器覆盖和错误检测杀菌器紧密结合,即:地址杀菌器、泄漏杀菌器、内存杀菌器、线程杀菌器和未定义的行为杀菌器,这些项目的使用确保了检测到广泛的内存损坏错误和不需要的应用程序行为。例如:堆/栈/全局越界、释放后使用、返回后使用、未初始化内存读取、内存泄漏或未初始化互斥使用。

所有功能都有相对较低的性能和内存开销,项目只需要一个可用的LLVM 5.0工具链或更新版本(发布日期:2017年9月7日)。

AFL自2014年上市以来,已经能够检测到超过1000种不同类型的软件错误。当最初的afl库的开发停滞不前很长一段时间后,社区在2018年创建了一个名为afl++的分支。google托管项目的开发最终由一个不同的google员工团队恢复(因为michałzalewski改变了他的工作场所)。因此,目前有两个独立的Aflin主动开发版本。

AFL的设计主要基于代码覆盖,通过跟踪代码路径的执行(使用SanitizerCoverage),然后向模糊器提供反馈(再次使用SanitizerCoverage回调),然后使用遗传算法来定制新的输入(使用标准输入)来扩大代码覆盖。AFL的简化算法如下:

尝试将测试用例修剪到不改变程序的测量行为的最小大小,

如果生成的任何突变导致转换到检测记录的新状态,则将突变输出作为新条目添加到队列中。

首先,AFL不能处理不同类型的覆盖,例如收敛比较指令的求值(在C==,>;,<;,!=等中);其次,AFL不知道跟踪根据输入返回特定值的标准(和非标准)函数的求值,例如用于比较字符串的函数(在C strcmp(),strncmp()等中)。最后,AFL不能足够灵活地集成到接受输入的环境中。

Google员工创建了一个新的Fuzzer来克服所有提到的限制,并利用LLVM中已有的错误检测功能。libFuzzer的理念是创建一个操作类似于单元测试的工具。我们编写了一个小的模糊程序(称为“Harness”),并创建编程环境以将其快速集成到由可调用的函数集(通常是库的API)组成的项目中。模糊输入作为常规C函数(而不是标准输入)的参数提供,如果可能,模糊过程以持久模式运行,以避免为每个输入重新生成它。

LLVMFuzzerTestOneInput()函数有两个参数,Data和Size.这是一个固定长度的缓冲区,然后必须由我们的类似单元测试的程序处理,并传递给我们的API。

行为应该尽可能是确定性的。相同的输入必须产生相同的输出。

对于有效的代码路径,被调用的库应该避免退出(通过exit()或引发信号)。

它可以使用线程,但是在返回到libFuzzer之前,所有新产生的线程都应该加入。

AFL更适合一种类型的项目,libFuzzer更适合另一种类型的项目,例如,不是作为库设计的项目很难与libFuzzer集成,而AFL测试的进入门槛可能会更低。这是一个重新构建软件并按原样运行Fuzzer的问题。

如上所述,libFuzzer完全在内存中操作,而AFL向模糊化的程序提供巧尽心思构建的文件,并将它们传递给它的输入。

模糊化过程从构建测试语料库开始,在libFuzzer的情况下,这不一定是真的-它可以生成完全随机的输入。当然,这实际上意味着在开始时没有代码覆盖率,并且在产生通过初始完整性检查的第一个输入之前需要很多小时的工作。因此,开始时甚至有几个/奇怪的测试用例也是值得的。

最小化测试文件的目标是以尽可能小的输入文件大小获得尽可能多的代码覆盖率。关系很简单:文件越小,每秒执行的次数就越多。值得一提的是,还有一个最小化单个输入文件的过程。此操作的目的是从测试用例中删除导致崩溃的任何冗余数据,以捕获到底发生了什么。根据libFuzzer文档,使用准备好的主体开始有意义的模糊的最低速度是每秒1000次。

崩溃管理通常归结为通过比较函数调用堆栈或堆栈跟踪来删除一组测试用例的重复数据。由于故障期间引信信息有限,这里有不同的方法来解决这个问题。LibFuzzer没有用于管理测试用例的模块,每个新的Fuzing作业都是它的“干净卡片”。用户必须通过自己编写报告解析器来自动执行此过程。

鼓励读者创建和维护他们的测试语料库-从长远来看,这比使用在互联网上找到的“现成”语料库要好得多,或者像前面提到的那样,从头开始生成一个语料库。如果我们使用相同的文件格式测试许多不同的程序,那么按应用程序(在单个应用程序的情况下)或按文件类型对语料库进行分类是值得的。

初始测试数据越多样化越好。语料库在模糊化过程中不断演变:您可以在代码中找到经过以前未知路径的输入。在操作期间,较小的文件将较大的文件推出语料库(如果它们提供同等的代码覆盖率)。当然,您会找到导致程序终止的测试用例。合并语料库的过程不过是最小化集合的总和:开始模糊化的语料库和新创建的测试用例。

如前所述,要在项目中使用libFuzzer,您需要准备轻量级库条目代码。

让我们看一个来自LLVM libFuzzer测试套件的真实示例,一个名为SingleStrcmpTest.cpp的示例。

//llvm项目的一部分,在apache License v2.0下,但有llvm异常。//有关许可信息,请参阅https://llvm.org/LICENSE.txt。//spdx-License-Identifier:apache-2.0with llvm-Exception//对Fuzzer进行简单测试。Fuzzer必须找到特定字符串。#include<;cstdint>;#include<;cstdio>;#include<;cstdlib>;#include<;cstring>;extern";C";int LLVMFuzzerTestOneInput(const uint8_t*data,size_t size){if(size>;=7){char copy[7];memcpy(copy,data,6);copy[6]=0;if(!strcmp(copy,";qwerty&34;)){fprintf(stderr,";bingo\n";);exit(1);}}返回0;}

在本例中,我们有两个触发错误的条件(在我们的示例中,调用exit(2)和退出):

我们注意到程序中没有main()函数,因为它是由libFuzzer库直接提供的。除此之外,该代码还包含SanCov仪器,用于反馈绒毛器。

这个Fuzzer使用strcmp(3)函数拦截器,这些拦截器通过address Sanitizer集成到libFuzzer的覆盖报告机制中。因此,有两种执行程序的方式,启用地址杀毒程序和不启用地址杀毒程序,我们可以看到不同之处。这也表明,即使代码路径相对简单,AFL也完全不能猜测代码来打破代码中的strcmp(3)条件,并传递随机输入,甚至可能永远不会在有限时间内打破程序。

DECLARE_WEAK_INTERCEPTOR_HOOK(__sanitizer_weak_hook_strcmp,uptr称为_pc,const char*s1,const char*s2,int result)拦截器(int,strcmp,const char*s1,const char*s2){void*ctx;common_interceptor_enter(ctx,strcmp,s1,s1,s2);unsign char c1,c2;uptr i;for(i=0;i++){c1=(Unsign Char)s1[i];c2=(Unsign Char)S2[i];if(c1!=c2||c1=';\0';)Break;}COMMON_INTERCECTOR_READ_STRING(ctx,s1,i+1);COMMON_INTERCECTOR_READ_STRING(ctx,s2,i+1);int result=charCmpX(c1,c2);CALL_WEAK_INTERCEPTOR_HOOK(__sanitizer_weak_hook_strcmp,GET_CALLER_PC(),s1,s2,Result);返回结果;}。

简单地说,Saniizer运行时通过过载的标准函数实现常规的健全性检查来捕获错误,并且在某些情况下,它们通过__saniizer_ware_hook_strcmp挂钩反馈backlibFuzzer,报告:

为了启动模糊化的程序,我们只需启动编译后的程序。我们将限制迭代次数,我们将传递-Runs=1000000参数。启动程序后,模糊器将:

探测低于限值的输入数量,触发退出代码路径,保存复制器并退出。

$./a.out-run=1000000INFO:SEED:567124526INFO:已加载1个模块(4个内联8位计数器):4[0x67d594,0x67d598),INFO:已加载1个PC表(4个):4[0x46abc8,0x46ac08),未提供信息:-max_len;LibFuzzer将不生成大于4096字节的输入INFO:不提供语料库,从空语料库开始#2初始化覆盖:2英尺:2公司:1/1b执行/秒:0 rss:41Mb#415新覆盖:3 ft:3公司:2/9b LIM:8执行/s:0 rss:41Mb L:8/8 MS:3插入字节-插入字节交叉-#464缩减覆盖:3 ft:3公司:2/8b LIM:8执行/s:0 rss:41Mb L:7/7 MS:4 ShuffleBytes-EraseBytes-InsertByte-InsertByte-#1000000完成覆盖:3 ft:3公司:2/8b LIM:4096。EXEC/秒:1000000 rss:41MB完成1000000在1秒内运行

让我们尝试使用Address Saniizer及其内置拦截器,包括strcmp(3)的拦截器。

$clang++-fsanitize=fuzzer,地址SingleStrcmpTest.cpp$./a.out-run=1000000INFO:SEED:2511417575INFO:已加载1个模块(4个内联8位计数器):4[0x767e94,0x767e98),INFO:已加载1个PC表(4个):4[0x5413c8,0x541408),未提供信息:-max_len;LibFuzzer将不生成大于4096字节的输入INFO:不提供语料库,从空的语料库开始#2初始化覆盖:2英尺:2公司:1/1b EXEC/s:0 RSS:41Mb#414新覆盖:3 ft:3 Corp:2/9b LIM:8 EXEC/s:0 RSS:41Mb L:8/8 MS:2 ChangeBit-Crossover-#427 Reduce Cover:3 ft:3 Corp:2/8b LIM:8 EXEC/s:0 RSS:41BLIM:8 EXEC/s:0 RSS:41。跟踪FUZZER中的/public/llvm/projects/compiler-rt/lib/asan/asan_stack.cc:86:3#1 0x459676::PrintStackTrace()FUZZER::FUZZER::ExitCallback()/public/llvm/projects/compiler-rt/lib/fuzzer/FuzzerUtil.cpp:205:5#2 0x43d48c中的/public/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:251:3#3 0x7f7ff634dfe5 in__cxa_finalize/usr/src/lib/libc/stdlib/atexit.c:222:7#4 0x7f7ff634dbe2 in exit/usr/src/lib/libc/stdlib/exit.c:60:2#5 0x523727 in LLVMFuzzerTestOneInput(/tmp/./a.out+0x523727)#6 0x43e924 in fuzzer::fuzzer::ExecuteCallback(unsign char const*,UNSIGNED LONG)FUZZER::FUZZER::RUNONE中的/public/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:553:15#7 0x43e045(UNSIGNED CHAR CONST*,UNSIGNED LONG,BOOL,FUZZER::INPUTINFO*,Bool*)Fuzzer中的/public/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:471:3#8 0x4405fb::Fuzzer::MutateAndTestOne()/public/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:695:19#9 0x4414b5中的Fuzzer::Fuzzer::Loop(std::Vector<;Std::__cxx11::BASIC_STRING<;字符,std::Char_特征<;字符>;,std::分配器<;字符>;,fuzzer::fuzzer_allocator<;std::__cxx11::basic_string<;char,std::Char_特征<;字符>;,std::分配器<;字符>;常量&;,std::向量<;Std::__cxx11::BASIC_STRING<;字符,std::Char_特征<;字符>;,std::分配器<;字符&>t;,fuzzer::fuzzer_allocator<;std::__cxx11::basic_string<;char,std::CHAR_特征<;字符>;,std::分配器<;字符>;>;常量&;)/public/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:840:5#10 0x42df05 in FUZZER::FUZZER Driver(INT*,CHAR*,INT(*)(UNSIGNED CHAR CONST*,UNSIGNED LONG))/public/llvm/projects/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:787:6#11 0x459f52 in Main/public/llvm/projects/compiler-rt/lib/fuzzer/FuzzerMain.cpp:19:10#12 0x422eba in_start(/tmp/./a.out+0x422eba)摘要:libFuzzer:FUZZ目标退出MS:3ShuffleBytes-ChangeBit-cmp-DE:";QWERTY";-;基本单元:655969223db839614fb5b3146504e1009c930fb30x71,0x77,0x65,0x72,0x74,0x79,0x0,QWERTY\x00工件_前缀=';./';;写入./crash-1489c3a435b0fd3b98f42947c07e26fb7b425f08Base64:cXdlcnR5AA==的测试单元。

//LLVM项目的一部分,在Apache License v2.0下,有LLVM例外。//有关许可证信息,请参阅https://llvm.org/LICENSE.txt。//spdx-License标识符:带有LLVM-Exception的Apache2.0//测试strstr和strcasestr挂钩。#include<;cstdint>;#include<;cstdio>;#include<;cstdlib>;#include<;string.h>;#include<;string>;//Windows没有strcasestr和memmem,所以我们不测试它们。#ifdef_win32#定义strcasestr strstr#定义memmem(a,b,c,d)true#endifextern";C";int LLVMFuzzerTestOneInput(const uint8_t*data,size_t size){if(size<;4)return 0;std::string s(reInterprete_cast<;const char*>;(Data),size);if(strstr(S.C_str(),";fuzz";&;)。Strcasestr(S.C_str(),";abcd";)&;&;memmem(s.data(),s.size(),";kuku";,4)){fprintf(stderr,";bingo\n&34;);exit(1);}返回0;}。

$./a.out-run=1000000INFO:种子:2285393126INFO:已加载1个模块(12个内联8位计数器):12[0x768034,0x768040),INFO:已加载1个PC表(12个):12[0x5419f8,0x541ab8),未提供信息:-max_len;LibFuzzer不会生成大于4096字节的输入INFO:不提供语料库,从空语料库#2 INITED cov:2 ft:2 corp:1/1b exec/s:0 RSS:39Mb#9 new cov:3 ft:3 corp:2/5b LIM:4 exec/s:0 RSS:39Mb L:4/4 MS:2 InsertByte-Crossover-#20 new cov:5 ft。-#1129new cov:7 ft:7 corp:4/21b LIM:14 EXEC/s:0 rss:39Mb L:12/12 MS:4persAutoDict-Cmp-DE:";FUZZ";-";aBcD";-#3693 Reduce Cov:7 ft:7 Corp:4/20b LIM:38 EXEC/s:0 RSS:39Mb L:11/11 MS:4 InsertByte-CMP-EraseBytes-PersAutoDict-DE:";\x00\x00";-";-#3727Reduce Cov:7 ft:7 corp:4/19b LIM:38 EXEC/s:0 rss:39Mb L:10/10 MS:4 ChangeBinInt-ChangeBit-EraseBytes-cmp-DE:";abcd";-bingo==22896==错误:libfuzzer:fuzz目标已退出#0