使用Afl-fuzz (American Fuzzy Lop) 进行fuzzing测试(二)——详细使用说明(README.txt)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/youkawa/article/details/76405468

不完全译自:http://lcamtuf.coredump.cx/afl/README.txt
转载请注明出处。

随手翻译的笔记,还有待继续整理。

AFL算法

1) 把用户提供的初始测试用例加载到队列(queue)中;
2) 从队列中获取下一个测试输入文件;
3) 在保持程序行为的前提下,尝试修剪(trim)测试用例(体积)到最小;
4) 使用传统的模糊测试策略中的各种已有的研究方法,重复变异文件;
5) 如果生成的变异结果能够驱动新的状态转换(通过插桩记录),则把这一测试用例加入到队列中;
6) 调到第2步。

已经发现的测试用例也会周期性地被新的、能驱动更高代码覆盖率的测试用例替代,或者使用若干其他插桩层驱动的最小化的措施删除。

作为模糊测试步骤的副作用(side effect),工具生成比较小的,自足的测试用例语料。这些是非常有用的。

因为以上的措施,使得AFL能比盲fuzz或者只有代码覆盖率驱动的工具具有更好地性能。

使用AFL对程序插桩

在有被测试程序源码的前提下,可以直接使用AFL替代gcc或者clang作为编译器,AFL的插桩具有非常低的性能影响。加上其他的afl-fuzz性能优化措施,大部分程序能被快速fuzzed,甚至会比传统的工具更快。

重新编译目标程序的配置可能会因不同程序而不同,但通用的步骤是:

$ CC=/path/to/afl/afl-gcc ./configure
$ make clean all

对C++程序,设置为CXX=/path/to/afl/afl-g++

对clang编译的程序,可以用同样的方式设置,如afl-clang和afl-clang++。clang用户同样可以选择利用llvm_mode/README.llvm提到的高性能的插桩模式。

当测试lib库的时候,需要找到或者写入简单程序,从stdin或者从文件读取数据,传入到被测试的lib库中。所以,以把被插桩的lib库以静态的方式连接到可执行文件显得至关重要。或者确保正确的.so文件在程序运行时加载(通过LD_LIBRARY_PATH)。最简单的选择是使用静态编译,通常采用以下方式:

$ CC=/path/to/afl/afl-gcc ./configure --disable-shared

在使用make命令时设置AFL_HARDEN=1的时候,可以促使CC自动化代码加固选项,使得检测简单的内存bug更加容易。Libdislocator是一个AFL提供的帮助者lib(具体详见libdislocator/README.dislocator),能帮助发现堆崩溃问题。

对二进制程序插桩

在没有源代码的时候,可以使用QUMU中的“user space emulation”模式对二进制进行插桩。具体操作:

$ cd qemu_mode
$ ./build_qemu_support.sh

关于QEMU插桩的说明及注意事项,可以参见qemu_mode/README.qemu

选择初始测试用例

为了正常操作,fuzzer要求一个或多个包含测试输入数据的好的初始文件,有两个基本规则:
1) 减小文件的大小。低于1kB的文件是比较理想的,虽然并没有严格要求。至于为什么初始文件大小会影响测试效果,详见perf_tips.txt;
2) 只有在每个测试用例都能驱动程序中的不同功能的情况下才有必要使用多个测试用例。在AFL中使用50个不同的图片fuzz图片处理类lib库并没有什么意义。

可以在本工具的testcases/目录下找到非常多好的测试用例作为初始种子文件。

如果有比较大的数据语料库,可以使用afl-cmin工具鉴别功能不同(驱动程序走不同的代码路径)的子集文件,即最小集处理。

对二进制进行模糊测试

使用afl-fuzz工具对目标二进制程序进行fuzz。在运行前,需要只读权限的包含初始测试用例的目录,分开的目录用于存放测试过程中的findings,以及被测试二进制程序的路径。

对直接从stdin中接收输入的目标二进制程序,常用的语法为

$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]

其中params被测试程序接受的命令行参数。

如果从文件中读取输入,则在命令行中使用@@标记,在实际执行的时候afl-fuzz会把“@@”替换成测试样本目录(testcase_dir )下的测试样本。对应的命令为:

$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

可以使用-f参数,把已经变异过的数据写入特定的文件。这对需要指定特定文件扩展名的需求特别有用。

没有插桩过得二进制能被QEMU模式fuzz(在命令行中添加-Q参数),或者以传统的盲fuzz的模式测试(以-n参数指定)。
可以使用-t和-m改写程序执行过程中默认的timeout和memory limit。但有这种需求的情况比较少见(包括在编译器和视频编解码器等)。

关于优化fuzzing性能的一些措施详见perf_tips.txt

afl-fuzz从数组fuzz开始,可能会需要几天的时间。但会产生比较整洁的测试用例。如果短时间内想要迅速和比较脏的结果,可以使用zzuf和其他传统的模糊测试工具(添加-d参数)。

输出解释

从status_screen.txt中可以找到显式界面中各字段的含义,以及监控被测试程序的健康状态。在界面中如果出现任何字段被红色高亮显式,可以查阅此文件确认情况。

fuzzing进程会一直持续知道用户按下Ctrl-C,即使想要fuzzer完成一个队列周期的测试,也可能会需要几小时到一周的时间。
在输出目录中有三个子目录,分别是:

