avatar

Catalog
AFL源码分析04:afl-fuzz.c

前言

本篇是对afl-fuzz.c中初始化部分的函数进行分析,由于源码过于庞大,部分较为简单的函数或者内容冗余的函数就不详细分析了,将只会摘取出部分源码进行解析。

afl-fuzz.c源码分析(初始配置)

关键变量

c
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
EXP_ST u8 *in_dir,                    /* Input directory with test cases  */
*out_file, /* File to fuzz, if any */
*out_dir, /* Working & output directory */
*sync_dir, /* Synchronization directory */
*sync_id, /* Fuzzer ID */
*use_banner, /* Display banner */
*in_bitmap, /* Input bitmap */
*doc_path, /* Path to documentation dir */
*target_path, /* Path to target binary */
/* 在check_binary被设置为可执行文件地址*/
/* 在get_qemu_argv被设置为afl-qemu-trace地址 */
*orig_cmdline; /* Original command line */

EXP_ST u32 exec_tmout = EXEC_TIMEOUT; /* Configurable exec timeout (ms) */
static u32 hang_tmout = EXEC_TIMEOUT; /* Timeout used for hang det (ms) */

static u32 stats_update_freq = 1; /* Stats update frequency (execs) */
/* 首次出现在calibrate_case */

EXP_ST u8 skip_deterministic, /* Skip deterministic stages? */
force_deterministic, /* Force deterministic stages? */
use_splicing, /* Recombine input files? */
dumb_mode, /* Run in non-instrumented mode? */
score_changed, /* Scoring for favorites changed? */
/* 首次出现在update_bitmap_score判断 */
/* top_rated[i]中的胜者是否改变 */
kill_signal, /* Signal that killed the child */
resuming_fuzz, /* Resuming an older fuzzing job? */
timeout_given, /* Specific timeout given? */
cpu_to_bind_given, /* Specified cpu_to_bind given? */
not_on_tty, /* stdout is not a tty */
term_too_small, /* terminal dimensions too small */
uses_asan, /* Target uses ASAN? */
no_forkserver, /* Disable forkserver? */
crash_mode, /* Crash mode! Yeah! */
in_place_resume, /* Attempt in-place resume? */
auto_changed, /* Auto-generated tokens changed? */
no_cpu_meter_red, /* Feng shui on the status screen */
no_arith, /* Skip most arithmetic ops */
shuffle_queue, /* Shuffle input queue? */
bitmap_changed = 1, /* Time to update bitmap? */
qemu_mode, /* Running in QEMU mode? */
skip_requested, /* Skip request, via SIGUSR1 */
run_over10m, /* Run time over 10 minutes? */
persistent_mode, /* Running in persistent mode? */
deferred_mode, /* Deferred forkserver mode? */
fast_cal; /* Try to calibrate faster? */
/* 若设置了环境变量AFL_FAST_CAL,则会 */
/* 设置该值,在calibrate_case中使用 */

static s32 forksrv_pid, /* PID of the fork server */
child_pid = -1, /* PID of the fuzzed program */
out_dir_fd = -1; /* FD of the lock file */

static s32 out_fd, /* Persistent fd for out_file */
dev_urandom_fd = -1, /* Persistent fd for /dev/urandom */
dev_null_fd = -1, /* Persistent fd for /dev/null */
/* 首次用于init_forkserver */
fsrv_ctl_fd, /* Fork server control pipe (write) */
fsrv_st_fd; /* Fork server status pipe (read) */

static s32 forksrv_pid, /* PID of the fork server */
child_pid = -1, /* PID of the fuzzed program */
out_dir_fd = -1; /* FD of the lock file */

EXP_ST u8* trace_bits; /* SHM with instrumentation bitmap */
static s32 shm_id; /* ID of the SHM region */

EXP_ST u8 virgin_bits[MAP_SIZE], /* Regions yet untouched by fuzzing */
virgin_tmout[MAP_SIZE], /* Bits we haven't seen in tmouts */
virgin_crash[MAP_SIZE]; /* Bits we haven't seen in crashes */

static u8 var_bytes[MAP_SIZE]; /* Bytes that appear to be variable */
/* 仅用于calibrate_case中 */

EXP_ST u32 queued_paths, /* Total number of queued testcases */
queued_variable, /* Testcases with variable behavior */
queued_at_start, /* Total number of initial inputs */
queued_discovered, /* Items discovered during this run */
queued_imported, /* Items imported via -S */
queued_favored, /* Paths deemed favorable */
queued_with_cov, /* Paths with new coverage bytes */
pending_not_fuzzed, /* Queued but not done yet */
pending_favored, /* Pending favored paths */
cur_skipped_paths, /* Abandoned inputs in cur cycle */
cur_depth, /* Current path depth */
max_depth, /* Max path depth */
useless_at_start, /* Number of useless starting paths */
var_byte_count, /* Bitmap bytes with var behavior */
current_entry, /* Current queue entry ID */
havoc_div = 1; /* Cycle count divisor for havoc */

/* 这里的大部分字段用于write_stats_file中更新状态界面 */
EXP_ST u64 total_crashes, /* Total number of crashes */
unique_crashes, /* Crashes with unique signatures */
total_tmouts, /* Total number of timeouts */
unique_tmouts, /* Timeouts with unique signatures */
unique_hangs, /* Hangs with unique signatures */
total_execs, /* Total execve() calls */
slowest_exec_ms, /* Slowest testcase non hang in ms */
/* 首次出现在run_target中,用于判断执行是否超时 */
start_time, /* Unix start time (ms) */
last_path_time, /* Time for most recent path (ms) */
last_crash_time, /* Time for most recent crash (ms) */
last_hang_time, /* Time for most recent hang (ms) */
last_crash_execs, /* Exec counter at last crash */
queue_cycle, /* Queue round counter */
cycles_wo_finds, /* Cycles without any new paths */
trim_execs, /* Execs done to trim input files */
bytes_trim_in, /* Bytes coming into the trimmer */
bytes_trim_out, /* Bytes coming outa the trimmer */
blocks_eff_total, /* Blocks subject to effector maps */
blocks_eff_select; /* Blocks selected as fuzzable */

static u8 *stage_name = "init", /* Name of the current fuzz stage */
*stage_short, /* Short stage name */
*syncing_party; /* Currently syncing with... */

static s32 stage_cur, stage_max; /* Stage progression */
/* 首次出现在calibrate_case */
static s32 splicing_with = -1; /* Splicing with which test case? */

struct extra_data {
u8* data; /* Dictionary token data */
u32 len; /* Dictionary token length */
u32 hit_cnt; /* Use count in the corpus */
};

static struct extra_data* extras; /* Extra tokens to fuzz with */
static u32 extras_cnt; /* Total number of tokens read */

static struct extra_data* a_extras; /* Automatically selected extras */
static u32 a_extras_cnt; /* Total number of tokens available */

/* Interesting values, as per config.h */
static s8 interesting_8[] = { INTERESTING_8 };
static s16 interesting_16[] = { INTERESTING_8, INTERESTING_16 };
static s32 interesting_32[] = { INTERESTING_8, INTERESTING_16, INTERESTING_32 };

/* List of interesting values to use in fuzzing. */
#define INTERESTING_8
-128, /* Overflow signed 8-bit when decremented */
-1, /* */
0, /* */
1, /* */
16, /* One-off with common buffer size */
32, /* One-off with common buffer size */
64, /* One-off with common buffer size */
100, /* One-off with common buffer size */
127 /* Overflow signed 8-bit when incremented */

#define INTERESTING_16
-32768, /* Overflow signed 16-bit when decremented */
-129, /* Overflow signed 8-bit */
128, /* Overflow signed 8-bit */
255, /* Overflow unsig 8-bit when incremented */
256, /* Overflow unsig 8-bit */
512, /* One-off with common buffer size */
1000, /* One-off with common buffer size */
1024, /* One-off with common buffer size */
4096, /* One-off with common buffer size */
32767 /* Overflow signed 16-bit when incremented */

#define INTERESTING_32
-2147483648LL, /* Overflow signed 32-bit when decremented */
-100663046, /* Large negative number (endian-agnostic) */
-32769, /* Overflow signed 16-bit */
32768, /* Overflow signed 16-bit */
65535, /* Overflow unsig 16-bit when incremented */
65536, /* Overflow unsig 16 bit */
100663045, /* Large positive number (endian-agnostic) */
2147483647 /* Overflow signed 32-bit when incremented */

setup_signal_handlers

理解这部分代码,需要掌握一些Linux进程间通信的知识

c
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
/* 设置信号处理程序 */
EXP_ST void setup_signal_handlers(void) {

/* 创建sigaction结构体sa,并设置如下成员:
sa_handler: 处理函数指针
sa_flags: 信号处理修改器,这里设置为SA_RESTART表示重启可中断的函数而不是给出EINTR错误
sa_sigaction: 信号行为
sa_mask: 指定一个信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程
的信号屏蔽字中。信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前进程接收到,这里
使用了sigemptyset()创建了控的信号屏蔽字,即不屏蔽任何信息 */
struct sigaction sa;

sa.sa_handler = NULL;
sa.sa_flags = SA_RESTART;
sa.sa_sigaction = NULL;

sigemptyset(&sa.sa_mask);

/* 用handle_stop_sig处理各种"stop"时的情况
handle_stop_sig会进行如下操作:
1.设置stop_soon的值为1
2.如果child_pid存在,就kill掉它
3.如果forksrv_pid存在,就kill掉它 */
sa.sa_handler = handle_stop_sig;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);

/* 用handle_timeout处理超时的情况
handle_timeout会进行如下操作:
1.如果child_pid存在,先设置child_time_out值为1,再kill掉child_pid
2.如果child_pid不存在但forksrv_pid存在,先设置child_time_out值为1,再kill掉forksrv_pid */
sa.sa_handler = handle_timeout;
sigaction(SIGALRM, &sa, NULL);

/* 用handle_resize处理窗口大小变化的情况,它会将clear_screen的值设置为1 */
sa.sa_handler = handle_resize;
sigaction(SIGWINCH, &sa, NULL);

/* 用handle_skipreq处理用户自定义的信号,它会将skip_requested的值设置为1 */
sa.sa_handler = handle_skipreq;
sigaction(SIGUSR1, &sa, NULL);

/* 将其它一些不关心的信号的处理函数设置为SIG_IGN */
sa.sa_handler = SIG_IGN;
sigaction(SIGTSTP, &sa, NULL);
sigaction(SIGPIPE, &sa, NULL);

}

check_asan_opts

c
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
/* 读取环境变量ASAN_OPTIONS和MASN_OPTIONS,做一些检查 */
static void check_asan_opts(void) {

/* 读取环境变量ASAN_OPTIONS,若存在,则进行如下判断:
1.是否设置了abort_on_error的值为1
2.是否设置了symbolize的值为0
若未进行上述设置,则抛出异常 */
u8* x = getenv("ASAN_OPTIONS");

if (x) {
if (!strstr(x, "abort_on_error=1"))
FATAL("Custom ASAN_OPTIONS set without abort_on_error=1 - please fix!");
if (!strstr(x, "symbolize=0"))
FATAL("Custom ASAN_OPTIONS set without symbolize=0 - please fix!");

}

/* 读取环境变量MSAN_OPTIONS,若存在,则进行如下判断:
1.是否设置了exit_code的值为经过'STRINGIFY(MASN_ERROR)'运算后的结果
2.是否设置了symbolize的值为0
若未进行上述设置,则抛出异常 */
x = getenv("MSAN_OPTIONS");

if (x) {
if (!strstr(x, "exit_code=" STRINGIFY(MSAN_ERROR)))
FATAL("Custom MSAN_OPTIONS set without exit_code="
STRINGIFY(MSAN_ERROR) " - please fix!");
if (!strstr(x, "symbolize=0"))
FATAL("Custom MSAN_OPTIONS set without symbolize=0 - please fix!");

}
}

fix_up_sync

c
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
/* 如果通过-M或者-S指定了sync_id(位于main函数中),则修复out_dir和sync_dir的值 */
static void fix_up_sync(void) {

u8* x = sync_id;

/* 如果位于dumb_mode模式,即参数设置了'-n'选项,提示'-S/-M'参数与'-n'参数互斥 */
if (dumb_mode)
FATAL("-S / -M and -n are mutually exclusive");

/* 如果skip_deterministic置位,说明参数设置了'-d'选项,
如果force_deterministic置位,说明参数设置了'-M'选项,
这里对于这俩参数使用的冲突进行纠正 */
if (skip_deterministic) {
if (force_deterministic)
FATAL("use -S instead of -M -d");
else
FATAL("-S already implies -d");
}

/* 1.检测sync_id是否以'数字'、'_'、'-'开头
2.检测sync_id的长度是否过大 */
while (*x) {
if (!isalnum(*x) && *x != '_' && *x != '-')
FATAL("Non-alphanumeric fuzzer ID specified via -S or -M");
x++;
}
if (strlen(sync_id) > 32)
FATAL("Fuzzer ID too long");

/* 1.设置sync_dir的值为out_dir
2.设置out_dir的值为拼接后的“out_dir/sync_id” */
x = alloc_printf("%s/%s", out_dir, sync_id);
sync_dir = out_dir;
out_dir = x;

/* 若参数中未设置'-M'选项,则对skip_deterministic和use_splicing进行置位,相当于设置'-d'选项 */
if (!force_deterministic) {
skip_deterministic = 1;
use_splicing = 1;
}
}

save_cmdline

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 将当前命令行中的参数复制到新申请的缓冲区buf中 */
static void save_cmdline(u32 argc, char** argv) {

u32 len = 1, i;
u8* buf;

for (i = 0; i < argc; i++)
len += strlen(argv[i]) + 1;

buf = orig_cmdline = ck_alloc(len);

for (i = 0; i < argc; i++) {
u32 l = strlen(argv[i]);
memcpy(buf, argv[i], l);
buf += l;

if (i != argc - 1)
*(buf++) = ' ';
}
*buf = 0;
}

fix_up_banner

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* (创建)并修剪一个运行横幅 */
static void fix_up_banner(u8* name) {

/* 如果没有设置use_banner,即参数中没有'-T'选项,则对use_banner进行设置:
如果存在sync_id,即参数中设置了'-M'或'-S'选项,就将use_banner设置为sync_id
如果不存在sync_id,就设置为传入参数name的实际值(name本身或最后一个'/'后面的值) */
if (!use_banner) {
if (sync_id) {
use_banner = sync_id;
} else {
u8* trim = strrchr(name, '/');
if (!trim) use_banner = name; else use_banner = trim + 1;
}
}

/* 如果use_banner的长度超过40字节,将超过40字节的地方截断,并设置成'.use_banner...'的形式 */
if (strlen(use_banner) > 40) {
u8* tmp = ck_alloc(44);
sprintf(tmp, "%.40s...", use_banner);
use_banner = tmp;
}
}

