0%

alf-fuzz source code(partly)

照着这篇博客先把fuzz的门入了

AFL源码粗略分析笔记

我是从这篇这篇开始看的,具体概念这两篇文章也写的很清楚了,下面只是记一些粗略的笔记

afl-gcc.c

afl-gcc主要是gcc的一个封装(wrapper),main中主要执行这三个函数:

1
2
3
4
5
find_as(argv[0]);

edit_params(argc, argv);

execvp(cc_params[0], (char**)cc_params);

find_as:

这个函数的作用是找到对应的编译器

edit_params:

主要是对编译参数做一些设置,用-B指定了编译汇编器等,主要是为了插桩

img

execvp:

用处理之后的编译参数进行源码编译

afl-as.c和afl-as.h

就是这里进行的插桩,可以看到编译出来的代码多了一些插入的东西,比如__afl_maybe_log

image-20210127135753497

afl-as.c中的关键函数add_instrumentation,使用fprintf将插桩用的代码插入,用来统计覆盖率,这里大致看一下插桩的逻辑

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
if (   !pass_thru  &&  !skip_intel  &&  !skip_app  &&  !skip_csect //这四个变量为1时跳过对应的插桩,具体看后面的分析
&& instr_ok && instrument_next && line[0] == '\t' && isalpha(line[1])//这里line[0]和isalpha应该是对应汇编文件的格式判断插入的位置,没有深入研究,instr_ok和instrument_next看后面的分析
)
{

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));//插桩写入输出文件,R(MAP_SIZE)的作用是提供一个随机数标识插桩点,在后面分析trampoline_fmt_32会写到
instrument_next = 0;
ins_lines++;
}

fputs(line, outf);//原来的代码再写入输出文件

//....(一些代码)

/*
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] == '.') //这个if分值就和注释写的一样,为了只在代码段进行插入,自己不习惯太乱的代码格式就自己稍微改了改了排版
{

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

/* Detect off-flavor assembly (rare, happens in gdb). When this is
encountered, we set skip_csect until the opposite directive is
seen, and we do not instrument.
*/
if (strstr(line, ".code"))//这里可以看到skip_csect是检测.code格式设置的,比较少见
{
if (strstr(line, ".code32")) skip_csect = use_64bit;
if (strstr(line, ".code64")) skip_csect = !use_64bit;
}

/* Detect syntax changes, as could happen with hand-written assembly.
Skip Intel blocks, resume instrumentation when back to AT&T. */
if (strstr(line, ".intel_syntax")) skip_intel = 1;
if (strstr(line, ".att_syntax")) skip_intel = 0;

/* Detect syntax changes, as could happen with hand-written assembly.
Skip Intel blocks, resume instrumentation when back to AT&T. */
if (strstr(line, ".intel_syntax")) skip_intel = 1;
if (strstr(line, ".att_syntax")) skip_intel = 0;

/* Detect and skip ad-hoc __asm__ blocks, likewise skipping them. */
if (line[0] == '#' || line[1] == '#')
{
if (strstr(line, "#APP")) skip_app = 1;
if (strstr(line, "#NO_APP")) skip_app = 0;
}
//上面这三个写的比较清楚就不解释辽

//....(一些代码)

/*
Conditional branch instruction (jnz, etc). We append the instrumentation
right after the branch (to instrument the not-taken path) and at the
branch destination label (handled later on).
*/
if (line[0] == '\t') //这里有个比较重要的分支,当指令不是jmp而是一些条件跳转指令时,会在条件跳转指令之后进行插桩,还有对应的目标地址处(在后面处理)
{
if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) //这个int_ratio暂时没管是干嘛的...
{
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));
ins_lines++;
}
continue;
}

