Fuzzing

本文最后更新于:2023年2月26日 晚上

初探Fuzz

安装

官网下载
https://lcamtuf.coredump.cx/afl/
然后安装
tar zxvf afl-2.52b.tgz
make
sudo make install
安装结束

初步使用AFL

样例程序(靶)

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
#include <stdio.h> 
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

int vuln(char *str)
{
int len = strlen(str);
if(str[0] == 'A' && len == 66)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为A并且长度为66,则异常退出
}
else if(str[0] == 'F' && len == 6)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为F并且长度为6,则异常退出
}
else
{
printf("it is good!\n");
}
return 0;
}

int main(int argc, char *argv[])
{
char buf[100]={0};
gets(buf);//存在栈溢出漏洞
printf(buf);//存在格式化字符串漏洞
vuln(buf);

return 0;
}

编译程序

1
afl-gcc -g -o afl_test afl_test.c

建立读入、输出文件夹

如fuzz_in,fuzz_out

开始fuzz

afl-fuzz -i fuzz_in -o fuzz_out ./afl_test
接下来会报错
Pipe at the beginning of 'core_pattern'
进行设置
sudo su
echo core >/proc/sys/kernel/core_pattern
再次执行

1
afl-fuzz -i fuzz_in -o fuzz_out ./afl_test

即可进入AFL仪表盘

AFL 仪表盘

AFL仪表盘

关注
stage progress
若exec speed 低于600,则速度太慢
unique crashes
发现的路径数

分析结果

Ctrl+C
结束运行
进入fuzz_out
输入xxd id:.........
查看结果,并分析

AFL源码分析

afl-gcc.c

afl-gcc.c是封装(wrapper)了gcc的一个文件
afl-gcc会寻找、配置环境变量以及许多配置数据。
变量:

1
2
3
4
5
6
static u8*  as_path;                /* Path to the AFL 'as' wrapper      */ as的路径,接下来的函数find_as就是为了确定这个参数
static u8** cc_params; /* Parameters passed to the real CC */
static u32 cc_par_cnt = 1; /* Param count, including argv0 */
static u8 be_quiet, /* Quiet mode */
clang_mode; /* Invoked as afl-clang*? */

函数 find_as(u8* argv0):

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
static void find_as(u8* argv0) {

u8 *afl_path = getenv("AFL_PATH"); //getenv()函数,直接寻找环境变量中有无目标值,有就以字符串+null形式返回,无则直接返回null
u8 *slash, *tmp;// slash 斜杠 “\”

if (afl_path) {

tmp = alloc_printf("%s/as", afl_path);//将找到的路径与/as拼接,尝试找到目标程序

if (!access(tmp, X_OK)) { // access()函数会尝试运行目标程序,成功返回值为0,否则返回-1,本句意为若通过环境变量找到了as,就...
as_path = afl_path;
ck_free(tmp);
return;
}

ck_free(tmp);

}

slash = strrchr(argv0, '/');//找到最后一次出现字符'/'的位置并返回之

if (slash) {//若找到'/'

u8 *dir;

*slash = 0;
dir = ck_strdup(argv0);//argv0是int main(int argc, char**argv)的第一个参数
*slash = '/';

tmp = alloc_printf("%s/afl-as", dir);//拼接运行时给入的地址与as

if (!access(tmp, X_OK)) {
as_path = dir;
ck_free(tmp);
return;
}

ck_free(tmp);
ck_free(dir);

}

if (!access(AFL_PATH "/as", X_OK)) {
as_path = AFL_PATH;
return;
}

FATAL("Unable to find AFL wrapper binary for 'as'. Please set AFL_PATH");//若找不到as,抛出错误

}

即本函数是寻找assembler的过程,没找到就报错

函数edit_params

  • 该函数将arvg[0]中的最后’'后的字符串赋给name,根据一系列判断,确定’afl-lang’ or ‘afl-lang++’ or 其他平台,如APPLE等。
  • 即读取第一个参数,然后确定平台、方式
  • 随后进入while循环,读取argv[1]开始的参数
  • 若为’-B’,提示"-B is already set, overriding"
  • 若为’-integrated-as’,略过
  • 等等

离开while循环后
若为clang_mode,设置cc_params[cc_par_cnt++] = '-no-integranted-as';
如果存在环境变量 AFL_HARDEN,则设置-fstack-protector-all。且如果没有设置 fortify_set ,追加-D_FORTIFY_SOURCE=2

之后还要一系列参数设置
总而言之,该函数是为了将参数设置好,符合运行环境以及指令。