-queue/ 每一条不同的执行路径对应的测试用例,包括所有由用户提供的初始化测试用例文件。这是之前提到的综合语料库。在使用这些语料到任意程序之前,可以使用afl-cmin工具对其大小进行紧缩。这一工具将寻找更小的测试集文件(保持相同的代码、边覆盖)。

-crashes/ 能导致被测试程序获得致命信号(fatal signal)(如SIGSEGV, SIGILL, SIGABRT等)的唯一性的测试用例。

-hangs/ 能导致被测试程序超时的唯一性测试用例。默认的时间限制。执行时间限制超过默认的1秒或者以-t参数设置的数值之后,改测试用例就会被记录在这个文件夹下。这些值也可以使用AFL_HANG_TMOUT设定,但通常很少会有这样的需求。

如果相关的执行路径包含的任意状态转换并没有包含在之前记录的错误相关的记录中,则crashes和hangs被当做是唯一的(unique)。如果单一的bug能通过多路径触发,则count计算会临时有所膨胀,但会在后续的操作中逐渐变少。

crashes和hangs的文件名和父节点以及没出错的队列节点相关。这样能帮助调试。

当不能使用afl-fuzz重现crash的时候,最有可能的原因是没有设置相同的内存限制。可以尝试以下命令:

$ LIMIT_MB=50
$ ( ulimit -Sv $[LIMIT_MB << 10]; /path/to/tested_binary ... )

在以上命令中,更改LIMIT_MB参数与-m参数相匹配。

任何已经存在的output目录对应的工作能被重新恢复:

$ ./afl-fuzz -i- -o existing_output_dir [...etc...]

如果安装了gnuplot,可以使用afl-plot为正在fuzzing的程序生成一些漂亮的图片,详见http://lcamtuf.coredump.cx/afl/plot/.

并行模糊测试

每个afl-fuzz实例只占用一个cpu的核。这对使得在多核系统中使用并行化来来增加对硬件的利用率显得很重要。
并行Fuzzing模式同时还提供简单的方式的接口给其他的测试工具,包括符号执行工具。具体详见parallel_fuzzing.txt

测试字典(fuzzing dictionaries)

默认情况下,AFL更适合对紧凑数据格式的fuzzing,包括图像、多媒体、压缩数据、正则表达式语法,或者shell脚本等;对一些格式繁琐、冗余的数据,如HTML,SQL和JavaScript等,支持并不好。

为了避免构建语法敏感型的工具所带来的麻烦,afl-fuzz提供了一种方式可以对语言关键字、magic headers或者其他特殊的符号和目标数据类型相关的符号。可选择字典的fuzzing进程。使用上面这种方式重建潜在的语法,目前正在开发中。详见:
http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html

如果要使用以上的特性,首先需要为dictionaries/README.dictionaries中说明的一种或两种格式建立一个字典,然后在命令中以-x参数选项指定。(一些通用的字典已经在子目录下提供)。

对潜在的语法(syntax),afl-fuzz并没有办法提供更加复杂的描述,但fuzzer将可能通过插桩层的反馈解决部分问题。详见:
http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html

另外,即使在没有详细字典提供的情况下,afl-fuzz将尝试通过观察插桩的形式从输入语料中抽取已有的语法标识。这些工作对一些类型的解析和语法有效,但效果不如上面提到的-x参数指定的模式。

如果字典非常难得到,另一个选择是让AFL先运行一段时间,然后使用标识(token)捕获作为AFL工具的库。具体详见libtokencap/README.tokencap

崩溃分类(Crash triage)

基于代码覆盖率的崩溃分类常常能生成小的数据集,能被快速手动分类,或者使用简单的GDB或Valgrind脚本分类。每个crash还是对其在队列中相应的非崩溃测试用例可追踪的,这样能更容易诊断错误。

一些fuzzing出来的crash很难被快速评估其可利用性,而需要大量的调试和代码分析工作。为了帮助解决这一问题,afl-fuzz支持以比较独创的“crash exploration”模式,具体在命令中以-C参数指定。

在这种模式下,fuzzer采用一个或多个崩溃测试用例作为输入,然后使用反馈驱动的fuzzing策略,快速枚举所有能达到崩溃点的代码路径。

对编译中生成的不会导致崩溃的样本将被丢弃,同时,没有触发新路径的测试输入也同样会被丢弃。
输出是包含崩溃样本的小的语料库(corpus),这些能被快速检查通过崩溃点能多大程度上被控制,或者是否可能通过越界读,查看其它内存中的信息。

对测试集最小化,仍然可以使用afl-tmin工具:

$ ./afl-tmin -i test_case -o minimized_result -- /path/to/program [...]

这一工具对崩溃样本集和普通样本集同样适用,在崩溃模式下,将接收以插桩和非插桩二进制的形式。在非崩溃模式,最小化处理依赖于标准的AFL插桩,这使得在没有更改执行路径的前提下让文件变的更简单。
另一项最近添加到AFL的功能是afl-analyze工具。这一工具在有测试输入的情况下,驱动程序执行,尝试连续地改变字节,然后观察被测试程序的行为。然后对比较重要(critical)的节区(sections)的代码标注颜色,这样通常能快速查看复杂文件格式。更多详情移步:technical_details.txt

猜你喜欢

转载自blog.csdn.net/youkawa/article/details/76405468