/*
Label of some sort. This may be a branch destination, but we need to
tread carefully and account for several different formatting
conventions. 这里就是在打上了Label的地方进行插桩,可能是跳转指令的目的地址
*/
/* Everybody else: .L<whatever>: */
if (strstr(line, ":"))
{
if (line[0] == '.')//判断到是一个Label
{
/* .L0: or LBB0_0: style jump destination 跳转Label*/
if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3))) && R(100) < inst_ratio)
{
/* An optimization is possible here by adding the code only if the
label is mentioned in the code in contexts other than call / jmp.
That said, this complicates the code by requiring two-pass
processing (messy with stdin), and results in a speed gain
typically under 10%, because compilers are generally pretty good
about not generating spurious intra-function jumps.

We use deferred output chiefly to avoid disrupting
.Lfunc_begin0-style exception handling calculations (a problem on
MacOS X). */
if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;//这里跟前面的那个标志也有关系
}
}
else
{
/* Function label (always instrumented, deferred mode). 函数Label*/
instrument_next = 1;
}
}
//到这里大循环的插桩就结束了,之后还有补充的小块代码就不写了,主要就是这些:只在代码段插桩、在有函数Label(函数入口)和跳转Label处插桩,在条件条状指令之后插桩

afl-as.h中就是具体的插桩代码,以32位为例来分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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" //这里%08x就是前面fprintf中R(MAP_SIZE)的写入点,用作随机数标识
"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";

payload根据实际效果删了一些宏定义啥的,看起来好看一点,emmm其实建议直接IDA反汇编看更好?….不过这里汇编有作者的注释帮着看,IDA反汇编图我在代码后面贴上了

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
main_payload_32 = 
"/* --- AFL MAIN PAYLOAD (32-BIT) --- */\n"
".text\n"
".att_syntax\n"
".code32\n"
".align 8\n"

"__afl_maybe_log:\n"
" lahf\n" //将EFLAGS 寄存器标志位加载到AH
" seto %al\n" //为溢出置位

" /* Check if SHM region is already mapped. */\n"
" movl __afl_area_ptr, %edx\n"
" testl %edx, %edx\n"
" je __afl_setup\n" //检查共享内存指针是否到位

"__afl_store:\n"
" /* Calculate and store hit for the code location specified in ecx. There\n"
" is a double-XOR way of doing this without tainting another register,\n"
" and we use it on 64-bit systems; but it's slower for 32-bit ones. */\n"
" movl __afl_prev_loc, %edi\n"
" xorl %ecx, %edi\n"
" shrl $1, %ecx\n"
" movl %ecx, __afl_prev_loc\n"
" incb (%edx, %edi, 1)\n" //根据随机数存储执行位置,算法在后文分析
The shared_mem[] array is a 64 kB SHM region passed to the instrumented binary
by the caller. Every byte set in the output map can be thought of as a hit for
a particular (branch_src, branch_dst) tuple in the instrumented code.

"__afl_return:\n"
" addb $127, %al\n"
" sahf\n"
" ret\n"

".align 8\n"

"__afl_setup:\n"//setup只会在最开始调用afl_maybe_log处触发,即main函数的最前面触发,主要是开启一个目标文件自己的fork循环用来记录执行路径(这个程序自己的fork循环就是forkserver)
" /* Do not retry setup if we had previous failures. */\n"
" cmpb $0, __afl_setup_failure\n"
" jne __afl_return\n"
" /* Map SHM, jumping to __afl_setup_abort if something goes wrong.\n"
" We do not save FPU/MMX/SSE registers here, but hopefully, nobody\n"
" will notice this early in the game. */\n"
" pushl %eax\n"
" pushl %ecx\n"
" pushl $.AFL_SHM_ENV\n"
" call getenv\n"
" addl $4, %esp\n"
" testl %eax, %eax\n"
" je __afl_setup_abort\n"
" pushl %eax\n"
" call atoi\n"
" addl $4, %esp\n"
" pushl $0 /* shmat flags */\n"
" pushl $0 /* requested addr */\n"
" pushl %eax /* SHM ID */\n"
" call shmat\n"
" addl $12, %esp\n"
" cmpl $-1, %eax\n"
" je __afl_setup_abort\n"
" /* Store the address of the SHM region. */\n"
" movl %eax, __afl_area_ptr\n"
" movl %eax, %edx\n"
" popl %ecx\n"
" popl %eax\n"