main函数

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
int main(int argc, char** argv) {

if (isatty(2) && !getenv("AFL_QUIET")) {

SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <lcamtuf@google.com>\n");

} else be_quiet = 1;

if (argc < 2) {

SAYF("\n"
"This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"
"for gcc or clang, letting you recompile third-party code with the required\n"
"runtime instrumentation. A common use pattern would be one of the following:\n\n"

" CC=%s/afl-gcc ./configure\n"
" CXX=%s/afl-g++ ./configure\n\n"

"You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.\n"
"Setting AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",
BIN_PATH, BIN_PATH);

exit(1);

}

find_as(argv[0]);

edit_params(argc, argv);

execvp(cc_params[0], (char**)cc_params);//调用以上两个函数,完成配置

FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);

return 0;

}

afl-as.c

参数

1
2
3
4
5
6
7
8
9
10
11
12
13
static u8** as_params;          /* Parameters passed to the real 'as'   */ // 传给as的参数

static u8* input_file; /* Originally specified input file */
static u8* modified_file; /* Instrumented file for the real 'as' */ //as进行插桩的文件

static u8 be_quiet, /* Quiet mode (no stderr output) */
clang_mode, /* Running in clang mode? */
pass_thru, /* Just pass data through? */
just_version, /* Just show version? */
sanitizer; /* Using ASAN / MSAN */

static u32 inst_ratio = 100, /* Instrumentation probability (%) */ // 插桩覆盖率
as_par_cnt = 1; /* Number of params to 'as' */

函数edit_params
同afl-gcc中一致,进行参数配置

函数add_instrumentation
插桩函数,此函数负责处理输入文件,生成modified_file,将instrumentations插入所有适当位置。

  • 读取input-file

    1
    2
    3
    4
    5
    6
    7
    if (input_file) {

    inf = fopen(input_file, "r");
    if (!inf) PFATAL("Unable to read '%s'", input_file);

    } else inf = stdin;
    //若打开失败,报错无法读取,或直接从stdin读取
  • 写入modified-file

    1
    2
    3
    4
    5
    6
    7
    outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600);

    if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file);

    outf = fdopen(outfd, "w");

    if (!outf) PFATAL("fdopen() failed");

    接下来看看下一段

真正有趣的地方,插桩逻辑开始之处

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
/* All right, this is where the actual fun begins. For one, we only want to
instrument the .text section. So, let's keep track of that in processed
files - and let's set instr_ok accordingly. */

if (line[0] == '\t' && line[1] == '.') {

/* OpenBSD puts jump tables directly inline with the code, which is
a bit annoying. They use a specific format of p2align directives
around them, so we use that as a signal. */

if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
isdigit(line[10]) && line[11] == '\n') skip_next_label = 1;

if (!strncmp(line + 2, "text\n", 5) ||
!strncmp(line + 2, "section\t.text", 13) ||
!strncmp(line + 2, "section\t__TEXT,__text", 21) ||
!strncmp(line + 2, "section __TEXT,__text", 21)) {
instr_ok = 1;
continue;
}

if (!strncmp(line + 2, "section\t", 8) ||
!strncmp(line + 2, "section ", 8) ||
!strncmp(line + 2, "bss\n", 4) ||
!strncmp(line + 2, "data\n", 5)) {
instr_ok = 0;
continue;
}

}
  • 由于插桩只向.text部分插入,故需要进行该部分的匹配(确认是否在此部分中)
  • 即匹配text section\t.text 等四个str,若匹配成功,continue跳出,进行下一次循环
  • 若匹配失败,再与非.text字段进行匹配,若匹配成功,证明不在.text段内,也continue跳出
  • 上述两种情况,参数instr_ok分别为1、0,即代表是否位于理想字段内。

后续进行三个判定,来确认一些参数的值。

  • 插桩时关注的重点是
    1
    2
    3
    4
    ^main:      - function entry point (always instrumented)
    ^.L0: - GCC branch label
    ^.LBB0_0: - clang branch label (but only in clang mode)
    ^\tjnz foo - conditional branches
    故检测形如\tj[^m]的格式的命令,即为条件跳转命令。当随机数小于覆盖率时,会将trampoline_fmt_64写入R(MAP_SIZE)位进入文件,随后插桩计数ins_lines加一,continue跳出。
    对于clang,APPLE是相似的过程,即适用于不同编译系统

最后回到while循环的开始处
从刚刚就一直困扰我的问题——判断条件合适(位于.text中),却跳转进下一个循环是为什么,得到了解决。
可以看到

1
2
3
4
5
6
7
8
9
10
if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
instrument_next && line[0] == '\t' && isalpha(line[1])) {

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE));

instrument_next = 0;
ins_lines++;

}

循环开始时便进行了一次条件检测,当合适时,进行插桩,再重新布置参数,使得不漏一处。

至此,插桩的逻辑基本完成。
总结:插桩是通过遍历目标文件行,寻找敏感处并进行标记,于本次循环或下次循环进行插桩,然后再去寻找下处可插处,直至lines遍历完毕

afl-as.h

