AFL总结
2022-04-28 22:20:12

AFL总结

简介:

基于覆盖引导的模糊测试工具,它通过记录输入样本的代码覆盖率==》从而调整输入样本以提高覆盖率==》

增加发现漏洞的概率

工作流程:

1、从源码编译程序时进行插桩,以记录代码覆盖率

2、选择一些输入文件,作为初始测试集加入输入队列(queue)

3、将队列中的文件按照一定的策略进行”变异”

4、如果经过变异文件更新了覆盖范围,则将其保留添加到队列中

5、上述流程会一直循环进行,期间触发crash的文件会被记录

相关概念:

代码覆盖率:

一种度量代码的覆盖程度的方式==》指源代码中的某行代码是否已经执行==》

计量方式:函数,基本快,边界三种级别的覆盖率检测

计算方式:计算在在一次运行中,相应边执行的次数,次数单位为2的幂

基本块(Basic Block):

缩写为BB,指一组顺序执行的指令,BB中第一条指令被执行后,后续的指令也会被全部执行

每一个BB中所有指令的执行次数是相同的==》

BB特征:

只有一个入口点,BB中的指令不是任何跳转指令的目标

只有一个退出点,只有最后一条指令使执行流程转移到另一个BB

边界(edge):

fuzzer通过插桩代码捕获边(edge)覆盖率==》

可以将程序看成是一个控制流图(CFG)==》图的每一个节点表示一个基本块==》

而edge就被表示在基本块之间的跳转==》

知道了每一个基本块和跳转的执行次数,就可以知道程序中的每一个语句和分支的执行次数==》

从而获得比记录BB更细粒度的覆盖率信息

元组(tuple):

在AFL实现中,使用二元组来记录当前基本块+前一个基本块的信息==》

从而获取目标的执行流程和代码覆盖率

插桩:

在AFL变异文件的时候afl-gcc会在规定的位置插入桩代码==》

一个个的探针,但是没有暂停功能,在后续的fuzz过程中会根据这些桩代码进行路径探索,测试等

AFL通过插桩的形式注入到被编译的程序中,实现分支覆盖率的捕获,以及分支节点计数

cur_location = <COMPILE_TIME_RANDOM>;
//使用一个随机数cur_location标记当前块

shared_mem[cur_location ^ prev_location]++;//将当前块和前一块异或保存到shared_mem[]
//shared_mem[]数组是一个被调用者传入插桩二进制的64KB SHM大小的区域

prev_location = cur_location >> 1;//cur_location右移1位区分从当前块到当前块的转跳

目标选择与使用

目标选择:

通常时接收外部输入的程序或库,输入一般来自文件

AFL主要用于C/C++程序的测试

目标应是该软件的最新版本

AFL的fuzz途径:

1、开源软件:AFL软件进行编译的同时进行插桩,效率相对于闭源软件来说更高

2、闭源软件:使用AFL的QEMU mode对二进制文件进行插桩

使用:

构建语料库:

AFL需要一些初始输入数据(种子文件)作为fuzzing的起点=》

这些输入可以是毫无意义的数据,AFL可以通过启发式算法自动确定文件格式机构==》

选择输入文件:有效的输入和尽量小的体积

寻找文件:使用项目自身提供的测试用例,目标程序bug提交页面,使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式,afl源码的testcases目录下提供了一些测试用例

其他开源的语料库

修剪:网上寻找的一些大型语料库中往往包含大量文件==》需要对其精简==》

该过程称为语料库蒸馏==》AFL提供两个工具来帮助完成:afl-cmin和afl-tmin

afl-cmin:移除执行相同代码的输入文件==》

核心:尝试找到与语料库全集具有相同覆盖范围的最小子集

(多个文件,都覆盖了相同的代码,丢弃多余的文件)

使用:

afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params]

#当需要从文件中获取输入==>使用@@代替被测试程序命令行中输入文件名的位置
#fuzzer会将其替换为实际执行的文件
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@

afl-tmin:减小单个输入文件的大小==》

两种工作方式:

instrumented mode(默认)

afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@

crash mode

指定参数-x==》会导致程序非正常退出的文件被直接剔除

afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@

目标构建:

1、afl-gcc模式:

afl-gcc/afl-g++作为gcc/g++的wrapper,它们的用法完全一样,前者会将接收到的参数传递给后者,我们编译程序时只需要将编译器设置为afl-gcc/afl-g++

./configure CC="afl-gcc" CXX="afl-g++"

Fuzzing共享库:需要编写一个demo,将输入传递给要Fuzzing的库

./configure --disable-shared CC="afl-gcc" CXX="afl-g++" 

2、LLVM模式:

LLVM mode模式编译程序可以获得更快的Fuzzing速度==》

进入llvm_mode目录进行编译:

cd llvm_mode
sudo apt-get clang
export LLVM_CONFIG=`which llvm-config` && make && cd ..
./configure --disable-shared CC="afl-clang-fast" CXX="afl-clang-fast++" 

Fuzzing:

白盒测试:

测试插桩程序

编译好插桩程序后,使用afl-showmap跟踪单个输入的执行路径,并打印程序的输出,捕获的元组

tuple用于获取分支信息,从而衡量程序的覆盖情况

afl-showmap -m none -o /dev/null -- ./build/bin/imagew 23.bmp out.png

afl-showmap -m none -o /dev/null -- ./build/bin/imagew 111.pgm out.png

使用不同的输入,正常情况下afl-showmap会捕获到不同二点tuples==》说明插桩有效

执行Fuzzing

在执行afl-fuzz之前,如果系统配置为将核心转储文件(core)通知发送到外部程序==》

将导致将崩溃信息发送到Fuzzer之间的延迟增大==》将崩溃误报为超时==》

echo core >/proc/sys/kernel/core_pattern

执行afl-fuzz:

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

可以使用@@替代输入文件,Fuzzer会将替换位实际执行的文件:

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

黑盒测试:

对没有源代码的程序进行测试==>使用AFL的QEMU模式==》

启用方式:与LLVM模式类似,先进行编译

启动方式:

afl-fuzz -Q -i testcase_dir -o findings_dir /path/to/program [params] @@

并行测试:

查看核心数:

cat /proc/cpuinfo| grep "cpu cores"| uniq

afl=fuzz并行Fuzzing,通过-M参数指定一个主Fuzzer,通过-S参数指定多个从Fuzzer

screen afl-fuzz -i testcases/ -o sync_dir/ -M fuzzer1 -- ./program

screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer2 -- ./program

screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer3 -- ./program

两种类型的Fuzzer执行不同的Fuzzing策略

-M执行确定性测试==》对输入文件进行一些特殊而非随机的变异

-S进行完全随机的变异

命令中使用-o指定的是一个同步目录,并行测试中,所有的Fuzzer将相互协作,在找到新的代码路径时,相互传递新的测试用例