"__afl_forkserver:\n"
" /* Enter the fork server mode to avoid the overhead of execve() calls. */\n"
" pushl %eax\n"
" pushl %ecx\n"
" pushl %edx\n"
" /* Phone home and tell the parent that we're OK. (Note that signals with\n"
" no SA_RESTART will mess it up). If this fails, assume that the fd is\n"
" closed because we were execve()d from an instrumented binary, or because\n"
" the parent doesn't want to use the fork server. */\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"//fork server向fuzzer传递执行状态码,199描述符(后面会说是什么)
" call write\n"
" addl $12, %esp\n"
" cmpl $4, %eax\n"
" jne __afl_fork_resume\n"

"__afl_fork_wait_loop:\n"
" /* Wait for parent by reading from the pipe. Abort if read fails. */\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY(FORKSRV_FD) " /* file desc */\n"//等待fuzzer传递命令,198描述符
" call read\n"
" addl $12, %esp\n"
" cmpl $4, %eax\n"
" jne __afl_die\n"
" /* Once woken up, create a clone of our process. This is an excellent use"
" case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly"
" caches getpid() results and offers no way to update the value, breaking"
" abort(), raise(), and a bunch of other things :-( */\n"
" call fork\n"
" cmpl $0, %eax\n"
" jl __afl_die\n"
" je __afl_fork_resume\n"
" /* In parent process: write PID to pipe, then wait for child. */\n"
" movl %eax, __afl_fork_pid\n"
" pushl $4 /* length */\n"
" pushl $__afl_fork_pid /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"//199描述符
" call write\n"
" addl $12, %esp\n"
" pushl $0 /* no flags */\n"
" pushl $__afl_temp /* status */\n"
" pushl __afl_fork_pid /* PID */\n"
" call waitpid\n"
" addl $12, %esp\n"
" cmpl $0, %eax\n"
" jle __afl_die\n"
" /* Relay wait status to pipe, then loop back. */\n" //wait statu指最开始那个进程等待到的 子进程返回来的 状态
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"
" call write\n"
" addl $12, %esp\n"
" jmp __afl_fork_wait_loop\n"

"__afl_fork_resume:\n"//这里是每次forkserver fork出来的子进程都要执行的
" /* In child process: close fds, resume execution. */\n"
" pushl $" STRINGIFY(FORKSRV_FD) "\n"
" call close\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) "\n"
" call close\n"
" addl $8, %esp\n"
" popl %edx\n"
" popl %ecx\n"
" popl %eax\n"
" jmp __afl_store\n"//跳到前面的__afl_store

"__afl_die:\n"
" xorl %eax, %eax\n"
" call _exit\n"

"__afl_setup_abort:\n"
" /* Record setup failure so that we don't keep calling\n"
" shmget() / shmat() over and over again. */\n"
" incb __afl_setup_failure\n"
" popl %ecx\n"
" popl %eax\n"
" jmp __afl_return\n"

".AFL_VARS:\n"
" .comm __afl_area_ptr, 4, 32\n"
" .comm __afl_setup_failure, 1, 32\n"
" .comm __afl_prev_loc, 4, 32\n"
" .comm __afl_fork_pid, 4, 32\n"
" .comm __afl_temp, 4, 32\n"
"\n"
".AFL_SHM_ENV:\n"
" .asciz \"" SHM_ENV_VAR "\"\n"
"\n"
"/* --- END --- */\n"
image-20210203133736042

alloc-inl.c

后面的代码中很多与内存相关的操作都是alloc-inl里面的,不缕一下的话不知所云…所以这里记录一下经常在afl-fuzz.c中看到的几个内存操作函数