上面读了两个文件中描述了插桩过程的逻辑,其中真正的插桩,trampoline_fmt_64/32是关键所在(插入了什么内容?)

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
```c
static const u8* trampoline_fmt_32 =

"\n"
"/* --- AFL TRAMPOLINE (32-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leal -16(%%esp), %%esp\n"
"movl %%edi, 0(%%esp)\n"
"movl %%edx, 4(%%esp)\n"
"movl %%ecx, 8(%%esp)\n"
"movl %%eax, 12(%%esp)\n"
"movl $0x%08x, %%ecx\n"
"call __afl_maybe_log\n"
"movl 12(%%esp), %%eax\n"
"movl 8(%%esp), %%ecx\n"
"movl 4(%%esp), %%edx\n"
"movl 0(%%esp), %%edi\n"
"leal 16(%%esp), %%esp\n"
"\n"
"/* --- END --- */\n"
"\n";

static const u8* trampoline_fmt_64 =

"\n"
"/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leaq -(128+24)(%%rsp), %%rsp\n"
"movq %%rdx, 0(%%rsp)\n"
"movq %%rcx, 8(%%rsp)\n"
"movq %%rax, 16(%%rsp)\n" // save rdx rcx rax
"movq $0x%08x, %%rcx\n" // set what will be print by fprintf() in rcx
"call __afl_maybe_log\n"
"movq 16(%%rsp), %%rax\n"
"movq 8(%%rsp), %%rcx\n"
"movq 0(%%rsp), %%rdx\n" // 恢复寄存器
"leaq (128+24)(%%rsp), %%rsp\n"
"\n"
"/* --- END --- */\n"
"\n";

其中,真正的核心是call __afl_maybe_log
这个指令来自main_payload_64/32
__afl_maybe_log
其中__afl_maybe_log定义如下

1
2
3
4
5
6
7
8
9
10
"__afl_maybe_log:\n"
"\n"
" lahf\n"
" seto %al\n"
"\n"
" /* Check if SHM region is already mapped. */\n"//检查共享内存是否进行了设置
"\n"
" movq __afl_area_ptr(%rip), %rdx\n"
" testq %rdx, %rdx\n" // 判断__afl_area_ptr是否为null
" je __afl_setup\n" // 为空则跳转

使用lahf指令,加载状态标志位到AH,即EFLAG寄存器低八位复制到AH中,被复制的标志位包括:符号标志位(SF)、零标志位(ZF)、辅助进位标志位(AF)、奇偶标志位(PF)和进位标志位(CF),使用该指令可以方便地将标志位副本保存在变量中;
然后,使用seto %al溢出置位,即“如果溢出,则%al设置为1”

__afl_setup
用于初始化__afl_area_ptr,且只在运行到第一个桩时进行本次初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"__afl_setup:\n"
"\n"
" /* Do not retry setup if we had previous failures. */\n"
"\n"
" cmpb $0, __afl_setup_failure(%rip)\n"
" jne __afl_return\n"//先检查是否为0,不是则直接返回,“防止预先可预见的失败”
"\n"
" /* Check out if we have a global pointer on file. */\n"
"\n"
" movq __afl_global_area_ptr(%rip), %rdx\n"
" testq %rdx, %rdx\n"
" je __afl_setup_first\n" // 如果global pointer也为空,进行初次建立
"\n"
" movq %rdx, __afl_area_ptr(%rip)\n"//否则直接赋值给area ptr,然后进入store
" jmp __afl_store\n"

__afl-setup_first

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
"__afl_setup_first:\n"
"\n"
" /* Save everything that is not yet saved and that may be touched by\n"
" getenv() and several other libcalls we'll be relying on. */\n"
"\n"
" leaq -352(%rsp), %rsp\n"
"\n"
" movq %rax, 0(%rsp)\n"
" movq %rcx, 8(%rsp)\n"
" movq %rdi, 16(%rsp)\n"
" movq %rsi, 32(%rsp)\n"
" movq %r8, 40(%rsp)\n"
" movq %r9, 48(%rsp)\n"
" movq %r10, 56(%rsp)\n"
" movq %r11, 64(%rsp)\n"
"\n"
" movq %xmm0, 96(%rsp)\n"
" movq %xmm1, 112(%rsp)\n"
" movq %xmm2, 128(%rsp)\n"
" movq %xmm3, 144(%rsp)\n"
" movq %xmm4, 160(%rsp)\n"
" movq %xmm5, 176(%rsp)\n"
" movq %xmm6, 192(%rsp)\n"
" movq %xmm7, 208(%rsp)\n"
" movq %xmm8, 224(%rsp)\n"
" movq %xmm9, 240(%rsp)\n"
" movq %xmm10, 256(%rsp)\n"
" movq %xmm11, 272(%rsp)\n"
" movq %xmm12, 288(%rsp)\n"
" movq %xmm13, 304(%rsp)\n"
" movq %xmm14, 320(%rsp)\n"
" movq %xmm15, 336(%rsp)\n"
"\n"
" /* Map SHM, jumping to __afl_setup_abort if something goes wrong. */\n"
"\n"
" /* The 64-bit ABI requires 16-byte stack alignment. We'll keep the\n"
" original stack ptr in the callee-saved r12. */\n"
"\n"
" pushq %r12\n"
" movq %rsp, %r12\n"
" subq $16, %rsp\n"
" andq $0xfffffffffffffff0, %rsp\n"
"\n"
" leaq .AFL_SHM_ENV(%rip), %rdi\n"
CALL_L64("getenv")
"\n"
" testq %rax, %rax\n"
" je __afl_setup_abort\n"
"\n"
" movq %rax, %rdi\n"
CALL_L64("atoi")
"\n"
" xorq %rdx, %rdx /* shmat flags */\n"
" xorq %rsi, %rsi /* requested addr */\n"
" movq %rax, %rdi /* SHM ID */\n"
CALL_L64("shmat")
"\n"
" cmpq $-1, %rax\n"
" je __afl_setup_abort\n"
"\n"
" /* Store the address of the SHM region. */\n"
"\n"
" movq %rax, %rdx\n"
" movq %rax, __afl_area_ptr(%rip)\n"
"\n"
#ifdef __APPLE__
" movq %rax, __afl_global_area_ptr(%rip)\n"
#else
" movq __afl_global_area_ptr@GOTPCREL(%rip), %rdx\n"
" movq %rax, (%rdx)\n"
#endif /* ^__APPLE__ */
" movq %rax, %rdx\n"

先将所有寄存器内容进行保存,然后寻找共享内存,如果找不到,就调用__afl_setup_abort
成功后,调用__afl_forkserver
__afl_forkserver
判断fork server是否成功启动。
__afl_fork_wait_loop

  • 从管道中等待parent的信息,读入__afl_temp内,成功则继续;
  • fork一个子进程,子进程执行__afl_fork_resume
  • 将子进程pid赋值给__afl_fork_pid,并写入状态管道通知父进程;
  • 子进程结束后告诉fuzzer,下一轮循环开始。

afl-fuzz.c

该文件是afl项目的核心中的核心
此文件篇幅过长,选择直接从main函数入口开始看(共8k余行,main位于7706行)

1
2
3
4
5
6
SAYF(cCYA "afl-fuzz " cBRI VERSION cRST " by <lcamtuf@google.com>\n");

doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH;

gettimeofday(&tv, &tz);
srandom(tv.tv_sec ^ tv.tv_usec ^ getpid());

感觉是很常见的函数开头,之前读过的几个函数开始也有这几行—-打印版本信息、寻找路径信息、获取时间、获取随机数
第一个while循环,进行参数读取
随后

1
2
setup_signal_handlers();// 注册信号处理函数。
check_asan_opts();// 读取环境变量

setup_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
25
26
27
28
29
30
31
EXP_ST void setup_shm(void) {

u8* shm_str;

if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);

memset(virgin_tmout, 255, MAP_SIZE);//记录所有程序超时
memset(virgin_crash, 255, MAP_SIZE);//记录所有程序崩溃

shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);

if (shm_id < 0) PFATAL("shmget() failed");

atexit(remove_shm);

shm_str = alloc_printf("%d", shm_id);

/* If somebody is asking us to fuzz instrumented binaries in dumb mode,
we don't want them to detect instrumentation, since we won't be sending
fork server commands. This should be replaced with better auto-detection
later on, perhaps? */

if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);

ck_free(shm_str);

trace_bits = shmat(shm_id, NULL, 0);

if (!trace_bits) PFATAL("shmat() failed");

}

其中:shmat方法用于追踪当前的tuple信息,trace_bits位于共享内存上,用于传送信息。

第一遍fuzz

calibrate_case函数
是AFL的一个关键函数,用于新测试用例的校准,希望能早期发现有问题的测试用例。

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem,
u32 handicap, u8 from_queue) {

static u8 first_trace[MAP_SIZE];

u8 fault = 0, new_bits = 0, var_detected = 0,
first_run = (q->exec_cksum == 0);

u64 start_us, stop_us;

s32 old_sc = stage_cur, old_sm = stage_max;
u32 use_tmout = exec_tmout;
u8* old_sn = stage_name;

/* Be a bit more generous about timeouts when resuming sessions, or when
trying to calibrate already-added finds. This helps avoid trouble due
to intermittent latency. */

if (!from_queue || resuming_fuzz)
use_tmout = MAX(exec_tmout + CAL_TMOUT_ADD,
exec_tmout * CAL_TMOUT_PERC / 100);

q->cal_failed++;

stage_name = "calibration";
stage_max = fast_cal ? 3 : CAL_CYCLES;

/* Make sure the forkserver is up before we do anything, and let's not
count its spin-up time toward binary calibration. */

if (dumb_mode != 1 && !no_forkserver && !forksrv_pid)
init_forkserver(argv); // 初始化启动forkserver

if (q->exec_cksum) memcpy(first_trace, trace_bits, MAP_SIZE);

start_us = get_cur_time_us();//

for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {// 该循环执行3或8次

u32 cksum;

if (!first_run && !(stage_cur % stats_update_freq)) show_stats();

write_to_testcase(use_mem, q->len);

fault = run_target(argv, use_tmout);//run_target函数通知forkserver可以开始fork

/* stop_soon is set by the handler for Ctrl+C. When it's pressed,
we want to bail out quickly. */

if (stop_soon || fault != crash_mode) goto abort_calibration;

if (!dumb_mode && !stage_cur && !count_bytes(trace_bits)) {
fault = FAULT_NOINST;
goto abort_calibration;
}

cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);//计算trace_bits的哈希值

if (q->exec_cksum != cksum) {//若哈希值不同

u8 hnb = has_new_bits(virgin_bits);
if (hnb > new_bits) new_bits = hnb;//如果hnb>new_bits,证明该new_bits已经跑过,将新的hnb赋值给new_bits,令其重新开始下一轮循环

if (q->exec_cksum) {// exec_cksum是循环次数,若不是第一次跑

u32 i;

for (i = 0; i < MAP_SIZE; i++) {

if (!var_bytes[i] && first_trace[i] != trace_bits[i]) {

var_bytes[i] = 1;
stage_max = CAL_CYCLES_LONG;

}

}

var_detected = 1;

} else {

q->exec_cksum = cksum;
memcpy(first_trace, trace_bits, MAP_SIZE);

}

}

}

stop_us = get_cur_time_us();


init_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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
EXP_ST void init_forkserver(char** argv) {

static struct itimerval it;
int st_pipe[2], ctl_pipe[2];//状态通道与控制通道
int status;
s32 rlen;

ACTF("Spinning up the fork server...");

if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL("pipe() failed");//两个管道都建立失败,报错

forksrv_pid = fork();

if (forksrv_pid < 0) PFATAL("fork() failed");

if (!forksrv_pid) {

struct rlimit r;

/* Umpf. On OpenBSD, the default fd limit for root users is set to
soft 128. Let's try to fix that... */

if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) {

r.rlim_cur = FORKSRV_FD + 2;
setrlimit(RLIMIT_NOFILE, &r); /* Ignore errors */

}

if (mem_limit) {

r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20;

#ifdef RLIMIT_AS

setrlimit(RLIMIT_AS, &r); /* Ignore errors */

#else

/* This takes care of OpenBSD, which doesn't have RLIMIT_AS, but
according to reliable sources, RLIMIT_DATA covers anonymous
maps - so we should be getting good protection against OOM bugs. */

setrlimit(RLIMIT_DATA, &r); /* Ignore errors */

#endif /* ^RLIMIT_AS */


}

/* Dumping cores is slow and can lead to anomalies if SIGKILL is delivered
before the dump is complete. */

r.rlim_max = r.rlim_cur = 0;

setrlimit(RLIMIT_CORE, &r); /* Ignore errors */

/* Isolate the process and configure standard descriptors. If out_file is
specified, stdin is /dev/null; otherwise, out_fd is cloned instead. */

setsid();

dup2(dev_null_fd, 1);
dup2(dev_null_fd, 2);

if (out_file) {

dup2(dev_null_fd, 0);

} else {

dup2(out_fd, 0);
close(out_fd);

}

/* Set up control and status pipes, close the unneeded original fds. */

if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed");
if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) PFATAL("dup2() failed");

close(ctl_pipe[0]);
close(ctl_pipe[1]);
close(st_pipe[0]);
close(st_pipe[1]);

close(out_dir_fd);
close(dev_null_fd);
close(dev_urandom_fd);
close(fileno(plot_file));

/* This should improve performance a bit, since it stops the linker from
doing extra work post-fork(). */

if (!getenv("LD_BIND_LAZY")) setenv("LD_BIND_NOW", "1", 0);

/* Set sane defaults for ASAN if nothing else specified. */

setenv("ASAN_OPTIONS", "abort_on_error=1:"
"detect_leaks=0:"
"symbolize=0:"
"allocator_may_return_null=1", 0);

/* MSAN is tricky, because it doesn't support abort_on_error=1 at this
point. So, we do this in a very hacky way. */

setenv("MSAN_OPTIONS", "exit_code=" STRINGIFY(MSAN_ERROR) ":"
"symbolize=0:"
"abort_on_error=1:"
"allocator_may_return_null=1:"
"msan_track_origins=0", 0);

execv(target_path, argv);

/* Use a distinctive bitmap signature to tell the parent about execv()
falling through. */

*(u32*)trace_bits = EXEC_FAIL_SIG;
exit(0);

}

/* Close the unneeded endpoints. */

close(ctl_pipe[0]);
close(st_pipe[1]);

fsrv_ctl_fd = ctl_pipe[1];
fsrv_st_fd = st_pipe[0];

/* Wait for the fork server to come up, but don't wait too long. */

it.it_value.tv_sec = ((exec_tmout * FORK_WAIT_MULT) / 1000);
it.it_value.tv_usec = ((exec_tmout * FORK_WAIT_MULT) % 1000) * 1000;

setitimer(ITIMER_REAL, &it, NULL);

rlen = read(fsrv_st_fd, &status, 4);

it.it_value.tv_sec = 0;
it.it_value.tv_usec = 0;

setitimer(ITIMER_REAL, &it, NULL);

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

if (child_timed_out)
FATAL("Timeout while initializing fork server (adjusting -t may help)");

if (waitpid(forksrv_pid, &status, 0) <= 0)
PFATAL("waitpid() failed");

if (WIFSIGNALED(status)) {

if (mem_limit && mem_limit < 500 && uses_asan) {

SAYF("\n" cLRD "[-] " cRST
"Whoops, the target binary crashed suddenly, before receiving any input\n"
" from the fuzzer! Since it seems to be built with ASAN and you have a\n"
" restrictive memory limit configured, this is expected; please read\n"
" %s/notes_for_asan.txt for help.\n", doc_path);

} else if (!mem_limit) {

SAYF("\n" cLRD "[-] " cRST
"Whoops, the target binary crashed suddenly, before receiving any input\n"
" from the fuzzer! There are several probable explanations:\n\n"

" - The binary is just buggy and explodes entirely on its own. If so, you\n"
" need to fix the underlying problem or find a better replacement.\n\n"

#ifdef __APPLE__

" - On MacOS X, the semantics of fork() syscalls are non-standard and may\n"
" break afl-fuzz performance optimizations when running platform-specific\n"
" targets. To fix this, set AFL_NO_FORKSRV=1 in the environment.\n\n"

#endif /* __APPLE__ */

" - Less likely, there is a horrible bug in the fuzzer. If other options\n"
" fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\n");

} else {

SAYF("\n" cLRD "[-] " cRST
"Whoops, the target binary crashed suddenly, before receiving any input\n"
" from the fuzzer! There are several probable explanations:\n\n"

" - The current memory limit (%s) is too restrictive, causing the\n"
" target to hit an OOM condition in the dynamic linker. Try bumping up\n"
" the limit with the -m setting in the command line. A simple way confirm\n"
" this diagnosis would be:\n\n"

#ifdef RLIMIT_AS
" ( ulimit -Sv $[%llu << 10]; /path/to/fuzzed_app )\n\n"
#else
" ( ulimit -Sd $[%llu << 10]; /path/to/fuzzed_app )\n\n"
#endif /* ^RLIMIT_AS */

" Tip: you can use http://jwilk.net/software/recidivm to quickly\n"
" estimate the required amount of virtual memory for the binary.\n\n"

" - The binary is just buggy and explodes entirely on its own. If so, you\n"
" need to fix the underlying problem or find a better replacement.\n\n"

#ifdef __APPLE__

" - On MacOS X, the semantics of fork() syscalls are non-standard and may\n"
" break afl-fuzz performance optimizations when running platform-specific\n"
" targets. To fix this, set AFL_NO_FORKSRV=1 in the environment.\n\n"

#endif /* __APPLE__ */

" - Less likely, there is a horrible bug in the fuzzer. If other options\n"
" fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\n",
DMS(mem_limit << 20), mem_limit - 1);

}

FATAL("Fork server crashed with signal %d", WTERMSIG(status));

}

if (*(u32*)trace_bits == EXEC_FAIL_SIG)
FATAL("Unable to execute target application ('%s')", argv[0]);

if (mem_limit && mem_limit < 500 && uses_asan) {

SAYF("\n" cLRD "[-] " cRST
"Hmm, looks like the target binary terminated before we could complete a\n"
" handshake with the injected code. Since it seems to be built with ASAN and\n"
" you have a restrictive memory limit configured, this is expected; please\n"
" read %s/notes_for_asan.txt for help.\n", doc_path);

} else if (!mem_limit) {

SAYF("\n" cLRD "[-] " cRST
"Hmm, looks like the target binary terminated before we could complete a\n"
" handshake with the injected code. Perhaps there is a horrible bug in the\n"
" fuzzer. Poke <lcamtuf@coredump.cx> for troubleshooting tips.\n");

} else {

SAYF("\n" cLRD "[-] " cRST
"Hmm, looks like the target binary terminated before we could complete a\n"
" handshake with the injected code. There are %s probable explanations:\n\n"

"%s"
" - The current memory limit (%s) is too restrictive, causing an OOM\n"
" fault in the dynamic linker. This can be fixed with the -m option. A\n"
" simple way to confirm the diagnosis may be:\n\n"

#ifdef RLIMIT_AS
" ( ulimit -Sv $[%llu << 10]; /path/to/fuzzed_app )\n\n"
#else
" ( ulimit -Sd $[%llu << 10]; /path/to/fuzzed_app )\n\n"
#endif /* ^RLIMIT_AS */

" Tip: you can use http://jwilk.net/software/recidivm to quickly\n"
" estimate the required amount of virtual memory for the binary.\n\n"

" - Less likely, there is a horrible bug in the fuzzer. If other options\n"
" fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\n",
getenv(DEFER_ENV_VAR) ? "three" : "two",
getenv(DEFER_ENV_VAR) ?
" - You are using deferred forkserver, but __AFL_INIT() is never\n"
" reached before the program terminates.\n\n" : "",
DMS(mem_limit << 20), mem_limit - 1);

}

FATAL("Fork server handshake failed");

}

perform_dry_run函数

遍历input队列,读取文件内容,调用calibrate_case进行校准,收集错误信息
run_target函数
执行目标程序,传回状态信息。
cull_queue函数
通过遍历top_rated[]条目,获取“获胜者”,这些获胜者会获得更多执行时间。优化作用。
等等,还要许多配置函数,为环境的顺利做准备。
主循环

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
while (1) {

u8 skipped_fuzz;

cull_queue();

if (!queue_cur) {//如果queue当前为空,即执行完一轮

queue_cycle++;
current_entry = 0;
cur_skipped_paths = 0;
queue_cur = queue;//准备下一轮

while (seek_to) {//从seek_to指定项开始执行。
current_entry++;
seek_to--;
queue_cur = queue_cur->next;
}

show_stats();

if (not_on_tty) {
ACTF("Entering queue cycle %llu.", queue_cycle);
fflush(stdout);
}

/* If we had a full queue cycle with no new finds, try
recombination strategies next. */

if (queued_paths == prev_queued) {//如果path数没变,即没有新发现

if (use_splicing) cycles_wo_finds++; else use_splicing = 1;//是否使用splicing策略

} else cycles_wo_finds = 0;

prev_queued = queued_paths;//更新路径数

if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST"))
sync_fuzzers(use_argv);

}

skipped_fuzz = fuzz_one(use_argv);//关键函数fuzz_one

if (!stop_soon && sync_id && !skipped_fuzz) {

if (!(sync_interval_cnt++ % SYNC_INTERVAL))
sync_fuzzers(use_argv);

}

if (!stop_soon && exit_1) stop_soon = 2;

if (stop_soon) break;

queue_cur = queue_cur->next;
current_entry++;

}

关键函数fuzz_one()

  • 先进行优化:概率跳过普通执行项或已执行项,去执行favored
  • 对于已经fuzz过的或者non-favored的有99%的概率跳过;无pending_favored,95%跳过fuzzed&non-favored,75%跳过not fuzzed&non-favored,不跳过favored;
  • 打开当前输入文件,读入len长度的buf,关闭文件
  • (当之前修理失败时)再次调用celibrate_case
  • 修剪:不要重复修剪,哪怕修建失败了
  • 将读入的buf复制到out_buf
  • 进行打分,对每个测试用例
  • 如果我们已经亲自对这里的测试用例进行了fuzzing或早些时候,进行了决定性的测试,跳过这里直接前往havoc_stage

简单位翻转bitflip

afl-llvm_mode

关于LLVM

  • LLVM 主要为了解决编译时多种多样的前端和后端导致编译环境复杂、苛刻的问题,其核心为设计了一个称为 LLVM IR 的中间表示,并以库的形式提供一些列接口,以提供诸如操作 IR 、生成目标平台代码等等后端的功能。
  • 不同的前端和后端使用统一的中间代码LLVM InterMediate Representation(LLVM IR),其结果就是如果需要支持一门新的编程语言,只需要实现一个新的前端;如果需要支持一款新的硬件设备,只需要实现一个新的后端;优化阶段为通用阶段,针对统一的 LLVM IR ,与新的编程语言和硬件设备无关。
  • GCC 的前后端耦合在一起,没有进行分离,所以GCC为了支持一门新的编程语言或一个新的硬件设备,需要重新开发前端到后端的完整过程。
  • Clang 是 LLVM 项目的一个子项目,它是 LLVM 架构下的 C/C++/Objective-C 的编译器,是 LLVM 前端的一部分。相较于GCC,具备编译速度快、占用内存少、模块化设计、诊断信息可读性强、设计清晰简单等优点。
  • 代码首先由编译器前端clang处理后得到中间代码IR,然后经过各 LLVM Pass 进行优化和转换,最终交给编译器后端生成机器码(LLVM Pass 是一些中间过程处理 IR 的可以用户自定义的内容,可以用来遍历、修改 IR 以达到插桩、优化、静态分析等目的。

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

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

关于此llvm_mode文件夹:

  • afl-llvm-rt.o重写了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操作通过链接完成。

源码分析

find_pbj
寻找运行时的librabies,若失败,abort。

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
static void find_obj(u8* argv0) {

u8 *afl_path = getenv("AFL_PATH");//寻找AFL_PATH
u8 *slash, *tmp;

if (afl_path) {

tmp = alloc_printf("%s/afl-llvm-rt.o", afl_path);//将afl-llvm-rt.o的路径写入tmp

if (!access(tmp, R_OK)) {//如果该路径可以访问,且可读
obj_path = afl_path;//赋值给obj_path
ck_free(tmp);
return;
}

ck_free(tmp);

}

slash = strrchr(argv0, '/');//返回斜杠最后一次出现处的指针

if (slash) {//存在斜杠

u8 *dir;

*slash = 0;
dir = ck_strdup(argv0);
*slash = '/';

tmp = alloc_printf("%s/afl-llvm-rt.o", dir);

if (!access(tmp, R_OK)) {
obj_path = dir;
ck_free(tmp);
return;
}

ck_free(tmp);
ck_free(dir);

}

if (!access(AFL_PATH "/afl-llvm-rt.o", R_OK)) {
obj_path = AFL_PATH;
return;
}

FATAL("Unable to find 'afl-llvm-rt.o' or 'afl-llvm-pass.so'. Please set AFL_PATH");

}
  • 首先,读取环境变量 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文件,该文件即为要用到的运行时库。

edit_params函数
即编辑对应参数

2.afl-llvm-pass.so.cc

文件实现了 LLVM-mode 下的一个插桩 LLVM Pass。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
该文件只有一个Transform pass:```AFLCoverage```,继承自 ```ModulePass```,实现了一个``` runOnModule``` 函数,这也是我们需要重点分析的函数。
```c
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";
// }

};

}