check_if_tty

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 检测是否在tty终端上运行 */
static void check_if_tty(void) {

struct winsize ws;

/* 检测环境变量AFL_NO_UI的值,如果存在,则设置not_on_tty的值为1,并返回 */
if (getenv("AFL_NO_UI")) {
OKF("Disabling the UI because AFL_NO_UI is set.");
not_on_tty = 1;
return;
}

/* int ioctl(int fd, unsigned long request, ...);
通过ioctl来读取windows size(TIOCGWINSZ时,用于获取窗口大小),
如果last error保存的值为ENOTTY,则设置not_on_tty的值为1,并返回 */
if (ioctl(1, TIOCGWINSZ, &ws)) {
if (errno == ENOTTY) {
OKF("Looks like we're not running on a tty, so I'll be a bit less verbose.");
not_on_tty = 1;
}
return;
}
}

CPU检查相关函数

c
1
2
3
4
5
6
7
8
9
10
11
/* 计算逻辑上CPU的核心数 */
static void get_core_count(void);

/* 根据亲缘性设置,将进程绑定到指定CPU核心,如果没找到合适的CPU则返回-1,CPU上限为4096个 */
bind_to_free_cpu();

/* 确保核心转储时不会进入程序 */
static void check_crash_handling(void);

/* 检查CPU的管理者 */
static void check_cpu_governor(void);

setup_post

c
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 setup_post(void) {

/* 获取环境变量AFL_POST_LIBRARY,如果未设置该值,则直接返回 */
void* dh;
u8* fn = getenv("AFL_POST_LIBRARY");
u32 tlen = 6;
if (!fn)
return;
ACTF("Loading postprocessor from '%s'...", fn);

/* 打开环境变量AFL_POST_LIBRARY指向的动态链接库,RTLD_NOW指定了在函数返回前解析出所有未定义的符号
调用dlsym获取'afl_postprocess'的函数地址,若获取失败,则抛出异常 */
dh = dlopen(fn, RTLD_NOW);
if (!dh)
FATAL("%s", dlerror());
post_handler = dlsym(dh, "afl_postprocess");
if (!post_handler)
FATAL("Symbol 'afl_postprocess' not found.");

/* 调用'afl_postprocess',做个简单的测试,用于发现可能存在的段错误 */
post_handler("hello", &tlen);
OKF("Postprocessor installed successfully.");

}

setup_shm

这部分涉及到的共享内存知识点可以参考此篇

c
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
/* 配置共享内存和virgin_bits,在程序启动时会被调用 */
EXP_ST void setup_shm(void) {

u8* shm_str;

/* 如果输入位图input bitmap为空,即参数中未设置'-B'选项,就用'\xff'初始化virgin_bits数组的每个元素
接下来,virgin_tmout数组与virgin_crash数组也进行类似的初始化 (index: 0-65535) */
if (!in_bitmap)
memset(virgin_bits, 255, MAP_SIZE);
memset(virgin_tmout, 255, MAP_SIZE);
memset(virgin_crash, 255, MAP_SIZE);

/* 函数原型:int shmget(key_t key, size_t size, int shmflg);
参数1:程序提供的一个key(非0整数),命名该共享内存段。这里IPC_PRIVATE表明创建一个新的共享内存段
参数2:指定创建的共享内存的大小为MAP_SIZE
参数3:IPC_CREAT -> 若共享内存不存在,则创建一个共享内存,否则直接打开它;
IPC_EXCL -> 只有在共享内存不存在的时候,新的共享内存才能创建,否则将报错
0600 -> Owner==6;Group==0;Other==0
返回值:将新创建的共享内存标识符返回给shm_id */
shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
if (shm_id < 0)
PFATAL("shmget() failed");

/* atexit将函数remove_shm注册为在afl-fuzz程序运行结束时要被调用的函数,
该函数执行了"shmctl(shm_id, IPC_RMID, NULL);"来删除共享内存。
函数原型:int shmctl(int shmid, int cmd, struct_ds *buf);
参数1:shmid为shmget函数返回的共享内存标识符
参数2:cmd是采取的操作;这里的IPC_RMID表示删除由shmid指定的共享内存段
参数3:buf指向一个shmid_ds结构体,该结构体用于设置共享内存模式和访问权限
返回值:根据cmd指定的操作不同返回值也会不同,在这里返回0表示操作成功,-1表示失败 */
atexit(remove_shm);

/* 将shm_str设置为shm_id的值
如果dumb_mode模式,即参数设置了'-n'选项,则将环境变量SHM_ENV_VAR的值设置为shm_str
然后将shm_str给free掉;如果不是dumb_mode模式,就直接free掉 */
shm_str = alloc_printf("%d", shm_id);
if (!dumb_mode)
setenv(SHM_ENV_VAR, shm_str, 1);
ck_free(shm_str);

/* 调用shmat,用shmid指定的共享内存来设置位图trace_bit,用于跟踪fuzz的执行流程
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数1:shmid,参考上面的shmctl
参数2:shmaddr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
参数3:shmflg,参考上面的shmget
返回值:调用成功时返回一个指向共享内存第一个字节的指针;调用失败返回-1 */
trace_bits = shmat(shm_id, NULL, 0);
if (trace_bits == (void *)-1)
PFATAL("shmat() failed");
}

这里通过”trace_bits”和”virgin_bits”两个位图来分别记录当前的tuple信息及整体tuple信息(tuple在前一篇提到过),其中“trace_bits”位于共享内存上,便于进行进程间通信。”virgin_tmout”和”virgin_crash”两个位图来记录fuzz过程中出现的所有目标程序超时以及崩溃的tuple信息

init_count_class16

这部分内容将count_class_lookup8换算成二进制后,就会好理解的多,这里参考了sakura师傅的说法。

c
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
/* 初始化count_class_lookup16数组,用于分支路径的规整 */

/* 先来理解这里的count_class_lookup8,它的结构如下所示,这里将其转换为2进制来看会更清晰,
它的存在是因为trace_bits是用一个字节来记录是否到达这个路径,以及这个路径被命中了多少次,
这个次数在0~255之间,但如果一个循环,它循环5次和循环6次可能是完全一样的效果,为了避免被
当成不同的路径,或者说尽可能减少因为命中次数导致的区别。在每次计算是否发现了新路径之前,先
把这个路径命中数进行规整,比如把命中5次和6次都统一认为是命中了8次,如下所示 */
static const u8 count_class_lookup8[256] = {

[0] = 0, /* 00000000 */
[1] = 1, /* 00000001 */
[2] = 2, /* 00000010 */
[3] = 4, /* 00000100 */
[4 ... 7] = 8, /* 00001000 */
[8 ... 15] = 16, /* 00010000 */
[16 ... 31] = 32, /* 00100000 */
[32 ... 127] = 64, /* 01000000 */
[128 ... 255] = 128 /* 10000000 */

};

/* 而为什么又需要用一个count_class_lookup16呢?是因为AFL中对于一条分支路径的表示是由一个
二元组来表示的。例如路径"A->B->D->C->B->D",就可以用[A,B][B,D][D,C][C,B]四个二元组
表示,这里[B,D]执行了2次,其余执行了1次,这些会被映射到一张哈希表中,这个哈希表在afl-as.h
的分析中提到过,是一个共享内存,在这里,其实就是前面提到的trace_bits。基于这种二元组的表示
的效率考虑,于是在下面初始化了count_class_lookup16 */
static u16 count_class_lookup16[65536];
EXP_ST void init_count_class16(void) {
u32 b1, b2;

for (b1 = 0; b1 < 256; b1++)
for (b2 = 0; b2 < 256; b2++)
count_class_lookup16[(b1 << 8) + b2] =
(count_class_lookup8[b1] << 8) | count_class_lookup8[b2];
}

setup_dirs_fds

