-
-
AFL二三事 -- 源码分析 2
-
2021-9-27 16:52 30313
-
AFL二三事 -- 源码分析 2
前言
本文为《AFL二三事》-- 源码分析系列的第二篇,主要阅读AFL的另外一种插桩方式 ——llvm
模式。这主要是因为通过 afl-gcc
的方式进行插桩,在效率和性能上已经不能完美应对现代复杂的软件程序。随着llvm的成熟发展,AFL提供了更好的插桩方式 afl-clang-fast
,通过llvm pass来实现插桩,从而提升性能。
当别人都要快的时候,你要慢下来。
一、LLVM 前置知识
LLVM 主要为了解决编译时多种多样的前端和后端导致编译环境复杂、苛刻的问题,其核心为设计了一个称为 LLVM IR
的中间表示,并以库的形式提供一些列接口,以提供诸如操作 IR 、生成目标平台代码等等后端的功能。其整体架构如下所示:
不同的前端和后端使用统一的中间代码LLVM InterMediate Representation(LLVM IR)
,其结果就是如果需要支持一门新的编程语言,只需要实现一个新的前端;如果需要支持一款新的硬件设备,只需要实现一个新的后端;优化阶段为通用阶段,针对统一的 LLVM IR ,与新的编程语言和硬件设备无关。
GCC 的前后端耦合在一起,没有进行分离,所以GCC为了支持一门新的编程语言或一个新的硬件设备,需要重新开发前端到后端的完整过程。
Clang 是 LLVM 项目的一个子项目,它是 LLVM 架构下的 C/C++/Objective-C 的编译器,是 LLVM 前端的一部分。相较于GCC,具备编译速度快、占用内存少、模块化设计、诊断信息可读性强、设计清晰简单等优点。
最终从源码到机器码的流程如下(以 Clang 做编译器为例):
(LLVM Pass 是一些中间过程处理 IR 的可以用户自定义的内容,可以用来遍历、修改 IR 以达到插桩、优化、静态分析等目的。)
代码首先由编译器前端clang处理后得到中间代码IR,然后经过各 LLVM Pass 进行优化和转换,最终交给编译器后端生成机器码。
二、 AFL的afl-clang-fast
1. 概述
AFL的 llvm_mode
可以实现编译器级别的插桩,可以替代 afl-gcc
或 afl-clang
使用的比较“粗暴”的汇编级别的重写的方法,且具备如下几个优势:
- 编译器可以进行很多优化以提升效率;
- 可以实现CPU无关,可以在非 x86 架构上进行fuzz;
- 可以更好地处理多线程目标。
在AFL的 llvm_mode
文件夹下包含3个文件: afl-clang-fast.c
,afl-llvm-pass.so.cc
, afl-llvm-rt.o.c
。
afl-llvm-rt.o.c
文件主要是重写了 afl-as.h
文件中的 main_payload
部分,方便调用;
afl-llvm-pass.so.cc
文件主要是当通过 afl-clang-fast
调用 clang 时,这个pass被插入到 LLVM 中,告诉编译器添加与 `afl-as.h
中大致等效的代码;
afl-clang-fast.c
文件本质上是 clang 的 wrapper,最终调用的还是 clang 。但是与 afl-gcc
一样,会进行一些参数处理。
llvm_mode
的插桩思路就是通过编写pass来实现信息记录,对每个基本块都插入探针,具体代码在 afl-llvm-pass.so.cc
文件中,初始化和forkserver操作通过链接完成。
2. 源码
1. afl-clang-fast.c
1. main 函数
main
函数的全部逻辑如下:
主要是对 find_obj(), edit_params(), execvp()
函数的调用,
其中主要有以下三个函数的调用:
find_obj(argv[0])
:查找运行时libraryedit_params(argc, argv)
:处理传入的编译参数,将确定好的参数放入cc_params[]
数组execvp(cc_params[0], (cahr**)cc_params)
:替换进程空间,传递参数,执行要调用的clang
这里后两个函数的作用与 afl-gcc.c
中的作用基本相同,只是对参数的处理过程存在不同,不同的主要是 find_obj()
函数。
2. find_obj 函数
find_obj()
函数的控制流逻辑如下:
- 首先,读取环境变量
AFL_PATH
的值:- 如果读取成功,确认
AFL_PATH/afl-llvm-rt.o
是否可以访问;如果可以访问,设置该目录为obj_path
,然后直接返回; - 如果读取失败,检查
arg0
中是否存在/
字符,如果存在,则判断最后一个/
前面的路径为 AFL 的根目录;然后读取afl-llvm-rt.o
文件,成功读取,设置该目录为obj_path
,然后直接返回。
- 如果读取成功,确认
- 如果上面两种方式都失败,到
/usr/local/lib/afl
目录下查找是否存在afl-llvm-rt.o
,如果存在,则设置为obj_path
并直接返回(之所以向该路径下寻找,是因为默认的AFL的MakeFile在编译时,会定义一个名为AFL_PATH
的宏,该宏会指向该路径); - 如果以上全部失败,抛出异常提示找不到
afl-llvm-rt.o
文件或afl-llvm-pass.so
文件,并要求设置AFL_PATH
环境变量 。
函数的主要功能是在寻找AFL的路径以找到 afl-llvm-rt.o
文件,该文件即为要用到的运行时库。
3. edit_params 函数
该函数的主要作用仍然为编辑参数数组,其控制流程如下:
首先,判断执行的是否为
afl-clang-fast++
:- 如果是,设置
cc_params[0]
为环境变量AFL_CXX
;如果环境变量为空,则设置为clang++
; - 如果不是,设置
cc_params[0]
为环境变量AFL_CC
;如果环境变量为空,则设置为clang
;
- 如果是,设置
判断是否定义了
USE_TRACE_PC
宏,如果有,添加-fsanitize-coverage=trace-pc-guard -mllvm(only Android) -sanitizer-coverage-block-threshold=0(only Android)
选项到参数数组;如果没有,依次将-Xclang -load -Xclang obj_path/afl-llvm-pass.so -Qunused-arguments
选项添加到参数数组;(这里涉及到llvm_mode使用的2种插桩方式:默认使用的是传统模式,使用afl-llvm-pass.so
注入来进行插桩,这种方式较为稳定;另外一种是处于实验阶段的方式——trace-pc-guard
模式,对于该模式的详细介绍可以参考llvm相关文档——tracing-pcs-with-guards)遍历传递给
afl-clang-fast
的参数,进行一定的检查和设置,并添加到cc_params
数组:- 如果存在
-m32
或armv7a-linux-androideabi
,设置bit_mode
为32; - 如果存在
-m64
,设置bit_mode
为64; - 如果存在
-x
,设置x_set
为1; - 如果存在
-fsanitize=address
或-fsanitize=memory
,设置asan_set
为1; - 如果存在
-Wl,-z,defs
或-Wl,--no-undefined
,则直接pass掉。
- 如果存在
检查环境变量是否设置了
AFL_HARDEN
:- 如果有,添加
-fstack-protector-all
选项; - 如果有且没有设置
FORTIFY_SOURCE
,添加-D_FORTIFY_SOURCE=2
选项;
- 如果有,添加
检查参数中是否存在
-fsanitize=memory
,即asan_set
为0:- 如果没有,尝试读取环境变量
AFL_USE_ASAN
,如果存在,添加-U_FORTIFY_SOURCE -fsanitize=address
; - 接下来对环境变量
AFL_USE_MSAN
的处理方式与AFL_USE_ASAN
类似,添加的选项为-U_FORTIFY_SOURCE -fsanitize=memory
;
- 如果没有,尝试读取环境变量
检查是否定义了
USE_TRACE_PC
宏,如果存在定义,检查是否存在环境变量AFL_INST_RATIO
,如果存在,抛出异常AFL_INST_RATIO
无法在trace-pc时使用;- 检查环境变量
AFL_NO_BUILTIN
,如果没有设置,添加-g -O3 -funroll-loops
; - 检查环境变量
AFL_NO_BUILTIN
,如果进行了设置,添加-fno-builtin-strcmp -fno-builtin-strncmp -fno-builtin-strcasecmp -fno-builtin-strcasecmp -fno-builtin-memcmp
; - 添加参数
-D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
; - 定义了两个宏
__AFL_LOOP(), __AFL_INIT()
; - 检查是否设置了
x_set
, 如果有添加-x none
; - 检查是否设置了宏
__ANDORID__
,如果没有,判断bit_mode
的值:- 如果为0,即没有
-m32
和-m64
,添加obj_path/afl-llvm-rt.o
; - 如果为32,添加
obj_path/afl-llvm-rt-32.o
; - 如果为64,添加
obj_path/afl-llvm-rt-64.o
。
- 如果为0,即没有
2. afl-llvm-pass.so.cc
afl-llvm-pass.so.cc
文件实现了 LLVM-mode 下的一个插桩 LLVM Pass。
本文不过多关心如何实现一个LLVM Pass,重点分析该pass的实现逻辑。
该文件只有一个Transform pass:AFLCoverage
,继承自 ModulePass
,实现了一个 runOnModule
函数,这也是我们需要重点分析的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | namespace { class AFLCoverage : public ModulePass { public: static char ID ; AFLCoverage() : ModulePass( ID ) { } bool runOnModule(Module &M) override; / / StringRef getPassName() const override { / / return "American Fuzzy Lop Instrumentation" ; / / } }; } |
1. pass注册
对pass进行注册的部分源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | static void registerAFLPass(const PassManagerBuilder &, legacy::PassManagerBase &PM) { PM.add(new AFLCoverage()); } static RegisterStandardPasses RegisterAFLPass( PassManagerBuilder::EP_ModuleOptimizerEarly, registerAFLPass); static RegisterStandardPasses RegisterAFLPass0( PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass); |
其核心功能为向PassManager注册新的pass,每个pass相互独立。
对于pass注册的细节部分请读者自行研究llvm的相关内容。
2. runOnModule 函数
该函数为该文件中的关键函数,其控制流程图如下:
首先,通过
getContext()
来获取LLVMContext
,获取进程上下文:1234LLVMContext &C
=
M.getContext();
IntegerType
*
Int8Ty
=
IntegerType::getInt8Ty(C);
IntegerType
*
Int32Ty
=
IntegerType::getInt32Ty(C);
设置插桩密度:读取环境变量
AFL_INST_RATIO
,并赋值给inst_ratio
,其值默认为100,范围为 1~100,该值表示插桩概率;获取只想共享内存shm的指针以及上一个基本块的随机ID:
1234567GlobalVariable
*
AFLMapPtr
=
new GlobalVariable(M, PointerType::get(Int8Ty,
0
), false,
GlobalValue::ExternalLinkage,
0
,
"__afl_area_ptr"
);
GlobalVariable
*
AFLPrevLoc
=
new GlobalVariable(
M, Int32Ty, false, GlobalValue::ExternalLinkage,
0
,
"__afl_prev_loc"
,
0
, GlobalVariable::GeneralDynamicTLSModel,
0
, false);
进入插桩过程:
通过
for
循环遍历每个BB(基本块),寻找BB中适合插入桩代码的位置,然后通过初始化IRBuilder
实例执行插入;12BasicBlock::iterator IP
=
BB.getFirstInsertionPt();
IRBuilder<> IRB(&(
*
IP));
随机创建当前BB的ID,然后插入load指令,获取前一个BB的ID;
12345678910111213if
(AFL_R(
100
) >
=
inst_ratio)
continue
;
/
/
如果大于插桩密度,进行随机插桩
/
*
Make up cur_loc
*
/
unsigned
int
cur_loc
=
AFL_R(MAP_SIZE);
ConstantInt
*
CurLoc
=
ConstantInt::get(Int32Ty, cur_loc);
/
/
随机创建当前基本块
ID
/
*
Load prev_loc
*
/
LoadInst
*
PrevLoc
=
IRB.CreateLoad(AFLPrevLoc);
PrevLoc
-
>setMetadata(M.getMDKindID(
"nosanitize"
), MDNode::get(C,
None
));
Value
*
PrevLocCasted
=
IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());
/
/
获取上一个基本块的随机
ID
插入load指令,获取共享内存的地址,并调用
CreateGEP
函数获取共享内存中指定index的地址;123456/
*
Load SHM pointer
*
/
LoadInst
*
MapPtr
=
IRB.CreateLoad(AFLMapPtr);
MapPtr
-
>setMetadata(M.getMDKindID(
"nosanitize"
), MDNode::get(C,
None
));
Value
*
MapPtrIdx
=
IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));
插入load指令,获取对应index地址的值;插入add指令加一,然后创建store指令写入新值,并更新共享内存;
1234567/
*
Update bitmap
*
/
LoadInst
*
Counter
=
IRB.CreateLoad(MapPtrIdx);
Counter
-
>setMetadata(M.getMDKindID(
"nosanitize"
), MDNode::get(C,
None
));
Value
*
Incr
=
IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty,
1
));
IRB.CreateStore(Incr, MapPtrIdx)
-
>setMetadata(M.getMDKindID(
"nosanitize"
), MDNode::get(C,
None
));
右移
cur_loc
,插入store指令,更新__afl_prev_loc
;12345/
*
Set
prev_loc to cur_loc >>
1
*
/
StoreInst
*
Store
=
IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc >>
1
), AFLPrevLoc);
Store
-
>setMetadata(M.getMDKindID(
"nosanitize"
), MDNode::get(C,
None
));
最后对插桩计数加1;
扫描下一个BB,根据设置是否为quiet模式等,并判断
inst_blocks
是否为0,如果为0则说明没有进行插桩;12345678if
(!be_quiet) {
if
(!inst_blocks) WARNF(
"No instrumentation targets found."
);
else
OKF(
"Instrumented %u locations (%s mode, ratio %u%%)."
,
inst_blocks, getenv(
"AFL_HARDEN"
) ?
"hardened"
:
((getenv(
"AFL_USE_ASAN"
) || getenv(
"AFL_USE_MSAN"
)) ?
"ASAN/MSAN"
:
"non-hardened"
), inst_ratio);
}
整个插桩过程较为清晰,没有冗余动作和代码。
3. afl-llvm-rt.o.c
该文件主要实现了llvm_mode的3个特殊功能:deferred instrumentation, persistent mode,trace-pc-guard mode
。
1. deferred instrumentation
AFL会尝试通过只执行一次目标二进制文件来提升性能,在 main()
之前暂停程序,然后克隆“主”进程获得一个稳定的可进行持续fuzz的目标。简言之,避免目标二进制文件的多次、重复的完整运行,而是采取了一种类似快照的机制。
虽然这种机制可以减少程序运行在操作系统、链接器和libc级别的消耗,但是在面对大型配置文件的解析时,优势并不明显。
在这种情况下,可以将 forkserver
的初始化放在大部分初始化工作完成之后、二进制文件解析之前来进行,这在某些情况下可以提升10倍以上的性能。我们把这种方式称为LLVM模式下的 deferred instrumentation
。
首先,在代码中找到可以进行延迟克隆的合适位置。 这需要极端小心地完成,以避免破坏二进制文件。 特别是,如果您在以下情况下选择一个位置,程序可能会出现故障:
首先,在代码中寻找可以进行延迟克隆的合适的、不会破坏原二进制文件的位置,然后添加如下代码:
1 2 3 | #ifdef __AFL_HAVE_MANUAL_CONTROL __AFL_INIT(); #endif |
以上代码插入,在 afl-clang-fast.c
文件中有说明:
1 2 3 4 5 6 7 8 9 10 | cc_params[cc_par_cnt + + ] = "-D__AFL_INIT()=" "do { static volatile char *_A __attribute__((used)); " " _A = (char*)\"" DEFER_SIG "\"; " #ifdef __APPLE__ "__attribute__((visibility(\"default\"))) " "void _I(void) __asm__(\"___afl_manual_init\"); " #else "__attribute__((visibility(\"default\"))) " "void _I(void) __asm__(\"__afl_manual_init\"); " #endif /* ^__APPLE__ */ |
__afl_manual_init()
函数实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | / * This one can be called from user code when deferred forkserver mode is enabled. * / void __afl_manual_init(void) { static u8 init_done; if (!init_done) { __afl_map_shm(); __afl_start_forkserver(); init_done = 1 ; } } |
首先,判断是否进行了初始化,没有则调用 __afl_map_shm()
函数进行共享内存初始化。 __afl_map_shm()
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | / * SHM setup. * / static void __afl_map_shm(void) { u8 * id_str = getenv(SHM_ENV_VAR); / / 读取环境变量 SHM_ENV_VAR 获取 id if (id_str) { / / 成功读取 id u32 shm_id = atoi(id_str); __afl_area_ptr = shmat(shm_id, NULL, 0 ); / / 获取shm地址,赋给 __afl_area_ptr / * Whooooops. * / if (__afl_area_ptr = = (void * ) - 1 ) _exit( 1 ); / / 异常则退出 / * Write something into the bitmap so that even with low AFL_INST_RATIO, our parent doesn't give up on us. * / __afl_area_ptr[ 0 ] = 1 ; / / 进行设置 } } |
然后,调用 __afl_start_forkserver()
函数开始执行forkserver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | / * Fork server logic. * / static void __afl_start_forkserver(void) { static u8 tmp[ 4 ]; s32 child_pid; u8 child_stopped = 0 ; / * Phone home and tell the parent that we 're OK. If parent isn' t there, assume we're not running in forkserver mode and just execute program. * / if (write(FORKSRV_FD + 1 , tmp, 4 ) ! = 4 ) return ; / / 写入 4 字节到状态管道,通知 fuzzer已准备完成 while ( 1 ) { u32 was_killed; int status; / * Wait for parent by reading from the pipe. Abort if read fails. * / if (read(FORKSRV_FD, &was_killed, 4 ) ! = 4 ) _exit( 1 ); / * If we stopped the child in persistent mode, but there was a race condition and afl - fuzz already issued SIGKILL, write off the old process. * / / / 处于persistent mode且子进程已被killed if (child_stopped && was_killed) { child_stopped = 0 ; if (waitpid(child_pid, &status, 0 ) < 0 ) _exit( 1 ); } if (!child_stopped) { / * Once woken up, create a clone of our process. * / child_pid = fork(); / / 重新fork if (child_pid < 0 ) _exit( 1 ); / * In child process: close fds, resume execution. * / if (!child_pid) { close(FORKSRV_FD); / / 关闭fd, close(FORKSRV_FD + 1 ); return ; } } else { / * Special handling for persistent mode: if the child is alive but currently stopped, simply restart it with SIGCONT. * / / / 子进程只是暂停,则进行重启 kill(child_pid, SIGCONT); child_stopped = 0 ; } / * In parent process: write PID to pipe, then wait for child. * / if (write(FORKSRV_FD + 1 , &child_pid, 4 ) ! = 4 ) _exit( 1 ); if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0 ) < 0 ) _exit( 1 ); / * In persistent mode, the child stops itself with SIGSTOP to indicate a successful run. In this case, we want to wake it up without forking again. * / if (WIFSTOPPED(status)) child_stopped = 1 ; / * Relay wait status to pipe, then loop back. * / if (write(FORKSRV_FD + 1 , &status, 4 ) ! = 4 ) _exit( 1 ); } } |
上述逻辑可以概括如下:
首先,设置
child_stopped = 0
,写入4字节到状态管道,通知fuzzer已准备完成;进入
while
,开启fuzz循环:- 调用
read
从控制管道读取4字节,判断子进程是否超时。如果管道内读取失败,发生阻塞,读取成功则表示AFL指示forkserver执行fuzz; 如果
child_stopped
为0,则fork出一个子进程执行fuzz,关闭和控制管道和状态管道相关的fd,跳出fuzz循环;如果
child_stopped
为1,在persistent mode
下进行的特殊处理,此时子进程还活着,只是被暂停了,可以通过kill(child_pid, SIGCONT)
来简单的重启,然后设置child_stopped
为0;- forkserver向状态管道
FORKSRV_FD + 1
写入子进程的pid,然后等待子进程结束; WIFSTOPPED(status)
宏确定返回值是否对应于一个暂停子进程,因为在persistent mode
里子进程会通过SIGSTOP
信号来暂停自己,并以此指示运行成功,我们需要通过SIGCONT
信号来唤醒子进程继续执行,不需要再进行一次fuzz,设置child_stopped
为1;- 子进程结束后,向状态管道
FORKSRV_FD + 1
写入4个字节,通知AFL本次执行结束。
- 调用
2. persistent mode
persistent mode
并没有通过fork子进程的方式来执行fuzz。一些库中提供的API是无状态的,或者可以在处理不同输入文件之间进行重置,恢复到之前的状态。执行此类重置时,可以使用一个长期存活的进程来测试多个用例,以这种方式来减少重复的 fork()
调用和操作系统的开销。不得不说,这种思路真的很优秀。
一个基础的框架大概如下:
1 2 3 4 5 6 7 8 9 | while (__AFL_LOOP( 1000 )) { / * Read input data. * / / * Call library code to be fuzzed. * / / * Reset state. * / } / * Exit normally * / |
设置一个 while
循环,并指定循环次数。在每次循环内,首先读取数据,然后调用想fuzz的库代码,然后重置状态,继续循环。(本质上也是一种快照。)
对于循环次数的设置,循环次数控制了AFL从头重新启动过程之前的最大迭代次数,较小的循环次数可以降低内存泄漏类故障的影响,官方建议的数值为1000。(循环次数设置过高可能出现较多意料之外的问题,并不建议设置过高。)
一个 persistent mode
的样例程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <string.h> / * Main entry point. * / int main( int argc, char * * argv) { char buf[ 100 ]; / * Example - only buffer , you'd replace it with other global or local variables appropriate for your use case. * / while (__AFL_LOOP( 1000 )) { / * * * PLACEHOLDER CODE * * * / / * STEP 1 : 初始化所有变量 * / memset(buf, 0 , 100 ); / * STEP 2 : 读取输入数据,从文件读入时需要先关闭旧的fd然后重新打开文件 * / read( 0 , buf, 100 ); / * STEP 3 : 调用待fuzz的code * / if (buf[ 0 ] = = 'f' ) { printf( "one\n" ); if (buf[ 1 ] = = 'o' ) { printf( "two\n" ); if (buf[ 2 ] = = 'o' ) { printf( "three\n" ); if (buf[ 3 ] = = '!' ) { printf( "four\n" ); abort(); } } } } / * * * END PLACEHOLDER CODE * * * / } / * 循环结束,正常结束。AFL会重启进程,并清理内存、剩余fd等 * / return 0 ; } |
宏定义 __AFL_LOOP
内部调用 __afl_persistent_loop
函数:
1 2 3 4 5 6 7 8 9 10 11 | cc_params[cc_par_cnt + + ] = "-D__AFL_LOOP(_A)=" "({ static volatile char *_B __attribute__((used)); " " _B = (char*)\"" PERSIST_SIG "\"; " #ifdef __APPLE__ "__attribute__((visibility(\"default\"))) " "int _L(unsigned int) __asm__(\"___afl_persistent_loop\"); " #else "__attribute__((visibility(\"default\"))) " "int _L(unsigned int) __asm__(\"__afl_persistent_loop\"); " #endif /* ^__APPLE__ */ "_L(_A); })" ; |
__afl_persistent_loop(unsigned int max_cnt)
的逻辑如下:
结合源码梳理一下其逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | / * A simplified persistent mode handler, used as explained in README.llvm. * / int __afl_persistent_loop(unsigned int max_cnt) { static u8 first_pass = 1 ; static u32 cycle_cnt; if (first_pass) { if (is_persistent) { memset(__afl_area_ptr, 0 , MAP_SIZE); __afl_area_ptr[ 0 ] = 1 ; __afl_prev_loc = 0 ; } cycle_cnt = max_cnt; first_pass = 0 ; return 1 ; } if (is_persistent) { if ( - - cycle_cnt) { raise (SIGSTOP); __afl_area_ptr[ 0 ] = 1 ; __afl_prev_loc = 0 ; return 1 ; } else { __afl_area_ptr = __afl_area_initial; } } return 0 ; } |
首先判读是否为第一次执行循环,如果是第一次:
- 如果
is_persistent
为1,清空__afl_area_ptr
,设置__afl_area_ptr[0]
为1,__afl_prev_loc
为0; - 设置
cycle_cnt
的值为传入的max_cnt
参数,然后设置first_pass=0
表示初次循环结束,返回1;
- 如果
如果不是第一次执行循环,在 persistent mode 下,且
--cycle_cnt
大于1:- 发出信号
SIGSTOP
让当前进程暂停 设置
__afl_area_ptr[0]
为1,__afl_prev_loc
为0,然后直接返回1如果
cycle_cnt
为0,设置__afl_area_ptr
指向数组__afl_area_initial
。
- 发出信号
最后返回0
重新总结一下上面的逻辑:
- 第一次执行loop循环,进行初始化,然后返回1,此时满足
while(__AFL_LOOP(1000)
, 于是执行一次fuzz,计数器cnt减1,抛出SIGSTOP信号暂停子进程; - 第二次执行loop循环,恢复之前暂停的子进程继续执行,并设置
child_stopped
为0。此时相当于重新执行了一次程序,重新对__afl_prev_loc
进行设置,随后返回1,再次进入while(_AFL_LOOP(1000))
,执行一次fuzz,计数器cnt减1,抛出SIGSTOP信号暂停子进程; - 第1000次执行,计数器cnt此时为0,不再暂停子进程,令
__afl_area_ptr
指向无关数组__afl_area_initial
,随后子进程结束。
3. trace-pc-guard mode
该功能的使用需要设置宏 AFL_TRACE_PC=1
,然后再执行 afl-clang-fast
时传入参数 -fsanitize-coverage=trace-pc-guard
。
该功能的主要特点是会在每个edge插入桩代码,函数 __sanitizer_cov_trace_pc_guard
会在每个edge进行调用,该函数利用函数参数 guard
指针所指向的 uint32
值来确定共享内存上所对应的地址:
1 2 3 | void __sanitizer_cov_trace_pc_guard(uint32_t * guard) { __afl_area_ptr[ * guard] + + ; } |
guard
的初始化位于函数 __sanitizer_cov_trace_pc_guard_init
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | void __sanitizer_cov_trace_pc_guard_init(uint32_t * start, uint32_t * stop) { u32 inst_ratio = 100 ; u8 * x; if (start = = stop || * start) return ; x = getenv( "AFL_INST_RATIO" ); if (x) inst_ratio = atoi(x); if (!inst_ratio || inst_ratio > 100 ) { fprintf(stderr, "[-] ERROR: Invalid AFL_INST_RATIO (must be 1-100).\n" ); abort(); } * (start + + ) = R(MAP_SIZE - 1 ) + 1 ; while (start < stop) { / / 这里如果计算stop - start,就是程序里总计的edge数 if (R( 100 ) < inst_ratio) * start = R(MAP_SIZE - 1 ) + 1 ; else * start = 0 ; start + + ; } } |
总结
其实在这里最主要的还是 persistent mode
中的逻辑,其他的两个模式对于实际的fuzz工作已经或尚且没有太大的指导意义。但是研究一下其源码实现,可以发现AFL的设计和开发真的很巧妙,其中很多思路值得开发人员和安全人员学习和借鉴。
参考链接
- https://eternalsakura13.com/2020/08/23/afl/
- https://bbs.pediy.com/thread-266025.htm
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课