runOnModule函数

1
LLVMContext &C = M.getContext();

获取上下文;

1
2
3
/* Decide instrumentation ratio */
char* inst_ratio_str = getenv("AFL_INST_RATIO");
unsigned int inst_ratio = 100;

设置插桩密度,默认为100

获取指向共享内存shm的指针和上一个基本块的id

开始插桩:

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
int inst_blocks = 0;

for (auto &F : M)
for (auto &BB : F) {// 遍历每一个BB(基本块)

BasicBlock::iterator IP = BB.getFirstInsertionPt();//寻找BB中合适位置
IRBuilder<> IRB(&(*IP));//初始化IRBuider实例执行插入

if (AFL_R(100) >= inst_ratio) continue;//如果大于插桩密度

/* Make up cur_loc */

unsigned int cur_loc = AFL_R(MAP_SIZE);//随机创建当前基本块ID

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());
//获取上一个BB的ID

/* Load SHM pointer */

LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);//获取SHM地址
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++;

}

3. afl-llvm-rt.o.c
该文件主要实现了llvm_mode的3个特殊功能:deferred instrumentation, persistent mode,trace-pc-guard mode

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

虽然这种机制可以减少程序运行在操作系统、链接器和libc级别的消耗,但是在面对大型配置文件的解析时,优势并不明显。