作者原话:This allocator is not designed to resist malicious attackers (the canaries are small and predictable), but provides a robust and portable way to detect use-after-free, off-by-one writes, stale pointers, and so on.

里面的宏定义有一些类似:__LINE____FUNCTION__的,这是C编译器的内置宏,具体代表什么意思百度一下即可

  • ALLOC_CHECK_SIZE(size) malloc前检查size是否超过设置的最大chunk大小
  • ret = malloc(size + ALLOC_OFF_TOTAL);经常出现的malloc方式,这里是+ ALLOC_OFF_TOTAL为了在chunk前8字节处存放一个4B的头部标志和size,还有一个1B的尾部标志
  • ALLOC_CHECK_RESULT(ret,size) malloc后检查是否分配成功,若ret为NULL,这个size就会用于打印错误信息
  • TRK_ck_strdup,(TRK开头 + ck + 一般函数名)的函数,主要进行如下两个操作
    • void* ret = DFL_ck_strdup(str);,(DFL开头)的函数就是正常的操作,比如这个DFL_ck_strdup就是用作者自己的一些逻辑+上面提到的三个主要步骤实现
    • TRK_alloc_buf(void* ret, const char* file, const char* func, u32 line) ,将哪个文件、哪个函数、哪一行申请内存的信息用 哈希散列+链地址法 的方法存在了一个散列表中,主要为了后续的检查。带alloc的函数用来存放,带free的就是用来判断有没有错误之类的

afl-fuzz.c

看完了插桩接下来就是看具体的fuzz逻辑了,这里大概有8000多行代码,慢慢缕吧

main函数前面的主逻辑就是一个处理输入参数的逻辑,里面还处理了一个non official参数 -B…这里是作者的注释,作用是指定bitmap,会影响in_bitmap这个全局变量

1
2
3
4
5
6
7
8
9
10
/* This is a secret undocumented option! It is useful if you find
an interesting test case during a normal fuzzing process, and want
to mutate it without rediscovering any of the test cases already
found during an earlier run.

To use this mode, you need to point -B to the fuzz_bitmap produced
by an earlier run for the exact same binary... and that's it.

I only used this once or twice to get variants of a particular
file, so I'm not making this an official setting. */

处理完参数之后,首先 setup_signal_handlers();check_asan_opts();,设置一系列程序运行时的信号处理,比如超时如何处理,检查asan参数,然后还有一系列的设置和检查,具体还是看代码的7910行左右吧(2.52版本)

处理完参数之后就是一些操作

main (part1)

因为函数属实有点多所以只记录了一些印象比较深的,建议想了解的话,每个函数点进去看看作者的注释说了什么,而且一些函数的作用在我前面开头提到的入门博客中有写,就不赘述了

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
  save_cmdline(argc, argv);//保存当前命令
fix_up_banner(argv[optind]);//跟运行时的标志有关系,显示一个运行示例的名字之类的,可以用-T指定这个banner
check_if_tty();
get_core_count();
#ifdef HAVE_AFFINITY
bind_to_free_cpu();
#endif /* HAVE_AFFINITY */
check_crash_handling();
check_cpu_governor();
setup_post();
setup_shm();//这一步为bitmap初始共享内存,供fuzzer分析、目标文件进行记录
//还初始化了virgin_bits(如果使用-B就不会),virgin_tmout,virgin_crash,内容均为0xFF

init_count_class16();//这里是为bitmap中的执行次数分类做准备
setup_dirs_fds();
read_testcases();
load_auto();
pivot_inputs();
if (extras_dir) load_extras(extras_dir);
if (!timeout_given) find_timeout();
detect_file_args(argv + optind + 1);
if (!out_file) setup_stdio_file();
check_binary(argv[optind]);
start_time = get_cur_time();
if (qemu_mode)
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;

