afl-llvm

llvm mode

afl-llvm

AFL的 llvm_mode 可以实现编译器级别的插桩,可以替代 afl-gcc 或 afl-clang 使用的比较粗暴的汇编级别的重写的方法,且具备如下几个优势:

  1. 编译器可以进行很多优化以提升效率;
  2. 可以实现CPU无关,可以在非 x86 架构上进行fuzz;
  3. 可以更好地处理多线程目标。

afl-clang-fast

整体实现思路和afl-gcc没什么太大的区别

edit_param将参数参数换成clang的

加载so文件

1
$ clang -Xclang load -Xclang afl-llvm-pass.so  ...

然后环境变量和参数和 afl-gcc大差不差

特性:USE_TRACE_PC

1
-fsanitize-coverage=trace-pc-guard -mllvm(only Android) -sanitizer-coverage-block-threshold=0(only Android)

定义了两个比较重要的宏 __AFL_LOOP(_A)__AFL_INIT()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  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); })";

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__ */
"_I(); } while (0)";

Pass

LLVM Pass: 在Pass遍历LLVM IR的同时,自然就可以往里面插入新的代码。

实现了一个pass,AFLCoverage,和插桩和覆盖率统计有关

1
2
3
4
5
6
7
8
9
10
11
12
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";
// }
};
}

获取全局变量,也就是SHM全局变量

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

pass ir每一个函数(Function),每个基本块(Basic Block),目的是为了插桩统计覆盖率

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
  for (auto &F : M)
for (auto &BB : F) {

// 寻找BB中适合插入桩代码的位置,然后通过初始化 IRBuilder 实例执行插入;
// getFirstInsertionPt 获得插入点
BasicBlock::iterator IP = BB.getFirstInsertionPt();
// IRBuild IR进行插桩
IRBuilder<> IRB(&(*IP));
// 随机插桩
if (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);

// 如下的3步就是更新SHM,之前位置,当前位置,更新map
/* 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));
// A->B != B->A
/* 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++;

}

/* Say something nice. */

if (!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);

}

return true;

}

插桩部分:IRBuilder

1
$ clang -Xclang load -Xclang afl-llvm-pass.so ...

需要一点LLVM的知识

构建pass.so -> 使用 so 编译样本 -> 构建运行时(rt) -> 链接生成可执行文件

IRB.CreateXxx 插入了 Xxx 类型的指令,类似汇编语言

CreateLoad: load xxx 获取地址
CreateXor: xor a1, a2

llvm-rt

main函数执行find_obj,寻找runtime library,与afl-as功能类似

该文件是运行时库文件,实现了 llvm-mode 的3个特殊功能:deferred instrumentationpersistent modetrace-pc-guard mode

重要的变量

1
2
3
4
u8  __afl_area_initial[MAP_SIZE];
u8* __afl_area_ptr = __afl_area_initial;

__thread u32 __afl_prev_loc;
deferred instrumentation

AFL会尝试通过只执行一次目标二进制文件来提升性能,在 main() 之前暂停程序,然后克隆“主”进程获得一个稳定的可进行持续fuzz的目标。简言之,避免目标二进制文件的多次、重复的完整运行,而是采取了一种类似快照的机制。

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

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {

is_persistent = !!getenv(PERSIST_ENV_VAR);

if (getenv(DEFER_ENV_VAR)) return;

__afl_manual_init();

}

void __afl_manual_init(void) {

static u8 init_done;

if (!init_done) {

__afl_map_shm();
__afl_start_forkserver();
init_done = 1;

}

}

获得fuzzer创建的共享内存

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
static void __afl_map_shm(void) {

u8 *id_str = getenv(SHM_ENV_VAR);

/* If we're running under AFL, attach to the appropriate region, replacing the
early-stage __afl_area_initial region that is needed to allow some really
hacky .init code to work correctly in projects such as OpenSSL. */

if (id_str) {

u32 shm_id = atoi(id_str);

__afl_area_ptr = shmat(shm_id, NULL, 0);

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

}

}

forkserver:pipe与fuzzer进程进行通信,然后fork(),子进程运行测试程序,父进程监视,与fuzzer进行通信

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

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);
}

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;

}

} 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);

}

}

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

一个样例

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;

}

主要的逻辑

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
/* 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) { // 第一次pass

/* Make sure that every iteration of __AFL_LOOP() starts with a clean slate.
On subsequent calls, the parent will take care of that, but on the first
iteration, it's our job to erase any trace of whatever happened
before the loop. */

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; // 设置为0
return 1;

}

// 不是第一次pass
if (is_persistent) {

if (--cycle_cnt) {

raise(SIGSTOP); // 让当前进程暂停

__afl_area_ptr[0] = 1;
__afl_prev_loc = 0;

return 1;

} else { // cycle_cnt 结束

/* When exiting __AFL_LOOP(), make sure that the subsequent code that
follows the loop is not traced. We do that by pivoting back to the
dummy output region. */

__afl_area_ptr = __afl_area_initial;

}

}

return 0;

}

逻辑

  • 第一次执行loop循环,进行初始化,然后返回1。
  • 执行一次fuzz,计数器cnt减1,抛出SIGSTOP信号暂停子进程;
  • 其余执行loop循环,恢复之前暂停的子进程继续执行(forkserver),并设置 child_stopped 为0。此时相当于重新执行了一次程序,重新对 __afl_prev_loc 进行设置,随后返回1,再次进入 while(_AFL_LOOP(1000)) ,执行一次fuzz,计数器cnt减1,抛出SIGSTOP信号暂停子进程;
  • 第1000次执行,计数器cnt此时为0,不再暂停子进程,令 __afl_area_ptr 指向无关数组 __afl_area_initial ,随后子进程结束。
trace-pc

该模式需要先构建 afl-clang-fast 时指定 AFL_TRACE_PC=1,在使用 afl-clang-fast 时加上 fsanitize-coverage=trace-pc-guard 参数来开启该功能。这种模式下的插桩,会在每个 edge 处都进行插桩,而不再是基本块。

afl-clang-lto

aflplusplus

LTO(Link Time Optimization)链接时优化是链接期间的程序优化,多个中间文件通过链接器合并在一起,并将它们组合为一个程序,缩减代码体积,因此链接时优化是对整个程序的分析和跨模块的优化。

源码安装afl-clang-lto,需要安装 llvmlld (version >= 11)

1
$ sudo apt install llvm lld

然后根据AFLplusplus/instrumentation/README.lto.md

1
2
3
4
# 需要注意这个版本,需要先查看一下
$ export LLVM_CONFIG=llvm-config-16
$ make
$ sudo make install

源码:todo

参考文章