在这种情况下,可以将 forkserver初始化放在大部分初始化工作完成之后、二进制文件解析之前来进行,这在某些情况下可以提升10倍以上的性能。我们把这种方式称为LLVM模式下的 deferred instrumentation

首先,在代码中寻找可以进行延迟克隆的合适的、不会破坏原二进制文件的位置,然后添加如下代码:

1
#ifdef __AFL_HAVE_MANUAL_CONTROL    __AFL_INIT();#endif

persistent mode

mode``` 并没有通过```fork```子进程的方式来执行fuzz。一些库中提供的API是无状态的,或者可以在处理不同输入文件之间进行重置,恢复到之前的状态。执行此类重置时,可以使用一个长期存活的进程来测试多个用例,以这种方式来减少重复的 ```fork()``` 调用和操作系统的开销。不得不说,这种思路真的很优秀。
1
2
3
4
5
6
7

设计框架如下:
设置一个 while 循环,并指定循环次数。在每次循环内,首先读取数据,然后调用想fuzz的库代码,然后重置状态,继续循环。(本质上也是一种快照。)

对于循环次数的设置,循环次数控制了AFL从头重新启动过程之前的最大迭代次数,较小的循环次数可以降低内存泄漏类故障的影响,官方建议的数值为1000。(循环次数设置过高可能出现较多意料之外的问题,并不建议设置过高。)
```c
while (__AFL_LOOP(1000)) { /* Read input data. */ /* Call library code to be fuzzed. */ /* Reset state. */}/* Exit normally */

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 值来确定共享内存上所对应的地址


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!