AFL源码分析(2)-LLVM插桩模块源码分析

这篇文章来讲讲 AFL 中 LLVM 模式插桩的实现。与 GCC 模式相比,LLVM 模式的插桩实现更加优雅,拓展起来更加灵活。所有要对 AFL 进行拓展,那么就有必要详细了解 LLVM 模式插桩的实现。这部分主要有三个文件alf-clang-fast.c, alf-llvm-pass.so.cc, afl-llvm-rt.o.c,同时有必要理解一些afl-fuzz.c内的代码。

0x00. 环境安装

llvm_mode这部分要单独安装,因为Ubuntu 20.04上clang的最低版本是7,所以我这里用的就是clang-7,实测能够编译通过:

1
2
3
4
5
6
7
8
9
apt install clang-7
export LLVM_CONFIG=`which llvm-config-7`
export CC=clang-7
export CXX=clang-7++
cd llvm_mode
make clean
make
cd ..
make install

0x01. afl-clang-fast.c

这一个文件本质上是clang/clang++的包装,跟afl-gcc类似。

find_obj

搜索afl-llvm-rt.o所在路径,与afl-gcc中的find_as类似。

afl-llvm-rt.o会在编译 fuzz target 时被链接进去。

edit_params

这个函数的作用是编辑参数,我们可以把所有的参数打印下来看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PARAM0: clang
PARAM1: -Xclang
PARAM2: -load
PARAM3: -Xclang
PARAM4: /usr/local/lib/afl/afl-llvm-pass.so
PARAM5: -Qunused-arguments
PARAM6: hello.c
PARAM7: -o
PARAM8: hello
PARAM9: -g
PARAM10: -O3
PARAM11: -funroll-loops
PARAM12: -D__AFL_HAVE_MANUAL_CONTROL=1
PARAM13: -D__AFL_COMPILER=1
PARAM14: -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
PARAM15: -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); })
PARAM16: -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)
PARAM17: /usr/local/lib/afl/afl-llvm-rt.o

第一部分是-Xclang -load -Xclang /usr/local/lib/afl/afl-llvm-pass.so。如果已经对 LLVM 比较熟悉的话就会知道这一部分加载了afl-llvm-pass.so这个 LLVM Pass 。

第二部分是:

1
2
3
4
5
6
-D__AFL_HAVE_MANUAL_CONTROL=1
-D__AFL_COMPILER=1
-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=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)

这里通过编译器参数-D直接插入了一些宏定义,涉及到 AFL 的 deferred instrumentation 和 persistent mode 。有关这两个模式的内容待会再展开说。

第三部分是/usr/local/lib/afl/afl-llvm-rt.o,这里并没有指定某个参数,不过根据反编译的结果来看这个参数的作用是将这个.o文件链接到编译产物中。

0x02. afl-llvm-pass.so.cc

这一部分的代码就是我比较熟悉的 LLVM Pass 了,代码核心逻辑在runOnModule函数内。

首先获取AFL_INST_RATIO参数,其含义为在每个基本块的开头插桩的概率。这里是通过环境变量获取的,而不是 clang 参数。默认值为 100:

1
2
char* inst_ratio_str = getenv("AFL_INST_RATIO");
unsigned int inst_ratio = 100;

定义两个32位整数全局变量__afl_area_ptr__afl_prev_loc,分别标记当前基本块和前一个基本块:

1
2
3
4
5
6
7
8
9
10
/* Get globals for the SHM region and the previous location. Note that
__afl_prev_loc is thread-local. */

GlobalVariable *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);

遍历函数中的每一个基本块:

1
2
for (auto &F : M)
for (auto &BB : F) {

接下来对每一个基本块进行插桩,用来记录代码覆盖率 (Code Coverage)。这一块就不细说了,注释给的非常明显:

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
unsigned int cur_loc = AFL_R(MAP_SIZE);

ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);

/* Load prev_loc */

LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());