perform_dry_run(use_argv);//这个函数可以看看,主要是初始化forkserver后用我们提供的testcase进行初始测试,然后输出测试用例的一些结果信息
//里面子函数有很多关于fuzz细节相关的代码,建议研读

calibrate_case

perform_dry_run中主要就是这个函数用我们给的所有testcase对进行循环测试,calibrate_case第一次先调用init_forkserver将结构初始化(函数见下文),然后根据临时变量stage指定的次数,使用run_target进行循环测试(函数见下文),而且每次执行完都进行了一次update_bitmap_score(这个函数用于找到更快或者规模更小的用例来达到相同的效果),这里是循环体内的一部分代码

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
u32 cksum;//这个变量在后面的hash会用到
if (!first_run && !(stage_cur % stats_update_freq)) show_stats();//这个是输出界面
write_to_testcase(use_mem, q->len);//输入测试用例的数据到一个临时文件,当做被测试文件的输入

fault = run_target(argv, use_tmout);//fault是目标测试返回的错误类型

/* 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;//如果不是crash(只记录crash的crash模式)

cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);//对执行的trace bitmap做一次hash

if (q->exec_cksum != cksum)//如果hash值发生变化,检测变化
{
u8 hnb = has_new_bits(virgin_bits);//always 0,return 1 hit-count for a particular tuple,2 if new tuples(二元组是干什么的见官方文档吧,相当于执行路径,用bitmap记录的)
if (hnb > new_bits) new_bits = hnb;
if (q->exec_cksum)//如果已经run过
{
u32 i;
for (i = 0; i < MAP_SIZE; i++)
{
if (!var_bytes[i] && first_trace[i] != trace_bits[i])//用var_bytes记录执行变化了的位置
{
var_bytes[i] = 1;
stage_max = CAL_CYCLES_LONG;
}
}
var_detected = 1;
}
else
{
q->exec_cksum = cksum;
memcpy(first_trace, trace_bits, MAP_SIZE);
}
}

循环体之后的代码,这里就针对perform_dry_run中单个测试用例的全部变化了,q就代表一个测试用例的结构体:

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
  stop_us = get_cur_time_us();
total_cal_us += stop_us - start_us;//统计时间用
total_cal_cycles += stage_max;
/* OK, let's collect some stats about the performance of this test case.
This is used for fuzzing air time calculations in calculate_score(). */
q->exec_us = (stop_us - start_us) / stage_max;//每次执行的平均时间
q->bitmap_size = count_bytes(trace_bits);//记录路径二元组的组数
q->handicap = handicap;
q->cal_failed = 0;

total_bitmap_size += q->bitmap_size;
total_bitmap_entries++;

update_bitmap_score(q);//更新top_rated结构体
//里面有一个minimize_bits函数,意思是设置一个只有0,1表示的是否有路径的minibitmap,相当于舍弃了原始bitmap的计数,只用一字节中的一位来表示
/* If this case didn't result in new output from the instrumentation, tell
parent. This is a non-critical problem, but something to warn the user
about. */
if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;

abort_calibration:
if (new_bits == 2 && !q->has_new_cov)
{
q->has_new_cov = 1;//循环执行过程中路径发生过变化
queued_with_cov++;
}
/* Mark variable paths. 在循环中如有某次的trace变化了,对该测试用例进行记录*/
if (var_detected)
{
var_byte_count = count_bytes(var_bytes);
if (!q->var_behavior)
{
mark_as_variable(q);
queued_variable++;
}
}
stage_name = old_sn;
stage_cur = old_sc;
stage_max = old_sm;
if (!first_run) show_stats();
return fault;

init_forkserver

calibrate_case中的函数,init_forkserver的代码不难懂建议直接看,又因为比较长这里就不写太多了,主要就是打开了两个pipe(一个用于forkserver发送状态、一个用于fuzzer发送命令,管道描述符分别设置值为198和199,对应之前的afl_maybe_log)之后fork出一个fuzzer子进程并setsid,把子进程的stdout、stderr全关、做了一些设置之后用execv替换成目标文件的进程映象,正式成为供fuzzer控制的forkserver。对于这个结构师傅们给出了图,对应的操作点一边是前面的afl-as.h分析中可以看到,还有一边在fuzzer中的run_target函数(下文分析)