c
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
/* 准备output_dir以及相关的文件描述符 */
EXP_ST void setup_dirs_fds(void) {

u8* tmp;
s32 fd;

/* 这部分代码用于设置输出文件夹:
如果设置了sync_id(即参数包含'-M'或'-S'),则以读写权限(0700)创建sync_dir文件夹
如果报错(mkdir返回0说明创建成功,出错返回-1),并且报错类型不是EEXIST,则抛出异常
以读写权限(0700)创建out_dir文件夹
如果报错,并且报错类型不是EEXIST,则抛出异常
如果报错,且报错类型是EEXIST,则说明文件夹已存在,调用maybe_delete_out_dir
如果没报错,说明创建成功,则判断是否设置了in_place_resume(参数包含'-i'且in_dir为'-')
如果设置了则抛出异常
如果没设置,则以只读模式打开文件out_dir,并将文件描述符返回给out_dir_fd
如果定义了宏__sun,则还会进一步检查打开out_dir是否失败?为out_dir建立互斥锁是否失败? */
ACTF("Setting up output directories...");
if (sync_id && mkdir(sync_dir, 0700) && errno != EEXIST)
PFATAL("Unable to create '%s'", sync_dir);

if (mkdir(out_dir, 0700)) {
if (errno != EEXIST)
PFATAL("Unable to create '%s'", out_dir);
maybe_delete_out_dir();
}
else {
if (in_place_resume)
FATAL("Resume attempted but old output directory not found");
out_dir_fd = open(out_dir, O_RDONLY);

#ifndef __sun
if (out_dir_fd < 0 || flock(out_dir_fd, LOCK_EX | LOCK_NB))
PFATAL("Unable to flock() output directory.");
#endif /* !__sun */
}


/* Queue directory for any starting & discovered paths.
Top-level directory for queue metadata used for session resume and related tasks.
Directory for flagging queue entries that went through deterministic fuzzing in the past
Directory with the auto-selected dictionary entries
The set of paths currently deemed redundant
The set of paths showing variable behavior
以读写权限创建文件夹out_dir/queue,用于保存任意开始和发现到的路径
以读写权限创建文件夹out_dir/queue/.state/,保存用于会话恢复及其相关任务的元数据
以读写权限创建文件夹out_dir/queue/.state/deterministic_done/,用于标记先前经过deterministic fuzzing(设置'-M')的队列条目
以读写权限创建文件夹out_dir/queue/.state/auto_extras/,用于保存带有自动选择的字典条目
以读写权限创建文件夹out_dir/queue/.state/reduntant_edges/,用于保存当前被认为是冗余的路径集
以读写权限创建文件夹out_dir/queue/.state/variable_behavior/,用于保存不同行为的路径集
*/
tmp = alloc_printf("%s/queue", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);

tmp = alloc_printf("%s/queue/.state/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);

tmp = alloc_printf("%s/queue/.state/deterministic_done/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);

tmp = alloc_printf("%s/queue/.state/auto_extras/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);

tmp = alloc_printf("%s/queue/.state/redundant_edges/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);

tmp = alloc_printf("%s/queue/.state/variable_behavior/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);


/* 以读写权限创建文件夹out_dir/.synced/,用于跟踪共同工作的fuzzers
以读写权限创建文件夹out_dir/crashes,用于记录所有(unique)crashed
以读写权限创建文件夹out_dir/hangs,用于记录所有超时的情况
以读写模式打开/dev/null,并将文件描述符返回给dev_urandom_fd
以只读模式打开/dev/urandom,并将文件描述符返回给dev_urandom_fd */
if (sync_id) {
tmp = alloc_printf("%s/.synced/", out_dir);

if (mkdir(tmp, 0700) && (!in_place_resume || errno != EEXIST))
PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
}

tmp = alloc_printf("%s/crashes", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);

tmp = alloc_printf("%s/hangs", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);

dev_null_fd = open("/dev/null", O_RDWR);
if (dev_null_fd < 0) PFATAL("Unable to open /dev/null");

dev_urandom_fd = open("/dev/urandom", O_RDONLY);
if (dev_urandom_fd < 0) PFATAL("Unable to open /dev/urandom");

/* 打开文件out_dir/plot_data,如果文件不存在就以读写模式(0600)创建文件
打开文件后将"# unix_time, ...... ,execs_per_sec\n"写入到文件中 */
tmp = alloc_printf("%s/plot_data", out_dir);
fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd < 0)
PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
plot_file = fdopen(fd, "w");
if (!plot_file)
PFATAL("fdopen() failed");
fprintf(plot_file, "# unix_time, cycles_done, cur_path, paths_total, "
"pending_total, pending_favs, map_size, unique_crashes, "
"unique_hangs, max_depth, execs_per_sec\n");
/* ignore errors */

}

read_testcases

c
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
/* 从input_dir中读取testcase到队列,然后调用add_to_queue()将testcase进行排队,
该函数会在启动时进行调用 */
static void read_testcases(void) {

/* struct dirent {
long d_ino; // 索引节点号
off_t d_off; // 在目录文件中的偏移
unsigned short d_reclen; // 文件名长
unsigned char d_type; // 文件类型
char d_name[NAME_MAX+1]; // 文件名,最长255字节
}
*/
struct dirent **nl;
s32 nl_cnt;
u32 i;
u8* fn;


/* 尝试访问in_dir/queue文件夹,如果存在就设置in_dir为in_dir/queue */
fn = alloc_printf("%s/queue", in_dir);
if (!access(fn, F_OK))
in_dir = fn;
else
ck_free(fn);
ACTF("Scanning '%s'...", in_dir);


/* 函数原型:
int scandir(
const char *dirp,
struct dirent ***namelist,
int (*filter)(const struct dirent *),
int (*compar)(const struct dirent **, const struct dirent **)
);
int alphasort(
const struct dirent **a,
const struct dirent **b
);
1.调用scandir扫描in_dir目录下的条目(不使用readdir是因为这样会导致测试用例随机而难以控制),
经alphasort排序后保存到nl里,nl是一个指向指针数组的指针
2.nl_cnt记录扫描目录时选中的条目,如果条目小于0,则进行出错处理
*/
nl_cnt = scandir(in_dir, &nl, NULL, alphasort);
if (nl_cnt < 0) {
if (errno == ENOENT || errno == ENOTDIR)
SAYF("\n" cLRD "[-] " cRST
"The input directory does not seem to be valid - try again. The fuzzer needs\n"
" one or more test case to start with - ideally, a small file under 1 kB\n"
" or so. The cases must be stored as regular files directly in the input\n"
" directory.\n");
PFATAL("Unable to open '%s'", in_dir);
}


/* 如果shuffle_queue存在(即设置了环境变量AFL_SHUFFLE_QUEUE)且nl_cnt大于1,
则调用shuffle_ptrs,该函数会对传入的指针数组进行洗牌 */
if (shuffle_queue && nl_cnt > 1) {
ACTF("Shuffling queue...");
shuffle_ptrs((void**)nl, nl_cnt);
}


/* 进入for循环,扫描数组中的每一项:
1.读取nl[i]->d_name,并与in_dir拼接形成路径fn(in_dir/d_name)
2.读取nl[i]->d_name,并与in_dir拼接形成路径dfn(in_dir/.state/deterministic_done/d_name)
3.设置passed_det的值为0
4.读取fn路径对应文件的状态到st中,并判断该文件是否可访问
5.根据st_mode(文件保护模式),st_size(文件总大小),以及文件名,从而筛选掉一些无关文件
6.检测st_size是否超过文件大小界限
7.判断dfn路径指定的文件是否可访问,实际上这里是判断fn路径下对应的文件是否fuzz过,从而在恢复扫描时
防止多余时间的消耗(如果fuzz过,就会被放到dfn路径指定的位置):
a.如果fuzz过,就设置passed_det的值为1
b.如果没fuzz过,就维持passed_det的值为0
8.调用add_to_queue将这个文件放入队列,同时附上该文件的大小和passed_det */
for (i = 0; i < nl_cnt; i++) {

struct stat st;

u8* fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name);
u8* dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name);
u8 passed_det = 0;
free(nl[i]);

if (lstat(fn, &st) || access(fn, R_OK))
PFATAL("Unable to access '%s'", fn);

if (!S_ISREG(st.st_mode) || !st.st_size || strstr(fn, "/README.testcases")) {
ck_free(fn);
ck_free(dfn);
continue;
}

if (st.st_size > MAX_FILE)
FATAL("Test case '%s' is too big (%s, limit is %s)", fn,
DMS(st.st_size), DMS(MAX_FILE));

if (!access(dfn, F_OK))
passed_det = 1;
ck_free(dfn);

add_to_queue(fn, st.st_size, passed_det);
}
free(nl);


/* 1.如果queued_paths的值为0,说明队列里没有用于测试的输入文件(testcase)了,则抛出异常
2.设置last_path_time值为0
3.用queued_paths的值更新原始输入文件的数量queued_at_start */
if (!queued_paths) {
SAYF("\n" cLRD "[-] " cRST
"Looks like there are no valid test cases in the input directory! The fuzzer\n"
" needs one or more test case to start with - ideally, a small file under\n"
" 1 kB or so. The cases must be stored as regular files directly in the\n"
" input directory.\n");
FATAL("No usable test cases in '%s'", in_dir);
}
last_path_time = 0;
queued_at_start = queued_paths;
}

add_to_queue

queue_entry结构体与部分相关变量

c
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
struct queue_entry {
u8* fname; /* File name for the test case */
u32 len; /* Input length */

u8 cal_failed, /* Calibration failed? */
trim_done, /* Trimmed? */
was_fuzzed, /* Had any fuzzing done yet? */
passed_det, /* Deterministic stages passed? */
has_new_cov, /* Triggers new coverage? */
var_behavior, /* Variable behavior? */
favored, /* Currently favored? */
fs_redundant; /* Marked as redundant in the fs? */

u32 bitmap_size, /* Number of bits set in bitmap */
exec_cksum; /* Checksum of the execution trace */

u64 exec_us, /* Execution time (us) */
handicap, /* Number of queue cycles behind */
depth; /* Path depth */

u8* trace_mini; /* Trace bytes, if kept */
u32 tc_ref; /* Trace bytes ref count 用于update_bitmap_score */

struct queue_entry *next, /* Next element, if any */
*next_100; /* 100 elements ahead */
};

static struct queue_entry *queue, /* Fuzzing queue (linked list) */
*queue_cur, /* Current offset within the queue */
*queue_top, /* Top of the list */
*q_prev100; /* Previous 100 marker */

static struct queue_entry*
top_rated[MAP_SIZE]; /* Top entries for bitmap bytes */

源码分析

c
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
/* 将新的testcase添加到队列 */

static void add_to_queue(u8* fname, u32 len, u8 passed_det) {

/* 分配一个queue_entry结构,并进行初始化:
1.fname设置testcase的为文件路径
2.len设置为文件大小
3.depth设置为cur_depth+1
4.passed_det设置为传入的passed_det
5.如果当前深度大于max_depth,则用当前深度替换max_depth
6.判断queue_top是否存在:
a.如果存在,将新初始化的q挂在queue_top单链表上,并将q设置为新的queue_top(链表尾)
b.如果不存在,则将q分别设置为queue_top、queue、q_prev100这3个链表的首元素(链表头)
7.队列中的测试用例数加1;待fuzz的测试用例数加1
8.设置cycles_wo_finds的值为0,如果在某一轮fuzz时没有发现新的路径则会设置该值,这里是第一次
遇到这个值,所以初始化为0 */
struct queue_entry* q = ck_alloc(sizeof(struct queue_entry));

q->fname = fname;
q->len = len;
q->depth = cur_depth + 1;
q->passed_det = passed_det;

if (q->depth > max_depth)
max_depth = q->depth;

if (queue_top) {
queue_top->next = q;
queue_top = q;
} else
q_prev100 = queue = queue_top = q;

queued_paths++;
pending_not_fuzzed++;

cycles_wo_finds = 0;


/* 每100个元素会设置一个next_100指针,用于更快速的迭代
如果队列中已经有100个testcase对应的q(用testcase的信息初始化的queue_entry):
1.将q_prev100->next_100设置为新入队的q(例如第101个),此时q_prev100下标为0,新入队的q下标
为100,这个q_prev100链表串着每100个q里面的第一个q,这么做是为了能够更快的迭代
2.接着将q_prev100也设置为q,即第101个q设置为q_prev100的链表尾
最后将last_path_time设置为当前的系统时间
*/
if ((queued_paths - 1) % 100 == 0 && queued_paths > 1) {
q_prev100->next_100 = q;
q_prev100 = q;
}
last_path_time = get_cur_time();
}

load_auto

c
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
/* 加载自动生成的额外内容 */
static void load_auto(void) {

u32 i;

/* 进入主循环,遍历USE_AUTO_EXTRAS次,默认是50:
1.拼接形成路径fn(in_dir/.state/auto_extras/auto_i)
2.以只读模式打开路径fn指定的文件,将文件描述符返给fd;若打开失败,则抛出异常
3.读取fd指定文件中MAX_AUTO_EXTRA+1个字节的内容(默认是32+1),这里多读取1个字节来检测token
是否过大:
a.如果读取失败,则抛出异常
b.如果读取成功,且读取的字节数在MIN_AUTO_EXTRA(默认为3)和MAX_AUTO_EXTRA(默认为32)
之间,则调用maybe_add_auto */
for (i = 0; i < USE_AUTO_EXTRAS; i++) {
u8 tmp[MAX_AUTO_EXTRA + 1];
u8* fn = alloc_printf("%s/.state/auto_extras/auto_%06u", in_dir, i);
s32 fd, len;

fd = open(fn, O_RDONLY, 0600);
if (fd < 0) {
if (errno != ENOENT) PFATAL("Unable to open '%s'", fn);
ck_free(fn);
break;
}

len = read(fd, tmp, MAX_AUTO_EXTRA + 1);
if (len < 0)
PFATAL("Unable to read from '%s'", fn);
if (len >= MIN_AUTO_EXTRA && len <= MAX_AUTO_EXTRA)
maybe_add_auto(tmp, len);
close(fd);
ck_free(fn);
}

/* 根据i是否存在,判断是否加载了自动生成的字典token */
if (i) OKF("Loaded %u auto-discovered dictionary tokens.", i);
else OKF("No auto-generated dictionary tokens to reuse.");
}

maybe_add_auto

c
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
/* 向自动生成的额外内容里面增加一些内容,mem为读取的auto_i文件,len为读取的长度 */
static void maybe_add_auto(u8* mem, u32 len) {

u32 i;

/* 如果MAX_AUTO_EXTRAS或USE_AUTO_EXTRAS没有设置,则直接return */
if (!MAX_AUTO_EXTRAS || !USE_AUTO_EXTRAS) return;

/* 从mem[1]开始扫描,找到第一个与mem[0]不同的位置
如果全都相同,则直接return */
for (i = 1; i < len; i++)
if (mem[0] ^ mem[i]) break;
if (i == len) return;

/* 如果mem的大小为2字节或者4字节,则用mem分别去与interesting_16或interesting_32进行比较,
若存在相同的情况,则直接return掉。这里的interesting_16/32定义在afl-fuzz.c的开头;里面
出现的INTERESTING_8/16/32定义在config.h;计算时用到的SWAP16/32算法定义在types.h */
if (len == 2) {
i = sizeof(interesting_16) >> 1;
while (i--)
if (*((u16*)mem) == interesting_16[i] ||
*((u16*)mem) == SWAP16(interesting_16[i])) return;
}
if (len == 4) {
i = sizeof(interesting_32) >> 2;
while (i--)
if (*((u32*)mem) == interesting_32[i] ||
*((u32*)mem) == SWAP32(interesting_32[i])) return;
}

/* 第一个循环:从已经读入的token里面找到第一个大小与mem相同的token下标,这么做是因为extras
数组是会经过优化按照大小顺序排列
第二个循环:从已经读入的token里面找到大小与mem相同的token,调用memcmp_nocase进行大小写
不敏感的比较,若发现token与mem完全相同,则直接return,这一步和上面一样,还是
筛选掉已经有的token */
for (i = 0; i < extras_cnt; i++)
if (extras[i].len >= len) break;

for (; i < extras_cnt && extras[i].len == len; i++)
if (!memcmp_nocase(extras[i].data, mem, len)) return;


/* 经过上面的校验,mem经过筛选,和interesting_16/32以及extras数组中的元素不同了,这时以同
样的校验手法判断a_extras数组中是否有与mem相同的元素。如果存在相同的元素:
1.令a_extras数组中该元素对应的hit_cnt字段加1,表示该语料被使用的次数增加1次
2.接着跳转到sort_a_extras进行排序处理 */
auto_changed = 1;

for (i = 0; i < a_extras_cnt; i++) {
if (a_extras[i].len == len && !memcmp_nocase(a_extras[i].data, mem, len)) {
a_extras[i].hit_cnt++;
goto sort_a_extras;
}
}


/* 如果能走到这一步,说明interesting_8/16/32以及extras数组和a_extras数组中都没有匹配
到这个mem元素,说明这是一个新的条目(entry),为此,如果仍有空间的话,就把这个entry放到
a_entras里面去;如果没有空间的话,就从a_extras数组的后半部分空间里,随机替换掉一个元素
具体操作如下:
1.如果a_extras_cnt < MAX_AUTO_EXTRAS,即余有空间:
a.为a_extras数组增加一个extra_data空间
b.设置新增extra_data.data为mem
c.设置新增extra_data.len为len
d.a_extra_cnt的值自增
2.如果空间不足:
a.从a_extras数组的后半部分空间里随机找到一个位置
b.替换该位置上extra_data.data为mem
c.替换该位置上extra_data.len为len
d.将该位置上extra_data.hit_cnt的值置0 */
if (a_extras_cnt < MAX_AUTO_EXTRAS) {
a_extras = ck_realloc_block(a_extras, (a_extras_cnt + 1) *
sizeof(struct extra_data));

a_extras[a_extras_cnt].data = ck_memdup(mem, len);
a_extras[a_extras_cnt].len = len;
a_extras_cnt++;

} else {
i = MAX_AUTO_EXTRAS / 2 +
UR((MAX_AUTO_EXTRAS + 1) / 2);

ck_free(a_extras[i].data);

a_extras[i].data = ck_memdup(mem, len);
a_extras[i].len = len;
a_extras[i].hit_cnt = 0;
}


/* 函数原型:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *, void *),
void *arg);
作用:对数组大小为nmemb,每个元素大小为size,由base指定的数组,按照compar算法进行排序
返回值:无
1.首先对a_extras所有元素按照元素大小进行降序排列(排序用的比较算法这里不展开,源码很简单)
2.然后将a_extras中的前USE_AUTO_EXTRAS个元素按照其语料被使用次数进行降序排列 */
sort_a_extras:
qsort(a_extras, a_extras_cnt, sizeof(struct extra_data),
compare_extras_use_d);

qsort(a_extras, MIN(USE_AUTO_EXTRAS, a_extras_cnt),
sizeof(struct extra_data), compare_extras_len);
}

pivot_inputs

c
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
/* 在out_dir/queue中为所有in_dir中的testcase创建硬链接 */
static void pivot_inputs(void) {

struct queue_entry* q = queue;
u32 id = 0;

ACTF("Creating hard links for all input files...");

while (q) {

/* 将q->fname中最后一个'/'后面的字符串赋给rsl;若没有'/',就直接把fname赋给rsl */
u8 *nfn, *rsl = strrchr(q->fname, '/');
u32 orig_id;
if (!rsl)
rsl = q->fname;
else
rsl++;

#ifndef SIMPLE_FILES
# define CASE_PREFIX "id:"
#else
# define CASE_PREFIX "id_"
#endif /* ^!SIMPLE_FILES */
/* 如果原始文件名符合语法,并且文件名记录下的ID匹配上之前指定的ID,那么就使用原始文件名,
这么做对于恢复fuzzing执行很有价值,具体需要满足的操作如下:
1.rsl前3位为'id_'或'id:'(根据是否为SIMPLE_FILES进行判断)
2.将rsl第4位开始的数字读取到orig_id,并且orig_id与id的值相等
如果满足上述条件,则进行一些恢复操作,具体如下:
a.设置resuming_fuzz的值为1
b.拼接路径out_dir/queue/rsl,并赋值给nfn
c.找到rsl第4位开始,第一个出现':'的位置,如果可以找到,就将':'后面开始的数字读取
到src_id中,如果成功读取了src_id:
1).从fuzzing队列头开始扫描,每扫描一个queue,与此同时src_id的值自减1
2).如果src_id的值归0了,但queue还没到末尾。则通过被扫描的queue深度+1来
设置当前的queue
3).判断max_depth是否发生了变化,并进行适当更新
*/
if (!strncmp(rsl, CASE_PREFIX, 3) &&
sscanf(rsl + 3, "%06u", &orig_id) == 1 && orig_id == id) {

u8* src_str;
u32 src_id;

resuming_fuzz = 1;
nfn = alloc_printf("%s/queue/%s", out_dir, rsl);

src_str = strchr(rsl + 3, ':');

if (src_str && sscanf(src_str + 1, "%06u", &src_id) == 1) {
struct queue_entry* s = queue;
while (src_id-- && s)
s = s->next;
if (s)
q->depth = s->depth + 1;
if (max_depth < q->depth)
max_depth = q->depth;
}
}

/* 如果不满足上述条件:
a.如果不是SIMPLE_FILES:
1).在rsl里搜索'orig:',若能搜到,则将use_name设置为'orig:'后面的部分;否则
直接将use_name设置为rsl
2).拼接路径out_dir/queue/id:id,orig:use_name,并赋给nfn
b.如果是SIMPLE_FILES:
1).拼接路径out_dir/queue/id_id,并赋给nfn
*/
else {
#ifndef SIMPLE_FILES
u8* use_name = strstr(rsl, ",orig:");

if (use_name)
use_name += 6;
else
use_name = rsl;
nfn = alloc_printf("%s/queue/id:%06u,orig:%s", out_dir, id, use_name);

#else
nfn = alloc_printf("%s/queue/id_%06u", out_dir, id);
#endif /* ^!SIMPLE_FILES */
}


/* 1.调用link_or_copy创建q->fname指定的输入测试用例文件到路径nfn的硬链接,内容也一并复制
2.将q->fname原先指向的路径free掉,重新设置为nfn这个硬链接
3.判断是否设置了passed_det的值,即是否fuzz过,如果fuzz过就调用mark_as_det_done打开
或创建out_dir/queue/.state/deterministic_done/fname这个文件
4.跳转到下一个q,用于外层循环的遍历
5.id自增1 */
link_or_copy(q->fname, nfn);
ck_free(q->fname);
q->fname = nfn;

if (q->passed_det)
mark_as_det_done(q);
q = q->next;
id++;
}

/* 如果设置了in_place_resume,则调用nuke_resume_dir删除用于本地会话恢复的临时文件夹 */
if (in_place_resume)
nuke_resume_dir();
}

nuke_resume_dir

c
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
/* 删除用于本地会话恢复的临时文件夹 */
static void nuke_resume_dir(void) {

u8* fn;

/* 1.删除out_dir/_resume/.state/deterministic_done目录下所有'id_'或'id:'前缀的文件
2.删除out_dir/_resume/.state/auto_extras目录下所有'auto_'前缀的文件
3.删除out_dir/_resume/.state/redundant_edges目录下所有'id_'或'id:'前缀的文件
4.删除out_dir/_resume/.state/variable_behavior目录下所有'id_'或'id:'前缀的文件
5.删除out_dir/_resume/.state/整个文件夹
6.删除out_dir/_resume/目录下所有'id_'或'id:'前缀的文件 */
fn = alloc_printf("%s/_resume/.state/deterministic_done", out_dir);
if (delete_files(fn, CASE_PREFIX)) goto dir_cleanup_failed;
ck_free(fn);

fn = alloc_printf("%s/_resume/.state/auto_extras", out_dir);
if (delete_files(fn, "auto_")) goto dir_cleanup_failed;
ck_free(fn);

fn = alloc_printf("%s/_resume/.state/redundant_edges", out_dir);
if (delete_files(fn, CASE_PREFIX)) goto dir_cleanup_failed;
ck_free(fn);

fn = alloc_printf("%s/_resume/.state/variable_behavior", out_dir);
if (delete_files(fn, CASE_PREFIX)) goto dir_cleanup_failed;
ck_free(fn);

fn = alloc_printf("%s/_resume/.state", out_dir);
if (rmdir(fn) && errno != ENOENT) goto dir_cleanup_failed;
ck_free(fn);

fn = alloc_printf("%s/_resume", out_dir);
if (delete_files(fn, CASE_PREFIX)) goto dir_cleanup_failed;
ck_free(fn);

return;

dir_cleanup_failed:
FATAL("_resume directory cleanup failed");
}

load_extras

c
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
/* Read extras from the extras directory and sort them by size.
从extras目录读取额外的符号,并按照大小将它们排序 */

static void load_extras(u8* dir) {

/* struct dirent {
long d_ino; // 索引节点号
off_t d_off; // 在目录文件中的偏移
unsigned short d_reclen; // 文件名长
unsigned char d_type; // 文件类型
char d_name[NAME_MAX+1]; // 文件名,最长255字节
}
*/
DIR* d;
struct dirent* de;
u32 min_len = MAX_DICT_FILE, max_len = 0, dict_level = 0;
u8* x;


/* 如果dir中包含'@',那么将'@'后面的字符串转换为数字,并赋值给dict_level */
if ((x = strchr(dir, '@'))) {
*x = 0;
dict_level = atoi(x + 1);
}
ACTF("Loading extra dictionary from '%s' (level %u)...", dir, dict_level);


/* 打开dir所在目录,如果不能打开且错误类型为ENOTDIR(说明不是一个目录文件),则调用
load_extras_file从文件中读取额外的符号,读取完毕后,跳转到check_and_sort排序 */
d = opendir(dir);
if (!d) {
if (errno == ENOTDIR) {
load_extras_file(dir, &min_len, &max_len, dict_level);
goto check_and_sort;
}
PFATAL("Unable to open '%s'", dir);
}

if (x)
FATAL("Dictionary levels not supported for directories.");


/* 走到这一步,说明传进来的dir是个目录。这里调用readdir来循环读取dir目录下的文件并进行如下操作:
1.拼接路径dir/d_name,并赋值给dir
2.读取fn路径对应文件的状态到st中,并判断该文件是否可访问
3.根据st_mode(文件保护模式)、st_size(文件总大小),筛选掉一些无关文件(参考read_testcases)
4.判断st_size是否超过文件大小界限,并根据其大小设置min_len/max_len
5.为extras数组增加一个extra_data的空间,并设置data的空间和len的值(参考maybe_add_auto)
6.打开fn路径指向的文件,从该文件读取内容到extra_data.data
7.关闭文件,free掉路径
8.结束循环,关闭文件夹 */
while ((de = readdir(d))) {
struct stat st;
u8* fn = alloc_printf("%s/%s", dir, de->d_name);
s32 fd;

if (lstat(fn, &st) || access(fn, R_OK))
PFATAL("Unable to access '%s'", fn);

if (!S_ISREG(st.st_mode) || !st.st_size) {
ck_free(fn);
continue;
}

if (st.st_size > MAX_DICT_FILE)
FATAL("Extra '%s' is too big (%s, limit is %s)", fn,
DMS(st.st_size), DMS(MAX_DICT_FILE));
if (min_len > st.st_size)
min_len = st.st_size;
if (max_len < st.st_size)
max_len = st.st_size;

extras = ck_realloc_block(extras, (extras_cnt + 1) *
sizeof(struct extra_data));
extras[extras_cnt].data = ck_alloc(st.st_size);
extras[extras_cnt].len = st.st_size;

fd = open(fn, O_RDONLY);

if (fd < 0)
PFATAL("Unable to open '%s'", fn);
ck_read(fd, extras[extras_cnt].data, st.st_size, fn);

close(fd);
ck_free(fn);
extras_cnt++;
}
closedir(d);


/* 下面这部分的核心就是调用qsort语句,对保存额外符号的extras数组,按照大小进行排序 */
check_and_sort:
if (!extras_cnt)
FATAL("No usable files in '%s'", dir);

qsort(extras, extras_cnt, sizeof(struct extra_data), compare_extras_len);

OKF("Loaded %u extra tokens, size range %s to %s.", extras_cnt,
DMS(min_len), DMS(max_len));
if (max_len > 32)
WARNF("Some tokens are relatively large (%s) - consider trimming.",
DMS(max_len));
if (extras_cnt > MAX_DET_EXTRAS)
WARNF("More than %u tokens - will use them probabilistically.",
MAX_DET_EXTRAS);

}

find_timeout

c
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
/* 防止当“恢复会话”时,因为没有设置参数'-t',而导致超时时间的不断调整的情况出现 */
static void find_timeout(void) {

static u8 tmp[4096];

u8 *fn, *off;
s32 fd, i;
u32 ret;

/* 主体流程:
1.如果没有设置resuming_fuzz,直接return
2.根据是否设置了in_place_resume,采取不同方式的路径拼接,并赋值给fn
3.打开路径fn指定的文件,打开失败就return;打开成功就把文件描述符返回给fd
4.从fd指定的文件中读取4095字节到tmp中;(void)i表示忽略读取是可能的错误;之后关闭文件
5.从tmp中找到第一个出现字符串'exec_timeout :'的位置:
a.如果找不到,就直接return
b.如果找到了,就将这个位置+20字节偏移后的字符串,转换为整数,并赋值给ret
1).如果ret小于等于4,就直接return
2).否则用ret赋值给exe_tmout
6.设置timeout_given的值为3,给出指定的超时 */
if (!resuming_fuzz) return;

if (in_place_resume)
fn = alloc_printf("%s/fuzzer_stats", out_dir);
else
fn = alloc_printf("%s/../fuzzer_stats", in_dir);

fd = open(fn, O_RDONLY);
ck_free(fn);
if (fd < 0) return;
i = read(fd, tmp, sizeof(tmp) - 1);
(void)i;
close(fd);

off = strstr(tmp, "exec_timeout : ");
if (!off)
return;

ret = atoi(off + 20);
if (ret <= 4)
return;

exec_tmout = ret;
timeout_given = 3;

}

detect_file_args

c
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
/* 检测参数中是否存在'@@'(对从文件读取输入的目标程序来说,要用'@@'),
如果有就替换为拼接后的字符串;如果没有就直接return */
EXP_ST void detect_file_args(char** argv) {

/* getcwd == GET Current Working Directory */
u32 i = 0;
u8* cwd = getcwd(NULL, 0);

if (!cwd)
PFATAL("getcwd() failed");

/* 遍历所有参数,进行如下操作:
1.在当前参数中找到,找到第一个出现'@@'的地方并赋值给aa_loc;如果没有找到就进行下一轮循环
2.如果没有指定out_file,就将out_dir/.cur_input拼接后赋值给out_file
3.判断out_file是否从'/'开始:
a.如果是的话,就直接将out_file赋值给aa_subst
b.否则将cwd/out_file拼接并赋值给aa_subst
4.先将aa_loc(当前指向的值为'@@')的第一个'@'替换为空字节
5.将现有的argv[i]、aa_subst、aa_loc第二个字节之后的字符串,进行拼接,返回给n_arg
6.将现有的argv[i]用n_arg替换
7.将aa_loc(当前指向的值为' @')的第一个空字节替换回'@'
*/
while (argv[i]) {
u8* aa_loc = strstr(argv[i], "@@");

if (aa_loc) {
u8 *aa_subst, *n_arg;

if (!out_file)
out_file = alloc_printf("%s/.cur_input", out_dir);

if (out_file[0] == '/')
aa_subst = out_file;
else
aa_subst = alloc_printf("%s/%s", cwd, out_file);

*aa_loc = 0;
n_arg = alloc_printf("%s%s%s", argv[i], aa_subst, aa_loc + 2);
argv[i] = n_arg;
*aa_loc = '@';

if (out_file[0] != '/')
ck_free(aa_subst);
}
i++;
}

free(cwd);
}

setup_stdio_file

c
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 如果没有设置'-f'参数,就删除旧的out_dir/.cur_input文件,并创建一个同名的新文件 */
EXP_ST void setup_stdio_file(void) {

/* 拼接out_dir/.cur_input作为路径赋值给fn;然后将该路径对应的文件删除 */
u8* fn = alloc_printf("%s/.cur_input", out_dir);
unlink(fn);

/* 根据路径fn创建新的文件作为输出文件,并将文件描述符赋给out_fd */
out_fd = open(fn, O_RDWR | O_CREAT | O_EXCL, 0600);
if (out_fd < 0)
PFATAL("Unable to create '%s'", fn);
ck_free(fn);
}

check_binary

这个函数不是非常核心的部分,代码又过于冗长,这里仅摘取一小段,并介绍功能,我这里直接引用本文借鉴的大佬们对这个函数的分析,并在这保留了源码中的注释。

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Do a PATH search and find target binary to see that it exists and
isn't a shell script - a common and painful mistake. We also check for
a valid ELF header and for evidence of AFL instrumentation. */

/* sakura:check指定路径处要执行的程序是否存在,且它不能是一个shell script */
/* hollk:检查指定路径要执行的程序是否存在,是否为shell脚本,同时检查elf文件头是否合法及程序是否被插桩 */
/* ScUpax0s:检查目标文件有效性:是否是可执行文件,是否是Mach-O还是ELF还是一个生成的shell文件 */

EXP_ST void check_binary(u8* fname){
...

/* 以下为对ELF文件和Mach-O文件格式特征的判断 */
#ifndef __APPLE__
if (f_data[0] != 0x7f || memcmp(f_data + 1, "ELF", 3))
FATAL("Program '%s' is not an ELF binary", target_path);

#else
if (f_data[0] != 0xCF || f_data[1] != 0xFA || f_data[2] != 0xED)
FATAL("Program '%s' is not a 64-bit Mach-O binary", target_path);

#endif /* ^!__APPLE__ */

...
}

get_cur_time

c
1
2
3
4
5
6
7
8
9
10
/* 获取unix系统的当前时间,单位为毫秒 */
static u64 get_cur_time(void) {

struct timeval tv;
struct timezone tz;

gettimeofday(&tv, &tz);

return (tv.tv_sec * 1000ULL) + (tv.tv_usec / 1000);
}

get_qemu_argv

c
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
/* 如果设置了参数'-Q',即处于QEMU模式下,需要重写QEMU的参数。QEMU模式的fuzz,Shell语句可能如下所示:
'afl-fuzz -i fuzz_in/ -o fuzz_out/ -Q ./normal_test' */

static char** get_qemu_argv(u8* own_loc, char** argv, int argc) {

/* 为存储新参数的new_argv数组申请空间 */
char** new_argv = ck_alloc(sizeof(char*) * (argc + 4));
u8 *tmp, *cp, *rsl, *own_copy;


/* 首先是QEMU稳定性故障的解决方法:
1.(覆盖)设置环境变量QEMU_LOG的值为'nochain'
2.将原先参数从第2个参数开始,依次复制到new_argv数组从第4个参数开始的位置
3.将new_argv数组的第3个参数设置为'target_path';第2个参数设置为'--'
*/
setenv("QEMU_LOG", "nochain", 1);
memcpy(new_argv + 3, argv + 1, sizeof(char*) * argc);
new_argv[2] = target_path;
new_argv[1] = "--";


/* 接下来负责找到QEMU二进制程序,并将其作为第1个参数传给数组new_argv,
这里有3种方法去实现这个操作,第一种方法:
1.获取环境变量AFL_PATH的值,并赋值给tmp
2.如果tmp存在:
a.将tmp/afl-qemu-trace拼接成路径,并赋给cp
b.如果路径cp可以访问,则将cp赋给new_argv数组的第一个参数,以及target_path
3.如果tmp不存在:
a.继续执行尝试第二种方法*/
tmp = getenv("AFL_PATH");
if (tmp) {
cp = alloc_printf("%s/afl-qemu-trace", tmp);

if (access(cp, X_OK))
FATAL("Unable to find '%s'", tmp);
target_path = new_argv[0] = cp;
return new_argv;
}


/* 第二种方法:
1.将own_loc的值赋给own_copy;这个own_loc就是原先传入的argv[0],通常是'afl-fuzz的路径'
2.接下来做的操作经常见,已经很熟悉了,其实就是在afl-fuzz的同一个目录下搜索afl-qemu-trace
3.搜索到以后,就获取这个afl-qemu-trace的路径,然后赋值给new_argv第一个参数和target_path
4.如果没能找到afl-qemu-trace,就继续执行,尝试第三种方法
*/
own_copy = ck_strdup(own_loc);
rsl = strrchr(own_copy, '/');

if (rsl) {
*rsl = 0;

cp = alloc_printf("%s/afl-qemu-trace", own_copy);
ck_free(own_copy);

if (!access(cp, X_OK)) {
target_path = new_argv[0] = cp;
return new_argv;
}
}
else
ck_free(own_copy);

/* 第三种方法:
1.直接访问环境变量BIN_PATH与'/afl-qemu-trace'拼接的路径
2.如果该路径可以访问,则将拼接后的路径赋给new_argv的第一个参数,并同时赋给target_path
3.如果该路径不可以访问,说明3种获取方法afl-qemu-trace的方式都失败了,则打印错误信息、抛出异常
*/
if (!access(BIN_PATH "/afl-qemu-trace", X_OK)) {
target_path = new_argv[0] = ck_strdup(BIN_PATH "/afl-qemu-trace");
return new_argv;
}

SAYF("\n" cLRD "[-] " cRST
"Oops, unable to find the 'afl-qemu-trace' binary. The binary must be built\n"
" separately by following the instructions in qemu_mode/README.qemu. If you\n"
" already have the binary installed, you may need to specify AFL_PATH in the\n"
" environment.\n\n"
" Of course, even without QEMU, afl-fuzz can still work with binaries that are\n"
" instrumented at compile time with afl-gcc. It is also possible to use it as a\n"
" traditional \"dumb\" fuzzer by specifying '-n' in the command line.\n");
FATAL("Failed to locate 'afl-qemu-trace'.");
}

perform_dry_run

c
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
/* 执行所有测试用例的试运行,以确保应用程序按照预取正常运行。这仅针对初始输入执行,并且仅执行一次 */
static void perform_dry_run(char** argv) {

/* 变量初始化:
1.q获取到fuzzing queue的链表头
2.设置cal_failures的值为0(这里第一次出现,不是全局变量)
3.获取环境变量AFL_SKIP_CRASHES,赋给skip_crashes
*/
struct queue_entry* q = queue;
u32 cal_failures = 0;
u8* skip_crashes = getenv("AFL_SKIP_CRASHES");


/* 遍历fuzzing queue的各个queue_entry:
1.匹配到q->fname中最后一个'/'之后的字符串(测试用例文件名)并说明这是即将试运行的测试用例文件
2.以只读模式打开q->fname路径指定测试用例文件,若打开失败则抛出异常
3.根据测试用例文件的大小q->len,申请一块非零的空间,返回给use_mem
4.将测试用例文件中的内容,读取到use_mem中
5.调用calibrate(argv, q, use_mem, 0, 1)进行测试用例校准,将执行结果返回给变量res,然后
free掉use_mem申请的空间
7.如果设置了stop_soon,(当按下Ctrl-C时,会设置该值),就直接return
8.根据res的值进行相应处理,如果值为crash_mode或者FAULT_NOBITS,那么会打印出一些执行时的参
数;如果为其它值,则进入下面的switch语句进行筛选处理 */
while (q) {
u8* use_mem;
u8 res;
s32 fd;

u8* fn = strrchr(q->fname, '/') + 1;
ACTF("Attempting dry run with '%s'...", fn);

fd = open(q->fname, O_RDONLY);
if (fd < 0)
PFATAL("Unable to open '%s'", q->fname);

use_mem = ck_alloc_nozero(q->len);

if (read(fd, use_mem, q->len) != q->len)
FATAL("Short read from '%s'", q->fname);
close(fd);

res = calibrate_case(argv, q, use_mem, 0, 1);
ck_free(use_mem);

if (stop_soon)
return;

if (res == crash_mode || res == FAULT_NOBITS)
SAYF(cGRA " len = %u, map size = %u, exec speed = %llu us\n" cRST,
q->len, q->bitmap_size, q->exec_us);


/* 9.Switch部分,依据res的结果查看错误类型进行判断:
a.FAULT_NONE:
1).如果q是头节点,即第一个测试用例,则调用check_map_coverage,对覆盖率进行评估:
0x1:如果trace_bits发现的路径数小于100,就直接返回
0x2:在trace_bits数组的后半段,如果有值就直接返回
2).如果为crash_mode,则抛出异常,指出“测试用例不会导致crash”

b.FAULT_TMOUT:
1).如果设置了timeout_give(即指定了'-t'参数),
0x1:如果timeout_give>1:
*设置该测试用例的q->cal_failed值为CAL_CHANCES,表示校准失败
*令cal_failures(局部变量)的值加1
0x2:否则显示警告并抛出异常
2).如果没设置timeout_give,直接显示警告,并抛出异常指出“测试用例导致了超时”

c.FAULT_CRASH:
1).如果设置了crash_mode(参数指定'-C'),直接break掉
2).如果skip_crashes存在(从环境变量AFL_SKIP_CRASHES获取),那么先抛出警告,然后:
0x1.设置q->cal_failed值为CAL_CHANCES,表示校准失败
0x2.令cal_failures的值加1
3).如果设置了mem_limit,则会抛出增加内存的建议
4).抛出异常,指出“测试用例导致了crash”

d.FAULT_ERROR:
1).抛出异常,指出“无法执行目标程序”

e.FAULT_NOINST:
1).抛出异常,指出“没有检测到桩代码”

f.FAULT_NOBITS:
1).令useless_at_start(无用的开始路径)加1
2).如果没有设置in_bitmap(即没有通过'-B'参数指定Input_bitmap),并且shuffle_queue
也不存在(即没有设置环境变量AFL_SHUFFLE_QUEUE),则抛出警告,指出“没有新的桩代码输出,
当前测试用例可能是无用的”。
3).直接break掉,不抛出异常
*/
switch (res) {

case FAULT_NONE:
if (q == queue)
check_map_coverage();
if (crash_mode)
FATAL("Test case '%s' does *NOT* crash", fn);
break;

case FAULT_TMOUT:
if (timeout_given) {
/* The -t nn+ syntax in the command line sets timeout_given to '2' and
instructs afl-fuzz to tolerate but skip queue entries that time
out. */
if (timeout_given > 1) {
WARNF("Test case results in a timeout (skipping)");
q->cal_failed = CAL_CHANCES;
cal_failures++;
break;
}
SAYF("\n" cLRD "[-] " cRST
"The program took more than %u ms to process one of the initial test cases.\n"
" Usually, the right thing to do is to relax the -t option - or to delete it\n"
" altogether and allow the fuzzer to auto-calibrate. That said, if you know\n"
" what you are doing and want to simply skip the unruly test cases, append\n"
" '+' at the end of the value passed to -t ('-t %u+').\n", exec_tmout,
exec_tmout);
FATAL("Test case '%s' results in a timeout", fn);
} else {
SAYF("\n" cLRD "[-] " cRST
"The program took more than %u ms to process one of the initial test cases.\n"
" This is bad news; raising the limit with the -t option is possible, but\n"
" will probably make the fuzzing process extremely slow.\n\n"

" If this test case is just a fluke, the other option is to just avoid it\n"
" altogether, and find one that is less of a CPU hog.\n", exec_tmout);
FATAL("Test case '%s' results in a timeout", fn);
}

case FAULT_CRASH:
if (crash_mode)
break;
if (skip_crashes) {
WARNF("Test case results in a crash (skipping)");
q->cal_failed = CAL_CHANCES;
cal_failures++;
break;
}

if (mem_limit) {
SAYF("\n" cLRD "[-] " cRST
"Oops, the program crashed with one of the test cases provided. There are\n"
" several possible explanations:\n\n"

" - The test case causes known crashes under normal working conditions. If\n"
" so, please remove it. The fuzzer should be seeded with interesting\n"
" inputs - but not ones that cause an outright crash.\n\n"

" - The current memory limit (%s) is too low for this program, causing\n"
" it to die due to OOM when parsing valid files. To fix this, try\n"
" bumping it up with the -m setting in the command line. If in doubt,\n"
" try something along the lines of:\n\n"

#ifdef RLIMIT_AS
" ( ulimit -Sv $[%llu << 10]; /path/to/binary [...]
#else
" ( ulimit -Sd $[%llu << 10]; /path/to/binary [...]
#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. Also,\n"
" if you are using ASAN, see %s/notes_for_asan.txt.\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"
" binaries. To fix this, set AFL_NO_FORKSRV=1 in the environment.\n\n"

#endif /* __APPLE__ */

" - Least likely, there is a horrible bug in the fuzzer. If other options\n"
" fail, poke for troubleshooting tips.\n",
DMS(mem_limit << 20), mem_limit - 1, doc_path);
} else {

SAYF("\n" cLRD "[-] " cRST
"Oops, the program crashed with one of the test cases provided. There are\n"
" several possible explanations:\n\n"

" - The test case causes known crashes under normal working conditions. If\n"
" so, please remove it. The fuzzer should be seeded with interesting\n"
" inputs - but not ones that cause an outright crash.\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"
" binaries. To fix this, set AFL_NO_FORKSRV=1 in the environment.\n\n"

#endif /* __APPLE__ */

" - Least likely, there is a horrible bug in the fuzzer. If other options\n"
" fail, poke for troubleshooting tips.\n");
}

FATAL("Test case '%s' results in a crash", fn);

case FAULT_ERROR:
FATAL("Unable to execute target application ('%s')", argv[0]);

case FAULT_NOINST:
FATAL("No instrumentation detected");

case FAULT_NOBITS:
useless_at_start++;
if (!in_bitmap && !shuffle_queue)
WARNF("No new instrumentation output, test case may be useless.");
break;

}

/* 10.如果q->var_behavior的值被设置,说明多次运行该测试用例,同等输入条件下,会表现出不同的行为
,则抛出警告,指出“该测试用例的路径输出可变”。最后读取下一个queue,回到循环开头继续执行 */
if (q->var_behavior)
WARNF("Instrumentation output varies across runs.");
q = q->next;
}

/* 如果cal_failures的值被设置,说明测试用例导致了afl-fuzz超时或者Crash,开始进一步判断:
1).如果cal_failures==queued_paths,说明所有的测试用例都超时或者Crash了,抛出异常
2).如果cal_failures*5 > queued_paths,则抛出警告指出“有问题的测试用例比例超过20%,可能
需要重新检查设置” */
if (cal_failures) {
if (cal_failures == queued_paths)
FATAL("All test cases time out%s, giving up!",
skip_crashes ? " or crash" : "");

WARNF("Skipped %u test cases (%0.02f%%) due to timeouts%s.", cal_failures,
((double)cal_failures) * 100 / queued_paths,
skip_crashes ? " or crashes" : "");
if (cal_failures * 5 > queued_paths)
WARNF(cLRD "High percentage of rejected test cases, check settings!");
}
OKF("All test cases processed.");
}

calibrate_case

c
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
/* 该函数将in_dir路径下的测试用例多次运行,评估是否存在异常行为,以便在早期就发现有问题的
测试用例;并且在发现新路径时,评估新发现的测试用例的是否可变(这里的可变是指多次执行这个
testcase,发现的路径不同)。该函数在perform_dry_run、save_if_interesting、
fuzz_one函数中均有调用。此外,该函数也将初始化并启动fork server
*/
static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem,
u32 handicap, u8 from_queue) {

/* 部分局部变量概述:
1.first_trace,将暂存trace_bits的值,也会用于var_bytes的设置
2.如果q->exec_cksum为0,表示这个testcase是第一次运行,且来自in_dir文件夹,此时first_run置1
3.old_sc/old_sm/old_sn用于暂存一些值,以便在程序出错时恢复
*/
static u8 first_trace[MAP_SIZE];
u8 fault = 0, new_bits = 0, var_detected = 0, hnb = 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;


/* 1.如果free_queue为0(在perfrom_dry_run中为0,其它时候被调用为1),并且设置了resuming_fuzz
,那么这里会将use_tmout设置为一个更大的值
2.q->cal_failed++
3.设置stage_name为calibration,表明fuzz当前到了评估测试用例的阶段
4.根据是否设置了fast_cal(环境变量AFL_FAST_CAL)来设置stage_max的值(CAL_CYCLES的值为8),
这里的stage_max表示每个新测试用例(以及显示出可变行为的测试用例)的校准周期数,也就是说这个阶段
(评估测试用例)需要执行(run_target)几次
*/
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;


/* 这部分代码确保在做任何事情之前,已经启动fork server:
1.检查forkserver初始化条件,如果合适则调用init_forkserver来启动fork server:
a.dumb_mode != 1(这里注意,设置'n'参数后,dumb_mode的值可能为1或2,由环境变量
AFL_DUMB_FORKSRV决定)
b.未设置no_forkserver(即未设置环境变量AFL_NO_FORKSRV)
c.forksrv_pid不存在,即尚未初始化fork server
2.判断q->exec_cksum的值是否存在(第一次执行时会被置0,之后会被设置为一个哈希值),
如果存在,说明这个testcase不是来自己in_dir文件夹,则进行如下操作:
a.将trace_bits的内容拷贝到first_trace中(如果路径可变,run_target后,first_trace
与trace_bits中的值可能有所不同)
b.调用has_new_bits判断是否有新的路径,并相应更新virgin_bits数组
c.根据has_new_bits的返回值,判断是否需要设置new_bits(new_bits初始为0)
3.获取进入循环前的开始时间
*/
if (dumb_mode != 1 && !no_forkserver && !forksrv_pid)
init_forkserver(argv);

if (q->exec_cksum) {
memcpy(first_trace, trace_bits, MAP_SIZE);
hnb = has_new_bits(virgin_bits);
if (hnb > new_bits)
new_bits = hnb;
}

start_us = get_cur_time_us();

/* calibrate_case的主循环,循环次数为stage_max,默认是8。如果设置了环境变量AFL_FAST_CAL,
则会设置为3:
1.如果不是第一次运行,并且到了状态更新的周期(stats_update_freq默认为1),则调用show_stats
显示fuzz状态界面
2.调用write_to_testcase,将当前测试用例的内容(use_mem)写入到out_file(在detect_file_args
中,out_file被设置为out_dir/.cur_input)
3.调用run_target,该函数调用目标程序,监控出现超时的情况,并返回状态信息。被调用的程序会更新
路径表trace_bits数组。同时这里也会通知fork server可以开始fork并fuzz
*/
for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {
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);

/* 1.如果摁下了Ctrl+C,或者run_target返回的错误类型不是crash_mode
a.执行abort_calibration处的代码
2.如果未设置dumb_mode,并且当前为第一次运行,并且trace_bits没有任何路径(这里的
count_bytes函数用于计算共享内存里有多少字节被置位了):
a.将错误类型设置为FAULT_NOINST
b.执行abort_calibration处的代码
*/
if (stop_soon || fault != crash_mode)
goto abort_calibration;

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

/* 1.计算hash32(trace_bits, MAP_SIZE, HASH_CONST)的结果,保存到cksum
2.如果cksum与测试用例自身的q->exec_cksum不同,说明可能是第一次运行或者,这是一个路径可变
的testcase,那么进行如下操作:
a.调用has_new_bits判断是否有新的路径,并更新virgin_bits数组
b.根据has_new_bits的返回值,决定是否更新new_bits的值
c.判断q->exec_cksum的值
1).如果q->exec_cksum不为0(说明可能是路径可变的testcase):
*.i从0~MAP_SIZE遍历,如果first_trace[i]不等于trace_bits[i],代表发现了
可变testcase,且var_bytes为空,则将该字节设置为1
*.将stage_max的值设置为CAL_CYCLES_LONG(CAL_CYCLES的值为40)
2).如果q->exec_cksum值为0(说明是第一次执行):
*.设置q->exec_cksum的值为本次执行计算出来的cksum
*.将trace_bits的内容直接复制到first_trace上
*/
cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);

if (q->exec_cksum != cksum) {

hnb = has_new_bits(virgin_bits);
if (hnb > new_bits)
new_bits = hnb;

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

total_cal_us += stop_us - start_us;
total_cal_cycles += stage_max;


/* 这部分主要是收集一些fuzzing执行时的统计信息:
1.设置测试用例对应的queue的一些值,包括
a.单次执行时间的平均值
b.最后一次执行后所覆盖到的路径数
c.执行轮数
d.失败次数
2.更新加上这个queue所覆盖到的路径数,以及用于计算路径的bitmap的个数加1
3.调用update_bitmap_score,更新一些比如偏好因子的信息,包括进行对应的trace_bits压缩,
以判断此判断此路径是否是更有利的
*/
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);


/* 如果没有从检测中得到new_bit,fault类型为FAULT_NONT,且该testcase是第一次执行,且不属于
dumb_mode;则告诉父进程,这是一个无关紧要的问题,但是需要提醒用户。这里将错误类型设置为
FAULT_NOBITS,交由处理perform_dry_run中的Switch语句进行处理 */
if (!dumb_mode && first_run && !fault && !new_bits)
fault = FAULT_NOBITS;

/* 如果摁下Ctrl+C,或者遇到一些执行出错的情况,就会执行这里的abort_calibration,其主要工作如下:
1.新路径出现的处理(has_new_bits的返回值如果为2,说明发现了新路径)
2.测试用例造成可变路径的处理,这里会调用mark_as_variable,其操作如下:
a.调用syslink创建符号链接out_dir/queue/.state/variable_behavior/fname
b.设置q->var_behavior=1
3.用先前保存的old_sn/sc/sm恢复stage_name/cur/max的值
4.如果不是第一次执行,就打印一下状态栏
5.最后返回错误类型
*/
abort_calibration:

if (new_bits == 2 && !q->has_new_cov) {
q->has_new_cov = 1;
queued_with_cov++;
}

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

理解这部分代码需要一些前置知识,包括Linux会话相关的系统调用Linux管道通信的用法以及管道通信时涉及到dup/dup2函数的用法。这里dup2的用法非常重要,因此作简单介绍(下面的说法源自《Unix环境高级编程》):

关于文件共享

  1. 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,可将视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
    • 文件描述符标志
    • 指向一个文件表项的指针
  2. 内核为所有打开文件维持一张文件表。每个文件表项包含:
    • 文件状态标志(读、写、增写、同步、非阻塞等)
    • 当前文件位移量
    • 指向该文件v节点表项的指针

dup/dup2的用途

系统调用dup和dup2能够复制文件描述符。dup返回新的文件文件描述符(没有用的文件描述符最小的编号)。dup2可以让用户指定返回的文件描述符的值,如果需要,则首先接近newfd的值,他通常用来重新打开或者重定向一个文件描述符。下面结合dup2与文件共享的关系,更好的理解dup2的用途。(图片来自此文

源码分析

c
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
/* Spin up fork server (instrumented mode only). 
In essence, the instrumentation allows us to skip execve(), and just keep
cloning a stopped child. So, we just execute once, and then send commands
through a pipe. The other part of this logic is in afl-as.h. */

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...");

/* 函数原型:int pipe(int pipefd[2]);
作用:创建一个可以用于进程间通信单项隧道。该方法将会创建出两个文件描述符,可以使用pipefd
这个数组来引用这两个描述符进行文件操作。pipefd[0]是读方式打开,作为管道的读描述符。
pipefd[1]是写方式打开,作为管道的写描述符。从管道写端写入的数据会被内核缓存直到有
人从另一端读取为止。
返回值:如果管道创建成功,返回0;如果出错返回-1
源码含义:创建2个管道用于父子进程间通信,st_pipe用于传递状态,ctl_pipe用于传递命令
*/
if (pipe(st_pipe) || pipe(ctl_pipe))
PFATAL("pipe() failed");

/* 函数原型:pid_t fork(void);
作用:通过复制调用它的进程来创建一个新进程
返回值:如果是父进程,则返回值为子进程的PID;如果是子进程,则返回0
源码含义:fork出一个子进程,根据返回值froksrv_pid来执行不同的代码
*/
forksrv_pid = fork();
if (forksrv_pid < 0)
PFATAL("fork() failed");

/* 如果当前进程为子进程,则进入if语句块继续执行 */
if (!forksrv_pid) {
struct rlimit r;

/* 首先是针对OpenBSD系统做的特殊处理,这部分可以忽略 */
if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) {
r.rlim_cur = FORKSRV_FD + 2;
setrlimit(RLIMIT_NOFILE, &r);
}
if (mem_limit) {
r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20;
#ifdef RLIMIT_AS
setrlimit(RLIMIT_AS, &r);
#else
setrlimit(RLIMIT_DATA, &r);
#endif /* ^RLIMIT_AS */
}
r.rlim_max = r.rlim_cur = 0;
setrlimit(RLIMIT_CORE, &r);


/* 隔离进程并配置标准描述符。 如果指定了 out_file,则 stdin 为 /dev/null; 否则,将克隆 out_fd。
从这里开始,为子进程将要进行的操作:
1.调用setsid()创建新的会话,从而使子进程完全独立,脱离父进程控制。具体细节参考文末链接
2.调用dup2重定向文件描述符1和2(在程序启动时,与流stdin、stdout、stderr关联的整数
文件描述符分别是0、1、2)到dev_null_fd(定义为-1,指向/dev/null),即关闭子进程
中的stdout和stderr,然后将其重定向到/dev/null中。相当于关闭了子进程的全部输出
3.判断是否指定了out_file
a.如果指定了out_file,就和第2步一样,调用dup2将子进程的stdin重定向到/dev/null
b.如果没有指定,就先重定向到out_fd,再关闭out_fd,从而关闭stdin(out_file和
out_fd指向的文件路径差不多,似乎都会被设置为out_dir/.cur_input)
*/
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);
}


