AFL(american fuzzy lop)学习二
@sizaif
@2022-04-10
设计思想
覆盖率计算
改进边缘覆盖:
向目标程序注入以下工具来捕获分支(边缘)覆盖率和分支命中计数
- 一条边表示为当前基本块(分配了一个随机常数)与前一个基本块(即元组 (prev_location, cur_location))之间的 XOR
- 覆盖信息存储在一个紧凑共享的64KB的哈希表中,称为跟踪位图(bitmap)
- 当访问一条边时,通过增加与该特定边的哈希相对应的位图中的值来记录命中
cur_location = <COMPILE_TIME_RANDOM>; # cur_location 值是随机生成的,以简化链接复杂项目的过程并保持 XOR 输出均匀分布。
shared_mem[cur_location ^ prev_location]++; # 由调用者传递给检测的二进制文件。
prev_location = cur_location >> 1; # 保持元组的方向性 以区分 A^B 和 B^A ,并保持紧密循环的标识,区分A^A,B^B
**简单块计算存在的问题:**对已执行的基本块进行技术会导致信息丢失
例如: 已执行 BB1->BB2->BB3->BB4, 新的路径: BB1->BB2->BB4, BB1->BB2->BB3 未被计入,路径丢失

相比简单的块覆盖计算:可以区分一下路径
BB1 -> BB2 -> BB3 -> BBB4 (tuples: BB1BB2, BB2BB3, BB3BB4)
BB1 -> BB2 ->BBB4 (tuples: BB1BB2, BB2BB4)
BB1 -> BB2 ->BBB3 (tuples: BB1BB2, BB2BB3)
检测新行为
采用以下方法:
- fuzzer 维护先前执行中元组的全局映射;当一个变异的输入产生一个包含新元组的执行跟踪时,相应输入文件被保留等后续处理。
- 不触发执行跟踪中新的局部规模状态转换(即不产生新元组)的输入将被丢弃,即使它们的整体控制流序列是唯一的。
优势:
- 避免路径爆炸
- 达到较高的细粒度探索
- 不必对复杂的执行轨迹进行任何计算密集型和脆弱的全局比较
example: #2 存在(CA,AE)被认为是新路径,
#1: A -> B -> C -> D -> E
#2: A -> B -> C -> A -> E
处理路径#2 时,由于存在(CD,CA), 会出现以下的路径,但不会被视为新的路径
#3: A -> B -> C -> A -> B -> C -> A -> B -> C -> D -> E
除了检测新元组外,模糊器还考虑粗略的元组命中计数, 分为以下桶:
1、2、3、4-7、8-15、16-31、32-127、128+
桶允许将仪器生成的 8 位计数器就地映射到 fuzzer 可执行文件依赖的 8 位位图,以跟踪已经看到的每个元组的执行计数
模糊策略
确定性策略包括:
-
具有不同长度和步距的连续位翻转,(Sequential bit flips with varying lengths and stepovers)
-
小整数的顺序加减法,(Sequential addition and subtraction of small integers)
-
已知有趣整数的顺序插入(0、1、INT_MAX 等),(Sequential insertion of known interesting integers (0, 1, INT_MAX, etc))
非确定性步骤包括:
- 堆叠位翻转 (stacked bit flips)
- 插入 (insertions)
- 删除 (deletions)
- 算术 (arithmetics)
- 不同测试用例的拼接 (splicing of different test cases)
afl-as.c
设计思想:
当使用 afl-gcc / afl-clang
编译程序时,工具链会自动寻找afl-as
的位置,并调用此文件编译的程序afl-as
。
此汇编包装器的目的是
-
预处理由
GCC/clang
生成的汇编文件- 处理输入的参数命令,为调用真正的
as
汇编器准备。
- 处理输入的参数命令,为调用真正的
-
并在分支处插桩
afl-as.h
中包含的检测位。-
处理输入文件,生成 modified_file。 根据
afl-as.h
在合适的的地方插桩。插桩执行代码:
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,R(MAP_SIZE));
其本质上想捕获如下位置:
^main: - function entry point (always instrumented) ^.L0: - GCC branch label ^.LBB0_0: - clang branch label (but only in clang mode) ^\tjnz foo - conditional branches
-
-
使用
execvp
调用as
进行真正的汇编execvp(as_params[0], (char**)as_params)
afl-as.h 插桩部分
// 伪代码
cur_location = <COMPILE_TIME_RANDOM>; # cur_location 值是随机生成的,以简化链接复杂项目的过程并保持 XOR 输出均匀分布。
shared_mem[cur_location ^ prev_location]++; # 由调用者传递给检测的二进制文件。
prev_location = cur_location >> 1; # 保持元组的方向性 以区分 A^B 和 B^A ,并保持紧密循环的标识,区分A^A,B^B
说明:
存储 XOR异或数据对: 1) 当前执行分支的标识符; 2)之前执行的分支的标识符。即TL,DR 执行 shm_trace_map[cur_loc ^ prev_loc]++
插桩操作
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,R(MAP_SIZE));
fprintf()
将格式化字符串添加到汇编文件的相应位置,trampoline_fmt_64
的具体内容如下:
/**
* 这一段汇编代码,主要的操作是:
* 保存rdx等寄存器
* 将rcx的值设置为fprintf()所要打印的变量内容
* 调用方法__afl_maybe_log()
* 恢复寄存器
*/
static const u8* trampoline_fmt_64 =
"\n"
"/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leaq -(128+24)(%%rsp), %%rsp\n"
"movq %%rdx, 0(%%rsp)\n"
"movq %%rcx, 8(%%rsp)\n"
"movq %%rax, 16(%%rsp)\n"
"movq $0x%08x, %%rcx\n"
"call __afl_maybe_log\n"
"movq 16(%%rsp), %%rax\n"
"movq 8(%%rsp), %%rcx\n"
"movq 0(%%rsp), %%rdx\n"
"leaq (128+24)(%%rsp), %%rsp\n"
"\n"
"/* --- END --- */\n"
"\n";
__afl_maybe_log
作为插桩代码所执行的实际内容,分析"movl $0x%08x, %%ecx\n"
这条指令
R(MAP_SIZE)
即为上述指令将rcx
设置的值,即为。根据定义,宏MAP_SIZE
为64K,;R(x)
的定义是(random() % (x))
,所以R(MAP_SIZE)
即为0到MAP_SIZE之间的一个随机数。
因此,在处理到某个分支,需要插入桩代码时,afl-as
会生成一个随机数,作为运行时保存在ecx
中的值。而这个随机数,便是用于标识这个代码块的key。
检查 SHM 区域是否已映射
static const u8* main_payload_64 =
·····
" /* Check if SHM region is already mapped. */\n"
"\n"
" movq __afl_area_ptr(%rip), %rdx\n"
" testq %rdx, %rdx\n"
" je __afl_setup\n"
"\n"
·····
分支记录
static const u8* main_payload_64 =
······
"__afl_store:\n"
"\n"
" /* Calculate and store hit for the code location specified in rcx. */\n"
"\n"
#ifndef COVERAGE_ONLY
" xorq __afl_prev_loc(%rip), %rcx\n"
" xorq %rcx, __afl_prev_loc(%rip)\n"
" shrq $1, __afl_prev_loc(%rip)\n"
#endif /* ^!COVERAGE_ONLY */
"\n"
#ifdef SKIP_COUNTS
" orb $1, (%rdx, %rcx, 1)\n"
#else
" incb (%rdx, %rcx, 1)\n"
#endif /* ^SKIP_COUNTS */
·····
对应伪代码 shared_mem[cur_location ^ prev_location]++;
具体地,变量
__afl_prev_loc
保存的是前一次跳转的”位置”,其值与rcx
做异或后,保存在rip
中,并以rdx
(共享内存)为基址,对rcx
下标处进行加一操作。而rip
的值右移1位后,保存在了变量__afl_prev_loc
中。则这里的
rcx
,保存的为伪代码中的cur_location
。回忆之前介绍代码插桩的部分:static const u8* trampoline_fmt_64 = ······ "movq $0x%08x, %%rcx\n" "call __afl_maybe_log\n" ······
在每个插桩处,afl-as会添加相应指令,将
rcx
的值设为0到MAP_SIZE之间的某个随机数,从而实现了伪代码中的cur_location = <COMPILE_TIME_RANDOM>;
因此,AFL为每个代码块生成一个随机数,作为其“位置”的记录;随后,对分支处的”源位置“和”目标位置“进行异或,并将异或的结果作为该分支的key,保存每个分支的执行次数。用于保存执行次数的实际上是一个哈希表,大小为MAP_SIZE=64K
。
afl-gcc.c
**设计思想: **
-
当前系统路径中找到
afl-as
汇编器所在位置,调用afls-as
,afl-as以as的别名形式存在
插桩检测位。编译器默认优先使用该路径中的汇编器和链接器,即
afl-as
,实际的插桩工作发生在汇编的时候 -
读取输入的参数命令,根据输入的参数命令
CC=%s/afl-gcc ./configure CXX=%s/afl-g++ ./configure
1). 判断使用的
afl编译器
对应真实的编译器gcc/g++
或clang/clang++
将真实编译器填入
cc_params[0]
中afl-gcc -> gcc afl-g++ -> g++ afl-clang++ ->clang++ afl-clang -> clang
2). 读取其他相应的参数,并填充到
cc_params
数组中 -
调用系统命令
execvp
执行真实的编译器execvp(cc_params[0], (char**)cc_params);
execvp()的基本语法 (Basic Syntax of execvp())
This function takes in the name of the UNIX command to run, as the first argument.
该函数将要运行的UNIX命令的名称作为第一个参数。
This is there in the
<unistd.h>
header file, so we must include it in our program.该文件位于
<unistd.h>
头文件中,因此我们必须将其包含在程序中。#include <unistd.h> int execvp(const char* command, char* argv[]);
Here, we refer to a “command” as any binary executable file that is a part of the
PATH
environment variable. So, if you want to run custom programs, make sure that you add it to yourPATH
variable!在这里,我们将“命令”称为
PATH
环境变量一部分的任何二进制可执行文件。 因此,如果要运行自定义程序,请确保将其添加到PATH
变量中!The second argument (
argv
) represents the list of arguments tocommand
. This is an array ofchar*
strings.第二个参数(
argv
)表示command
的参数列表。 这是一个char*
字符串数组。Here,
argv
contains the complete command, along with it’s arguments.在这里,
argv
包含完整的命令及其参数。