image-20210203145001 QQ图片20210203145001

run_target

这里截取了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
/* Execute target application, monitoring for timeouts. Return status
information. The called program will update trace_bits[]. */
static u8 run_target(char** argv, u32 timeout) {
static struct itimerval it;//设置判断超时用
static u32 prev_timed_out = 0;
int status = 0;
u32 tb4;
child_timed_out = 0;

/* After this memset, trace_bits[] are effectively volatile, so we
must prevent any earlier operations from venturing into that
territory. */
memset(trace_bits, 0, MAP_SIZE);//每次runtarget都会将bitmap置零
MEM_BARRIER();//保护内存用

s32 res;
/* In non-dumb mode, we have the fork server up and running, so simply
tell it to have at it, and then read back PID. */

//下文的write和read是发送命令接收状态的具体位置
if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4) //4B trigger forkserver,这里对应前面的hello message,正式启动,prev_time_out在这里是啥东西应该无所谓
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to request new process from fork server (OOM?)");
}

if ((res = read(fsrv_st_fd, &child_pid, 4)) != 4) //get the child's PID by the fork of forkserver
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to request new process from fork server (OOM?)");
}

if (child_pid <= 0) FATAL("Fork server is misbehaving (OOM?)");

/* Configure timeout, as requested by user, then wait for child to terminate. */
it.it_value.tv_sec = (timeout / 1000);
it.it_value.tv_usec = (timeout % 1000) * 1000;
setitimer(ITIMER_REAL, &it, NULL);

/* The SIGALRM handler simply kills the child_pid and sets child_timed_out. */
s32 res;
if ((res = read(fsrv_st_fd, &status, 4)) != 4)//这里调用read应该会阻塞,因为这个statu得等子进程退出或者出错啥的
//此时forkserver还在waitpid,forkserver一旦接收到子进程的信号量就发statu到fuzzer这里
{
if (stop_soon) return 0;
RPFATAL(res, "Unable to communicate with fork server (OOM?)");
}

if (!WIFSTOPPED(status)) child_pid = 0;//已经停止了就将child_pid置零,防止在执行这几行代码时记录成time_out
it.it_value.tv_sec = 0;
it.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &it, NULL);//这里都是0的话就是取消计时器

total_execs++;

/* Any subsequent operations on trace_bits must not be moved by the
compiler below this point. Past this location, trace_bits[] behave
very normally and do not have to be treated as volatile. */
MEM_BARRIER();

tb4 = *(u32*)trace_bits;

classify_counts((u32*)trace_bits);//对trace_bits中的次数进行分类、重写
//这里比较重要,第一次分析的时候没注意在run_target分类之后bitmap中每个字节只有9种状态,在前面calibrate_case里面有一个has_new_bits一直没看懂细节

prev_timed_out = child_timed_out;

/*Report outcome to caller. 下面都是判断测试用例退出类型*/
if (WIFSIGNALED(status) && !stop_soon) {
kill_signal = WTERMSIG(status);
if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;
return FAULT_CRASH;
}

/* A somewhat nasty hack for MSAN, which doesn't support abort_on_error and
must use a special exit code. */
if (uses_asan && WEXITSTATUS(status) == MSAN_ERROR)
{
kill_signal = 0;
return FAULT_CRASH;
}
return FAULT_NONE;
}

main(part2)

fuzzer的第二部分,部分删减,虽然说前面分析跑测试用例的部分已经把fuzzer原理缕了很多,但是这里开始才是变异的核心部分,有些可以参考sakura的这篇文章,可能写的更详细或者侧重点不同啥的

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
  cull_queue();//用于精简提供的测试用例,算法在文章最开始提到的文章里面有写,是一个贪心策略,这里面的标记都是用的单个bit,用到了前面的minibitmap