/* 设置控制管道与状态管道,关闭不需要的原始文件描述符:
1.通过调用dup2,分别重定向文件描述符FORKSRV_FD和FORKSRV_FD+1重定向到控制管道的读描述
符与状态管道的写描述符;从而使得子进程只能读取控制管道中的命令,以及往状态管道中写入状态。
这部分内容,我们在分析afl-as.h中的桩代码时已经提过,这里重定向的两个文件描述符刚好对应
0xC6和0xC7,它们分别用来读取父进程发来的命令,以及将其自己的子进程fuzz返回的状态写入到
状态管道中。
2.接下来关闭子进程里一些不需要的文件描述符,在分析本函数之前已经简单解释过了dup2的原理,因
此关闭这些文件描述符是不会影响到那些重定向到这些描述符的描述符
*/
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));


/* 下面是一些琐碎的处理:
1.若没设置环境变量LD_BIND_LAZY,则将环境变量LD_BIND_NOW设置为1;这么做能稍微
提高性能,它会阻止链接器在fork之后做额外的工作
2.若没有特殊指定,接下来会通过环境变量ASAN_OPTIONS为ASAN设置合理的默认值
3.类似的,为MSAN设置默认值
4.调用execv执行目标二进制程序;除非执行出错,否则该函数不会返回。这里有一点很特殊,第一个
target会进入__afl_maybe_log里的__afl_fork_wait_loop,并充当fork server,在整
个fuzz的过程中,它都不会结束,每次要fuzz一次目标程序,都会从这个fork serve fork出来
一个子进程(这个fork server本身也是init_forkserver里fork出来的子进程)去fuzz
5.execv是不会返回的,如果执行到这里,说明execv执行失败了。这里使用一个独特的位图签名
EXEC_FAIL_SIG(0xfee1dead)来告诉父进程execv()执行失败,并结束子进程
*/
if (!getenv("LD_BIND_LAZY"))
setenv("LD_BIND_NOW", "1", 0);

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

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

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


