初探AFL-Fuzz
AFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzing - FreeBuf网络安全行业门户
AFL漏洞挖掘技术漫谈(二):Fuzz结果分析和代码覆盖率 - FreeBuf网络安全行业门户
google/AFL: american fuzzy lop - a security-oriented fuzzer (github.com)
fuzz实战之afl - SecPulse.COM | 安全脉搏
AFL二三事——源码分析(上篇) - 先知社区 (aliyun.com)
AFL二三事——源码分析(下篇) - 先知社区 (aliyun.com)
AFL++学习日志(一)开始Fuzz与crashes分析 - Hanyin’s Space (mundi-xu.github.io)
什么是Fuzz
“Fuzz” 即模糊测试,通常用于描述在计算机编程和软件测试中的一种技术或方法。Fuzzing 是一种自动化的软件测试技术,Fuzzing 的基本原理是通过输入大量的随机或半随机数据来测试程序,观察程序如何处理这些数据。通过观察程序对不同输入的反应,可以发现潜在的漏洞和错误。
可以先看一下(https://pan.baidu.com/s/1UdLaijUA9AH7GpHcRKhDGQ?pwd=ccc6)
这篇论文对fuzz做了一个详细的介绍,从定义与分类到算法与实现,读完以后应该就对fuzz有一个基础的认识了
并且指出一个完整的fuzzer应该包含以下几个部分
- Preprocess
- Schduling
- Input Generation
- Input Evaluation
- Configuration Updating
现在已经有十分多的成熟Fuzz技术,例如AFLFuzz,libfuzzer,boofuzz等等等等
AFLFuzz
这里以较为经典的AFLFuzz为例进行讲解
american fuzzy lop(AFL)是由安全研究员Michał Zalewski(@lcamtuf)开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率,从而调整输入样本以提高覆盖率,增加发现漏洞的概率。其工作流程大致如下:
①从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);
②选择一些输入文件,作为初始测试集加入输入队列(queue);
③将队列中的文件按一定的策略进行“突变”;
④如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;
⑤上述过程会一直循环进行,期间触发了crash的文件会被记录下来。
AFL既可以对源码进行编译时插桩,也可以使用AFL的QEMU mode
对二进制文件进行插桩,但是前者的效率相对来说要高很多,在Github上很容易就能找到很多合适的项目。
AFL主要用于C/C++程序的测试,所以这是我们寻找软件的最优先规则。
AFL变异策略
确定性变异
- 比特翻转(bitflip):按位翻转,1变为0,0变为1.这一阶段还会按照不同的长度和步长进行多种不同的翻转,每次翻转1/2/4/8/16/32 bit,依次进行。
- 算术运算(arithmetic):整数加/减算术运算。跟bitflip类似,arithmetic根据目标大小的不同,也分为了多个子阶段,依次对8/16/32 bit进行加减运算。
- 特殊值替换(interest):把一些特殊内容替换到原文件中。同样每次对8/16/32 bit进行替换。所谓的特殊内容是AFL预设的一些比较特殊的数,比如可能造成溢出的数。
- 字典值(dictionary):把自动生成或用户提供的字典值替换或插入到原测试用例中。
随机变异
- havoc大破坏:对文件进行大量破坏,此阶段会对原文件进行大量随机变异。包括随机翻转、加减、替换和删除等操作。
- 文件拼接splice:此阶段会将两个文件拼接起来得到一个新的文件,并对这个新文件继续执行havoc变异。
如何使用AFL
安装
白盒模式
1 | git clone https://github.com/google/AFL.git |
黑盒模式
1 | cd qemu-mode |
修改build_qemu_support.sh
两处
1 | QEMU_URL="https://download.qemu.org/qemu-${VERSION}.tar.xz" |
修改./patchs/syscall.diff为
1 | --- qemu-2.10.0-clean/linux-user/syscall.c 2020-03-12 18:47:47.898592169 +0100 |
执行./build_qemu_support.sh
如果要安装其他架构,需要在执行构建之前声明变量
export CPU_TARGET=arm
构建语料库
AFL需要一些初始输入数据(也叫种子文件)作为Fuzzing的起点,这些输入甚至可以是毫无意义的数据,AFL可以通过启发式算法自动确定文件格式结构
如果要获得更快的Fuzzing速度,那么就有必要生成一个高质量的语料库,这一节就解决如何选择输入文件、从哪里寻找这些文件、如何精简找到的文件三个问题。
选择
(1) 有效的输入
尽管有时候无效输入会产生bug和崩溃,但有效输入可以更快的找到更多执行路径。
(2) 尽量小的体积
较小的文件会不仅可以减少测试和处理的时间,也能节约更多的内存,AFL给出的建议是最好小于1 KB,但其实可以根据自己测试的程序权衡,这在AFL文档的perf_tips.txt
中有具体说明。
寻找
- 使用项目自身提供的测试用例
- 目标程序bug提交页面
- 使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式:
- afl源码的testcases目录下提供了一些测试用例
- 其他开源的语料库
- afl generated image test sets
- fuzzer-test-suite
- libav samples
- ffmpeg samples
- fuzzdata
- moonshine
修剪
网上找到的一些大型语料库中往往包含大量的文件,这时就需要对其精简,这个工作有个术语叫做——语料库蒸馏(Corpus Distillation)。AFL提供了两个工具来帮助我们完成这部工作——afl-cmin
和afl-tmin
。
移除执行相同代码的输入文件——afl-cmin
afl-cmin
的核心思想是:尝试找到与语料库全集具有相同覆盖范围的最小子集。举个例子:假设有多个文件,都覆盖了相同的代码,那么就丢掉多余的文件。其使用方法如下:
1 | $ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] |
更多的时候,我们需要从文件中获取输入,这时可以使用“@@”代替被测试程序命令行中输入文件名的位置。Fuzzer会将其替换为实际执行的文件:
1 | $ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@ |
减小单个输入文件的大小——afl-tmin
整体的大小得到了改善,接下来还要对每个文件进行更细化的处理
afl-tmin
有两种工作模式,instrumented mode
和crash mode
。默认的工作方式是instrumented mode
,如下所示:
1 | $ afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@ |
如果指定了参数-x
,即crash mode
,会把导致程序非正常退出的文件直接剔除。
1 | $ afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@ |
afl-tmin
接受单个文件输入,所以可以用一条简单的shell脚本批量处理。如果语料库中文件数量特别多,且体积特别大的情况下,这个过程可能花费几天甚至更长的时间!
1 | for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done; |
构建被测试程序
AFL从源码编译程序时进行插桩,以记录代码覆盖率。这个工作需要使用其提供的两种编译器的wrapper编译目标程序
afl-gcc模式
afl-gcc
/afl-g++
作为gcc
/g++
的wrapper,它们的用法完全一样,前者会将接收到的参数传递给后者,我们编译程序时只需要将编译器设置为afl-gcc
/afl-g++
就行,如下面演示的那样。如果程序不是用autoconf构建,直接修改Makefile
文件中的编译器为afl-gcc/g++
也行。
1 | $ ./configure CC="afl-gcc" CXX="afl-g++" |
在Fuzzing共享库时,可能需要编写一个简单demo,将输入传递给要Fuzzing的库(其实大多数项目中都自带了类似的demo)。这种情况下,可以通过设置LD_LIBRARY_PATH
让程序加载经过AFL插桩的.so文件,不过最简单的方法是静态构建,通过以下方式实现:
1 | $ ./configure --disable-shared CC="afl-gcc" CXX="afl-g++" |
LLVM模式
LLVM Mode模式编译程序可以获得更快的Fuzzing速度,进入llvm_mode
目录进行编译,之后使用afl-clang-fast
构建序程序即可,如下所示:
1 | $ cd llvm_mode |
开始Fuzz测试插桩程序
白盒测试
编译好程序后,可以选择使用afl-showmap
跟踪单个输入的执行路径,并打印程序执行的输出、捕获的元组(tuples),tuple用于获取分支信息,从而衡量衡量程序覆盖情况,下一篇文章中会详细的解释,这里可以先不用管。
1 | $ afl-showmap -m none -o /dev/null -- ./build/bin/imagew 23.bmp out.png |
使用不同的输入,正常情况下afl-showmap
会捕获到不同的tuples,这就说明我们的的插桩是有效的,还有前面提到的afl-cmin
就是通过这个工具来去掉重复的输入文件。
1 | $ $ afl-showmap -m none -o /dev/null -- ./build/bin/imagew 111.pgm out.png |
执行fuzzer
在执行afl-fuzz
前,如果系统配置为将核心转储文件(core)通知发送到外部程序。 将导致将崩溃信息发送到Fuzzer之间的延迟增大,进而可能将崩溃被误报为超时,所以我们得临时修改core_pattern
文件,如下所示:
1 | echo core >/proc/sys/kernel/core_pattern |
之后就可以执行afl-fuzz
了,通常的格式是:
1 | $ afl-fuzz -i testcase_dir -o findings_dir /path/to/program [params] |
或者使用“@@”替换输入文件,Fuzzer会将其替换为实际执行的文件:
1 | $ afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@ |
如果没有什么错误,Fuzzer就正式开始工作了。首先,对输入队列中的文件进行预处理;然后给出对使用的语料库可警告信息,且输入文件过多;最后,开始Fuzz主循环,显示状态窗口。
使用screen
一次Fuzzing过程通常会持续很长时间,如果这期间运行afl-fuzz实例的终端终端被意外关闭了,那么Fuzzing也会被中断。而通过在screen session
中启动每个实例,可以方便的连接和断开。关于screen的用法这里就不再多讲,大家可以自行查询。
1 | $ screen afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@ |
也可以为每个session命名,方便重新连接。
1 | $ screen -S fuzzer1$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program [params] @@[detached from 6999.fuzzer1] |
黑盒测试
所谓黑盒测试,通俗地讲就是对没有源代码的程序进行测试,这时就要用到AFL的QEMU模式了。启用方式和LLVM模式类似,也要先编译。但注意,因为AFL使用的QEMU版本太旧,util/memfd.c
中定义的函数memfd_create()
会和glibc中的同名函数冲突,在这里可以找到针对QEMU的patch,之后运行脚本build_qemu_support.sh
就可以自动下载编译。
1 | $ apt-get install libini-config-dev libtool-bin automake bison libglib2.0-dev -y$ cd qemu_mode$ build_qemu_support.sh$ cd .. && make install |
现在起,只需添加-Q
选项即可使用QEMU模式进行Fuzzing。
1 | $ afl-fuzz -Q -i testcase_dir -o findings_dir /path/to/program [params] @@ |
并行测试
单系统并行测试
如果你有一台多核心的机器,可以将一个afl-fuzz
实例绑定到一个对应的核心上,也就是说,机器上有几个核心就可以运行多少afl-fuzz
实例:
1 | $ cat /proc/cpuinfo| grep "cpu cores"| uniq |
afl-fuzz
并行Fuzzing,一般的做法是通过-M
参数指定一个主Fuzzer(Master Fuzzer
)、通过-S
参数指定多个从Fuzzer(Slave Fuzzer
)。
1 | $ screen afl-fuzz -i testcases/ -o sync_dir/ -M fuzzer1 -- ./program |
这两种类型的Fuzzer执行不同的Fuzzing策略,前者进行确定性测试(deterministic ),即对输入文件进行一些特殊而非随机的的变异;后者进行完全随机的变异。
可以看到这里的-o
指定的是一个同步目录,并行测试中,所有的Fuzzer将相互协作,在找到新的代码路径时,相互传递新的测试用例,如下图中以Fuzzer0的角度来看,它查看其它fuzzer的语料库,并通过比较id来同步感兴趣的测试用例。
afl-whatsup
工具可以查看每个fuzzer的运行状态和总体运行概况,加上-s
选项只显示概况,其中的数据都是所有fuzzer的总和。
afl-gotcpu
工具可以查看每个核心使用状态。
多系统并行测试
多系统并行的基本工作原理类似于单系统并行中描述的机制,你需要一个简单的脚本来完成两件事。在本地系统上,压缩每个fuzzer实例目录中queue
下的文件,通过SSH分发到其他机器上解压。
来看一个例子,假设现在有两台机器,基本信息如下:
fuzzer1 | fuzzerr2 |
---|---|
172.21.5.101 | 172.21.5.102 |
运行2个实例 | 运行4个实例 |
为了能够自动同步数据,需要使用authorized_keys
的方式进行身份验证。现要将fuzzer2中每个实例的输入队列同步到fuzzer1中,可以下面的方式:
1 |
|
成功执行上述shell脚本后,不仅SESSION000
SESSION002
中的内容更新了,还将SESSION003
SESSION004
也同步了过来。
状态窗口
① Process timing:Fuzzer运行时长、以及距离最近发现的路径、崩溃和挂起经过了多长时间。
② Overall results:Fuzzer当前状态的概述。其中,总周期数可以用来作为何时停止fuzzing的参考。随着不断地fuzzing,周期数会不断增大,其颜色也会由洋红色,逐步变为黄色、蓝色、绿色,当其变为绿色时代表可执行的内容已经很少了此时便可以通过Ctrl-C,中止当前的fuzzing
③ Cycle progress:我们输入队列的距离。
④ Map coverage:目标二进制文件中的插桩代码所观察到覆盖范围的细节。
⑤ Stage progress:Fuzzer现在正在执行的文件变异策略、执行次数和执行速度。执行速度可以直观地反映当前跑的快不快,如果速度过慢,比如低于500次每秒,那么测试时间会变得非常漫长。如果发生了这种情况,我们需要进一步优化我们的Fuzzing
⑥ Findings in depth:有关我们找到的执行路径,异常和挂起数量的信息。
⑦ Fuzzing strategy yields:关于突变策略产生的最新行为和结果的详细信息。
⑧ Path geometry:有关Fuzzer找到的执行路径的信息。
⑨ CPU load:CPU利用率
更多可以参考官方文档status_screen (coredump.cx)
crashes处理
AFL++学习日志(一)开始Fuzz与crashes分析 - Hanyin’s Space (mundi-xu.github.io)
crash exploration mode
这是afl-fuzz的一种运行模式,也称为peruvian rabbit mode,用于确定bug的可利用性
1 | afl-fuzz -C -i out/default/crashes/ -o crash_exploration/ ./vulnerable |
将一个导致crash测试用例作为afl-fuzz的输入,使用-C选项开启crash exploration模式后,可以快速地产生很多和输入crash相关、但稍有些不同的crashes,从而判断能够控制某块内存地址的长度
可以参考这篇博客peruvian were-rabbit | Count Upon Security
triage_crashes
AFL源码的experimental目录中有一个名为triage_crashes.sh的脚本,可以帮助我们触发收集到的crashes
例如
1 | $ ~/afl-2.52b/experimental/crash_triage/triage_crashes.sh fuzz_out ~/src/LuPng/a.out @@ out.png 2>&1 | grep SIGNAL |
crashwalk
这个工具基于gdb的exploitable插件,安装也相对简单,在ubuntu上,只需要如下几步即可:
1 | $ apt-get install gdb golang |
crashwalk支持AFL/Manual两种模式。前者通过读取crashes/README.txt文件获得目标的执行命令(前面第三节中提到的),后者则可以手动指定一些参数。两种使用方式如下:
1 | #Manual Mode |
afl-collect
它也是afl-utils套件中的一个工具,同样也是基于exploitable来检查crashes的可利用性。它可以自动删除无效的crash样本、删除重复样本以及自动化样本分类。使用起来命令稍微长一点,如下所示:
1 | $ afl-collect -j 8 -d crashes.db -e gdb_script ./afl_sync_dir ./collection_dir -- /path/to/target --target-opts |
实例demo
我们以这个简单的c语言程序为demo尝试一下AFLFuzz的使用
1 |
|
白盒
使用afl-gcc -g -o demo ./demo.c
编译
创建两个文件夹fuzz_in
和fuzz_out
,然后在fuzz_in
随便放个文本文件,写入任意字符串,这里写入的是hello
之后启动afl-fuzz -i fuzz_in -o fuzz_out demo
在跑了十分钟左右之后结果如下
发现了3条路径6个crash,进入fuzz_out,文件作用大致如下
- queue:存放所有具有独特执行路径的测试用例。
- crashes:导致目标接收致命signal而崩溃的独特测试用例。
- crashes/README.txt:保存了目标执行这些crash文件的命令行参数。
- hangs:导致目标超时的独特测试用例。
- fuzzer_stats:afl-fuzz的运行状态。
- plot_data:用于afl-plot绘图。
分析一下crash样本
1 | find ./fuzz_out/crashes/ -type f -name 'id*' -exec sh -c 'for file do echo "Hex dump of $file:"; xxd "$file"; echo; done' sh {} + |
可以看到所有漏洞都被Fuzz到了
黑盒
现在我们改为使用gcc -g -o demo_gcc demo.c
进行编译
进行无源码fuzz,命令与之前差不多,只不过多了个-Q选项,记得先export AFL_PATH=~/fuzz/AFL-2.57b/
不然找不到qemu
afl-fuzz -i fuzz_in -o fuzz_out2 -Q ./demo_gcc
在一分钟多时就已经发现了5个crash,不过同样到10分钟左右才完整的跑出六个
可以看到虽然时间差异不大,但通过对比执行速度,可以看出黑盒模式下速度只有白盒的三分之一左右
Fuzz网络程序
网络程序往往是从一个socket读取输入,那么如何进行fuzz,其实也可以将其转化为从标准输入读取
AFL文档阅读
基本概念
代码覆盖率(Code Coverage)
代码覆盖率是模糊测试中一个极其重要的概念,使用代码覆盖率可以评估和改进测试过程,执行到的代码越多,找到bug的可能性就越大,毕竟,在覆盖的代码中并不能100%发现bug,在未覆盖的代码中却是100%找不到任何bug的
代码覆盖率是一种度量代码的覆盖程度的方式,也就是指源代码中的某行代码是否已执行;对二进制程序,还可将此概念理解为汇编代码中的某条指令是否已执行。其计量方式很多,但无论是GCC的GCOV还是LLVM的SanitizerCoverage,都提供函数(function)、基本块(basic-block)、边界(edge)三种级别的覆盖率检测,更具体的细节可以参考LLVM的官方文档
基本块(Basic Block)
这个在之前的angr使用学习中已经有一些了解了
缩写为BB,指一组顺序执行的指令,BB中第一条指令被执行后,后续的指令也会被全部执行,每个BB中所有指令的执行次数是相同的,也就是说一个BB必须满足以下特征:
- 只有一个入口点,BB中的指令不是任何跳转指令的目标。
- 只有一个退出点,只有最后一条指令使执行流程转移到另一个BB
边(edge)
fuzzer通过插桩代码捕获边(edge)覆盖率
我们可以将程序看成一个控制流图(CFG),图的每个节点表示一个基本块,而edge就被用来表示在基本块之间的转跳。知道了每个基本块和跳转的执行次数,就可以知道程序中的每个语句和分支的执行次数,从而获得比记录BB更细粒度的覆盖率信息。
元组(tuple)
AFL的实现中,使用二元组(branch_src, branch_dst)来记录当前基本块 + 前一基本块 的信息,从而获取目标的执行流程和代码覆盖情况,伪代码如下:
1 | cur_location = <COMPILE_TIME_RANDOM>;//用一个随机数标记当前基本块 |
实际插入的汇编代码,如下图所示,首先保存各种寄存器的值并设置ecx/rcx,然后调用__afl_maybe_log
,这个方法的内容相当复杂,这里就不展开讲了,但其主要功能就和上面的伪代码相似,用于记录覆盖率,放入一块共享内存中。
AFL源码导读
AFLplusplus
The fuzzer afl++ is afl with community patches, qemu 5.1 upgrade, collision-free coverage, enhanced laf-intel & redqueen, AFLfast++ power schedules, MOpt mutators, unicorn_mode, and a lot more!
相比原版AFL++整合完善了许多功能,例如unicorn模式,frida模式等等
安装方法可见Building | AFLplusplus
默认全部安装可如下操作
1 | $ git clone https://github.com/AFLplusplus/AFLplusplus |
注意安装过程中会覆盖普通afl版本的二进制文件