/* 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));

/* 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));

/* 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));

inst_blocks++;

总的来说,插桩的代码跟以下代码等价:

1
2
3
cur_location = <COMPILE_TIME_RANDOM>; 
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;

可以看到 LLVM 模式下的插桩并不像 GCC 模式下调用了__afl_maybe_log这样的函数,原因是在 LLVM 模式下 fork server 的启动被移到了其他地方,待会细说。

0x03. afl-llvm-rt.o.c

__afl_auto_init

在编译 fuzz target 时,afl-llvm-rt.o也会被同时链接。也就是说afl-llvm-rt.o.c里定义的函数会被塞到 fuzz target 里去。

其中有一个函数最为特殊:

1
2
3
4
5
6
7
8
9
10
/* Proper initialization routine. */

__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {

is_persistent = !!getenv(PERSIST_ENV_VAR);

if (getenv(DEFER_ENV_VAR)) return;
__afl_manual_init();

__attribute__((constructor))用法解析

标记__attribute__((constructor(CONST_PRIO)))即代表这个函数要做 main 函数之前执行。详见__attribute__((constructor))用法解析,这个函数完成的工作是 fork server 的初始化。

这里的PERSIST_ENV_VARDEFER_ENV_VAR涉及 LLVM 插桩模式下的两种子模式 Deferred Instrumentation 和 Persistent Mode 。详情见AFL官方文档,接下来就简单讲一下这两种模式。

Deferred Instrumentation

简单来说 Deferred Instrumentation 即在特定的位置完成 fork server 的初始化,提高 fuzz 效率:

1
2
3
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif

__AFL_INIT()宏定义如下,可以看到其包含了一个特殊的字符串标记"##SIG_AFL_DEFER_FORKSRV##"

1
2
3
4
5
6
#define __AFL_INIT() 
do {
static char *_A;
_A = (char*)"##SIG_AFL_DEFER_FORKSRV##";
__afl_manual_init();
} while (0)

afl-fuzz在对目标进行 fuzz 时,会检查目标文件是否包含##SIG_AFL_DEFER_FORKSRV##字符串,以判断是否需要使用 Deferred Instrumentation 模式:

1
2
3
4
5
6
7
8
9
10
11
if (memmem(f_data, f_len, DEFER_SIG, strlen(DEFER_SIG) + 1)) {

OKF(cPIN "Deferred forkserver binary detected.");
setenv(DEFER_ENV_VAR, "1", 1);
deferred_mode = 1;

} else if (getenv("AFL_DEFER_FORKSRV")) {

WARNF("AFL_DEFER_FORKSRV is no longer supported and may misbehave!");

}

Persistent Mode

Persistent Mode 认为需要 fuzz 的 API 是无状态的,用循环代替 fork 以减少 fuzz 开销。用法如下:

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 */

__AFL_LOOP的宏定义如下,可以看到主要是设置了一个 Persistent Mode 字符串表示以及调用__afl_persistent_loop函数:

1
2
3
4
5
6
#define __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);
})

__afl_persistent_loop

如果first_pass = 1,即第一次进入循环,则:

  • 初始化共享内存__afl_area_ptr和变量__afl_prev_loc

后续的循环中,每次均将循环计数器cycle_cnt减一:

  • 如果cycle_cnt不为0,则:

    • 调用raise(SIGSTOP)发送SIGSTOP信号让当前进程暂停
    • 重新初始化共享内存__afl_area_ptr和变量__afl_prev_loc
  • 如果cycle_cnt为0,则:

    • __afl_area_ptr指向一个无关指针__afl_area_initial

通过raise(SIGSTOP)挂起进程后,__afl_start_forkserver函数中,即 fork server 中会捕获这个信号:

1
2
if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0)
_exit(1);

可以看到,在 Persistent Mode 下,waitpid函数传入了WUNTRACED选项:

WUNTRACED

also return if a child has stopped (but not traced via ptrace(2)). Status for traced children which have stopped is provided even if this option is not specified.

接下来判断接收到的是不是SIGSTOP信号,如果是,将child_stopped设置为1:

1
if (WIFSTOPPED(status)) child_stopped = 1;

在下一次混淆中,fork server 会给子进程发送SIGCONT信号,使其继续执行,而不是再次 fork ,通过种子方法减少 fork 带来的开销:

1
2
3
4
5
6
7
8
9
else { // child_stopped

/* 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;

}

__afl_manual_init

这个函数的代码很简单,主要就是调用了__afl_map_shm__afl_start_forkserver两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
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-fuzz.c中初始化的共享内存
  • __afl_start_forkserver():初始化 fork server

接下来主要就讲一下__afl_start_forkserver()__afl_map_shm比较简单就不说了。

__afl_start_forkserver()

这里要结合afl-fuzz.c中的run_target函数才能完全能看懂,因为 fork server 和 afl-fuzz 还需要通信。

首先明确两个通信管道:

  • FORKSRV_FD:即198,控制管道,只允许 afl-fuzz 向 fork server 写命令
  • FORKSRV_FD+1:即199,状态管道,只允许 fork server 向 afl-fuzz 写状态

另外afl-fuzz的大致实现也要了解一下,因为 fork server 和 afl-fuzz 需要互相通信。这里有必要请出 Understand 工具了,可以通过这个工具的 Calls Graph 找到一些关键的函数调用:

image-20220422220245572

fork server 和 afl-fuzz 的通信过程以及 fork server 的工作流程大致是这样:

fork server 向状态管道写入一个四字节的随机数据,表示 fork server 可以开始工作了:

1
2
3
4
/* 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;

afl-fuzz 从状态管道读取四字节的数据,确认 fork server 正常工作:

1
2
3
4
5
6
7
8
9
10
11
rlen = read(fsrv_st_fd, &status, 4);

...

/* If we have a four-byte "hello" message from the server, we're all set.
Otherwise, try to figure out what went wrong. */

if (rlen == 4) {
OKF("All right - fork server is up.");
return;
}

fork server 开始循环从控制管道接受来自 afl-fuzz 的命令,如果接收到来自 afl-fuzz 的数据是 true,那么结束 fork server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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. */

if (child_stopped && was_killed) {
child_stopped = 0;
if (waitpid(child_pid, &status, 0) < 0) _exit(1);
}

afl-fuzz 的逻辑有点复杂,但整个代码只有一个地方往控制管道写了数据,所以也能轻易找到。根据变量名推测这一段通信的作用是判断 fuzz 是否超时:

1
if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4) {

然后不管是啥模式,首先都要进行一次 fork。fork 之后子进程从 fork server 中退出,执行正常的程序功能,父进程则继续担任 fork server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (!child_stopped) {

/* Once woken up, create a clone of our process. */

child_pid = fork();
if (child_pid < 0) _exit(1);

/* In child process: close fds, resume execution. */

if (!child_pid) {

close(FORKSRV_FD);
close(FORKSRV_FD + 1);
return;

}

}

然后 fork server 将子进程 pid 写入状态管道,并通过 waitpid 等待子进程执行完毕。在 President Mode 下这里会有所不同,之前已经讲过了,这里不再赘述:

1
2
3
4
if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) _exit(1);

if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0)
_exit(1);

随后是 President Mode 下的特殊处理,不再赘述,以及最后向状态管道写入数据,表示这一轮 fuzz 已经结束,准备开始下一轮:

1
2
3
4
5
6
7
8
9
/* 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);

afl-fuzz 接收来自状态管道的数据,开始下一轮 fuzz:

1
2
3
4
5
6
if ((res = read(fsrv_st_fd, &status, 4)) != 4) {

if (stop_soon) return 0;
RPFATAL(res, "Unable to communicate with fork server (OOM?)");

}

如果不是 President Mode ,则 fork server 继续 fork 新的子进程进行 fuzz,如果是 President Mode 则简单发送一个 SIGCONT继续让程序运行。

以上就是 fork server 与 afl-fuzz 的大致通信流程,有关 afl-fuzz 的完整工作原理我们在下一篇博客讲解。