/* 以下为父进程进行的操作:
1.关掉父进程不需要的控制管道的读描述符、状态管道的写描述符
2.将控制管道的写描述符、状态管道的读描述符,分别赋值给对应的fork server全局变量
3.设置等待fork server启动的时间,但是不能等待太久
4.从fsrv_st_fd,也就状态管道中读取4个字节的状态信息。如果读取成功,则代表fork server
成功启动,直接return
*/
close(ctl_pipe[0]);
close(st_pipe[1]);

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


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 (rlen == 4) {
OKF("All right - fork server is up.");
return;
}


/* 以下为进程启动失败、超时等异常情况的判断以及处理,主要的处理方式是给出提示并抛出异常。
由于文章排版问题,这部分暂不阐述,可先参考hollk或初号机的分析文章 */
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 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 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 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 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");
}

流程梳理

根据上述对init_forkserver的分析,结合前一篇中对桩代码的分析,可以梳理出如下流程:(图片出自此处

  1. afl-fuzz会创建两个管道(控制管道、状态管道),并在init_forkserver中fork出一个子进程
  2. 在子进程中调用execv执行afl-gcc编译出来的、已经被插桩的目标程序
  3. 由于已被插桩,目标程序的控制流会交到__afl_maybe_log手中
  4. 如果fuzz是第一次运行,此时的子进程便成为了fuzz server。它会将状态写入状态管道(st_pipe),通知父进程准备ok。父进程会从状态管道中读取子进程的状态,从而知道fork server已经启动
  5. 之后进行fuzz时:
    1. 子进程(fork server)会在__afl_fork_wait_loop等待父进程的指令
    2. 父进程会将fork指令写入控制管道(ctl_pipe)
    3. 子进程(fork server)会从控制管道中读取命令,并fork出一个它的子进程用于fuzz,并将它的子进程的结束状态通过写入状态管道,告知父进程
  6. 上述进程间的关系可以参考下图

has_new_bits

c
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
/* 在先前分析calibrate_case时经常遇到has_new_bits,在该函数内它的调用方式大致为:
hnb = has_new_bits(virgin_bits);
1.virgin_bits记录着整体tuple信息
2.hub接收返回值并设置局部变量new_bits,然后通过new_bits的值判断是否发现了新的路径。

这个函数主要是检查当前执行路径是否为表带来了任何新内容,通过对virgin_bits的更新来表现
出这一点。如果发现了新的tuple,那么该函数将返回2;如果只是特定tuple的命中数增加,那么
该函数将返回1
*/
static inline u8 has_new_bits(u8* virgin_map) {

/* 1.根据系统位数,初始化变量current、virgin、i,并将ret置0
2.这里的"MAP_SIZE>>3"即"MAP_SIZE/8"表示8个字节一组
3.下面的"MAP_SIZE>>4"同理,本篇按照64位的角度来分析
*/
#ifdef WORD_SIZE_64
u64* current = (u64*)trace_bits;
u64* virgin = (u64*)virgin_map;

u32 i = (MAP_SIZE >> 3);
#else
u32* current = (u32*)trace_bits;
u32* virgin = (u32*)virgin_map;

u32 i = (MAP_SIZE >> 2);
#endif /* ^WORD_SIZE_64 */

u8 ret = 0;


/* 每次从current(trace_bits)和virgin(virgin_map)中取8个字节进行比较:
1.如果current不为0(发现了新的路径)并且"*current&*virgin"也不为0(发现某条路径的执行次数
和之前有所不同):
a.如果ret的值小于2:
1).先将cur和vir分别指向current和virgin的第一个字节
2).依次判断cur与vir开始的8个字节,是否满足:
a).vir[i]==0xff,即之前从未被覆盖到(virgin_bits位图会初始为0xff)
b).cur[i]存在,即trace_bits中对应的字节被命中
如果这8组校验中,有任何1组满足上述条件,说明覆盖到了新的路径,将ret设置
位2;否则说明只是某个tuple的命中次数的更新,将ret设置为1
b.设置*virgin &= ~*current,这样做下次进行*current&*virgin时,就可以筛选出重复命中了
2.current和virgin移动到下一组8个字节
3.回到循环开始,直到MAPSIZE全被遍历完
*/
while (i--) {

if (unlikely(*current) && unlikely(*current & *virgin)) {
if (likely(ret < 2)) {
u8* cur = (u8*)current;
u8* vir = (u8*)virgin;

#ifdef WORD_SIZE_64
if ((cur[0] && vir[0] == 0xff) || (cur[1] && vir[1] == 0xff) ||
(cur[2] && vir[2] == 0xff) || (cur[3] && vir[3] == 0xff) ||
(cur[4] && vir[4] == 0xff) || (cur[5] && vir[5] == 0xff) ||
(cur[6] && vir[6] == 0xff) || (cur[7] && vir[7] == 0xff))
ret = 2;
else
ret = 1;
#else
if ((cur[0] && vir[0] == 0xff) || (cur[1] && vir[1] == 0xff) ||
(cur[2] && vir[2] == 0xff) || (cur[3] && vir[3] == 0xff))
ret = 2;
else
ret = 1;
#endif /* ^WORD_SIZE_64 */

}
*virgin &= ~*current;
}
current++;
virgin++;
}

/* 1.如果传入给has_new_bits的参数virgin_map是virgin_bits(其它情况可能是virgin_tmout
或者virgin_crash),并且ret的值不为0,说明发现新的路径或者某条路径的执行次数增加:
2.那么就修改bitmap_changed的值为1
*/
if (ret && virgin_map == virgin_bits)
bitmap_changed = 1;
return ret;
}

count_bytes

c
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
/* (_b) << 3 即 _b*8 
FF(_b) 即 0x000000ff 左移 _b*8 位
这里_b的取值范围为0、1、2、3,
则最终结果取值范围为0xff000000、0x00ff0000、0x0000ff00、0x000000ff
*/
#define FF(_b) (0xff << ((_b) << 3))


/* 本函数用于计算bitmap中设置的字节数,主要是为了更新状态界面、样例评估、检测确认新路径:
1.循环读取mem中的值,每次读取4个字节到变量v中
2.初始化计算器ret的值为0
3.依次将v与0xff000000、0x00ff0000、0x0000ff00、0x000000ff进行与操作 若结果不
为0,则计数器ret加1.
*/
static u32 count_bytes(u8* mem) {

u32* ptr = (u32*)mem;
u32 i = (MAP_SIZE >> 2);
u32 ret = 0;

while (i--) {
u32 v = *(ptr++);

if (!v) continue;
if (v & FF(0)) ret++;
if (v & FF(1)) ret++;
if (v & FF(2)) ret++;
if (v & FF(3)) ret++;

}
return ret;
}

run_target

c
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
/* 该函数调用目标程序,监控出现超时的情况,并返回状态信息。被调用的程序会更新路径表trace_bits */
static u8 run_target(char** argv, u32 timeout) {

static struct itimerval it;
static u32 prev_timed_out = 0;
static u64 exec_ms = 0;

int status = 0;
u32 tb4;

child_timed_out = 0;

/* 首先调用memset将trace_bits清零。
要注意的一点是,在这个memset之后,trace_bits[]实际上是不稳定的,所以我们必须防止任何早期
的操作冒险进入这个领域。这里调用了MEM_BARRIER(),在types.h中可以看到其被定义为如下的宏:
#define MEM_BARRIER() __asm__ volatile("" ::: "memory")
volatile表明某个变量的值在外部可能被改变,因此对这些变量的存取不能缓存到寄存器,每次使用都
要重新存取
*/
memset(trace_bits, 0, MAP_SIZE);
MEM_BARRIER();


/* 如果在dumb_mode模式下允许,我们不能依赖编译到目标程序中的fork server逻辑,因此我们将要
继续调用execve(),这部分和init_forkserver()中的代码有一些重复,but c'est la vie
这里对重复的代码仅作简要注释,完整的可以参考上方的init_forkserver()
*/
if (dumb_mode == 1 || no_forkserver) {
child_pid = fork();

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


/* 通过该if语句进入子进程的处理逻辑流程 */
if (!child_pid) {
struct rlimit r;


/* 首先仍然是针对OpenBSD系统做的一些特殊处理,可以忽略 */
if (mem_limit) {
r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20;
#ifdef RLIMIT_AS
setrlimit(RLIMIT_AS, &r); /* Ignore errors */
#else
setrlimit(RLIMIT_DATA, &r); /* Ignore errors */
#endif /* ^RLIMIT_AS */
}
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.
1.调用setsid()创建新的会话,脱离父进程控制
2.重定向stdout/stderr到dev_null_fd,从而关闭子进程的全部输出
3.如果指定了out_file,则重定向stdin搭配dev_null_fd;如果没指定,就重定向到out_fd,
再关闭out_fd描述符(out_file和out_fd指向的文件路径差不多)
*/
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);
}


/* 关闭子进程里一些不需要的文件描述符 */
close(dev_null_fd);
close(out_dir_fd);
close(dev_urandom_fd);
close(fileno(plot_file));


/* 1.若没有特殊指定,则通过环境变量为ASAN和MASN设置合理的默认值
2.调用execv执行目标二进制程序;除非执行出错,否则该函数不会返回
3.若execv执行失败,则用一个独特的位图签名EXEC_FAIL_SIG(0xfee1dead)来告诉父进程
execv()执行失败,并结束子进程
*/
setenv("ASAN_OPTIONS", "abort_on_error=1:"
"detect_leaks=0:"
"symbolize=0:"
"allocator_may_return_null=1", 0);

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

execv(target_path, argv);

*(u32*)trace_bits = EXEC_FAIL_SIG;
exit(0);
}
}
/* 与init_forkserver中重复的部分至此结束 */