show_init_stats();
seek_to = find_start_position();//resume时用,正常状态为0

write_stats_file(0, 0, 0);
save_auto();

if (stop_soon) goto stop_fuzzing;

while (1) {
u8 skipped_fuzz;
cull_queue();
if (!queue_cur) {//queue_cur用来判断是否执行完一轮,当然初始进来的时候应该是默认为null
queue_cycle++;//整个队列循环的次数
current_entry = 0;//这个好像就是指第几个测试用例
cur_skipped_paths = 0;//
queue_cur = queue;

show_stats();
/* If we had a full queue cycle with no new finds, try
recombination strategies next. */
if (queued_paths == prev_queued) //queue里的case数是否未变化
{
if (use_splicing) cycles_wo_finds++; //开启拼接时cycles_wo_finds++
else use_splicing = 1;//否则开启拼接
}
else cycles_wo_finds = 0;

prev_queued = queued_paths;
if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST"))//这里是synchronize fuzz用的
sync_fuzzers(use_argv);
}
skipped_fuzz = fuzz_one(use_argv);//关键函数,fuzz_one对当前queue进行一次完整测试,也是目前最长的一个函数,大约有1500行

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++;
}
if (queue_cur) show_stats();
write_bitmap();
write_stats_file(0, 0, 0);
save_auto();

stop_fuzzing:
SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing aborted %s +++\n" cRST,
stop_soon == 2 ? "programmatically" : "by user");
/* Running for more than 30 minutes but still doing first cycle? */
if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) {
SAYF("\n" cYEL "[!] " cRST
"Stopped during the first cycle, results may be incomplete.\n"
" (For info on resuming, see %s/README.)\n", doc_path);
}

fclose(plot_file);
destroy_queue();
destroy_extras();
ck_free(target_path);
ck_free(sync_id);
alloc_report();
OKF("We're done here. Have a nice day!\n");
exit(0);

fuzz_one

先看一下他的跳过策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (pending_favored) 
{
/* If we have any favored, non-fuzzed new arrivals in the queue,
possibly skip to them at the expense of already-fuzzed or non-favored
cases. */
if ((queue_cur->was_fuzzed || !queue_cur->favored) && UR(100) < SKIP_TO_NEW_PROB) return 1;
//这里不满足favored或者non-fuzzed的话跳过概率是99%
}
else if (!dumb_mode && !queue_cur->favored && queued_paths > 10)
{
/* Otherwise, still possibly skip non-favored cases, albeit less often.
The odds of skipping stuff are higher for already-fuzzed inputs and
lower for never-fuzzed entries. */
if (queue_cycle > 1 && !queue_cur->was_fuzzed)
{
if (UR(100) < SKIP_NFAV_NEW_PROB) return 1;
//queue_cycle大于1且没有被fuzz过,跳过概率是75%
}
else
{
if (UR(100) < SKIP_NFAV_OLD_PROB) return 1;
//fuzzed&&no-favored,有90%概率跳过
}
}

进入fuzz流程的话,就先将case map到内存

1
2
3
4
5
fd = open(queue_cur->fname, O_RDONLY);
len = queue_cur->len;
orig_in = in_buf = mmap(0, len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);//MAP_PRIVATE在内存对文件的修改不会影响文件本身

close(fd);

然后有一个CALIBRATION阶段和TRIMMING阶段,前者是在之前proform_dry_run中如果校准失败就再次校准,后者主要是为了修剪文件长度啥的,如果修剪文件对执行路径没有影响就make it permanent,优化后续运行

之后是一个PERFORMANCE SCORE阶段为该case计分,影响后续的havoc stage

到这里就是主要的变异策略了,推荐直接看文章吧,后面节约时间我就先不写了,以后自己要写变异策略的时候再参考吧