[TOC]
总结_AFL源码学习
简要
- 本文用于分析AFL源码,之前用了xmind, 感觉太杂了,本来想用OmniGraffle,但感觉还是差不多
- xmind 和 OmniGraffle 适合整理思路,不适合仔细记录
- 用例子学习 [[总结_fuzzing101_例子]]
创建cmakelists.txt方便vscode调试
cmake_minimum_required(VERSION 3.4) |
cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug -B build_debug |
afl-gcc-对源码插装
命令
- [[总结_fuzzing101_例子#练习 1-Xpdf]]
export LLVM_CONFIG="llvm-config-11" |
main
- 如果参数个数小于2(表示没有给出任何参数),则打印程序的用法信息,并退出程序
- 根据AFL_PATH环境变量,去构造afl-as路径。
find_as构造afl-as路径
标记内存构造
- 申请内存 44 + ALLOC_OFF_HEAD(8) + 1=53,
(((u32*)(_ptr))[-2])=0xFF00FF00
,(((u32*)(_ptr))[-1]) = 44
,(((u8*)(_ptr))[ALLOC_S(_ptr)])=0xF0
x/48xb 0x000055555555a2a0 |
- 拷贝内存从 0x55555555a2a8 到 0xf0 前
edit_params
- 该函数用于处理传递给包装器的命令行参数,并根据参数设置各种编译器选项。
- 包装器获取参数, 构造命令行参数 cc_params
- 第一参数 遍历
argv[0]
- afl-clang
- 获取环境变量 AFL_CC 作为第一参数
- afl-clang++
- 获取环境变量 AFL_CXX 作为第一参数
- afl-g++
- 获取环境变量 AFL_CXX 作为第一参数
- afl-g++
- 获取环境变量 AFL_CC 作为第一参数
- afl-gcj
- 获取环境变量 AFL_GCJ 作为第一参数
- afl-clang
- 后续参数解析
-fsanitize=address
or-fsanitize=memory
- asan_set=1
FORTIFY_SOURCE
- fortify_set=1
- 如果有环境变量
AFL_HARDEN
-fstack-protector-all
- 如果没设置了 fortify_set 设置
-D_FORTIFY_SOURCE=2
- 如果设置了 asan_set
- 设置 AFL_USE_ASAN 1
- asan和 msan 和 AFL_HARDEN 不能同时指定
- 获取环境变量
AFL_USE_ASAN
- 设置
-U_FORTIFY_SOURCE
-fsanitize=address
- 设置
- 获取环境变量
AFL_USE_MSAN
- 设置
-U_FORTIFY_SOURCE
-fsanitize=memory
- 设置
- 如果没设置环境变量
AFL_DONT_OPTIMIZE
如果关闭就设置AFL_DONT_OPTIMIZE=1
- 设置
-g
- 最加上
-O3
-funroll-loops
-D__AFL_COMPILER=1
-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
- 设置
- 如果设置了环境变量
AFL_NO_BUILTIN
- “-fno-builtin-strcmp”, “-fno-builtin-strncmp”, “-fno-builtin-strcasecmp”, “-fno-builtin-strncasecmp”, “-fno-builtin-memcmp”, “-fno-builtin-strstr”, “-fno-builtin-strcasestr”
- 最后参数 NULL
execvp来执行实际的编译器
/home/wutang/Desktop/google_afl/afl-g++ -g -O2 -DHAVE_CONFIG_H -I.. -I. -c /home/wutang/Desktop/fuzzing_xpdf/xpdf-3.02/goo/GHash.cc |
- 调用 [[#afl-as]]
afl-as
命令
/home/wutang/Desktop/google_afl/as -I .. -I . --64 -o GHash.o /tmp/ccC1Zxd9.s |
gettimeofday时间加进程pid构造随机种子
gettimeofday(&tv, &tz); |
edit_params设置参数
- 函数用于处理传递给GNU as的参数,添加修改后的文件名等信息。
- 获取环境变量
TMPDIR
- tmp_dir
- 获取环境变量
AFL_AS
- afl_as 后面的as目录
- 获取环境变量
TEMP
TMP
- 写入tmp_dir 否则用
/tmp
- 写入tmp_dir 否则用
- 申请
(8+32) *8
大内存 - 第一参数
argv[0]
as 或者设置的 afl_as - 后续参数
- 如果有
--64
use_64bit=1 默认为1 - 如果有
--32
use_64bit=0 - input_file 为参数尾部,这里是
/tmp/ccC1Zxd9.s
- 如果是
-version
就退出 - 如果不为
/tmp/
/var/tmp/
就会设置 pass_thru=1 不会修改指令
- 如果是
- 设置尾部参数为
/tmp/pid/time(NULL)
/tmp/.afl-124181-1690797188.s
- 如果有
add_instrumentation
- 函数用于在适当的位置插入插桩代码。
读取汇编文件,写入afl汇编文件
- 遍历文件内容
/tmp/ccC1Zxd9.s
, 写入文件/tmp/.afl-124181-1690797188.s
- 如果设置
pass_thru
continue - 检测文本段
- 函数会扫描输入的汇编代码,以确定是否在正确的文本段(
.text
)内。由于插桩是针对代码段的,因此只有当程序在代码段内时才会进行插桩处理。 - 表示处于正确的文本段内,可以进行插桩
- 检查
text
,section\t.text
,section\t__TEXT,__text
,section __TEXT,__text
- 将
instr_ok
设置为 1,表示处于正确的文本段内,可以进行插桩
- 将
- 检查
section\t
,section
,bss\n
,data\n
让他 instr_ok=0x- 将
instr_ok
设置为 0,表示不在文本段内,不需要进行插桩。
- 将
- 函数会扫描输入的汇编代码,以确定是否在正确的文本段(
- 处理不同格式的汇编代码
- 这部分代码主要处理一些特殊格式的汇编代码,如若发现了注释中的
.code32
或.code64
,则会跳过一些代码块,因为它们通常不需要插桩
- 这部分代码主要处理一些特殊格式的汇编代码,如若发现了注释中的
- 处理 Intel 语法
- 如果当前行包含
.intel_syntax
或.att_syntax
字符串,表示当前代码是 Intel 或 AT&T 语法的汇编代码。 .intel_syntax
表示在处理 Intel 语法代码,会将skip_intel
设置为 1,以跳过相关代码块。.att_syntax
会将skip_intel
设置为 1- 后面自己添加的payload有 [[#64位&32位汇编payload]]
- 如果当前行包含
- 处理
__asm__
块- 如果当前行是注释行(以
#
开头),并且包含#APP
或#NO_APP
,表示当前处于__asm__
块内部,会将skip_app
设置为 1
- 如果当前行是注释行(以
- 插入条件分支插桩代码
Conditional branch instruction (jnz, etc)
- 如果当前行以一个制表符
\t
开始,表示这是一个汇编指令。如果这个指令是条件分支指令(以j
开头,但不是jmp
),并且随机数小于插桩概率inst_ratio
,则会在这个指令后面插入条件分支的插桩代码
- 插入跳转目标插桩代码
- 如果当前行包含
:
字符,表示这是一个标签。函数会判断这个标签是否是跳转目标(条件分支的目标),如果是,则会在这个标签处插入插桩代码,用于收集跳转目标的覆盖信息。
- 如果当前行包含
- 输出主要插桩代码:在汇编代码的末尾,会插入一段主要的插桩代码(
main_payload_32
或main_payload_64
),用于处理插桩逻辑的初始化和收尾工作fputs(use_64bit ? main_payload_64 : main_payload_32, outf);
加到末尾?- [[#64位&32位汇编payload]]
- 如果设置
- 最后输出信息
[+] Instrumented 435 locations (64-bit, non-hardened mode, ratio 100%).
最重要的代码-插入逻辑
- afl的插桩相当简单粗暴,就是通过汇编的前导命令来判断这是否是一个分支或者函数,然后插入instrumentation trampoline。
^func: - function entry point (always instrumented) |
32|64跳转指令
- 在合适时机插入跳转指令
if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok && |
trampoline_fmt_64
static const u8* trampoline_fmt_64 = |
- 这段代码是 American Fuzzy Lop(AFL)模糊测试工具注入到被测程序中的一部分汇编级别的插桩(Instrumentation)
64位&32位汇编payload
- 下面每段解释 代码+解释
- 在整个 fuzzing 过程中,父进程(afl-fuzz进程)会不断地向子进程发送命令,子进程根据这些命令执行不同的操作。这样,父进程可以控制子进程的行为,例如在发现新的输入样本时,将其发送给子进程执行,然后收集覆盖率数据。而子进程则负责实际执行目标程序,并将覆盖率数据写入共享内存,供父进程收集。这样做可以有效地分担负担并提高 fuzzing 性能。
__afl_fork_wait_loop
是一个循环,在__afl_forkserver
函数中用于等待父进程(afl-fuzz进程)发来的信号并执行相应的操作
__afl_maybe_log
static const u8* main_payload_64 = |
__afl_maybe_log
包含一些汇编代码,用于在日志中记录AFL的执行状态- 这是一个条件编译块,根据操作系统定义了
__OpenBSD__
或__FreeBSD__
并且版本较低于 9,它会使用.byte
汇编指令来加载0x9f
到%al
寄存器(加载寄存器 AH 中的标志位)。否则,直接使用lahf
汇编指令。然后使用seto
指令将%al
寄存器中的溢出标志位设置为 1。 lahf
将标志寄存器的低 8 位(FLAGS 寄存器的低 8 位)的内容加载到AH
寄存器中- 首先检查是否已经存在共享内存,如果已经存在则跳转到
__afl_store
以开始插桩。 - 否则进入
__afl_setup
- 这是一个条件编译块,根据操作系统定义了
__afl_area_ptr
共享内存- 这段代码首先将
__afl_area_ptr
符号地址(偏移量)加载到%rdx
寄存器中,然后使用testq
指令检查%rdx
的值是否为零。如果%rdx
的值为零(也就是指针为空),则跳转到标签__afl_setup
,表示 SHM 区域尚未映射。 - [[#__afl_setup_first]]
- 这段代码首先将
__afl_store
"__afl_store:\n" |
__afl_store
主要的插桩点- 用于计算和存储代码位置的执行路径信息。
- 这是一个条件编译块,当未定义
COVERAGE_ONLY
宏时,执行其中的代码。它将%rcx
寄存器的值与__afl_prev_loc
符号地址指向的值进行异或运算,然后将结果再次与__afl_prev_loc
指向的值进行异或运算。接着,将__afl_prev_loc
指向的值右移一位。 %rcx
存储为原来的__afl_prev_loc(%rip)
- 条件编译
- 这是另一个条件编译块,当定义了
SKIP_COUNTS
宏时,执行其中的代码。- 它将立即数 1 与地址
(%rdx, %rcx, 1)
处的内存进行或运算 - 否则直接在
(%rdx, %rcx, 1)
处加一。incb (%rdx, %rcx, 1)\n
将位于地址%rdx + %rcx*1
处的字节内容加一。
- 这个内存地址是
__afl_area_ptr
指向的 SHM 区域中的某个计数器位置,用于记录覆盖情况。 - Added SKIP_COUNTS and changed the behavior of COVERAGE_ONLY in config.h. Useful only for internal benchmarking. 用于内部benchmarking
- 它将立即数 1 与地址
- 这是另一个条件编译块,当定义了
__afl_return
"__afl_return:\n" |
__afl_return
在记录完当前代码块的执行信息后,将控制权返回到调用main_payload_64
函数的地方,继续执行后续的代码- 这段代码首先将立即数 127 加到
%al
寄存器中。接着,根据操作系统的不同,使用.byte
汇编指令加载0x9e
到sahf
指令(将标志寄存器的内容从%ah
寄存器中加载),或者直接使用sahf
指令。最后,使用ret
指令返回到调用者。
- 这段代码首先将立即数 127 加到
- 这里返回到用户代码继续
__afl_setup
"\n" |
__afl_setup
这是初始化共享内存(SHM)的部分,用于存储测试用例和覆盖率信息。- 将
__afl_setup_failure
标签的值(一个字节)与零进行比较。__afl_setup_failure
是用于记录初始化失败次数的变量,初始值为零。 - 读取数据段指针
__afl_global_area_ptr
,取出一个指针到rdx- 如果rdx为空 跳转到 [[#__afl_setup_first]] 就执行一次
- 尝试从环境变量中获取共享内存 ID,并调用
shmat()
将其映射到当前进程的地址空间。
- 将
__afl_setup_first
|
__afl_setup_first
- 调用
getenv()
函数来获取环境变量__AFL_SHM_ID
的值,__afl_area_ptr
变量所对应的内存地址中,即保存共享内存区域的地址, 将共享内存区域的地址保存在__afl_global_area_ptr
变量中
- 调用
__afl_forkserver&__afl_fork_wait_loop
"__afl_forkserver:\n" |
- __afl_forkserver
- 通过 write 199 去进程间通信
- 它会进入 Fork Server 模式,该模式的目的是为了避免重复执行
execve()
的开销,提高执行效率
- __afl_fork_wait_loop
- 开始fork进程
- fork后 rax为0 表示子进程 跑到 [[#__afl_fork_resume子进程处理]]
- fork后 rax>0 表示父进程
- 将子进程的 PID 存入
__afl_fork_pid
变量 - 将子进程的 PID 写入文件描述符
(FORKSRV_FD + 1) = 199
所对应的文件中 - 使用
waitpid
系统调用等待子进程退出,返回值在%rax
中。如果返回值小于等于0,则跳转到__afl_die
标签处终止进程。 - 然后继续循环执行
__afl_fork_wait_loop
,等待父进程的指令
- 将子进程的 PID 存入
- 开始fork进程
__afl_fork_resume子进程处理
"\n" |
- __afl_fork_resume
- 表示在子进程中执行。在子进程中,关闭文件描述符
FORKSRV_FD
和(FORKSRV_FD + 1)
,然后进行一系列寄存器的恢复操作,最后跳转到__afl_store
标签处,继续进行 fuzzing 过程。- [[#__afl_store]] 代码覆盖记录
- 表示在子进程中执行。在子进程中,关闭文件描述符
__afl_die退出
"__afl_die:\n" |
__afl_setup_abort配置失败
"__afl_setup_abort:\n" |
变量申请
|
main
- 函数是程序的主入口,它读取环境变量和命令行参数,然后执行相应的操作,包括调用上述函数来进行插桩处理。
- 获取环境变量
__AFL_CLANG_MODE
- clang_mode
- 获取环境变量
AFL_QUIET
设置 be_quiet 不打印信息 - [[#gettimeofday时间加进程pid构造随机种子]]
- [[#edit_params设置参数]]
- 获取环境变量
AFL_INST_RATIO
- inst_ratio_str 可以设置指令比例 比如 AFL_INST_RATIO=30 设置 inst_ratio
- 获取环境变量
__AFL_AS_LOOPCHECK
- 之后设置他
AS_LOOP_ENV_VAR 1
- 这段代码是为了防止在执行
as
(GNU as汇编器)时发生无限循环,以提高程序的稳定性和安全性。
- 之后设置他
- 如果设置
AFL_USE_ASAN
AFL_USE_MSAN
sanitizer = 1; inst_ratio /= 3; 指令随机变1/3
- [[#add_instrumentation]]
- 插入汇编指令
- 子进程执行
as -I .. -I . --64 -o /tmp/GHash.o /tmp/.afl-125229-1690800161.s
- 父进程等待子进程执行完毕
if (waitpid(pid, &status, 0) <= 0) PFATAL("waitpid() failed");
- 设置环境变量
AFL_KEEP_ASSEMBLY
保存这个/tmp/.afl-133075-1690873059.s
- 否则
unlink
删除文件系统中的文件/tmp/.afl-133075-1690873059.s
- 否则
afl-fast-clang
- 因为AFL对于上述通过
afl-gcc
来插桩这种做法已经属于不建议,并提供了更好的工具afl-clang-fast,通过llvm pass来插桩。
编译
- [[总结_llvm_clang命令_学习_llvm编译]]
# use llvm16 error |
测试命令
cd /home/wutang/Desktop/google_afl |
find_obj
- 该函数用于查找 AFL 的运行时库文件
afl-llvm-rt.o
路径。它首先尝试从环境变量AFL_PATH
中获取路径,然后尝试从可执行文件的路径中获取路径,最后尝试使用默认路径AFL_PATH
来查找运行时库文件。 - 获取环境变量
AFL_PATH
寻找afl-llvm-rt.o
文件存储路径到 obj_path - 若没有
AFL_PATH
找argv[0]
寻找afl-llvm-rt.o
文件存储路径到 obj_path
edit_params
- 该函数用于编辑编译器参数,将必要的参数添加到
cc_params
数组中。它会根据参数、环境变量和其他设置来添加编译器参数,如 ASAN(AddressSanitizer)和 MSAN(MemorySanitizer)的参数、优化参数、路径等。 argv[0]
判断- 如果是
afl-clang-fast++
获取环境变量AFL_CXX
- 否则获取环境变量
AFL_CC
- 如果是
- 如果编译时设置了
USE_TRACE_PC
两种方式- USE_TRACE_PC 添加参数
-fsanitize-coverage=trace-pc-guard
- 此时非android 指定
-mllvm -sanitizer-coverage-block-threshold=0
- LLVM replaced “-sanitizer-coverage-block-threshold” with “
--sanitizer-coverage-level
“ in certain commit (probably in LLVM 5.0) and make it default to 0.
- LLVM replaced “-sanitizer-coverage-block-threshold” with “
- 此时非android 指定
- 非USE_TRACE_PC 否则使用 添加llvm 加载pass参数的方式
-Xclang -load -Xclang /home/wutang/Desktop/google_afl/afl-llvm-pass.so
- USE_TRACE_PC 添加参数
- 接着过滤参数
- 设置bit_mode=32 如果遇到参数如下
-m32
armv7a-linux-androideabi
- 设置 bit_mode = 64 如果遇到参数如下
-m64
- 设置 x_set=1 如果遇到参数如下
-x
后续设置-x none
-fsanitize=address
or-fsanitize=memory
- 设置 asan_set = 1
FORTIFY_SOURCE
- 设置 fortify_set = 1
- continue 如果遇到参数如下
-Wl,-z,defs
- 或者
-Wl,--no-undefined
- 设置bit_mode=32 如果遇到参数如下
- 获取环境变量
AFL_HARDEN
-fstack-protector-all
- 如果没设置了 fortify_set 设置
-D_FORTIFY_SOURCE=2
- asan和 msan 和 AFL_HARDEN 不能同时指定
- 获取环境变量
AFL_USE_ASAN
- 设置
-U_FORTIFY_SOURCE
-fsanitize=address
- 设置
- 获取环境变量
AFL_USE_MSAN
- 设置
-U_FORTIFY_SOURCE
-fsanitize=memory
- 设置
- 获取环境变量
- 如果没设置环境变量
AFL_DONT_OPTIMIZE
- 设置
-g
-O3
-funroll-loops
- 设置
- 如果设置了环境变量
AFL_NO_BUILTIN
- “-fno-builtin-strcmp”, “-fno-builtin-strncmp”, “-fno-builtin-strcasecmp”, “-fno-builtin-strncasecmp”, “-fno-builtin-memcmp” 比afl-gcc少了两个
- 设置参数
-D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
- 通过宏添加代码
-D__AFL_LOOP(_A)=({ static volatile char *_B __attribute__((used)); _B = (char*)"##SIG_AFL_PERSISTENT##"; __attribute__((visibility("default"))) int _L(unsigned int) __asm__("__afl_persistent_loop"); _L(_A); }) -D__AFL_INIT()=do { static volatile char *_A __attribute__((used)); _A = (char*)"##SIG_AFL_DEFER_FORKSRV##"; __attribute__((visibility("default"))) void _I(void) __asm__("__afl_manual_init"); _I(); } while (0) |
- 判断-m32 -m64 架构添加对应架构
afl-llvm-rt-XX.o
- 添加代码到目标下,这里是
/home/wutang/Desktop/google_afl/afl-llvm-rt.o
- 添加代码到目标下,这里是
- 最后添加NULL
非USE_TRACE_PC方式-使用llvmpass
cd /home/wutang/Desktop/google_afl |
- afl-llvm-pass.so
AFLCoverage::runOnModule
- 获取环境变量
AFL_INST_RATIO
- 作为 inst_ratio
- 创建全局变量
__afl_area_ptr
AFLMapPtr__afl_prev_loc
AFLPrevLoc- 分别用于指向 AFL 的共享内存区域和上一个位置。
- 2的16次方=65536 取随机数
unsigned int cur_loc = AFL_R(MAP_SIZE);
- 将之前加载的值
PrevLoc
进行零扩展,转换为一个更大的整数类型。这里使用getInt32Ty()
是为了将之前加载的值转换为 32 位整数类型。 - 遍历函数和基本块,为每个基本块插入 AFL 插桩代码。通过计算随机数和插桩比例,确定是否要在当前基本块插入插桩代码。插桩代码会更新覆盖地图(bitmap)。
- 输出插桩结果,显示已插桩的基本块数量和插桩模式等信息
- 总的来说就是通过遍历每个基本块,向其中插入实现了如下伪代码功能的instruction ir来进行插桩。
cur_location = <COMPILE_TIME_RANDOM>; |
test-instr.c插桩前后IR对比
- 将pass加入clang passmanager 即可调试pass
|
- 插桩前的ir
; ModuleID = 'nopt_test-instr.ll' |
- 插桩后的ir
; ModuleID = 'm2r_nopt_test-instr.ll'\ |
内部变量 __afl_prev_loc&__afl_area_ptr
- 参考 [[#afl-llvm-rt]] 实现
afl-llvm-rt
- AFL LLVM_Mode中存在着三个特殊的功能。这三个功能的源码位于afl-llvm-rt.o.c中。
- 被 [[#afl-fast-clang]] 依赖编译
解释内部变量
__afl_prev_loc
__thread u32 __afl_prev_loc; |
- 在永久模式下设置
__afl_prev_loc=0
__afl_area_initial
u8 __afl_area_initial[MAP_SIZE]; |
__afl_map_shm()
会设置__afl_area_ptr
- 永久模式也会设置
deferred instrumentation
延迟初始化配置+代码
afl-fuzz模式 |
- AFL会尝试通过仅执行一次目标二进制文件来优化性能。它会暂停控制流,然后复制该“主”进程以持续提供fuzzer的目标。该功能在某些情况下可以减少操作系统、链接与libc内部执行程序的成本。
- 选好位置后,将下述代码添加到该位置上,之后使用afl-clang-fast重新编译代码即可
- 主动添加代码
|
__afl_manual_init
|
- 所以,整个宏定义的目的是在满足条件
__AFL_HAVE_MANUAL_CONTROL
时,调用_I
函数,实际上就是调用__afl_manual_init
函数。 __AFL_INIT()
内部调用__afl_manual_init
函数。该函数的源代码如下
// google_afl/llvm_mode/afl-llvm-rt.o.c |
__afl_manual_init
用于在启用了延迟 fork 服务器模式(deferred forkserver mode)时进行初始化操作- 如果还没有被初始化,就初始化共享内存,然后开始执行forkserver,然后设置init_done为1。
__afl_map_shm
- 简单的通过读取环境变量
SHM_ENV_VAR
来获取共享内存,然后将地址赋值给__afl_area_ptr
。否则,默认的__afl_area_ptr
指向的是一个数组。
__afl_start_forkserver
static void __afl_start_forkserver(void) { |
可能失灵初始化
In particular, the program will probably malfunction if you select |
persistent mode
- 上面我们其实已经介绍过persistent mode的一些特点了,那就是它并不是通过fork出子进程去进行fuzz的,而是认为当前我们正在fuzz的API是无状态的,当API重置后,一个长期活跃的进程就可以被重复使用,这样可以消除重复执行fork函数以及OS相关所需要的开销。
- 比llvm ‘s libfuzzer 弱,但比fork强
持久模式配置
- afl-clang-fast编译
- 循环次数不能设置过大,因为较小的循环次数可以将内存泄漏和类似故障的影响降到最低。所以循环次数设置成1000是个不错的选择。
while (__AFL_LOOP(1000)) { |
__afl_auto_init
- 默认持久模式下
|
- 一开始就调用 [[#__afl_manual_init]]
__afl_persistent_loop
/* A simplified persistent mode handler, used as explained in README.llvm. */ |
while (__AFL_LOOP(1000)) { |
- 过程
- 首先在main函数之前读取共享内容,然后以当前进程为fork server,去和AFL fuzz通信。
- 当AFL fuzz通知进行一次fuzz,由于此时child_stopped为0,则fork server先fork出一个子进程。
- 这个子进程会很快执行到
__AFL_LOOP
包围的代码,因为是第一次执行loop,所以会先清空__afl_area_ptr
和设置__afl_prev_loc
为0,并向共享内存的第一个元素写一个值,然后设置循环次数1000,随后返回1,此时while(__AFL_LOOP)
满足条件,于是执行一次fuzzAPI。 - 然后因为是while循环,会再次进入
__AFL_LOOP
里,此时将循环次数减一,变成999,然后发出信号SIGSTOP
来让当前进程暂停,因为我们设置了WUNTRACED
,所以waitpid
函数就会返回,fork server将继续执行。 - fork server在收到
SIGSTOP
信号后就知道fuzzAPI已经被成功执行结束了,就设置child_stopped为1,并告知AFL fuzz - 然后当AFL fuzz通知再进行一次fuzz的时候,fork server将不再需要去fork出一个新的子进程去进行fuzz,只需要恢复之前的子进程继续执行,并设置
child_stopped
为0 - 因为我们是相当于重新执行一次程序,所以将
__afl_prev_loc
设置为0,并向共享内存的第一个元素写一个值,随后直接返回1,此时while(__AFL_LOOP)
满足条件,于是执行一次fuzzAPI,然后因为是while循环,会再次进入__AFL_LOOP
里,再次减少一次循环次数变成998,并发出信号暂停。 - 上述过程重复执行,直到第1000次执行时,先恢复执行,然后返回1,然后执行一次fuzzAPI,然后因为是while循环,会再次进入
__AFL_LOOP
里,再次减少一次循环次数变成0,此时循环次数cnt已经被减到0,就不会再发出信号暂停子进程,而是设置__afl_area_ptr
指向一个无关数组__afl_area_initial
,随后将子进程执行到结束。 - 这是因为程序依然会向后执行并触发到instrument,这会向
__afl_area_ptr
里写值,但是此时我们其实并没有执行fuzzAPI
,我们并不想向共享内存里写值,于是将其指向一个无关数组,随意写值。同理,在deferred instrumentation模式里,在执行__afl_manual_init
之前,也是向无关数组里写值,因为我们将fork点手动设置,就代表在这个fork点之前的path我们并不关心。 - 重新整理一下上面的逻辑
- loop第一次执行的时候,会初始化,然后返回1,执行一次fuzzAPI,然后cnt会减到999,然后抛出信号暂停子进程。
- loop第二次执行的时候,恢复执行,清空一些值,然后返回1,执行一次fuzzAPI,然后cnt会减到998,然后抛出信号暂停子进程。
- loop第1000次执行的时候,恢复执行,清空一些值,然后返回1,执行一次fuzzAPI,然后cnt会减到0,然后就设置指向无关数组,返回0,while循环结束,程序也将执行结束。
- 此时fork server将不再收到SIGSTOP信号,于是child_stopped仍为0。
- 所以当AFL fuzz通知fork server再进行一次fuzz的时候,由于此时child_stopped为0,则fork server会先fork出一个子进程,然后后续过程和之前一样了
trace-pc-guard mode-测试
使用trace-pc
- 要使用这个功能,需要先通过
AFL_TRACE_PC=1
来定义DUSE_TRACE_PC
宏,从而在执行afl-clang-fast的时候传入-fsanitize-coverage=trace-pc-guard
参数,来开启这个功能,和之前我们的插桩不同,开启了这个功能之后,我们不再是仅仅只对每个基本块插桩,而是对每条edge都进行了插桩。
export LLVM_CONFIG="llvm-config-11" |
ifdef AFL_TRACE_PC |
- 跟
AFL_INST_RATIO
不兼容
|
-fsanitize-coverage=trace-pc-guard
官网介绍
- With
-fsanitize-coverage=trace-pc-guard
the compiler will insert the following code on every edge:
__sanitizer_cov_trace_pc_guard(&guard_variable) |
- Every edge will have its own guard_variable (uint32_t).
- The compiler will also insert calls to a module constructor:
// The guards are [start, stop). |
- With an additional
...=trace-pc,indirect-calls
flag__sanitizer_cov_trace_pc_indirect(void *callee)
will be inserted on every indirect call.
afl实现方法
/* Init callback. Populates instrumentation IDs. Note that we're using |
- 这个函数在程序初始化时被调用,用于初始化代码覆盖率的相关信息。它会根据一些配置和环境变量来设置代码覆盖率的初始化情况。具体来说:
start
和stop
参数表示一个范围,用于初始化代码覆盖率数据的位图。这个范围通常涵盖了可执行程序的所有基本块(basic block)。- 首先,它会尝试从环境变量中获取
AFL_INST_RATIO
,表示代码覆盖率的比率,默认为 100。如果设置了这个值,则按照比率确定哪些基本块需要被标记为被执行。 - 如果比率设置无效(不在 1-100 范围内),就会输出错误信息并中止程序。
- 接着,它会确保范围内的第一个元素被设置,以避免重复调用。然后,它会根据比率随机选择基本块进行标记,从而在执行过程中收集代码覆盖率信息。
/* The following stuff deals with supporting -fsanitize-coverage=trace-pc-guard. |
- 这个函数是一个回调函数,会在程序执行过程中的每个边界(edge)触发时被调用。边界指的是基本块(basic block)之间的跳转点。它的作用是将被执行的边界的计数器递增,从而收集代码覆盖率信息。
guard
参数表示一个边界的标识符,会用来索引代码覆盖率数据的位图,将对应的位设置为被访问。 - 如果我们的edge足够多,而
MAP_SIZE
不够大,就有可能重复,而这个加一是因为我们会把0当成一个特殊的值,其代表对这个edge不进行插桩。
afl-fuzz
调试配置
echo "">/home/wutang/Desktop/fuzz_test/fuzz_input/1 |
字典编写规则
// 字典文件 |
初始配置
所有流程
- 获取
AFL_BENCH_JUST_ONE
- 设置 exit_1
- 时间作为随机种子
- 解析参数
-i
设置 输入为 in_dir- 如果in_dir 是
-
设置in_place_resume
=1 - If you wish to start a new session, remove or rename the directory manually, or specify a different output location for this job. To resume the old session, put ‘-‘ as the input directory in the command line (-i -) and try again.
- 如果in_dir 是
-o
设置 输出为 out_dir-M
用于存储同步标识符.master sync ID
- 可能存在
123:456
- 设置 master_id master_max
- 用于存储同步标识符, 设置 sync_id
force_deterministic=1
表示后续的 fuzzing 进程将被强制设置为确定性模式,这是为了确保同步的正确性。- 与
-S
不能同时指定
- 可能存在
-S
用于存储同步标识符Slave fuzzer
- 用于存储同步标识符, 设置 sync_id
- [[#fix_up_sync]] 相关
skip_deterministic=1
不进行确定性fuzz 直接到[[#RANDOM HAVOC阶段]]- 设置
use_splicing=1
Recombine input files 重组输入
-n
dumb mode模式Dumb" fork server
fork server 不会执行目标程序的初始化代码,而是直接执行测试用例。这样可以快速启动 fork server,但可能会导致目标程序在没有正确初始化的情况下运行,因此可能会遗漏一些重要的程序行为。- 这是因为在某些情况下,启动目标程序的初始化代码可能会干扰到 fuzzing 进程的运行,导致无法正常 fuzzing。
- 例如,
某些目标程序的初始化可能会导致程序崩溃或无法正常退出,从而影响到 fuzzing 的效果
。在这种情况下,使用 “dumb” fork server 可以绕过这些问题,快速执行测试用例。 - 但需要注意,使用 “dumb” fork server 模式
可能会忽略一些初始化和环境设置,导致无法模拟真实的运行环境
。
- 例如,
-d
- 设置确定模式
skip_deterministic=1
直接去[[#RANDOM HAVOC阶段]] - 设置
use_splicing=1
Recombine input files 重组输入
- 设置确定模式
-x
设置字典 extras_dir 还要键值对?-f
可设置目标文件 或者用@@
指定预料-t
设置exec_tmout-C
设置FAULT_CRASH
非一般的crash方式?
- [[#setup_signal_handlers]]
- [[#check_asan_opts]]
- [[#fix_up_sync]]
AFL_NO_FORKSRV
设置 no_forkserverAFL_NO_CPU_RED
设置 no_cpu_meter_red- 将禁用 CPU 利用率的自动调整
AFL_NO_ARITH
设置 no_arith- 将禁用算术变异策略 [[#ARITHMETIC INC/DEC阶段]]
AFL_SHUFFLE_QUEUE
设置 shuffle_queue- 将启用测试用例队列的随机洗牌
AFL_FAST_CAL
设置 fast_cal- 这里涉及 [[#u8 calibrate_case(char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue)]]
- 默认是3次stage_max
AFL_HANG_TMOUT
设置 hang_tmout- 将设置程序运行的超时时间,以防止程序进入无限循环或挂起
AFL_PRELOAD
- 将设置
LD_PRELOAD
和DYLD_INSERT_LIBRARIES
环境变量,用于加载指定的共享库。
- 将设置
- [[#save_cmdline]] 到 orig_cmdline
- [[#fix_up_banner]] 更新程序的 banner 信息,use_banner 文件路径
- [[#check_if_tty]] 检查是否在tty终端上面运行。
- [[#get_core_count]] 计数logical CPU cores,多少cpu cores/正在使用多少
- [[#bind_to_free_cpu]] 找到未被绑定到特定CPU核心的空闲CPU核心,然后将程序绑定到其中一个空闲核心上,以优化性能
- [[#check_crash_handling]] 其主要目的是检查系统的崩溃处理设置,以确保在程序运行过程中产生的崩溃不会被外部崩溃报告工具处理,从而影响程序运行和崩溃处理的准确性。
- [[#check_cpu_governor]] 检查系统的 CPU 调频策略(CPU Scaling Governor)是否对 Fuzzing 运行有不良影响
- [[#setup_post]] 加载后处理器(post-processor)库,并检测后处理器是否成功加载和正常运行,任意语言包括go编译的so
- [[#setup_shm]] 主要功能是配置共享内存和初始化
virgin_bits
数组。这些操作在程序启动时进行, 映射内存到本地 - [[#init_count_class16]] 定义了一个处理执行计数的函数和两个查找表
count_class_lookup8 count_class_lookup16
,用于对执行计数进行分类。- 给trace_bits 用的
- [[#setup_dirs_fds]] 各种需要的路径创建
- [[#read_testcases]] 从输入文件夹中读取所有文件,然后将它们排队进行测试。
- [[#add_to_queue(u8 *fname, u32 len, u8 passed_det)]] 将新的测试用例添加到队列中,以供后续进行测试。
- [[#load_auto]] load自动生成的提取出来的词典token
- [[#maybe_add_auto(u8 *mem, u32 len)]] 是向自动生成的字典中添加新的条目(字典项)
- [[#pivot_inputs]] 函数负责将输入测试用例文件重新命名、链接(或拷贝)到输出目录中,并在需要的情况下更新文件名以及关联的信息。这有助于模糊测试过程的组织和恢复
- [[#load_extras]] 这段代码负责加载额外的字典文件(extra dictionary)并对其进行排序,使其按照字典条目的大小进行排列
- [[#find_timeout]] 寻找之前会话中的超时设置,并将其应用于当前会话
- [[#detect_file_args]] 这个函数其实就是识别参数里面有没有
@@
,如果有就替换为out_dir/.cur_input
,如果没有就返回 - [[#setup_stdio_file]] 如果out_file为NULL , 如果没有使用-f && 或者没有指定
@@
,删除out_dir/.cur_input
- [[#check_binary]] 它检查二进制文件是否存在、是否为可执行文件、是否为 ELF 或 Mach-O 格式,是否包含 AFL 仪表插装标记,以及其他相关特征。
- get_cur_time 记录时间
- [[#perform_dry_run]] 尝试去fuzz程序,计算是否符合要求等
- calibrate_case
- init_forkserver
- write_to_testcase
- run_target
- count_bytes
- hash32
- [[#has_new_bits(u8 *virgin_map)]]
- update_bitmap_score
- check_map_coverage
- calibrate_case
- [[#cull_queue]] 该函数的目标是在模糊测试期间调整队列,以便更多地探索未被完全探索的路径
- [[#show_init_stats]] 这段代码用于在处理输入目录后显示初始化统计信息和一些警告
- [[#find_start_position]] resume模式下有意义,其目的是在恢复模式下寻找一个合适的队列起始位置
- [[#void write_stats_file(double bitmap_cvg, double stability, double eps)]] 数负责将各项统计信息写入名为
fuzzer_stats
的文件中,这些信息用于监控 fuzzer 的性能和状态。这可以帮助用户了解 fuzzer 的执行情况、性能和资源使用情况 - [[#save_auto]]目的是将用于自动筛选的额外测试用例保存到磁盘中。
- sleep 4秒
- 进入循环-fuzzing [[#fuzz-主要流程]]
setup_signal_handlers
- 函数负责配置各种信号的信号处理程序,这些信号可能在 AFL fuzzer 运行过程中出现。信号处理程序是在进程接收到特定信号时执行的函数。通过设置适当的信号处理程序,程序可以处理中断、超时和其他可能影响其执行的情况
/* Handle stop signal (Ctrl-C, etc). */ |
- 这里handle_timeout往后看 [[#init_forkserver设置子进程超时]]
EXP_ST void setup_signal_handlers(void) { |
check_asan_opts
- 这个函数的目的是确保在使用 ASan 和 MSan 运行 AFL-Fuzz 时,相应的运行选项被正确地设置,以便在检测内存错误时能够正常运行。
- 读取环境变量ASAN_OPTIONS和MSAN_OPTIONS,做一些检查
/* Check ASAN options. */ |
fix_up_sync
-S
指定了sync_id-M
指定了sync_id- 设置
sync_dir
的值为 -o的那个out_dir
/home/wutang/Desktop/fuzz_test/fuzz_output
- 设置
out_dir
的值为 -o的那个out_dir/sync_id
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1
- 设置
save_cmdline
- 拷贝字符串 orig_cmdline
static uint8_t *orig_cmdline; /* Original command line * |
fix_up_banner
- 它的主要作用是在启动 AFL-Fuzz 时更新程序的 banner 信息,即程序运行时在终端上显示的版本信息和标题。它接受一个参数,即目标文件的路径,然后根据该文件的内容更新 banner 信息
check_if_tty
- 检查是否在tty终端上面运行。
- 启动 AFL-Fuzz 时更新程序的 banner 信息,即程序运行时在终端上显示的版本信息和标题
- 读取环境变量
AFL_NO_UI
的值,如果为真,则设置not_on_tty为1,并返回 ioctl(1, TIOCGWINSZ, &ws)
通过ioctl来读取window size,如果报错为ENOTTY,则代表当前不在一个tty终端运行,设置not_on_tty
get_core_count
- 计数logical CPU cores, 多少cpu cores/正在使用多少
- ``c_linux_cpu
bind_to_free_cpu
- 找到未被绑定到特定CPU核心的空闲CPU核心,然后将程序绑定到其中一个空闲核心上,以优化性能
- 函数会尝试打开
/proc
目录,然后遍历其中的所有进程。目的是扫描所有进程的/proc/<pid>/status
文件,检查进程是否绑定到特定的 CPU 核心。这样的检查通过读取进程的Cpus_allowed_list
字段来实现
- 函数会尝试打开
遍历进程 |
check_crash_handling
- 其主要目的是检查系统的崩溃处理设置,以确保在程序运行过程中产生的崩溃不会被外部崩溃报告工具处理,从而影响程序运行和崩溃处理的准确性。
- 首先,代码在 Apple 系统中使用命令
launchctl list
检查是否启用了外部的崩溃报告工具(.ReportCrash
)。如果启用了,说明系统将崩溃信息发送到外部报告工具,而不是直接让 fuzzer 通过标准的waitpid()
API 获取崩溃信息。在这种情况下,函数会输出警告信息,提示用户需要禁用该功能。
- 首先,代码在 Apple 系统中使用命令
SL=/System/Library; PL=com.apple.ReportCrash |
- 如果不是 Apple 系统,则在 Linux 系统中,代码会读取
/proc/sys/kernel/core_pattern
文件,检查其中的核心转储模式设置。如果核心转储模式以|
符号开头,说明系统将核心转储信息通过管道传递给了外部工具。这也会导致崩溃信息的延迟传递,从而影响 fuzzer 对崩溃的处理。在这种情况下,函数会输出警告信息,提示用户需要修改核心转储模式- 设置
echo core >/proc/sys/kernel/core_pattern
- 设置
check_cpu_governor
- 检查系统的 CPU 调频策略(CPU Scaling Governor)是否对 Fuzzing 运行有不良影响。Fuzzing 运行通常需要稳定的 CPU 运行频率以保证稳定的性能,而某些 CPU 调频策略可能会导致性能下降,从而影响 Fuzzing 运行的准确性和效率。
- 检查
AFL_SKIP_CPUFREQ
不检查 - 首先,代码尝试打开文件
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
,读取系统当前的 CPU 调频策略。如果无法打开该文件,说明系统可能不支持 CPU 调频策略设置,或者文件路径不正确,那么函数直接返回。
setup_post
- 加载后处理器(post-processor)库,并检测后处理器是否成功加载和正常运行,任意语言包括go编译的so。在 Fuzzing 运行结束后,后处理器可以用于对 Fuzzing 的结果进行进一步的处理、分析或报告。
- Allows AFL_POST_LIBRARY postprocessors to be written in arbitrary languages that don’t have C / .so bindings. Includes examples in Go.
- 设置宏
AFL_POST_LIBRARY=xxx.so
- 里面包含
afl_postprocess
调用post_handler("hello", &tlen);
- [[#u8 common_fuzz_stuff(char **argv, u8 *out_buf, u32 len)]]
- 里面包含
setup_shm
- 主要功能是配置共享内存和初始化
virgin_bits
数组。这些操作在程序启动时进行, 映射内存到本地
/* Get rid of shared memory (atexit handler). */ |
init_count_class16
- 定义了一个处理执行计数的函数和两个查找表,用于对执行计数进行分类。
- 这些查找表的主要目的是在进行后续的执行计数处理时,能够将执行计数转换为更具有统计意义的分类值,从而减少内存占用和计算成本。这在大规模的测试用例集中,对于执行计数的统计分析非常有用。
- count_class_lookup8
- 对于0,映射到分类值0。
- 对于1,映射到分类值1。
- 对于2,映射到分类值2。
- 对于3,映射到分类值4。
- 对于4到7,映射到分类值8。
- 对于8到15,映射到分类值16。
- 对于16到31,映射到分类值32。
- 对于32到127,映射到分类值64。
- 对于128到255,映射到分类值128。
- count_class_lookup16
- 这是一个长度为65536的数组,用于将两个字节(16位)的执行计数组合成一个更粗粒度的分类值。它通过对每个字节在 count_class_lookup8 查找表中的分类值进行位运算来实现。
- 这其实是因为
trace_bits
是用一个字节来记录是否到达这个路径,和这个路径被命中了多少次的,而这个次数在0-255之间,但比如一个循环,它循环5次和循环6次可能是完全一样的效果,为了避免被当成不同的路径,或者说尽可能减少因为命中次数导致的
setup_dirs_fds
- 各种需要的路径创建
maybe_delete_out_dir
- 这段代码主要用于管理和清理输出目录,以确保能够正常开始一个新的会话或者继续之前的会话。
- 读取
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/fuzzer_stats
- 首先,代码检查输出目录是否被其他实例的 afl-fuzz 进程使用。如果目录被锁定,会给出相应的错误信息并终
char * out_dir; |
- 如果没设置
-i -
,in_place_resume=0
&& 已经跑了last_update - start_time > OUTPUT_GRACE * 60
25分钟了- 退出不能继续跑
- 如果设置
-i -
,in_place_resume=1
输出Output directory exists, will attempt session resume.
函数主程序
- 准备输出文件夹和fd
- 如果
sync_id
存在,且创建sync_dir
文件夹,设置权限为0700
(读写执行) /home/wutang/Desktop/fuzz_test/fuzz_output
- 如果报错,且errno不是EEXIST,则抛出异常。
- 创建
out_dir
,设置权限为0700(读写执行)/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1
- 创建失败
- 如果报错,且errno不是EEXIST,则抛出异常。
- 是EEXIST 执行函数 [[#maybe_delete_out_dir]] - 如果创建成功
- 如果设置
-i -
,in_place_resume=1
, 就抛出异常 Resume attempted but old output directory not found out_dir_fd = open(out_dir, O_RDONLY)
以只读模式打开这个文件,并返回文件句柄out_dir_fd
- 如果没有定义宏
__sun
- 如果打开out_dir失败,或者为out_dir通过flock建立互斥锁定失败,就抛出异常
Unable to flock() output directory.
- 如果设置
- 建立queue文件夹
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue
- 创建
out_dir/queue
文件夹,设置权限为0700 - 创建
out_dir/queue/.state/
,设置权限为0700,该文件夹主要保存用于session resume和related tasks的queue metadata - 创建
out_dir/queue/.state/deterministic_done/
,设置权限为0700,该文件夹标记过去经历过deterministic fuzzing的queue entries。 - 创建
out_dir/queue/.state/auto_extras/
,设置权限为0700,Directory with the auto-selected dictionary entries. - 创建
out_dir/queue/.state/redundant_edges/
,设置权限为0700,保存当前被认为是多余的路径集合 - 创建
out_dir/queue/.state/variable_behavior/
,设置权限为0700,The set of paths showing variable behavior. - 如果
sync_id
存在- 创建
out_dir/.synced/
,设置权限为0700, 同步文件夹,用于跟踪cooperating fuzzers.
- 创建
- 建立crashes文件夹
- 创建
out_dir/crashes
文件夹,设置权限为0700,用于记录crashes
- 创建
- 建立hangs文件夹
- 创建
out_dir/hangs
文件夹,设置权限为0700,用于记录hangs
- 创建
- 通常有用的文件描述符
dev_null_fd = open("/dev/null", O_RDWR);
以读写模式打开/dev/null
dev_urandom_fd = open("/dev/urandom", O_RDONLY);
,以只读模式打开/dev/urandom
- 建立Gnuplot输出文件夹
fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
- 以只写方式打开
out_dir/plot_data
文件,如果文件不存在,就创建,并获取句柄 plot_file = fdopen(fd, "w");
根据句柄得到FILE* plot_file- 使用 `fdopen()` 函数将文件描述符 `fd` 关联到一个文件流,以便后续进行文件操作。参数 "w" 表示以写入模式打开文件流。
- 向其中写入
# unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec\n
- 如果
read_testcases
- 从输入文件夹中读取所有文件,然后将它们排队进行测试。
- 尝试访问
in_dir/queue
文件夹,如果存在就重新设置in_dir
为in_dir/queue
- Auto-detect non-in-place resumption attempts.
scandir
扫描in_dir,并将结果保存在struct dirent **nl
里- 不使用readdir,因为测试用例的顺序将随机地有些变化,并且将难以控制。
- 如果设置宏
AFL_SHUFFLE_QUEUE
,shuffle_queue
的值为真,且nl_cnt大于1- 则
shuffle_ptrs((void **) nl, nl_cnt)
,字面意思上就是重排nl里的指针的位置。随机化
- 则
- 遍历
nl,nl[i]->d_name
的值为input文件夹下的文件名字符串u8 *fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name);
u8 *dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name);
- 通过
lstat(fn, &st) && !S_ISREG(st.st_mode)
, 文件属性过滤掉.
和..
这样的regular文件,并检查文件大小,如果文件大小大于MAX_FILE,默认是1024*1024
字节,即1M find . -type f -size +1M
- 通过access检查
in_dir/.state/deterministic_done/nl[i]->d_name
是否存在,这应该是为了用在resume恢复扫描使用 - 如果存在就设置
passed_det
为1 - 这个检查是用来判断是否这个entry已完成
deterministic fuzzing
。在恢复异常终止的扫描时,我们不想重复deterministic fuzzing
,因为这将毫无意义,而且可能非常耗时 add_to_queue(fn, st.st_size, passed_det);
- [[#add_to_queue(u8 *fname, u32 len, u8 passed_det)]]
- 如果
queued_paths
为0,则代表输入文件夹为0,抛出异常 - 设置
last_path_time
为0 queued_at_start
的值设置为queued_paths- Total number of initial inputs
- 尝试访问
add_to_queue(u8 *fname, u32 len, u8 passed_det)
- 将新的测试用例添加到队列中,以供后续进行测试。
- 形成一个链表
queue_top
- 会添加测试用例 然后
queued_paths++
- 形成一个链表
queue_entry
是一个链表数据结构- 先通过calloc动态分配一个
queue_entry
结构体,并 初始化其fname为文件名fn,len为文件大小,depth为cur_depth + 1, passed_det为传递进来的 passed_det
q->fname = fname; |
- 如果
q->depth > max_depth
,则设置max_depth为q->depth
- 如果queue_top不为空,则设置
queue_top->next为q,queue_top = q;
,否则q_prev100 = queue = queue_top = q;
static struct queue_entry *queue, /* Fuzzing queue (linked list)*/ |
- queue计数器 queued_paths 和待fuzz的样例计数器
pending_not_fuzzed
加一 - cycles_wo_finds 设置为0
Cycles without any new paths
- 如果
queued_paths % 100
得到0,则设置q_prev100->next_100 = q; q_prev100 = q;
- 设置
last_path_time
为当前时间。
load_auto
- load自动生成的提取出来的词典token
- 遍历循环从i等于0到
USE_AUTO_EXTRAS
,默认50
- 以只读模式尝试打开文件名为
alloc_printf("%s/.state/auto_extras/auto_%06u", in_dir, i)
的文件- `/home/wutang/Desktop/fuzz_test/fuzz_input/.state/auto_extras/auto_000000`
- 如果打开失败,则结束
- 如果打开成功,则从fd读取最多
MAX_AUTO_EXTRA+1=32+1
个字节到tmp数组里,这是单个auto extra文件的最大大小,读取出的长度保存到len里。- `3<= len <= 32` 会 `maybe_add_auto(tmp, );` [[#maybe_add_auto(u8 *mem, u32 len)]]
- 遍历循环从i等于0到
maybe_add_auto(u8 *mem, u32 len)
- 如果用户设置了
MAX_AUTO_EXTRAS
或者USE_AUTO_EXTRAS
为0,则直接返回。 - 如果不允许自动生成字典项,则直接返回
/* Allow users to specify that they don't want auto dictionaries. */ |
- 跳过连续相同的字节
- 循环遍历i从1到len,将
tmp[0]
和tmp[i]
异或,如果相同,则结束循环。 - 如果结束时
i=0
,即tmp[0]
和tmp[1]
就相同,就直接返回。这里我推断tmp应该是从小到大排序的字节流。
- 循环遍历i从1到len,将
/* Skip runs of identical bytes. */ |
- 根据长度,检查字节序列是否与内置的有趣值相匹配,如果匹配则跳过
- 如果len的长度为2,就和interesting_16数组里的元素比较,如果和其中某一个相同,就直接return
- 如果len的长度为4,就和interesting_32数组里的元素比较,如果和其中某一个相同,就直接return
if (i == len) return; |
- 检查待添加的自动生成字典项是否与已经存在的字典项相匹配,以避免重复添加相同的字典项
- 这段代码通过优化策略,避免了对所有已存在的字典项进行不必要的比较,而是通过比较待添加字典项的长度来减少实际比较的次数,提高了性能。这样可以有效地防止将相同的字典项重复添加到字典中
/* Reject anything that matches existing extras. Do a case-insensitive |
- 标记自动生成字典项已更改
auto_changed = 1;
:标记自动生成的字典项已经发生更改。- 遍历
a_extras
数组,比较memcmp_nocase(a_extras[i].data, mem, len)
, 如果相同,就将其hit_cnt
值加一,这是代表在语料中被use的次数,然后跳转到sort_a_extras
struct extra_data { |
- 将新的自动生成的字典项添加到
a_extras[]
数组中,管理这个数组的容量
/* At this point, looks like we're dealing with a new entry. So, let's |
- 标签
sort_a_extras
对a_extras[]
数组中的自动生成字典项进行排序
static int compare_extras_use_d(const void* p1, const void* p2) { |
pivot_inputs
- 函数负责将输入测试用例文件重新命名、链接(或拷贝)到输出目录中,并在需要的情况下更新文件名以及关联的信息。这有助于模糊测试过程的组织和恢复
- 逻辑上说这个函数就是为
inputdir
里的testcase
,在output dir
里创建hard link
- 初始化
id=0
- 依次遍历queue里的queue_entry
u8 *nfn, *rsl = strrchr(q->fname, '/');
q->fname /home/wutang/Desktop/fuzz_test/fuzz_input/1
if (!rsl) rsl = q->fname; else rsl++;
- rsl 变成
1
- rsl 变成
- 将rsl的前三个字节和
id:
进行比较- 如果相等,则设置
resuming_fuzz
为1,然后做一些恢复操作,不叙述。- 赋值 orig_id 为
id:123
的123 - …
- 赋值 orig_id 为
- 如果不相等
- 在rsl里寻找
,orig:
子串,如果找到了,将use_name指向该子串的冒号后的名字;如果没找到,就另use_name = rsl
nfn = alloc_printf("%s/queue/id:%06u,orig:%s", out_dir, id, use_name);
nfn /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/id:000000,orig:1
- 在rsl里寻找
- 如果相等,则设置
- 尝试创建从
q->fname
到nfn
的硬链接link_or_copy(q->fname, nfn);
- 可能性执行链接(link)或者拷贝(copy)操作,将一个文件从一个路径复制到另一个路径
- ``c_link_copy
- 修改q的
fname
指向这个硬链接nfn
- 如果q的
passed_det
为1,则mark_as_det_done(q)
,这主要是对应上面的resuming_fuzz
的情况。- mark_as_det_done简单的说就是打开
out_dir/queue/.state/deterministic_done/use_name
这个文件,如果不存在就创建这个文件,然后设置q的passed_det为1。 - 这里的
use_name就是orig:后面的字符串
- mark_as_det_done简单的说就是打开
- 如果设置了
-i -
也就是in_place_resume
为1,则nuke_resume_dir()
nuke_resume_dir()
- 删除
out_dir/_resume/.state/deterministic_done
文件夹下所有id:
前缀的文件 - 删除
out_dir/_resume/.state/auto_extras
文件夹下所有auto_
前缀的文件 - 删除
out_dir/_resume/.state/redundant_edges
文件夹下所有id:
前缀的文件 - 删除
out_dir/_resume/.state/variable_behavior
文件夹下所有id:
前缀的文件 - 删除文件夹
out_dir/_resume/.state
- 删除
out_dir/_resume
文件夹下所有id:
前缀的文件 - 如果全部删除成功就正常返回,如果有某一个删除失败就抛出异常。
- 删除
- 初始化
load_extras
- 这段代码负责加载额外的字典文件(extra dictionary)并对其进行排序,使其按照字典条目的大小进行排列
- 如果定义了extras_dir,则从extras_dir读取extras到extras数组里,并按size排序。
-x @123
表示要提取字典级别(level),并将@
之后的数字解析为字典级别dict_level
。- 打开目录
dir
,使用opendir
函数。- 如果打开目录失败,首先检查错误类型。
- 如果错误是
ENOTDIR
,则表示dir
不是一个目录,可能是一个文件路径,因此尝试以文件方式加载字典文件,跳转到load_extras_file
进行处理。goto check_and_sort;
之后退出循环
- 如果不是
ENOTDIR
错误,而是其他错误,使用PFATAL
报错,指示无法打开目录
- 如果错误是
- 如果
x
存在(之前解析了字典级别),则报错,因为当前版本不支持目录级别的字典文件。 - 如果打开目录成功
- 遍历目录文件
- 过滤
.
or..
- 大小大雨 MAX_DICT_FILE 128 ,用
FATAL
报错,指示该额外字典文件过大。
- 过滤
- 跟踪最小和最大字典文件大小,以便在之后显示有关额外字典文件的信息
- 为
extras
数组分配更多的内存以容纳新的字典文件。- 分配足够的内存来存储字典文件的内容,并将大小设置为文件大小。
- 增加
extras_cnt
计数器,表示已加载的额外字典文件数量
- 遍历目录文件
check_and_sort
- 使用
qsort
对extras
数组中的字典文件进行按照大小排序。 - 如果最大字典条目大小超过 32,使用
WARNF
发出警告,建议考虑裁剪字典条目。
- 使用
- 如果打开目录失败,首先检查错误类型。
find_timeout
- 这段代码的作用是用于寻找之前会话中的超时设置,并将其应用于当前会话。主要用于在恢复会话时,不希望超时设置重复地自动进行缩放,以防止由于随机因素而导致超时设置不断增大
- 如果
timeout_given
没有被设置,则进入find_timeout- 这个想法是,在不指定
-t
的情况下resuming sessions
时,我们不希望一遍又一遍地自动调整超时时间,以防止超时值因随机波动而增长 - 如果
resuming_fuzz
为0,则直接 return ,不需要恢复 - 如果
in_place_resume
为1,则fn = alloc_printf("%s/fuzzer_stats", out_dir);
,否则fn = alloc_printf("%s/../fuzzer_stats", in_dir);
- 以只读方式打开fd,读取内容到
tmp[4096]
里,并在里面搜索exec_timeout :
,如果搜索不到就直接返回,如果搜索到了,就读取这个timeout的数值,如果大于4就设置为exec_tmout的值。EXP_ST u32 exec_tmout = EXEC_TIMEOUT; /* Configurable exec timeout (ms) */
timeout_given = 3;
timeout_given, /* Specific timeout given? */
- 这个想法是,在不指定
detect_file_args
- 这个函数其实就是识别参数里面有没有
@@
,如果有就替换为out_dir/.cur_input
,如果没有就返回- 替换为
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/.cur_input
- 替换为
setup_stdio_file
- 如果out_file为NULL , 如果没有使用-f && 或者没有指定
@@
,就删除原本的out_dir/.cur_input
,创建一个新的out_dir/.cur_input
,保存其文件描述符在out_fd中
check_binary
- 这段代码用于验证目标二进制文件的合法性和是否进行了适当的 AFL 仪表插装(instrumentation)。具体来说,它检查二进制文件是否存在、是否为可执行文件、是否为 ELF 或 Mach-O 格式,是否包含 AFL 仪表插装标记,以及其他相关特征。
/home/wutang/Desktop/google_afl/llvm_mode/test-instr
- 可以指定
AFL_SKIP_BIN_CHECK
跳过check - …
- check指定路径处要执行的程序是否存在,且它不能是一个shell script
perform_dry_run
输入参数
/home/wutang/Desktop/google_afl/llvm_mode/test-instr /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/.cur_input |
流程
- 确认目标应用程序是否按预期工作。干扰运行是在初始的输入测试用例上进行的,用于验证应用程序在不进行实际变异的情况下是否会崩溃、超时或产生其他异常行为。
- 执行所有的测试用例,以检查是否按预期工作
- 读取环境变量
AFL_SKIP_CRASHES
到skip_crashes
,设置cal_failures为0 - 遍历queue
- 打开q->fname,并读取到分配的内存use_mem里
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/id:000000,orig:1
res = calibrate_case(argv, q, use_mem, 0, 1);
- 校准测试用例 [[#u8 calibrate_case(char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue)]] 返回res
- 如果
stop_soon
被置为1,就直接return - 如果res的结果为
crash_mode
或者FAULT_NOBITS
- 打印
SAYF("len = %u, map size = %u, exec speed = %llu us\n", q->len, q->bitmap_size, q->exec_us);
- 比如
len = 4, map size = 2, exec speed = 3237980 us
- 打印
- 依据res的结果查看是哪种错误并进行判断。一共有以下几种错误类型
- FAULT_NONE
- 如果q是头结点,即第一个测试用例,则
check_map_coverage
,用以评估map coverage [[#void check_map_coverage(void)]] 测试覆盖率低直接告警- 检查覆盖率地图(coverage map)的覆盖情况。覆盖率地图是记录在模糊测试中哪些代码路径被执行过的数据结构
- 计数trace_bits发现的路径数,如果小于100,就直接返回
- 在trace_bits的数组后半段,如果有值就直接返回。
- 抛出警告
WARNF("Recompile binary with newer version of afl to improve coverage!")
- 设置
-C
也就是设置crash_mode为FAULT_CRASH
则抛出异常,FATAL("Test case '%s' does *NOT* crash", fn);
,该文件不崩溃
- 如果q是头结点,即第一个测试用例,则
- FAULT_TMOUT
- 如果指定了-t参数,
-t 360000+
则timeout_given值为2- 抛出警告
WARNF("Test case results in a timeout (skipping)");
,并设置q的cal_failed为CAL_CHANCES,cal_failures计数器加一。
- 抛出警告
- 如果指定了-t参数,
- FAULT_CRASH
- 设置
-Q
会设置mem_limit
, 判断是否指定mem_limit
,给出不同的内存的建议 - 但不管指定了还是没有,都会抛出异常
FATAL("Test case '%s' results in a crash", fn);
- 设置
- FAULT_ERROR
- 抛出异常
Unable to execute target application
- 抛出异常
- FAULT_NOINST
- 这个样例运行没有出现任何路径信息,抛出异常
No instrumentation detected
- 这个样例运行没有出现任何路径信息,抛出异常
- FAULT_NOBITS
- 如果这个样例有出现路径信息,但是没有任何新路径,抛出警告
WARNF("No new instrumentation output, test case may be useless.")
,认为这是无用路径。useless_at_start计数器加一
- 如果这个样例有出现路径信息,但是没有任何新路径,抛出警告
- FAULT_NONE
- 如果这个样例q的var_behavior为真,则代表它多次运行,同样的输入条件下,却出现不同的覆盖信息。
- 抛出警告
WARNF("Instrumentation output varies across runs.");
,代表这个样例的路径输出可变
- 抛出警告
- 然后读取下一个queue,继续测试,直到结束。
- 读取环境变量
enum { |
u8 calibrate_case(char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue)
- 对一个新的测试用例进行校准(calibration)。校准是为了验证测试用例的执行结果,确认应用程序的行为,同时检测出变量行为等情况。
- 这个函数评估input文件夹下的case,来发现这些testcase的行为是否异常;以及在发现新的路径时,用以评估这个新发现的testcase的行为是否是可变(这里的可变是指多次执行这个case,发现的路径不同)等等
- 创建
first_trace[MAP_SIZE]
- 如果
q->exec_cksum
为0,代表这是这个case第一次运行,即来自input文件夹下,所以将first_run置为1。 - 保存原有的stage_cur、stage_max、stage_name
- 设置use_tmout为exec_tmout,如果from_queue是0或者resuming_fuzz被置为1,即代表不来自于queue中或者在resuming sessions的时候,则use_tmout的值被设置的更大。
q->cal_failed++
- 设置stage_name为”calibration”,以及根据是否fast_cal为1,来设置stage_max的值为3还是CAL_CYCLES(默认为8),含义是每个新测试用例(以及显示出可变行为的测试用例)的校准周期数,也就是说这个stage要执行几次的意思。
- 如果当前不是以dumb mode运行,且no_forkserver(禁用forkserver)为0,且forksrv_pid为0,则
init_forkserver(argv)
启动fork server [[#init_forkserver]]
- 如果这个queue不是来自input文件夹,而是评估新case,则此时
q->exec_cksum
不为空,拷贝trace_bits到first_trace里,然后计算has_new_bits
的值,赋值给new_bits。- [[#has_new_bits(u8 *virgin_map)]]
- 开始执行
calibration stage
,共执行stage_max循环- 如果这个queue不是来自input文件夹,而是评估新case,且第一轮calibration stage执行结束时,刷新一次展示界面
show_stats
,用来展示这次执行的结果,此后不再展示。 write_to_testcase(use_mem, q->len)
- 将从
q->fname
中读取的内容写入到.cur_input
中 - [[#void write_to_testcase(void *mem, u32 len)]]
- 将从
u8 run_target(argv, use_tmout)
,通过Fork-server 开始fork出子进程进进行fuzz, 对trace_bits进行赋值,运行目标应用程序并监控是否超时,返回运行结果的状态信息- [[#u8 run_target(char **argv, u32 timeout)]] 返回res
- 如果这是
calibration stage
第一次运行,且不在dumb_mode
,且共享内存里没有任何路径(即没有任何byte被置位),设置fault为FAULT_NOINST
,然后goto abort_calibration
- 计算共享内存里有多少字节被置位了,通过count_bytes函数。 计算在位图中设置为1的字节数量的函数, 验证 0x4000 字节的数据
- [[#u32 count_bytes(u8 *mem)]]
- 计算共享内存里有多少字节被置位了,通过count_bytes函数。 计算在位图中设置为1的字节数量的函数, 验证 0x4000 字节的数据
- 计算
hash32(trace_bits, MAP_SIZE, HASH_CONST)
的结果,其值为一个32位uint值,保存到cksum中.计算长度为2的16次方hash- [[#inline u32 hash32(const void* key, u32 len, u32 seed)]]
- 如果
q->exec_cksum
不等于cksum,即代表这是第一次运行,或者在相同的参数下,每次执行,cksum却不同,是一个路径可变的queuehnb = has_new_bits(virgin_bits)
[[#char has_new_bits(u8 *virgin_map)]] 目的是检查当前执行路径是否带来了新的信息(新的位)。它会更新”virgin_map”以反映新发现的位,并返回不同的值来表示发现的情况- 如果hnb大于new_bits,设置new_bits的值为hnb
- 如果
q->exec_cksum
不等于0,即代表这是判断是否是可变queue- i从0到MAP_SIZE遍历,如果
first_trace[i]
不等于trace_bits[i]
,代表发现了可变queue,且var_bytes为空,则将该字节设置为1,并将stage_max设置为CAL_CYCLES_LONG
,即需要执行40次。 - 将var_detected设置为1
- i从0到MAP_SIZE遍历,如果
- 否则,即
q->exec_cksum
等于0,即代表这是第一次执行这个queue- 设置
q->exec_cksum
的值为之前计算出来的本次执行的cksum - 拷贝trace_bits到first_trace中。
- 设置
- 如果这个queue不是来自input文件夹,而是评估新case,且第一轮calibration stage执行结束时,刷新一次展示界面
- 保存所有轮次总的执行时间,加到
total_cal_us
里,总的执行轮次,加到total_cal_cycles
里 - 计算出一些统计信息,包括
- 计算出单次执行时间的平均值保存到
q->exec_us
里 - 将最后一次执行所覆盖到的路径数保存到
q->bitmap_size
里 q->handicap = handicap;
q->cal_failed = 0;
- total_bitmap_size里加上这个queue所覆盖到的路径数
- total_bitmap_entries++
- [[#update_bitmap_score(struct queue_entry *q)]] 更新路径的评分,以便确定哪些路径是更有利于模糊测试的
- 如果fault为
FAULT_NONE
,且该queue是第一次执行,且不属于dumb_mode
,而且new_bits
为0,代表在这个样例所有轮次的执行里,都没有发现任何新路径和出现异常,设置fault为FAULT_NOBITS
- 计算出单次执行时间的平均值保存到
- 如果new_bits为2,且
q->has_new_cov
为0,设置其值为1,并将queued_with_cov加一,代表有一个queue发现了新路径。 - 如果这个queue是可变路径,即
var_detected
为1,则计算var_bytes
里被置位的tuple个数,保存到var_byte_count
里,代表这些tuple具有可变的行为。- 将这个queue标记为一个variable
mark_as_variable(struct queue_entry *q)
- 创建符号链接
out_dir/queue/.state/variable_behavior/fname
- 设置queue的var_behavior为1
- 创建符号链接
- 计数variable behavior的计数器
queued_variable
的值加一
- 将这个queue标记为一个variable
- 恢复之前的stage值
- 如果不是第一次运行这个queue,展示
show_stats
- 返回fault的值
- 创建
init_forkserver
- 建立管道
st_pipe
传递状态和ctl_pipe
传递命令,在父子进程之间,是通过管道进行通信- 在继续往下读之前需要仔细阅读这篇文章
- Linux 的进程间通信:管道
- fork出一个子进程,fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
forksrv_pid = fork()
- 子进程和父进程都会向下执行,我们通过pid来使它们执行不同的代码
if(!forksrv_pid)
- 以下都是子进程要执行的代码
- 在继续向下读之前,需要仔细阅读这篇文章
- 进程间通信管道进阶篇:linux下dup/dup2函数的用法
- ``linux_dup_dup2
- 在继续向下读之前,需要仔细阅读这篇文章
- 以下都是子进程要执行的代码
init_forkserver父进程行为
- 以下都是父进程要执行的代码
- 关闭不需要的endpoints
// 关闭不是需要的endpoints |
- init_forkserver设置子进程超时
- 之前设置了 timeout方法 [[#setup_signal_handlers]]
/* Wait for the fork server to come up, but don't wait too long. */ |
- 等待fork server启动,但是不能等太久。(所以在调试时要注意这个…) 设置-t
- 从管道里读取4个字节到status里,如果读取成功,则代表fork server成功启动,就结束这个函数并返回。
- 如果超时,就抛出异常。
ACTF("read pipe block"); |
- 这里阻塞进入子进程 [[#init_forkserver子进程行为]]
it.it_value.tv_sec = 0; |
- 之后关闭timer 然后读取到4字节退出返回父进程
init_forkserver子进程行为
- 进入
if(!forksrv_pid)
- 在 fork 服务器初始化时执行的,其主要目的是设置子进程的执行环境,然后使用
execv
调用目标二进制文件 - 设置文件描述符限制
- 做重定向
dev_null_fd = open("/dev/null", O_RDWR);
之前dup2(dev_null_fd, 1);
标准输出(文件描述符 1)dup2(dev_null_fd, 2);
标准错误(文件描述符 2)- 如果指定了
out_file
dup2(dev_null_fd, 0);
将标准输入重定向到/dev/null
- 否则
dup2(out_fd, 0);
标准输入重定向到文件
- 否则
- 子进程只能读取命令
dup2(ctl_pipe[0], FORKSRV_FD)
- 子进程只能发送(“写出”)状态
dup2(st_pipe[1], FORKSRV_FD + 1) < 0)
- 关闭子进程里的一些文件描述符
- 在 fork 服务器初始化时执行的,其主要目的是设置子进程的执行环境,然后使用
struct rlimit r; |
execv(target_path, argv)
带参数执行target,这个函数除非出错不然不会返回。
- `/home/wutang/Desktop/google_afl/llvm_mode/test-instr /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/.cur_input` |
- 使用一个独特的
bitmaps EXEC_FAIL_SIG(0xfee1dead)
写入trace_bits
,来告诉父进程执行失败,并结束子进程。
- 后续有检测 `if (*(u32*)trace_bits == EXEC_FAIL_SIG)` |
execv子进程继续执行插入的代码
- 这里使用trace-pc模式 [[#trace-pc-guard mode-测试]]
/home/wutang/Desktop/google_afl/llvm_mode/test-instr /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/.cur_input |
void write_to_testcase(void *mem, u32 len)
- 将从
mem
中读取len
个字节,写入到.cur_input
中 - 如果没指定
@@
也就是out_fd=null
就继续写入 out_fd- [[#setup_stdio_file]] 里设置的out_fd
u8 run_target(char **argv, u32 timeout)
- 通过Fork-server 开始fork出子进程进进行fuzz, 对trace_bits进行赋值,运行目标应用程序并监控是否超时,返回运行结果的状态信息
- 先清空
trace_bits[MAP_SIZE]
,将其全置为0,也就是清空共享内存。 - 如果
dumb_mode
等于1,且no_forkserver
,则 - 直接fork出一个子进程,然后让子进程execv去执行target,如果execv执行失败,则向
trace_bits
写入EXEC_FAIL_SIG
类似上面的操作 - 否则与子进程通信
- 向
fsrv_ctl_fd
写入prev_timed_out
的值,命令Fork server
开始fork出一个子进程进行fuzz,然后从状态管道读取fork server
返回的fork出的子进程的ID到child_pid
- 超时任务
- 无论实际执行的是上面两种的哪一种,在执行target期间,都设置计数器为
timeout
,如果超时,就杀死正在执行的子进程,并设置child_timed_out
为1; setitimer(ITIMER_REAL, &it, NULL);
- 等待target执行结束,如果是dumb_mode ,target执行结束的状态码将直接保存到status中,如果不是dumb_mode,则从状态管道中读取target执行结束的状态码。
read(fsrv_st_fd, &status, 4)
获取状态码 256if (!WIFSTOPPED(status)) child_pid = 0;
设置child_pid=0
- 计算target执行时间
exec_ms
,并将total_execs
这个执行次数计数器加一。 classify_counts((u64 *) trace_bits)
[[#classify_counts(u64 *mem)]] 用于将跟踪计数转换为分类信息的函数,目的是优化稀疏位图的处理。 对trace_bits进行赋值。- 具体地,target是将每个分支的执行次数用1个byte来储存,而fuzzer则进一步把这个执行次数归入到buckets中,举个例子,如果某分支执行了1次,那么落入第2个bucket,其计数 byte 仍为1;如果某分支执行了4次,那么落入第5个bucket,其计数byte将变为8,等等。
- 这样处理之后,对分支执行次数就会有一个简单的归类。例如,如果对某个测试用例处理时,分支A执行了32次;对另外一个测试用例,分支A执行了33次,那么AFL就会认为这两次的代码覆盖是相同的。当然,这样的简单分类肯定不能区分所有的情况,不过在某种程度上,处理了一些因为循环次数的微小区别,而误判为不同执行结果的情况.
- 设置
prev_timed_out
的值为child_timed_out
- 接着依据status的值,向调用者返回结果。
WIFSIGNALED(status)
若为异常结束子进程返回的状态,则为真- `WTERMSIG(status)`取得子进程因信号而中止的信号代码
- 如果child_timed_out为1,且状态码为`SIGKILL`,则返回`FAULT_TMOUT`
- 否则返回`FAULT_CRASH`- 如果是
dumb_mode
,且trace_bits
为EXEC_FAIL_SIG
,就返回FAULT_ERROR
- 设置执行时间
slowest_exec_ms
- 如果`timeout`小于等于`exec_tmout`,且`slowest_exec_ms`小于`exec_ms`,设置`slowest_exec_ms`等于`exec_ms`
- 返回
FAULT_NONE
- 先清空
classify_counts(u64 *mem)
- 用于将跟踪计数转换为分类信息的函数,目的是优化稀疏位图的处理。
- 8个字节一组去循环读入,直到遍历完整个mem
- 每次取两个字节
u16 *mem16 = (u16 *) mem
- i从0到3,计算
mem16[i]
的值,在count_class_lookup16[mem16[i]]
里找到对应的取值,并赋值给mem16[i]
u32 count_bytes(u8 *mem)
- 计算在位图中设置为1的字节数量的函数, 验证 0x4000 字节的数据
- [[CSAPP_深入理解计算机系统#掩码]]
// 这个宏定义 FF(_b) 是用来生成一个指定位数的字节掩码的,具体解释如下: |
inline u32 hash32(const void* key, u32 len, u32 seed)
- 计算哈希值的函数
hash32
,用于计算32位哈希值。计算长度为2的16次方hash
char has_new_bits(u8 *virgin_map)
- 这段代码的主要目的是检查当前执行路径是否带来了新的信息(新的位)。它会更新”virgin_map”以反映新发现的位,并返回不同的值来表示发现的情况
- 如果只有特定元组的命中计数发生变化,则返回1。
- 如果发现了新的元组,则返回2。
- 如果没有发现新的位,则返回0。
/* Check if the current execution path brings anything new to the table. |
- 检查有没有新路径或者某个路径的执行次数有所不同。
- 初始化current和virgin为trace_bits和virgin_map的u64首元素地址,设置ret的值为0
- 8个字节一组,每次从trace_bits,也就是共享内存里取出8个字节
- 如果current不为0,且`current & virgin`不为0,即代表current发现了新路径或者某条路径的执行次数和之前有所不同 |
- 如果传入给has_new_bits的参数
virgin_map
是virgin_bits
,且ret不为0,就设置bitmap_changed为1- virgin_bits保存还没有被Fuzz覆盖到的byte,其初始值每位全被置位1,然后每次按字节置位。
- 返回ret的值
- 这里可以优化
update_bitmap_score(struct queue_entry *q)
- 这段代码用于更新路径的评分,以便确定哪些路径是更有利于模糊测试的。在进行模糊测试时,通常希望尽可能选择一些路径,这些路径可以触发目标程序的不同位,从而更好地发现漏洞。为此,这段代码维护了一个
top_rated
数组,用于存储每个字节的顶级路径(评分最高的路径)- 每当我们发现一个新的路径,都会调用这个函数来判断其是不是更加地
favorable
,这个favorable的意思是说是否包含最小的路径集合来遍历到所有bitmap中的位,我们专注于这些集合而忽略其他的。- 首先计算出这个case的
fav_factor
,计算方法是q->exec_us * q->len
即执行时间和样例大小的乘积,以这两个指标来衡量权重。 - 遍历
trace_bits
数组,如果该字节的值不为0,则代表这是已经被覆盖到的path - 然后检查对应于这个path的
top_rated
是否存在- `static struct queue_entry *top_rated[MAP_SIZE]; /* Top entries for bitmap bytes */`
- 如果存在,就比较
fav_factor > top_rated[i]->exec_us * top_rated[i]->len
,即比较执行时间和样例大小的乘积,哪个更小。- 如果`top_rated[i]`的更小,则代表`top_rated[i]`的更优,不做任何处理continue,继续遍历下一个path。
- 如果q更小,就将`top_rated[i]`原先对应的queue entry的tc_ref字段减一,并将其trace_mini字段置为空。
- `u8 *trace_mini; /* Trace bytes, if kept */`
- `u32 tc_ref; /* Trace bytes ref count */` - 然后设置
top_rated[i]
为q
,即当前case,然后将其tc_ref的值加一 - 如果
q->trace_mini
为空,则将trace_bits经过minimize_bits
压缩,然后存到trace_mini字段里 [[#void minimize_bits(u8 *dst, u8 *src)]] BitMap数据压缩 trace_bits - 设置
score_changed
为1
- 首先计算出这个case的
- 每当我们发现一个新的路径,都会调用这个函数来判断其是不是更加地
void minimize_bits(u8 *dst, u8 *src)
- 将trace_bits压缩为较小的位图。 BitMap数据压缩 trace_bits
- 简单的理解就是把原本是包括了是否覆盖到和覆盖了多少次的byte,压缩成是否覆盖到的bit。
- 在看这个函数和下一个函数cull_queue之前,建议把经典算法系列之(一) - BitMap [数据的压缩存储]读完。
Index(N)代表N的索引号,Position(N)代表N的所在的位置号 |
if (!q->trace_mini) { |
void check_map_coverage(void)
- 此函数主要用于在模糊测试开始时检查覆盖率地图的覆盖情况。它采用两个步骤:
- 如果覆盖的字节数小于100,就认为覆盖不足,直接返回。这是一个简单的阈值,如果模糊测试的初始覆盖率太低,可能意味着测试效果不佳。
- 遍历从
1 << (MAP_SIZE_POW2 - 1)
到MAP_SIZE
范围内的字节。如果在这个范围内的任何一个字节的trace_bits
不为零,表示至少有一个路径经过了这个位置,覆盖率已经存在。如果没有,那么发出警告,建议重新编译二进制文件以改善覆盖率。
- 总之,这段代码用于判断模糊测试初始时覆盖率地图的情况,以便根据情况提出警告或建议。
cull_queue
/* The second part of the mechanism discussed above is a routine that |
- 该函数的目标是在模糊测试期间调整队列,以便更多地探索未被完全探索的路径。这在实际的模糊测试中非常有用,因为它可以帮助集中资源在最有希望的路径上,提高测试的效率。函数的具体步骤包括:
- 精简队列
- 如果score_changed为0,即top_rated没有变化,或者 dumb_mode ,就直接返回
- 设置score_changed的值为0
- 创建u8 temp_v数组,大小为
MAP_SIZE/8
,并将其初始值设置为0xff,其每位如果为1就代表还没有被覆盖到,如果为0就代表以及被覆盖到了。 - 设置queued_favored为0,
pending_favored
为0 - 开始遍历queue队列,设置其favored的值都为0
- 将i从0到MAP_SIZE迭代,这个迭代其实就是筛选出一组queue entry,它们就能够覆盖到所有现在已经覆盖到的路径,而且这个case集合里的case要更小更快,这并不是最优算法,只能算是贪婪算法。
- 这又是个不好懂的位运算,
temp_v[i >> 3] & (1 << (i & 7))
与上面的差不多,中间的或运算改成了与,是为了检查该位是不是0,即判断该path对应的bit有没有被置位。
- 这又是个不好懂的位运算,
for (i = 0; i < MAP_SIZE; i++) |
- 如果
top_rated[i]
有值,且该path在temp_v里被置位- 就从temp_v中清除掉所有
top_rated[i]
覆盖到的path,将对应的bit置为0 - 设置
top_rated[i]->favored
为1,queued_favored计数器加一 - 如果
top_rated[i]
的was_fuzzed字段是0,代表其还没有fuzz过,则将pending_favored计数器加一
- 就从temp_v中清除掉所有
- 遍历queue队列
- `mark_as_redundant(q, !q->favored)` |
mark_as_redundant(struct queue_entry *q, u8 state)
- 该函数用于标记/取消标记队列中的项目为冗余(仅涉及边缘)。主要步骤包括:
- 如果新状态与项目之前的状态相同,则直接返回,没有任何操作。
- 更新项目的冗余状态。
- 从项目的文件名中获取最后一个 ‘/‘ 后的部分,这部分将用于构建冗余文件的路径。
- 如果新状态为 1,表示要将项目标记为冗余,则进行下面的操作:
- 创建一个新文件,并设置文件权限为 0600。
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/.state/redundant_edges/xxx
- 如果新状态为 0,表示要取消项目的冗余标记,则进行下面的操作:
- 删除之前创建的冗余文件。
- 释放为构建文件路径而分配的内存。
show_init_stats
- 在处理输入目录的末尾显示统计信息,以及一堆警告,以及几个硬编码的常量。
- 这段代码用于在处理输入目录后显示初始化统计信息和一些警告
find_start_position
- resume模式下有意义,其目的是在恢复模式下寻找一个合适的队列起始位置。这个功能只有在进行恢复且能找到原始的
fuzzer_stats
文件时才有意义。 - resume时,请尝试查找要从其开始的队列位置,这仅在resume时以及当我们可以找到原始的fuzzer_stats时才有意义.
- 如果不是
resuming_fuzz
,就直接返回 - 如果是
in_place_resume
,就打开out_dir/fuzzer_stats
文件,否则打开in_dir/../fuzzer_stats
文件 - 读这个文件的内容到
tmp[4096]
中,找到cur_path
,并设置为ret的值,如果大于queued_paths
就设置ret为0,返回ret
- 如果不是
void write_stats_file(double bitmap_cvg, double stability, double eps)
更新用于无人值守监控的统计文件
函数负责将各项统计信息写入名为
fuzzer_stats
的文件中,这些信息用于监控 fuzzer 的性能和状态。这可以帮助用户了解 fuzzer 的执行情况、性能和资源使用情况。/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/fuzzer_stats
fuzzer_stats
- 创建文件
out_dir/fuzzer_stats
- 写入统计信息
- start_time
- fuzz运行的开始时间,start_time / 1000
- last_update
- 当前时间
- fuzzer_pid
- 获取当前pid
- cycles_done
queue_cycle
在queue_cur
为空,即执行到当前队列尾的时候才增加1,所以这代表queue队列被完全变异一次的次数。
- execs_done
- total_execs,target的总的执行次数,每次
run_target
的时候会增加1
- total_execs,target的总的执行次数,每次
- execs_per_sec
- 每秒执行的次数
- paths_total
- queued_paths在每次
add_to_queue
的时候会增加1,代表queue里的样例总数
- queued_paths在每次
- paths_favored
- queued_favored,有价值的路径总数
- paths_found
- queued_discovered在每次
common_fuzz_stuff
去执行一次fuzz时,发现新的interesting case的时候会增加1,代表在fuzz运行期间发现的新queue entry。
- queued_discovered在每次
- paths_imported
- queued_imported是master-slave模式下,如果sync过来的case是interesting的,就增加1
- max_depth
- 最大路径深度
- cur_path
- current_entry一般情况下代表的是正在执行的queue entry的整数ID,queue首节点的ID是0
- pending_favs
- pending_favored 等待fuzz的favored paths数
- pending_total
- pending_not_fuzzed 在queue中等待fuzz的case数
- variable_paths
- queued_variable在
calibrate_case
去评估一个新的test case的时候,如果发现这个case的路径是可变的,则将这个计数器加一,代表发现了一个可变case
- queued_variable在
- stability
- bitmap_cvg
- unique_crashes
- unique_crashes这是在
save_if_interesting
时,如果fault是FAULT_CRASH,就将unique_crashes计数器加一
- unique_crashes这是在
- unique_hangs
- unique_hangs这是在
save_if_interesting
时,如果fault是FAULT_TMOUT,且exec_tmout小于hang_tmout,就以hang_tmout为超时时间再执行一次,如果还超时,就让hang计数器加一。
- unique_hangs这是在
- last_path
- 在
add_to_queue
里将一个新case加入queue时,就设置一次last_path_time为当前时间,last_path_time / 1000
- 在
- last_crash
- 同上,在unique_crashes加一的时候,last_crash也更新时间,
last_crash_time / 1000
- 同上,在unique_crashes加一的时候,last_crash也更新时间,
- last_hang
- 同上,在unique_hangs加一的时候,last_hang也更新时间,
last_hang_time / 1000
- 同上,在unique_hangs加一的时候,last_hang也更新时间,
- execs_since_crash
- total_execs - last_crash_execs,这里last_crash_execs是在上一次crash的时候的总计执行了多少次
- exec_tmout
- 配置好的超时时间,有三种可能的配置方式,见上文
- 创建文件
save_auto
- 目的是将用于自动筛选的额外测试用例保存到磁盘中。
- 保存自动生成的extras
- 如果auto_changed为0,则直接返回
- 如果不为0,就设置为0,然后创建名为
alloc_printf("%s/queue/.state/auto_extras/auto_%06u", out_dir, i);
的文件,并写入a_extras的内容。
Fuzz执行
fuzz-主要流程
- 进入while循环
- [[#cull_queue]] 该函数的目标是在模糊测试期间调整队列,以便更多地探索未被完全探索的路径
- [[#show_stats]] 在终端上显示有关Fuzzer状态和统计信息的用户界面
- 如果在一轮执行之后的queue里的case数,和执行之前一样,代表在完整的一轮执行里都没有发现任何一个新的case
- 如果
use_splicing为1
,就设置cycles_wo_finds计数器加1 - 否则,设置
use_splicing为1
,代表我们接下来要通过splice重组queue里的case。
- 如果
- 设置-M -S && 宏
AFL_IMPORT_FIRST
去从其他fuzzer中导入test casessync_fuzzers
[[#sync_fuzzers(char **argv)]]- 这段代码用于从其他模糊测试器中获取有趣的测试案例并执行
/home/wutang/Desktop/fuzz_test/fuzz_output/
- 感觉没必要
skipped_fuzz = fuzz_one(use_argv);
[[#fuzz_one]]
show_stats
- 该函数用于在终端上显示有关Fuzzer状态和统计信息的用户界面。这个界面是一个详细的终端输出,显示有关Fuzzer执行进度、发现路径数量、执行速度、覆盖率等的信息。
- 获取当前时间,并检查是否已经足够时间过去以进行下一次界面更新,如果没有则返回。
cur_ms - last_ms < 1000 / UI_TARGET_HZ(5)
- 检查是否已经运行了超过10分钟,如果是则将变量
run_over10m
设置为1 - 计算平滑的执行速度统计信息。如果是第一次调用该函数,直接计算执行速度。否则,计算当前的执行速度并进行平滑处理,以避免突然的速度变化。
- 更新时间和执行次数的记录,用于下一次函数调用时计算速度。
last_ms
last_execs
- 根据平均执行速度计算多久需要调用一次
stats_update
函数以更新状态。 - 计算位图统计信息,包括非255字节的数量、比特位密度等。
t_bytes = count_non_255_bytes(virgin_bits)
这段代码用于计算在位图(bitmap)中设置为非 255(0xFF)的字节数。它主要用于状态屏幕,大约每秒钟调用几次- 每隔一分钟,更新模糊器的状态、保存自动 tokens 和写入位图。
write_stats_file(t_byte_ratio, stab_ratio, avg_exec)
save_auto()
write_bitmap();
1. `/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/fuzz_bitmap`
- 每隔一段时间,更新绘图数据。
maybe_update_plot_file(t_byte_ratio, avg_exec);
更新/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/plot_data
- 根据条件设置
stop_soon
变量,用于控制 Fuzzer 的退出。 - 如果终端不可用,直接返回。
- 计算位图的比特位统计信息。
count_bits(virgin_bits)
- 计算给定位图中设置的位数(即置为1的位数),主要用于状态屏幕,每秒钟多次调用,不必要求非常高的速度
- 清除屏幕并显示 Fuzzer 的横幅信息,其中包括 Fuzzer 的名称、版本号等。
- 绘制统计信息表格,包括运行时间、路径数量、独特崩溃数量等。
- 绘制运行阶段进展信息,包括当前阶段、运行的执行次数等。
- 绘制策略和路径几何信息,包括位翻转、字节翻转、算术操作等。
- 显示CPU利用率信息,根据实际情况使用不同颜色显示。
fuzz_one
- all
- 设置ret_val=1
- 如果
pending_favored
不为0,则对于queue_cur被fuzz过或者不是favored的,有99%的几率直接返回1。 - 如果
pending_favored
为0且queued_paths(即queue里的case总数)大于10- 如果queue_cycle大于1且queue_cur没有被fuzz过,则有75%的概率直接返回1
- 如果queue_cur被fuzz过,否则有95%的概率直接返回1
- 设置len为
queue_cur->len
- 打开该case对应的文件,并通过mmap映射到内存里,地址赋值给
in_buf
和orig_in
- 打开预料
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/id:000000,orig:1
- 分配len大小的内存,并初始化为全0,然后将地址赋值给out_buf
- [[#CALIBRATION阶段]]
- [[#TRIMMING阶段]]
- [[#PERFORMANCE SCORE阶段]]
- [[#SIMPLE BITFLIP (+dictionary construction)阶段]]
- [[#ARITHMETIC INC/DEC阶段]]
- [[#INTERESTING VALUES阶段]]
- [[#RANDOM HAVOC阶段]]
- [[#SPLICING阶段]]
- 设置ret_val的值为0
- 如果queue_cur通过了评估,且was_fuzzed字段是0
- 就设置
queue_cur->was_fuzzed
为1,然后pending_not_fuzzed计数器减一 - 如果queue_cur是favored, pending_favored计数器减一。
- return ret_val
各个阶段
CALIBRATION阶段
假如当前项有校准错误,并且校准错误次数小于3次,那么就用 calibrate_case
再次校准。
TRIMMING阶段
- 如果该case没有trim过,
queue_cur->trim_done
为0- 调用函数
trim_case(argv, queue_cur, in_buf)
进行trim(修剪) [[#trim_case(char **argv, struct queue_entry *q, u8 *in_buf)]] in_buf 就是预料信息 - 设置queue_cur的trim_done为1
- 重新读取一次
queue_cur->len
到len中
- 调用函数
- 将in_buf拷贝len个字节到out_buf中
PERFORMANCE SCORE阶段
- perf_score =
calculate_score(queue_cur)
[[#u32 calculate_score(struct queue_entry *q)]] - 如果
skip_deterministic
为1,或者queue_cur->was_fuzzed
被fuzz过,或者queue_cur->passed_d
et为1,则跳转去havoc_stage阶段 - 设置doing_det为1
SIMPLE BITFLIP (+dictionary construction)阶段
bitflip 1/1
翻转需要注意 不会在 -d mode or -S 生成字典
|
- 流程
- 设置stage_name为
bitflip 1/1
, ar的取值是out_buf,而_bf的取值在[0: len << 3)
也就是测试长度*8 次 - 所以用
_bf & 7
能够得到0,1,2...7 0,1,2...7
这样的取值一共len组 (每组8个) - 然后 index ->
(_bf) >> 3
又将[0: len<<3)
映射回了[0: len)
,对应到buf里的每个byte - 然后 value ->
(128 >> ((_bf) & 7))
对应128,64,32,16,8,4,2,1
- 遍历out_buf 中字符 , 与上面进行 异或
out_buf[0] ^= 128
out_buf[0] ^= 64
out_buf[0] ^= 32
out_buf[0] ^= 16
out_buf[0] ^= 8
out_buf[0] ^= 4
out_buf[0] ^= 2
out_buf[0] ^= 1
out_buf[1] ^= 128
- …
common_fuzz_stuff(argv, out_buf, len)
[[#u8 common_fuzz_stuff(char **argv, u8 *out_buf, u32 len)]]- 所以在从
0-len*8
的遍历过程中会通过xor运算,依次将每个位翻转,然后执行一次common_fuzz_stuff
,然后再翻转回来。 - 在进行bitflip 1/1变异时,对于每个byte的最低位(least significant bit)翻转还进行了额外的处理
- 如果连续多个bytes的最低位被翻转后,程序的执行路径都未变化,而且与原始执行路径不一致,那么就把这一段连续的bytes判断是一条token。
比如对于SQL的SELECT *
,如果SELECT
被破坏,则肯定和正确的路径不一致,而被破坏之后的路径却肯定是一样的,比如AELECT
和SBLECT
,显然都是无意义的,而只有不破坏token,才有可能出现和原始执行路径一样的结果,所以AFL在这里就是在猜解关键字token。 - token默认最小是3,最大是32,每次发现新token时,通过
maybe_add_auto
添加到a_extras
数组里。 [[#maybe_add_auto(u8 *mem, u32 len)]]
- 如果连续多个bytes的最低位被翻转后,程序的执行路径都未变化,而且与原始执行路径不一致,那么就把这一段连续的bytes判断是一条token。
stage_finds[STAGE_FLIP1]
的值加上在整个FLIP_BIT中新发现的路径和Crash总和stage_cycles[STAGE_FLIP1]
的值加上在整个FLIP_BIT中执行的target次数stage_max
- 设置stage_name为
bitflip 2/1
翻转2次
stage_max = (len << 3) - 1; |
- 流程
- 设置stage_name为
bitflip 2/1
,原理和之前一样,只是这次是连续翻转 out_buf[0] = 0x31^128^64
out_buf[0] = 0x31^64^32
- …
- 然后保存结果到
stage_finds[STAGE_FLIP2]和stage_cycles[STAGE_FLIP2]
里。
- 设置stage_name为
- 同理,设置stage_name为
bitflip 4/1
,翻转连续4次并记录。 - 流程
out_buf[0] = 0x31^128^64^32^16 = 0xC1
out_buf[0] = 0x31^64^32^16^8 = 0x49
out_buf[0] = 0x31^32^16^8^4 = 0xD
out_buf[0] = 0x31^16^8^4^2 = 0x2F
out_buf[0] = 0x31^8^4^2^1 = 0x3E
out_buf[0] = 0x31^4^2^1 = 0x36 out_buf[1] = 0x32^128 = 0xB2
out_buf[0] = 0x31^2^1 = 0x32 out_buf[1] = 0x32^128^64 = 0xF2
out_buf[0] = 0x31^1 = 0x30 out_buf[1] = 0x32^128^64^32 = 0xD2
- …
生成effector map
- 在进行bitflip 8/8变异时,AFL还生成了一个非常重要的信息:effector map。这个effector map几乎贯穿了整个deterministic fuzzing的始终。
- 具体地,在对每个byte进行翻转时,如果其造成执行路径与原始路径不一致,就将该byte在effector map中标记为1,即“有效”的,否则标记为0,即“无效”的。
- 这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,
会参考effector map,跳过那些“无效”的byte,从而节省了执行资源
。 - 由此,通过极小的开销(没有增加额外的执行次数),AFL又一次对文件格式进行了
启发式的判断
。看到这里,不得不叹服于AFL实现上的精妙。 - 不过,
在某些情况下并不会检测有效字符
。 - 第一种情况就是
dumb mode
或者从fuzzer
,此时文件所有的字符都有可能被变异。 - 第二、第三种情况与文件本身有关:
bitflip 8/8变异
- 设置stage_name为
bitflip 8/8
,以字节为单位,直接通过和0xff
亦或运算去翻转整个字节的位,然后执行一次,并记录。
- 设置stage_name为
bitflip 16/8变异
- 设置stage_name为
bitflip 16/8
,设置stage_max
为len - 1
,以字为单位和0xffff
进行亦或运算,去翻转相邻的两个字节(即一个字的)的位
。 out_buf[0] = 0x3231 ^ 0xffff = 0xCDCE
out_buf[0] = 0x3332 ^ 0xffff = 0xCCCD
- …
- 这里要注意在翻转之前会先检查eff_map里对应于这两个字节的标志是否为0,如果为0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一个字。
- common_fuzz_stuff执行变异后的结果,然后还原。
- 设置stage_name为
如果预料len<4 就跳出bitflip goto skip_bitflip
bitflip 32/8变异
out_buf[0] = 0xA333231 ^ 0xFFFFFFFF = 0xF5CCCDCE
- 同理,设置stage_name为
bitflip 32/8
,然后设置stage_max
为len - 3
,以双字为单位,直接通过和0xffffffff
亦或运算去相邻四个字节的位,然后执行一次,并记录。- 在每次翻转之前会检查eff_map里对应于这四个字节的标志是否为0,如果是0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一组双字。
ARITHMETIC INC/DEC阶段
- 判断是否指定
AFL_NO_ARITH
略过 - 流程
- 在bitflip变异全部进行完成后,便进入下一个阶段:arithmetic。与bitflip类似的是,arithmetic根据目标大小的不同,也分为了多个子阶段:
- 算法运算有个
could_be_bitflip
确定给定的xor_val
是否可能是由位翻转引起的变化。- 举例
arith 8/8
- 取第一位
- 检查
orig ^ (orig + j) = 0x31 ^ 0x32 = 0x3
跳过- 用于检查特定的变化(通过
xor_val = old ^ new
表示)是否可能是由afl-fuzz
尝试的确定性位翻转引起的。这在后续的一些确定性模糊测试操作中用于避免重复。如果xor_val
为零,还会返回 1,这意味着旧值和尝试的新值是相同的,执行将是浪费时间。
- 用于检查特定的变化(通过
- 检查
orig ^ (orig - j) = 0x31 - 0x30 = 0x1
跳过 - 取第二位
- 检查
orig ^ (orig + j) = 0x31 ^ 0x33 = 0x2
0x2 >>=1 = 0x1
跳过 - 检查
orig ^ (orig - j) = 0x31 ^ 0x2F = 0x1E
0x1e >>=1 = 0xF
跳过 - 取三位
- 检查
orig ^ (orig + j) = 0x31 ^ 0x34 = 0x5
通过- 然后使用
out_buf[0] = 0x34
这个值 去common_fuzz_stuff
- 然后使用
- 继续检查
orig ^ (orig - j) = 0x31 ^ 0x2E = 0x1F
通过- 然后使用
out_buf[0] = 0x2E
这个值 去common_fuzz_stuff
- 然后使用
- 举例
下面要分大小端测试
arith 8/8
,每次对8个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个byte进行整数加减变异arith 16/8
,每次对16个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个word进行整数加减变异arith 32/8
,每次对32个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个dword进行整数加减变异- 加减变异的上限,在
config.h
中的宏ARITH_MAX
定义,默认为35。所以,对目标整数会进行+1, +2, …, +35, -1, -2, …, -35的变异
。特别地,由于整数存在大端序和小端序两种表示方式,AFL会贴心地对这两种整数表示方式都进行变异
。 - 此外,AFL还会智能地跳过某些arithmetic变异。
- 第一种情况就是前面提到的
effector map
:如果一个整数的所有bytes都被判断为“无效”,那么就跳过对整数的变异。 - 第二种情况是之前bitflip已经生成过的变异:如果加/减某个数后,其效果与之前的某种bitflip相同,那么这次变异肯定在上一个阶段已经执行过了,此次便不会再执行。
INTERESTING VALUES阶段
- 流程
- 下一个阶段是interest,具体可分为:
- interest 8/8,每次对8个bit进替换,按照每8个bit的步长从头开始,即对文件的每个byte进行替换
- interest 16/8,每次对16个bit进替换,按照每8个bit的步长从头开始,即对文件的每个word进行替换
- interest 32/8,每次对32个bit进替换,按照每8个bit的步长从头开始,即对文件的每个dword进行替换
- 而用于替换的”interesting values”,是AFL预设的一些比较特殊的数,这些数的定义在config.h文件中
static s8 interesting_8[] = { INTERESTING_8 }; |
- 与之前类似,effector map仍然会用于判断是否需要变异;此外,如果某个interesting value,是可以通过bitflip或者arithmetic变异达到,那么这样的重复性变异也是会跳过的。 Skip if the value could be a product of bitflips or arithmetics.
out_buf[0] = interesting_8[0] = -128;
- …
*(u16*)(out_buf + 0) = interesting_16[0] = -128; = 0xffff-128+1 = 0xff80
- …
*(u32*)(out_buf + 0) = interesting_32[0] = -128; = 0xffffffff-128+1 = 0xFFFFFF80
0x005320c8: 0x80 0xff 0xff 0xff 0xf0 0x00 0x00 0x00
DICTIONARY STUFF阶段
- [[#字典编写规则]]
- 流程
extras_cnt=0
跳过- 改变他 用
load_extras_file
自己指定的字典
- 改变他 用
- 遍历字典
- 如果发现跟预料一致跳过
- 如果发现超过200跳过?
- 进入到这个阶段,就接近deterministic fuzzing的尾声了。具体有以下子阶段:
user extras(over)
从头开始,将用户提供的tokens依次替换到原文件中,stage_max为extras_cnt * len
ABCD
user extras(insert)
从头开始,将用户提供的tokens依次插入到原文件中,stage_max为extras_cnt * len
123123
auto extras(over)
从头开始,将自动检测的tokens
依次替换到原文件中,stage_max为MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len
- 这里是自己检测得到的 a_extras_cnt个 token
RANDOM HAVOC阶段
- 设置确定性检测,创建文件
mark_as_det_done
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/.state/deterministic_done/id:000000,orig:1
- 流程
- 对于非dumb mode的主fuzzer来说,完成了上述
deterministic fuzzing
后,便进入了充满随机性的这一阶段;对于dumb mode或者从fuzzer来说,则是直接从这一阶段开始。 - havoc,顾名思义,是充满了各种随机生成的变异,是对原文件的“大破坏”。具体来说,havoc包含了对原文件的多轮变异,每一轮都是将多种方式组合(stacked)而成:
- 随机选取某个bit进行翻转
FLIP_BIT(out_buf, UR(temp_len << 3));
- 随机选取某个byte,将其设置为随机的interesting value
out_buf[UR(temp_len)] = interesting_8[UR(sizeof(interesting_8))];
- 随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value
*(u16*)(out_buf + UR(temp_len - 1)) = interesting_16[UR(sizeof(interesting_16) >> 1)];
- 随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value
*(u32*)(out_buf + UR(temp_len - 3)) = interesting_32[UR(sizeof(interesting_32) >> 2)];
- 随机选取某个byte,对其减去一个随机数
out_buf[UR(temp_len)] -= 1 + UR(ARITH_MAX);
- 随机选取某个byte,对其加上一个随机数
out_buf[UR(temp_len)] += 1 + UR(ARITH_MAX);
- 随机选取某个word,并随机选取大、小端序,对其减去一个随机数
*(u16*)(out_buf + pos) = SWAP16(SWAP16(*(u16*)(out_buf + pos)) - num);
- 随机选取某个word,并随机选取大、小端序,对其加上一个随机数
*(u16*)(out_buf + pos) = SWAP16(SWAP16(*(u16*)(out_buf + pos)) + num);
- 随机选取某个dword,并随机选取大、小端序,对其减去一个随机数
- 随机选取某个dword,并随机选取大、小端序,对其加上一个随机数
- 随机选取某个byte,将其设置为随机数
out_buf[UR(temp_len)] ^= 1 + UR(255);
- 随机删除一段bytes
Clone bytes (75%) or insert a block of constant bytes (25%).
- 随机选取一个位置 75% 的概率是克隆(复制)一段数据,25% 的概率是插入一段常量数据
clone_len = choose_block_len(temp_len);
在一定的范围内选择合适的块长度,以便在块操作中产生不同大小的修改clone_from = UR(temp_len - clone_len + 1);
一个更简单的随机数生成函数,用于从 0 到limit - 1
之间选择一个随机数。它的主要目的是在需要产生随机数的地方使用。new_buf = ck_alloc_nozero(temp_len + clone_len);
- 创建8+clone_len(随机长度) 长度 新buf=new_buf
memcpy(new_buf + clone_to, out_buf + clone_from, clone_len);
- out_buf + clone_from(随机数) 拷贝长度为clone_len(随机长度) 到 new_buf+clone_to(随机数)
Overwrite bytes with a randomly selected chunk (75%) or fixed bytes (25%).
- 15 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换
- 16 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入
- 怎么样,看完上面这么多的“随机”,有没有觉得晕?还没完,AFL会生成一个随机数,作为变异组合的数量,并根据这个数量,每次从上面那些方式中随机选取一个(可以参考高中数学的有放回摸球),依次作用到文件上。如此这般丧心病狂的变异,原文件就大概率面目全非了,而这么多的随机性,也就成了fuzzing过程中的不可控因素,即所谓的“看天吃饭”了。
- 对于非dumb mode的主fuzzer来说,完成了上述
SPLICING阶段
use_splicing
在之前一整轮没有发现就会被设置,retry_splicing
循环。 重新尝试 splicing 15次后退出循环- 随机读取一个预料 到 new_buf
- 对比in_buf 旧预料 和 随机预料 new_buf不同的第一和最后的offset
locate_diffs(in_buf, new_buf, MIN(len, target->len), &f_diff, &l_diff);
- 比如 预料
123
躲避2222
得到f_diff=0 l_diff=3
- 随机得到分割offset
split_at = f_diff + UR(l_diff - f_diff);
- 然后拷贝 in_buf的长度为 split_at 到 new_buf
memcpy(new_buf, in_buf, split_at);
- 跳转到 [[#RANDOM HAVOC阶段]]
sync_fuzzers(char **argv)
trim_case(char **argv, struct queue_entry *q, u8 *in_buf)
- 用于在进行确定性检查deterministic checks时修剪测试用例,以便节省时间和资源
u32 calculate_score(struct queue_entry *q)
- 用于计算测试用例(case)的可取得分(desirability score),以便在执行混沌模糊测试(havoc fuzzing)时调整模糊测试的长度。
u8 common_fuzz_stuff(char **argv, u8 *out_buf, u32 len)
/* Write a modified test case, run program, process results. Handle |
- [[#show_stats]]
- 简单的说就是写入文件并执行,然后处理结果,如果出现错误,就返回1.
- 如果定义了
post_handler
,就通过out_buf = post_handler(out_buf, &len)
处理一下out_buf,如果out_buf或者len有一个为0,则直接返回0- 这里其实很有价值,尤其是如果需要对变异完的queue,做一层wrapper再写入的时候。
write_to_testcase(out_buf, len)
- [[#void write_to_testcase(void *mem, u32 len)]]
fault = run_target(argv, exec_tmout)
- [[#u8 run_target(char **argv, u32 timeout)]]
- 如果fault是FAULT_TMOUT
- 如果
subseq_tmouts++ > TMOUT_LIMIT
(默认250),就将cur_skipped_paths加一,直接返回1 - subseq_tmout是连续超时数
- 如果
- 否则设置subseq_tmouts为0
- 如果skip_requested为1
- 设置skip_requested为0,然后将cur_skipped_paths加一,直接返回1
queued_discovered += save_if_interesting(argv, out_buf, len, fault)
,即如果发现了新的路径才会加一。- [[#u8 save_if_interesting(char **argv, void *mem, u32 len, u8 fault)]] 会把什么阶段的样例记录下来
- 如果stage_cur除以stats_update_freq余数是0,或者其加一等于stage_max,就更新展示界面
show_stats
- 返回0
- 如果定义了
u8 save_if_interesting(char **argv, void *mem, u32 len, u8 fault)
- 判断目标程序的执行结果是否有趣,如果有趣则保存或排队。
- [[#char has_new_bits(u8 *virgin_map)]] 没发现新路径就return
/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/id:000003,src:000000,op:flip2,pos:2,+cov
- 会把什么阶段的样例记录下来
- 将其添加到队列里
- [[#add_to_queue(u8 *fname, u32 len, u8 passed_det)]]
- 计算hash 保存hash到其exec_cksum
- [[#inline u32 hash32(const void* key, u32 len, u32 seed)]]
- 评估这个queue
- [[#u8 calibrate_case(char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue)]]
- 设置keeping值为1
- 根据fault结果进入不同的分支
- FAULT_TMOUT
- 设置total_tmouts计数器加一
- 如果unique_hangs的个数超过能保存的最大数量
KEEP_UNIQUE_HANG
,就直接返回keeping的值 - 如果不是dumb mode,就
simplify_trace((u64 *) trace_bits)
进行规整。 - [[#simplify_trace(u64 *mem)]] 这段代码的目的是对位图中的活动位进行简化,以减少位图的复杂性,提高执行效率
- 如果没有发现新的超时路径,就直接返回keeping
- 否则,代表发现了新的超时路径,unique_tmouts计数器加一
- 如果hang_tmout大于exec_tmout,则以hang_tmout为timeout,重新执行一次runt_target
- 如果结果为
FAULT_CRASH
,就跳转到keep_as_crash - 如果结果不是
FAULT_TMOUT
,就返回keeping,否则就使unique_hangs
计数器加一,然后更新last_hang_time的值,并保存到alloc_printf("%s/hangs/id:%06llu,%s", out_dir, unique_hangs, describe_op(0))
文件。
- 如果结果为
- FAULT_CRASH
- total_crashes计数器加一
- 如果unique_crashes大于能保存的最大数量
KEEP_UNIQUE_CRASH
即5000,就直接返回keeping的值 - 同理,如果不是dumb mode,就
simplify_trace((u64 *) trace_bits)
进行规整 [[#simplify_trace(u64 *mem)]] - 如果没有发现新的crash路径,就直接返回keeping
- 否则,代表发现了新的crash路径,unique_crashes计数器加一,并将结果保存到
alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,unique_crashes, kill_signal, describe_op(0))
文件。 /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/crashes/id:000000,sig:06,src:000005,op:arith8,pos:5,val:-14
- 更新
last_crash_time
和last_crash_execs=total_execs
- FAULT_ERROR
- 抛出异常
- FAULT_TMOUT
simplify_trace(u64 *mem)
- 对一个64位整数数组(位图)进行简化操作,这段代码的目的是对位图中的活动位进行简化,以减少位图的复杂性,提高执行效率。
- 按8个字节为一组循环读入,直到完全读取完mem
- 如果mem不为空
- i从0-7,
mem8[i] = simplify_lookup[mem8[i]]
,代表规整该路径的命中次数到指令值,这个路径如果没有命中,就设置为1,如果命中了,就设置为128,即二进制的1000 0000
- i从0-7,
- 否则设置mem为
0x0101010101010101ULL
,即代表这8个字节代表的path都没有命中,每个字节的值被置为1。
- 如果mem不为空