/* 如果dumb_mode不等于1,且已经启动并运行了fork server,那么进行如下流程:
1.向控制管道写入prev_timed_out的值,命令fork server开始fork一个子进程进行fuzz
2.从状态管道读取fork server返回的fork出的子进程的PID到child_pid中
3.检查child_pid的值
*/
else {
s32 res;

if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4) {
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) {
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?)");
}


/* 1.根据用户的配置,调用setitimer设置超时时间。ITIMER_REAL按实际时间计时,计时到达将给进程
发送SIGALRM信号(参考文末链接16)。先前的setup_signal_handlers已将SIGALRM信号的处
理函数设置为handle_timeout,它将杀死子进程,并设置全局变量child_timed_out
2.判断是否为dumb_mode,或无fork server的模式(以子进程的方式execve)
a.如果是,则等待子进程结束返回的status
b.如果不是,则从状态管道中读取fork server返回的status
3.计算执行时间exec_ms
4.重置timer
5.execve()调用次数计数器,total_execs的值加1
*/
it.it_value.tv_sec = (timeout / 1000);
it.it_value.tv_usec = (timeout % 1000) * 1000;
setitimer(ITIMER_REAL, &it, NULL);

if (dumb_mode == 1 || no_forkserver) {
if (waitpid(child_pid, &status, 0) <= 0)
PFATAL("waitpid() failed");
} else {
s32 res;
if ((res = read(fsrv_st_fd, &status, 4)) != 4) {
if (stop_soon)
return 0;
RPFATAL(res, "Unable to communicate with fork server (OOM?)");
}
}

if (!WIFSTOPPED(status))
child_pid = 0;

getitimer(ITIMER_REAL, &it);
exec_ms = (u64) timeout - (it.it_value.tv_sec * 1000 +
it.it_value.tv_usec / 1000);

it.it_value.tv_sec = 0;
it.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &it, NULL);

total_execs++;

/* 编译器不允许将trace_bits上的任何后续操作移至此处之后。
越过这个位置,trace_bits的行为非常正常,不必被视为volatile */
MEM_BARRIER();


/* 1.将trace_bits前4字节的值赋值给局部变量tb4,用于后面检查处于dumb_mode或无fork server
的模式时,子进程的execv()是否执行成功
2.调用classify_counts对trace_bits的值进行规整
3.用prev_timed_out保存child_timed_out的值
*/
tb4 = *(u32*)trace_bits;

#ifdef WORD_SIZE_64
classify_counts((u64*)trace_bits);
#else
classify_counts((u32*)trace_bits);
#endif /* ^WORD_SIZE_64 */

prev_timed_out = child_timed_out;


/* 以下涉及到的处理进程退出状态的宏可以参考文末链接[17].
1.调用WIFSIGNALED()判断子进程是否因为异常而结束,如果是,该函数返回真;并且不是因为
!stop_soon(按下Ctrl+C)而结束子进程;则接下来调用WTERMSIG()获取导致子进程终止
的信号,以判断异常退出的原因:
a.如果终止信号为SIGKILL,并且child_timed_out被设置,则返回FAULT_TMOUT
b.否则返回FAULT_CRASH
2.如果设置了uses_asan,并且子进程异常退出时返回的错误码为MASN_ERROR,则将kill_signal
置0,并返回FAULT_CRASH
3.如果处于dumb_mode或无fork server的模式,并且trace_bits被设置为EXEC_FAIL_SIG,
则返回FAULT_ERROR
4.如果测试用例在用户定义的超时时间下(timeout),执行时间超过设置的最慢的执行时间,则
重新设置最慢的执行时间,这里不返回
5.如果执行到这仍未返回,则返回FAULT_NONE
*/
if (WIFSIGNALED(status) && !stop_soon) {
kill_signal = WTERMSIG(status);
if (child_timed_out && kill_signal == SIGKILL)
return FAULT_TMOUT;
return FAULT_CRASH;
}

if (uses_asan && WEXITSTATUS(status) == MSAN_ERROR) {
kill_signal = 0;
return FAULT_CRASH;
}

if ((dumb_mode == 1 || no_forkserver) && tb4 == EXEC_FAIL_SIG)
return FAULT_ERROR;

if (!(timeout > exec_tmout) && (slowest_exec_ms < exec_ms)) {
slowest_exec_ms = exec_ms;
}

return FAULT_NONE;
}

classify_count

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* classify_counts只出现了run_target中,被用作对trace_bits进程规整,这里我们只关心64位的版本 */
static inline void classify_counts(u64* mem) {

/* 1.每次从mem(trace_bits)读8个字节,一共i组
2.判断每组的mem是否为空,为空就扫描下一组,不为空,就对这个组的内容进行规整
3.每组的mem16[0]~mem16[3],每个都是2字节,依次从count_class_lookup16中找到对应的
count_class_lookup16[mem16[i]]并写入回mem16[i]。对count_class_lookup16这
张表的初始化可以参考前文分析的init_count_class16
4.规整完以后继续扫描下一组
*/
u32 i = MAP_SIZE >> 3;

while (i--) {
if (unlikely(*mem)) {
u16* mem16 = (u16*)mem;

mem16[0] = count_class_lookup16[mem16[0]];
mem16[1] = count_class_lookup16[mem16[1]];
mem16[2] = count_class_lookup16[mem16[2]];
mem16[3] = count_class_lookup16[mem16[3]];
}
mem++;
}
}

update_bitmap_score

c
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
/* 该函数只在"calibrate_case"和"trim_case"中被调用 

当我们遇到一条新路径时,调用它以查看该路径是否比任何现有路径看起来更"有利"。达到这个"有利条件"
的标准是有一组最小的路径来触发,到目前为止在位图中看到的所有位。接下来,专注于这些"有利"路径进
行模糊测试,忽略其他的。
该过程的第一步是维护位图中每个字节的top_rated[]条目列表。如果没有先前的竞争者,或者如果当前
竞争者有更"有利"的偏好因子(速度*大小),则可以赢得该位置。
具体操作则是,遍历当前case所能够触发的路径里,这个case是否为最优的那一个(计算出的偏好因子),
如果是,则替换所在路径的case为自己,即top_rated数组中的位置
*/
static void update_bitmap_score(struct queue_entry* q) {

/* 先计算出当前queue的偏好因子fav_factor,然后进入循环开始筛选
循环会检查trace_bits的每个字节,查看是否有先前的胜者,然后将它的偏好因子与当前的queue对比
*/
u32 i;
u64 fav_factor = q->exec_us * q->len;

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

/* 1.首先确保trace_bits[i]处有值,即命中该路径
2.判断top_rated[i]处是否有值,即是否有先前的胜者:
a.如果有则用当前queue的偏好因子fav_factor,与top_rated[i]算出的偏好因子比对。
执行更快会更小测试用例会胜出。若是top_rateed[i]胜出,则回到循环开头,继续比对
当前queue命中的下一个路径;若是当前queue胜出,则继续执行
b.如果执行到这,说明当前queue要赢了,这里把输家的"top_rated[i]->tc_ref"减一,
把"top_rated[i]->trace_mini"清空
3.把当前queue插入top_rated[i]中作为新的胜者,并令q->tc_ref加1
4.如果q->trace_mini为空,则申请MAP_SIZE>>3大小的空间,然后则将trace_bits经过
minimize_bits压缩后存到trace_mini中
5.设置score_changed的值为1,表明胜者发生了变化
*/
if (trace_bits[i]) {

if (top_rated[i]) {
if (fav_factor > top_rated[i]->exec_us * top_rated[i]->len)
continue;

if (!--top_rated[i]->tc_ref) {
ck_free(top_rated[i]->trace_mini);
top_rated[i]->trace_mini = 0;
}
}

top_rated[i] = q;
q->tc_ref++;

if (!q->trace_mini) {
q->trace_mini = ck_alloc(MAP_SIZE >> 3);
minimize_bits(q->trace_mini, trace_bits);
}

score_changed = 1;
}
}

minimize_bits

在阅读这部分代码之前,建议先参考一下此文对数据压缩算法的介绍,非常优雅。

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 场景:仅在update_bitmap_score中被调用,将trace_bits压缩到大小只有其1/8的trace_mini中
作用:将trace_bits数组压缩成更小的位图。由于压缩后仅使用1比特存储路径,因此经过压缩后计数信
息将被删除。当然,这只在发现新路径时才进行调用,并不频繁
算法:1.进入循环,共MAP_SIZE轮
2.*(src++)用来取trace_bits数组中元素的值,取完后自增1,到下一个元素的位置
3.判断*(src++)处是否有值,若有值,则进行如下操作:
a.i>>3,即i在trace_mini中的索引号,换句话说,就是在trace_mini中的第几个字节上
b.i&7相当于i%8,只保留低3bits,即trace_mini中的第几个字节上的第几个bit的位置
c.将1左移i%8个比特后,再和以前的数据做|运算,这样,它所在的这个bit 位置就替换成1了
4.若没有值,或已完成上述操作,则进入下轮循环
*/
static void minimize_bits(u8* dst, u8* src) {

u32 i = 0;
while (i < MAP_SIZE) {
if (*(src++))
dst[i >> 3] |= 1 << (i & 7);
i++;
}
}

cull_queue

c
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
/* 遍历top_rated[]条目,然后按顺序抓取先前未见过、并且在这一轮成为获胜者的字节(temp_v),
在下一轮运行前,将它们标记为"有利"的。在所有模糊测试的步骤中,这些"有利"的条目有着更多的
播出时间(air time)
*/
static void cull_queue(void) {

/* 1.如果处于dumb_mode或者score_changed为0(即没有新的胜者进入top_rated),就直接返回
2.将score_changed置0
3.将temp_v数组(大小为MAP_SIZE/8)的每个字节设置为0xff。这里和前面minimize_bits的思
路是一样的。将用字节表示的路径压缩到temp_v数组上的每个bit上。这里用0xff先将所有bit置1
,后面若发现被覆盖到,则用0替换
4.初始化queue_favored和pending_favored为0
5.遍历fuzzing queue链表,将q->favored均设置为0
*/
struct queue_entry* q;
static u8 temp_v[MAP_SIZE >> 3];
u32 i;

if (dumb_mode || !score_changed)
return;

score_changed = 0;

memset(temp_v, 255, MAP_SIZE >> 3);

queued_favored = 0;
pending_favored = 0;

q = queue;
while (q) {
q->favored = 0;
q = q->next;
}

/* Let's see if anything in the bitmap isn't captured in temp_v.
If yes, and if it has a top_rated[] contender, let's use it.

这个迭代的目的就是筛选出一组queue,它们能够覆盖到所有现在已经覆盖到的路径
1.遍历top_rated,判断top_rated[i]对应在temp_v上的bit有没有置位
2.如果top_rated[i]有值,且该path在temp_v中对应的bit被置位
a.就从temp_v中清除掉(也可以理解为标记出,置0后,其它case有着相同路径的情况下,就不会通过
这个if筛选了)所有top_rated[i]覆盖到的path,将对应的bit置为0
b.然后将top_rated[i]->favored置1,计数器queued_favored加1
c.如果top_rated[i]还没有被fuzz过,则令计数器pending_favored加1
*/
for (i = 0; i < MAP_SIZE; i++)
if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {
u32 j = MAP_SIZE >> 3;

while (j--)
if (top_rated[i]->trace_mini[j])
temp_v[j] &= ~top_rated[i]->trace_mini[j];

top_rated[i]->favored = 1;
queued_favored++;

if (!top_rated[i]->was_fuzzed)
pending_favored++;
}

/* 遍历所有的fuzzing queue链表,调用mark_as_redundant筛选掉不是favored的queue,
也就是说,如果不是favored的case,就被标记为redundant_edges */
q = queue;
while (q) {
mark_as_redundant(q, !q->favored);
q = q->next;
}
}

为了更好的理解该过程,这里选择网上公开的一个例子进行讲解(这部分取自此文

现假设有如下tuple和seed信息:

  • tuple: t0, t1, t2, t3, t4
  • seed: s0, s1, s2
  • 初始化temp_v = [1,1,1,1,1]
  • s1可覆盖t2, t3,s2覆盖t0, t1, t4,并且top_rated[0] = s2,top_rated[2]=s1

将按照如下过程进行筛选和判断:

  1. 首先判断 temp_v[0]=1,说明t0没有被覆盖;
  2. top_rated[0] 存在 (s2) -> 判断s2可以覆盖的范围 -> trace_mini = [1,1,0,0,1]
  3. 更新 temp_v=[0,0,1,1,0], 标记s2为 “favored”;
  4. 继续判断 temp_v[1]=0,说明t1此时已经被覆盖,跳过;
  5. 继续判断 temp_v[2]=1,说明t2没有被覆盖;
  6. top_rated[2] 存在 (s1) -> 判断s1可以覆盖的范围 -> trace_mini=[0,0,1,1,0]
  7. 更新 temp_v=[0,0,0,0,0],标记s1为 “favored”;
  8. 此时所有tuple都被覆盖,具备”favored’标记的为s1, s2,过程结束。

mark_as_reduntant

c
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
/* 标记/取消标记是否为冗余(仅针对边)。这么做不是用于恢复状态,但可能对后处理数据集有用 */
static void mark_as_redundant(struct queue_entry* q, u8 state) {

u8* fn;
s32 fd;

/* 1.如果state==q->fs_redundant,就直接返回
2.用state(!q->favored)设置q->fs_redundant的值
3.找到q->fname字符串最后一个'/'出现的位置
4.将q->fname最后一个'/'后面的字符串拼接成"out_dir/queue/.state/redundant_edges/fname"
并赋值给fn
5.如果state为1,则创建文件out_dir/queue/.state/redundant_edges/fname
6.如果state为0,则删除文件out_dir/queue/.state/redundant_edges/fname
*/
if (state == q->fs_redundant)
return;

q->fs_redundant = state;

fn = strrchr(q->fname, '/');
fn = alloc_printf("%s/queue/.state/redundant_edges/%s", out_dir, fn + 1);

if (state) {
fd = open(fn, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd < 0)
PFATAL("Unable to create '%s'", fn);
close(fd);

} else {
if (unlink(fn))
PFATAL("Unable to remove '%s'", fn);
}
ck_free(fn);
}

show_init_stats

c
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
/* 在处理输入目录结束时(一些校准的东西也在这里结束了),显示快速统计信息,以及一堆警告 */
static void show_init_stats(void) {

struct queue_entry* q = queue;
u32 min_bits = 0, max_bits = 0;
u64 min_us = 0, max_us = 0;
u64 avg_us = 0;
u32 max_len = 0;

/* 这里根据calibrate_case中计算出来的total_cal_us和total_cal_cycles,计算出单轮执行的
时间arv_us */
if (total_cal_cycles)
avg_us = total_cal_us / total_cal_cycles;

while (q) {
if (!min_us || q->exec_us < min_us) min_us = q->exec_us;
if (q->exec_us > max_us) max_us = q->exec_us;

if (!min_bits || q->bitmap_size < min_bits) min_bits = q->bitmap_size;
if (q->bitmap_size > max_bits) max_bits = q->bitmap_size;

if (q->len > max_len) max_len = q->len;

q = q->next;
}

SAYF("\n");

/* 判断avg_us的值,如果大于10000,就抛出警告"The target binary is pretty slow!"
1.如果avg_us大于50000,设置havoc_div为10
2.如果avg_us大于20000,设置havoc_div为5
3.如果avg_us大于10000,设置havoc_div为2
*/
if (avg_us > (qemu_mode ? 50000 : 10000))
WARNF(cLRD "The target binary is pretty slow! See %s/perf_tips.txt.",
doc_path);

if (avg_us > 50000) havoc_div = 10; /* 0-19 execs/sec */
else if (avg_us > 20000) havoc_div = 5; /* 20-49 execs/sec */
else if (avg_us > 10000) havoc_div = 2; /* 50-100 execs/sec */


/* 如果不是resuming_fuzz下,则根据queue的大小、个数等属性存在的问题,抛出相应的警告。
然后显示一些较为有用的状态信息 */
if (!resuming_fuzz) {
if (max_len > 50 * 1024)
WARNF(cLRD "Some test cases are huge (%s) - see %s/perf_tips.txt!",
DMS(max_len), doc_path);
else if (max_len > 10 * 1024)
WARNF("Some test cases are big (%s) - see %s/perf_tips.txt.",
DMS(max_len), doc_path);

if (useless_at_start && !in_bitmap)
WARNF(cLRD "Some test cases look useless. Consider using a smaller set.");

if (queued_paths > 100)
WARNF(cLRD "You probably have far too many input files! Consider trimming down.");
else if (queued_paths > 20)
WARNF("You have lots of input files; try starting small.");
}

OKF("Here are some useful stats:\n\n"

cGRA " Test case count : " cRST "%u favored, %u variable, %u total\n"
cGRA " Bitmap range : " cRST "%u to %u bits (average: %0.02f bits)\n"
cGRA " Exec timing : " cRST "%s to %s us (average: %s us)\n",
queued_favored, queued_variable, queued_paths, min_bits, max_bits,
((double)total_bitmap_size) / (total_bitmap_entries ? total_bitmap_entries : 1),
DI(min_us), DI(max_us), DI(avg_us));


/* 找到适当的超时时间。找出适当的超时时间。 基本思想是:平均5倍或最大1倍,四舍五入到EXEC_TM_ROUND
毫秒,并以 1 秒为上限。如果程序很慢,则乘数会降低到2倍或3倍,因为随机调度器抖动不太可能产生任何影响
1.如果timeout_given值为0,即未指定'-t'参数:
a.根据avg_us,计算出exec_tmout(avg_us单位是微秒,exec_tmout单位是毫秒,所以要除以1000)
b.将计算出的exec_tmout与max_us/1000,取最大值赋给exec_tmout
c.然后四舍五入到EXEC_TM_ROUND毫秒
d.再将exec_tmout设置为exec_tmout、EXEC_TIMEOUT中的最大值
e.打印未指定'-t'参数的相关提示信息,设置timeout_given值为1
2.如果timeout_given值为3,代表这是resuming_fuzz,此时的timeout_given是从历史记录里读取的,
并打印相应的提示信息
*/
if (!timeout_given) {

if (avg_us > 50000)
exec_tmout = avg_us * 2 / 1000;
else if (avg_us > 10000)
exec_tmout = avg_us * 3 / 1000;
else
exec_tmout = avg_us * 5 / 1000;

exec_tmout = MAX(exec_tmout, max_us / 1000);
exec_tmout = (exec_tmout + EXEC_TM_ROUND) / EXEC_TM_ROUND * EXEC_TM_ROUND;

if (exec_tmout > EXEC_TIMEOUT)
exec_tmout = EXEC_TIMEOUT;

ACTF("No -t option specified, so I'll use exec timeout of %u ms.",
exec_tmout);

timeout_given = 1;

} else if (timeout_given == 3) {
ACTF("Applying timeout settings from resumed session (%u ms).", exec_tmout);
}


/* 在dumb_mode下,且未设置环境变量AFL_HANG_TMOUT时。重新运行每个超时测试用例的时间限制
是非常昂贵的,所以让我们选择一个更保守的默认值 */
if (dumb_mode && !getenv("AFL_HANG_TMOUT"))
hang_tmout = MIN(EXEC_TIMEOUT, exec_tmout * 2 + 100);

OKF("All set and ready to roll!");

}

find_start_position

c
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
/* resuming_fuzz时,尝试找到要从其开始的队列位置;只在resuming_fuzz、并且可以找到
原始的fuzzer_stats时才有意义 */
static u32 find_start_position(void) {

static u8 tmp[4096];

u8 *fn, *off;
s32 fd, i;
u32 ret;

/* 如果不是resuming_fuzz,直接返回 */
if (!resuming_fuzz)
return 0;

/* 如果设置了in_place_resume,则打开out_dir/fuzzer_stats文件;否则
打开in_dir/../fuzzer_stats文件 */
if (in_place_resume)
fn = alloc_printf("%s/fuzzer_stats", out_dir);
else
fn = alloc_printf("%s/../fuzzer_stats", in_dir);
fd = open(fn, O_RDONLY);
ck_free(fn);
if (fd < 0)
return 0;

/* 1.将文件的内容读取到tmp中
2.在tmp中找到字符串"cur_path :",并将其对应的值转换为整数,赋给ret
3.判断ret的值是否超过queued_paths,如果超过则将ret置0
4.返回ret
*/
i = read(fd, tmp, sizeof(tmp) - 1); (void)i;
close(fd);

off = strstr(tmp, "cur_path : ");
if (!off)
return 0;

ret = atoi(off + 20);
if (ret >= queued_paths)
ret = 0;
return ret;
}

write_stats_file

该函数中格式化的状态属性有部分为fuzz_one中计算出的内容,这些内容暂时引用sakura师傅的结论。

c
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
/* 更新状态文件以对fuzzing过程进行监控 */
static void write_stats_file(double bitmap_cvg, double stability, double eps) {

static double last_bcvg, last_stab, last_eps;
static struct rusage usage;

/* 创建out_dir/fuzzer_stats文件,准备写入统计信息 */
u8* fn = alloc_printf("%s/fuzzer_stats", out_dir);
s32 fd;
FILE* f;

fd = open(fn, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0)
PFATAL("Unable to create '%s'", fn);

ck_free(fn);

f = fdopen(fd, "w");
if (!f)
PFATAL("fdopen() failed");

/* 如果bitmap_cvg、stability、eps都为0,则用last_bcvg、last_stab、last_eps去设置它们
否则(仅在show_stats中被调用时不为空)将它们保存到last_bcvg、last_stab、last_eps中
*/
if (!bitmap_cvg && !stability && !eps) {
bitmap_cvg = last_bcvg;
stability = last_stab;
eps = last_eps;
} else {
last_bcvg = bitmap_cvg;
last_stab = stability;
last_eps = eps;
}

fprintf(f, "start_time : %llu\n" // start_time / 1000,fuzz运行的开始时间
"last_update : %llu\n" // get_cur_time() / 1000,当前时间
"fuzzer_pid : %u\n" // getpid(),获取当前pid
"cycles_done : %llu\n" // queue_cycle ? (queue_cycle - 1) : 0,表示queue队列被完全变异一次的次数
"execs_done : %llu\n" // total_execs,target的总的执行次数,每次run_target的时候会增加1
"execs_per_sec : %0.02f\n"// eps,每秒执行的次数
"paths_total : %u\n" // queued_paths,每次add_to_queue的时候会增加1,代表queue里的样例总数
"paths_favored : %u\n" // queued_favored,"有利"的路径总数,在cull_queue中计算出来
"paths_found : %u\n" // queued_discovered,在每次common_fuzz_stuff去执行一次fuzz时,发现新的interesting case的时候会增加1,代表在fuzz运行期间发现的新queue entry
"paths_imported : %u\n" // queued_imported,master-slave模式下,如果sync过来的case是interesting的,就增加1
"max_depth : %u\n" // max_depth,最大路径深度
"cur_path : %u\n" // current_entry,current_entry一般情况下代表的是正在执行的queue entry的整数ID,queue首节点的ID是0,必须符合find_start_position()
"pending_favs : %u\n" // pending_favored,等待fuzz的favored paths数
"pending_total : %u\n" // pending_not_fuzzed,在queue中等待fuzz的case数
"variable_paths : %u\n" // queued_variable,导致可变路径的case数
"stability : %0.02f%%\n"
"bitmap_cvg : %0.02f%%\n"
"unique_crashes : %llu\n" // 在save_if_interesting时,如果fault是FAULT_CRASH,则unique_crashes计数器加1
"unique_hangs : %llu\n" // 在save_if_interesting时,如果fault是FAULT_TMOUT,且exec_tmout小于hang_tmout,就以hang_tmout为超时时间再执行一次,如果还超时,就让unique_hangs计数器加一
"last_path : %llu\n" // last_path_time / 1000,当add_to_queue将一个新case加入queue时,就设置一次last_path_time为当前时间
"last_crash : %llu\n" // last_crash_time / 1000,在unique_crashes加一的时候,last_crash更新为当前时间
"last_hang : %llu\n" // last_hang_time / 1000,在unique_hangs加一的时候,last_hang更新为当前时间
"execs_since_crash : %llu\n" // total_execs - last_crash_execs,表示在上一次crash之后总计执行了多少次
"exec_timeout : %u\n" // exec_tmout,配置好的超时时间,必须符合find_timeout()
"afl_banner : %s\n"
"afl_version : " VERSION "\n"
"target_mode : %s%s%s%s%s%s%s\n" // fuzz时采用的所有执行模式
"command_line : %s\n" // 原始参数,在save_cmdline中保存
"slowest_exec_ms : %llu\n", // 单个用例最慢执行时间,在run_target中设置
start_time / 1000, get_cur_time() / 1000, getpid(),
queue_cycle ? (queue_cycle - 1) : 0, total_execs, eps,
queued_paths, queued_favored, queued_discovered, queued_imported,
max_depth, current_entry, pending_favored, pending_not_fuzzed,
queued_variable, stability, bitmap_cvg, unique_crashes,
unique_hangs, last_path_time / 1000, last_crash_time / 1000,
last_hang_time / 1000, total_execs - last_crash_execs,
exec_tmout, use_banner,
qemu_mode ? "qemu " : "", dumb_mode ? " dumb " : "",
no_forkserver ? "no_forksrv " : "", crash_mode ? "crash " : "",
persistent_mode ? "persistent " : "", deferred_mode ? "deferred " : "",
(qemu_mode || dumb_mode || no_forkserver || crash_mode ||
persistent_mode || deferred_mode) ? "" : "default",
orig_cmdline, slowest_exec_ms);
/* ignore errors */

/* 统计子进程的资源用量。在调用getrusage之前需要保证杀死fork server进程,并调用了waitpid */
if (getrusage(RUSAGE_CHILDREN, &usage)) {
WARNF("getrusage failed");
} else if (usage.ru_maxrss == 0) {
fprintf(f, "peak_rss_mb : not available while afl is running\n");
} else {
#ifdef __APPLE__
fprintf(f, "peak_rss_mb : %zu\n", usage.ru_maxrss >> 20);
#else
fprintf(f, "peak_rss_mb : %zu\n", usage.ru_maxrss >> 10);
#endif /* ^__APPLE__ */
}
fclose(f);
}

save_auto

c
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
/* 保存自动生成的extras(token) */
static void save_auto(void) {

u32 i;

/* 如果auto_changed为0,就直接return;否则,就将其置0 */
if (!auto_changed)
return;
auto_changed = 0;

/* 1.拼接路径out_dir/queue/.state/auto_extras/auto_i,并赋值给fn
2.创建路径fn指定的文件
3.将a_extras写入创建的文件中
*/
for (i = 0; i < MIN(USE_AUTO_EXTRAS, a_extras_cnt); i++) {

u8* fn = alloc_printf("%s/queue/.state/auto_extras/auto_%06u", out_dir, i);
s32 fd;

fd = open(fn, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0)
PFATAL("Unable to create '%s'", fn);

ck_write(fd, a_extras[i].data, a_extras[i].len, fn);

close(fd);
ck_free(fn);
}
}

完结撒花 (๑•̀ㅂ•́)و✧

参考资料

  1. skr:sakuraのAFL源码全注释

  2. hollk:AFL源码分析之afl-fuzz.c详细注释

  3. ScUpax0s:AFL源码阅读笔记之gcc与fuzz部分

  4. Seebug:AFL 二三事——源码分析

  5. AFL:afl-fuzz.c

  6. HICOOKIE:AFL-Learning

  7. AFL内部实现细节小记

  8. 博客园:Linux进程间通信(一):信号signal()、sigaction()

  9. 博客园:Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl

  10. 知乎:Linux 的进程间通信-管道

  11. 博客园:进程间通信管道进阶篇-linux下dup/dup2函数的用法

  12. AFL源码分析03:afl-as.h

  13. CSDN:setsid的作用

  14. 博客园:Linux–setsid()与进程组、会话、守护进程

  15. 博客园:C语言中volatile关键字与汇编

  16. CSDN:linux系统编程之信号(八):三种时间结构及定时器setitimer()详解

  17. 博客园:WIFEXITED WEXITSTATUS WIFSIGNALED

  18. CSDN:经典算法系列之(一) - BitMap [数据的压缩存储]

Author: cataLoc
Link: http://cata1oc.github.io/2022/02/06/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%9004/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