简介

afl-fuzz 便是 afl 中的核心文件,模糊测试的代码都在该文件中,从功能上大致可以分为三个部分:

  • 初始配置:进行初始化的配置操作
  • 模糊测试:fuzz 的主要循环过程
  • 变异策略:测试用例的变异策略和实现

初始配置

输入参数处理

在设置完随机数种子(使用时间 + pid)后,会进行输入参数的处理过程:

1
2
3
4
5
while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)

switch (opt) {
// opt
}

大致的可输入参数如下:

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
afl-fuzz 2.57b by <lcamtuf@google.com>

afl-fuzz [ options ] -- /path/to/fuzzed_app [ ... ]

Required parameters:

-i dir - input directory with test cases
-o dir - output directory for fuzzer findings

Execution control settings:

-f file - location read by the fuzzed program (stdin)
-t msec - timeout for each run (auto-scaled, 50-1000 ms)
-m megs - memory limit for child process (50 MB)
-Q - use binary-only instrumentation (QEMU mode)

Fuzzing behavior settings:

-d - quick & dirty mode (skips deterministic steps)
-n - fuzz without instrumentation (dumb mode)
-x dir - optional fuzzer dictionary (see README)

Other stuff:

-T text - text banner to show on the screen
-M / -S id - distributed mode (see parallel_fuzzing.txt)
-C - crash exploration mode (the peruvian rabbit thing)
-V - show version number and exit

-b cpu_id - bind the fuzzing process to the specified CPU core

For additional tips, please consult /usr/local/share/doc/afl/README.

setup_signal_handlers

初始化 sigaction 结构体,然后使用 sigemptyset 函数初始化信号量集合,最后使用 sigaction 注册信号处理函数并设置信号句柄,具体而言设置了以下的信号量:

  • SIGHUP/SIGINT/SIGTERM :处理各种停止的情况
1
2
3
4
5
6
7
8
9
10
/* Handle stop signal (Ctrl-C, etc). */

static void handle_stop_sig(int sig) {

stop_soon = 1;

if (child_pid > 0) kill(child_pid, SIGKILL);
if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL);

}
  • SIGALRM :处理超时情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Handle timeout (SIGALRM). */

static void handle_timeout(int sig) {

if (child_pid > 0) {

child_timed_out = 1;
kill(child_pid, SIGKILL);

} else if (child_pid == -1 && forksrv_pid > 0) {

child_timed_out = 1;
kill(forksrv_pid, SIGKILL);

}

}
  • SIGWINCH :处理重新设置窗口大小情况
1
2
3
4
5
/* Handle screen resize (SIGWINCH). */

static void handle_resize(int sig) {
clear_screen = 1;
}
  • SIGUSR1 :用户自定义信号,处理 skip request
1
2
3
4
5
6
7
/* Handle skip request (SIGUSR1). */

static void handle_skipreq(int sig) {

skip_requested = 1;

}
  • SIGTSTP/SIGPIPE :忽略的情况

check_asan_opts

主要是处理 asan 的各种配置:

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
static void check_asan_opts(void) {
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!");

}

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

}

}

主要是从环境变量中读取 ASAN_OPTIONS 和 MSAN_OPTIONS 并进行检查

fix_up_sync

如果通过 -M / -S 设置了 sync_id , 就会进入到 fix_up_sync 中:

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
/* Validate and fix up out_dir and sync_dir when using -S. */

static void fix_up_sync(void) {

u8* x = sync_id;

if (dumb_mode)
FATAL("-S / -M and -n are mutually exclusive");

if (skip_deterministic) {

if (force_deterministic)
FATAL("use -S instead of -M -d");
else
FATAL("-S already implies -d");

}

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

x = alloc_printf("%s/%s", out_dir, sync_id);

sync_dir = out_dir;
out_dir = x;

if (!force_deterministic) {
skip_deterministic = 1;
use_splicing = 1;
}

}

做了一些冲突和输入检查,最后设置 sync_dir 和 out_dir 分别为之前的 out_dir 和 out_dir + sync_id

输入的检查和环境变量的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if (!strcmp(in_dir, out_dir))
FATAL("Input and output directories can't be the same");

if (dumb_mode) {

if (crash_mode) FATAL("-C and -n are mutually exclusive");
if (qemu_mode) FATAL("-Q and -n are mutually exclusive");

}

if (getenv("AFL_NO_FORKSRV")) no_forkserver = 1;
if (getenv("AFL_NO_CPU_RED")) no_cpu_meter_red = 1;
if (getenv("AFL_NO_ARITH")) no_arith = 1;
if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue = 1;
if (getenv("AFL_FAST_CAL")) fast_cal = 1;

if (getenv("AFL_HANG_TMOUT")) {
hang_tmout = atoi(getenv("AFL_HANG_TMOUT"));
if (!hang_tmout) FATAL("Invalid value of AFL_HANG_TMOUT");
}

if (dumb_mode == 2 && no_forkserver)
FATAL("AFL_DUMB_FORKSRV and AFL_NO_FORKSRV are mutually exclusive");

if (getenv("AFL_PRELOAD")) {
setenv("LD_PRELOAD", getenv("AFL_PRELOAD"), 1);
setenv("DYLD_INSERT_LIBRARIES", getenv("AFL_PRELOAD"), 1);
}

if (getenv("AFL_LD_PRELOAD"))
FATAL("Use AFL_PRELOAD instead of AFL_LD_PRELOAD");

主要是对输入的一些 mode 进行冲突检查,随后读取环境变量并进行设置

save_cmdline

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
/* Make a copy of the current command line. */

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;

}

对 argv 进行备份存储

fix_up_banner

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
/* Trim and possibly create a banner for the run. */

static void fix_up_banner(u8* 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;

}

}

if (strlen(use_banner) > 40) {

u8* tmp = ck_alloc(44);
sprintf(tmp, "%.40s...", use_banner);
use_banner = tmp;

}

}

主要是修剪并创建 banner

check_if_tty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Check if we're on TTY. */

static void check_if_tty(void) {

struct winsize ws;

if (getenv("AFL_NO_UI")) {
OKF("Disabling the UI because AFL_NO_UI is set.");
not_on_tty = 1;
return;
}

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

}

检查是否在 tty 终端上运行

  • 如果有环境变量 AFL_NO_UI 则设置 not_on_tty 为 1
  • 通过 ioctl 读取 windows size ,如果报错为 ENOTTY 则设置 not_on_tty 为 1

get_core_count

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
/* Count the number of logical CPU cores. */

static void get_core_count(void) {

u32 cur_runnable = 0;

#if defined(__APPLE__) || defined(__FreeBSD__) || defined (__OpenBSD__)

size_t s = sizeof(cpu_core_count);

/* On *BSD systems, we can just use a sysctl to get the number of CPUs. */

#ifdef __APPLE__

if (sysctlbyname("hw.logicalcpu", &cpu_core_count, &s, NULL, 0) < 0)
return;

#else

int s_name[2] = { CTL_HW, HW_NCPU };

if (sysctl(s_name, 2, &cpu_core_count, &s, NULL, 0) < 0) return;

#endif /* ^__APPLE__ */

#else

#ifdef HAVE_AFFINITY

cpu_core_count = sysconf(_SC_NPROCESSORS_ONLN);

#else

FILE* f = fopen("/proc/stat", "r");
u8 tmp[1024];

if (!f) return;

while (fgets(tmp, sizeof(tmp), f))
if (!strncmp(tmp, "cpu", 3) && isdigit(tmp[3])) cpu_core_count++;

fclose(f);

#endif /* ^HAVE_AFFINITY */

#endif /* ^(__APPLE__ || __FreeBSD__ || __OpenBSD__) */

if (cpu_core_count > 0) {

cur_runnable = (u32)get_runnable_processes();

#if defined(__APPLE__) || defined(__FreeBSD__) || defined (__OpenBSD__)

/* Add ourselves, since the 1-minute average doesn't include that yet. */

cur_runnable++;

#endif /* __APPLE__ || __FreeBSD__ || __OpenBSD__ */

OKF("You have %u CPU core%s and %u runnable tasks (utilization: %0.0f%%).",
cpu_core_count, cpu_core_count > 1 ? "s" : "",
cur_runnable, cur_runnable * 100.0 / cpu_core_count);

if (cpu_core_count > 1) {

if (cur_runnable > cpu_core_count * 1.5) {

WARNF("System under apparent load, performance may be spotty.");

} else if (cur_runnable + 1 <= cpu_core_count) {

OKF("Try parallel jobs - see %s/parallel_fuzzing.txt.", doc_path);

}

}

} else {

cpu_core_count = 0;
WARNF("Unable to figure out the number of CPU cores.");

}

}

从 /proc/stat 中获取 CPU 核心数

bind_to_free_cpu

如果有宏定义 HAVE_AFFINITY 则进入 bind_to_free_cpu(在 linux 下默认为 1 )

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
#ifdef HAVE_AFFINITY

/* Build a list of processes bound to specific cores. Returns -1 if nothing
can be found. Assumes an upper bound of 4k CPUs. */

static void bind_to_free_cpu(void) {

DIR* d;
struct dirent* de;
cpu_set_t c;

u8 cpu_used[4096] = { 0 };
u32 i;

if (cpu_core_count < 2) return;

if (getenv("AFL_NO_AFFINITY")) {

WARNF("Not binding to a CPU core (AFL_NO_AFFINITY set).");
return;

}

d = opendir("/proc");

if (!d) {

WARNF("Unable to access /proc - can't scan for free CPU cores.");
return;

}

ACTF("Checking CPU core loadout...");

/* Introduce some jitter, in case multiple AFL tasks are doing the same
thing at the same time... */

usleep(R(1000) * 250);

/* Scan all /proc/<pid>/status entries, checking for Cpus_allowed_list.
Flag all processes bound to a specific CPU using cpu_used[]. This will
fail for some exotic binding setups, but is likely good enough in almost
all real-world use cases. */

while ((de = readdir(d))) {

u8* fn;
FILE* f;
u8 tmp[MAX_LINE];
u8 has_vmsize = 0;

if (!isdigit(de->d_name[0])) continue;

fn = alloc_printf("/proc/%s/status", de->d_name);

if (!(f = fopen(fn, "r"))) {
ck_free(fn);
continue;
}

while (fgets(tmp, MAX_LINE, f)) {

u32 hval;

/* Processes without VmSize are probably kernel tasks. */

if (!strncmp(tmp, "VmSize:\t", 8)) has_vmsize = 1;

if (!strncmp(tmp, "Cpus_allowed_list:\t", 19) &&
!strchr(tmp, '-') && !strchr(tmp, ',') &&
sscanf(tmp + 19, "%u", &hval) == 1 && hval < sizeof(cpu_used) &&
has_vmsize) {

cpu_used[hval] = 1;
break;

}

}

ck_free(fn);
fclose(f);

}

closedir(d);
if (cpu_to_bind_given) {

if (cpu_to_bind >= cpu_core_count)
FATAL("The CPU core id to bind should be between 0 and %u", cpu_core_count - 1);

if (cpu_used[cpu_to_bind])
FATAL("The CPU core #%u to bind is not free!", cpu_to_bind);

i = cpu_to_bind;

} else {

for (i = 0; i < cpu_core_count; i++) if (!cpu_used[i]) break;

}

if (i == cpu_core_count) {

SAYF("\n" cLRD "[-] " cRST
"Uh-oh, looks like all %u CPU cores on your system are allocated to\n"
" other instances of afl-fuzz (or similar CPU-locked tasks). Starting\n"
" another fuzzer on this machine is probably a bad plan, but if you are\n"
" absolutely sure, you can set AFL_NO_AFFINITY and try again.\n",
cpu_core_count);

FATAL("No more free CPU cores");

}

OKF("Found a free CPU core, binding to #%u.", i);

cpu_aff = i;

CPU_ZERO(&c);
CPU_SET(i, &c);

if (sched_setaffinity(0, sizeof(c), &c))
PFATAL("sched_setaffinity failed");

}

#endif /* HAVE_AFFINITY */

通过读取 /proc 下的内容构建绑定到特定核心的进程列表

check_crash_handling

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
/* Make sure that core dumps don't go to a program. */

static void check_crash_handling(void) {

#ifdef __APPLE__

/* Yuck! There appears to be no simple C API to query for the state of
loaded daemons on MacOS X, and I'm a bit hesitant to do something
more sophisticated, such as disabling crash reporting via Mach ports,
until I get a box to test the code. So, for now, we check for crash
reporting the awful way. */

if (system("launchctl list 2>/dev/null | grep -q '\\.ReportCrash$'")) return;

SAYF("\n" cLRD "[-] " cRST
"Whoops, your system is configured to forward crash notifications to an\n"
" external crash reporting utility. This will cause issues due to the\n"
" extended delay between the fuzzed binary malfunctioning and this fact\n"
" being relayed to the fuzzer via the standard waitpid() API.\n\n"
" To avoid having crashes misinterpreted as timeouts, please run the\n"
" following commands:\n\n"

" SL=/System/Library; PL=com.apple.ReportCrash\n"
" launchctl unload -w ${SL}/LaunchAgents/${PL}.plist\n"
" sudo launchctl unload -w ${SL}/LaunchDaemons/${PL}.Root.plist\n");

if (!getenv("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"))
FATAL("Crash reporter detected");

#else

/* This is Linux specific, but I don't think there's anything equivalent on
*BSD, so we can just let it slide for now. */

s32 fd = open("/proc/sys/kernel/core_pattern", O_RDONLY);
u8 fchar;

if (fd < 0) return;

ACTF("Checking core_pattern...");

if (read(fd, &fchar, 1) == 1 && fchar == '|') {

SAYF("\n" cLRD "[-] " cRST
"Hmm, your system is configured to send core dump notifications to an\n"
" external utility. This will cause issues: there will be an extended delay\n"
" between stumbling upon a crash and having this information relayed to the\n"
" fuzzer via the standard waitpid() API.\n\n"

" To avoid having crashes misinterpreted as timeouts, please log in as root\n"
" and temporarily modify /proc/sys/kernel/core_pattern, like so:\n\n"

" echo core >/proc/sys/kernel/core_pattern\n");

if (!getenv("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"))
FATAL("Pipe at the beginning of 'core_pattern'");

}

close(fd);

#endif /* ^__APPLE__ */

}

确保 core dumps 不会转储进入程序

check_cpu_governor

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
/* Check CPU governor. */

static void check_cpu_governor(void) {

FILE* f;
u8 tmp[128];
u64 min = 0, max = 0;

if (getenv("AFL_SKIP_CPUFREQ")) return;

f = fopen("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", "r");
if (!f) return;

ACTF("Checking CPU scaling governor...");

if (!fgets(tmp, 128, f)) PFATAL("fgets() failed");

fclose(f);

if (!strncmp(tmp, "perf", 4)) return;

f = fopen("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq", "r");

if (f) {
if (fscanf(f, "%llu", &min) != 1) min = 0;
fclose(f);
}

f = fopen("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", "r");

if (f) {
if (fscanf(f, "%llu", &max) != 1) max = 0;
fclose(f);
}

if (min == max) return;

SAYF("\n" cLRD "[-] " cRST
"Whoops, your system uses on-demand CPU frequency scaling, adjusted\n"
" between %llu and %llu MHz. Unfortunately, the scaling algorithm in the\n"
" kernel is imperfect and can miss the short-lived processes spawned by\n"
" afl-fuzz. To keep things moving, run these commands as root:\n\n"

" cd /sys/devices/system/cpu\n"
" echo performance | tee cpu*/cpufreq/scaling_governor\n\n"

" You can later go back to the original state by replacing 'performance' with\n"
" 'ondemand'. If you don't want to change the settings, set AFL_SKIP_CPUFREQ\n"
" to make afl-fuzz skip this check - but expect some performance drop.\n",
min / 1024, max / 1024);

FATAL("Suboptimal CPU scaling governor");

}

通过读取 /sys/devices/system/cpu/cpu0/cpufreq/ 下的一些内容检查 CPU 管理者

setup_post

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
/* Load postprocessor, if available. */

static void setup_post(void) {

void* dh;
u8* fn = getenv("AFL_POST_LIBRARY");
u32 tlen = 6;

if (!fn) return;

ACTF("Loading postprocessor from '%s'...", fn);

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

/* Do a quick test. It's better to segfault now than later =) */

post_handler("hello", &tlen);

OKF("Postprocessor installed successfully.");

}

加载 postprocessor

setup_shm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* Configure shared memory and virgin_bits. This is called at startup. */

EXP_ST void setup_shm(void) {

u8* shm_str;

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

memset(virgin_tmout, 255, MAP_SIZE);
memset(virgin_crash, 255, MAP_SIZE);

// 调用 shmget 分配共享内存并保存至 shm_id
shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);

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

// 注册 atexit handler 为 remove_shm
atexit(remove_shm);

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

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

// 如果不是 dumb mode ,设置 shm_str 到 SHM_ENV_VAR
if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);

ck_free(shm_str);

// 启动对共享内存的访问,返回一个指向共享内存第一个字节的指针
trace_bits = shmat(shm_id, NULL, 0);

if (trace_bits == (void *)-1) PFATAL("shmat() failed");

}

/* Get rid of shared memory and temp files (atexit handler). */

static void remove_shm(void) {

unlink(prog_in); /* Ignore errors */
shmctl(shm_id, IPC_RMID, NULL);

}
  • 如果 in_bitmap 为空,则填充 virgin_bits 为 255
  • 填充 virgin_tmout 和 virgin_crash 为 255
  • 调用 shmget 申请共享内存,如果出错则抛出异常
  • 注册 atexit handler 为 remove_shm
    • remove_shm 进行了 unlink 和 shmctl
  • 如果不是 dumb mode ,设置 shm_str 到 SHM_ENV_VAR
  • 调用 shmat 启动对共享内存的访问,并把共享内存指针赋值给 trace_bits

这里通过 trace_bits 和 virgin_bits 两个 bitmap 来分别记录当前的 tuple 及整体的 tuple 信息

  • trace_bits 位于共享内存上,用于进程间通信
  • virgin_tmout 和 virgin_crash 记录 fuzz 过程中出现的所有目标程序超时及崩溃的 tuple 信息

另外,MAP_SIZE 为 65536

init_count_class16

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
/* Destructively classify execution counts in a trace. This is used as a
preprocessing step for any newly acquired traces. Called on every exec,
must be fast. */

static const u8 count_class_lookup8[256] = {

[0] = 0,
[1] = 1,
[2] = 2,
[3] = 4,
[4 ... 7] = 8,
[8 ... 15] = 16,
[16 ... 31] = 32,
[32 ... 127] = 64,
[128 ... 255] = 128

};

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

}

trace_bits 是用一个字节来记录是否到达这个路径,并且命中了多少次。这个值在 0 - 255 之间。count_class_lookup8 数组用来将这些次数做了一个规整,比如 4 - 7 次的计数都默认为 8 次。

而 AFL 中对一条分支路径的表示通常由一个二元组来进行表示,后面 AFL 进行规整的时候也是读取的两个字节,为了提高效率便使用了 count_class_lookup16 。(拿空间换时间)

setup_dirs_fds

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
/* Prepare output directories and fds. */

EXP_ST void setup_dirs_fds(void) {

u8* tmp;
s32 fd;

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

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

/* Top-level directory for queue metadata used for session
resume and related tasks. */

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

/* Directory for flagging queue entries that went through
deterministic fuzzing in the past. */

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

/* Directory with the auto-selected dictionary entries. */

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

/* The set of paths currently deemed redundant. */

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

/* The set of paths showing variable behavior. */

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

/* Sync directory for keeping track of cooperating fuzzers. */

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

}

/* All recorded crashes. */

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

/* All recorded hangs. */

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

/* Generally useful file descriptors. */

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

/* Gnuplot output file. */

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

}

提前初始化 out_dir 目录下的各个子目录

read_testcases

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
/* Read all testcases from the input directory, then queue them for testing.
Called at startup. */

static void read_testcases(void) {

struct dirent **nl;
s32 nl_cnt;
u32 i;
u8* fn;

/* Auto-detect non-in-place resumption attempts. */

fn = alloc_printf("%s/queue", in_dir);
if (!access(fn, F_OK)) in_dir = fn; else ck_free(fn);

ACTF("Scanning '%s'...", in_dir);

/* We use scandir() + alphasort() rather than readdir() because otherwise,
the ordering of test cases would vary somewhat randomly and would be
difficult to control. */

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

}

if (shuffle_queue && nl_cnt > 1) {

ACTF("Shuffling queue...");
shuffle_ptrs((void**)nl, nl_cnt);

}

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

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

/* This also takes care of . and .. */

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

/* Check for metadata that indicates that deterministic fuzzing
is complete for this entry. We don't want to repeat deterministic
fuzzing when resuming aborted scans, because it would be pointless
and probably very time-consuming. */

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

add_to_queue(fn, st.st_size, passed_det);

}

free(nl); /* not tracked */

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;

}

从 in_dir 中读取所有测试用例,测试用例大小不能大于 100MB。同时不加载变异的输入测试用例。最后调用 add_to_queue 函数将测试用例加入队列。

add_to_queue

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

/* Append new test case to the queue. */

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

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;

/* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. */
if ((queued_paths - 1) % 100 == 0 && queued_paths > 1) {

q_prev100->next_100 = q;
q_prev100 = q;

}

last_path_time = get_cur_time();

}

将测试用例加入队列,会初始化 fname 文件名称,增加 cur_depth 、queued_paths 和 pending_not_fuzzed 等参数,并初始化 last_path_time

load_auto

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
/* Load automatically generated extras. */

static void load_auto(void) {

u32 i;

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;

}

/* We read one byte more to cheaply detect tokens that are too
long (and skip them). */

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

}

if (i) OKF("Loaded %u auto-discovered dictionary tokens.", i);
else OKF("No auto-generated dictionary tokens to reuse.");

}

加载自动生成的额外内容

pivot_inputs

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
/* Create hard links for input test cases in the output directory, choosing
good names and pivoting accordingly. */

static void pivot_inputs(void) {

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

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

while (q) {

u8 *nfn, *rsl = strrchr(q->fname, '/');
u32 orig_id;

if (!rsl) rsl = q->fname; else rsl++;

/* If the original file name conforms to the syntax and the recorded
ID matches the one we'd assign, just use the original file name.
This is valuable for resuming fuzzing runs. */

#ifndef SIMPLE_FILES
# define CASE_PREFIX "id:"
#else
# define CASE_PREFIX "id_"
#endif /* ^!SIMPLE_FILES */

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

/* Since we're at it, let's also try to find parent and figure out the
appropriate depth for this entry. */

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;

}

} else {

/* No dice - invent a new name, capturing the original one as a
substring. */

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

}

/* Pivot to the new queue entry. */

link_or_copy(q->fname, nfn);
ck_free(q->fname);
q->fname = nfn;

/* Make sure that the passed_det value carries over, too. */

if (q->passed_det) mark_as_det_done(q);

q = q->next;
id++;

}

if (in_place_resume) nuke_resume_dir();

}

/* Mark deterministic checks as done for a particular queue entry. We use the
.state file to avoid repeating deterministic fuzzing when resuming aborted
scans. */

static void mark_as_det_done(struct queue_entry* q) {

u8* fn = strrchr(q->fname, '/');
s32 fd;

fn = alloc_printf("%s/queue/.state/deterministic_done/%s", out_dir, fn + 1);

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

ck_free(fn);

q->passed_det = 1;

}

/* Delete the temporary directory used for in-place session resume. */

static void nuke_resume_dir(void) {

u8* fn;

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

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
/* Read extras from the extras directory and sort them by size. */

static void load_extras(u8* dir) {

DIR* d;
struct dirent* de;
u32 min_len = MAX_DICT_FILE, max_len = 0, dict_level = 0;
u8* x;

/* If the name ends with @, extract level and continue. */

if ((x = strchr(dir, '@'))) {

*x = 0;
dict_level = atoi(x + 1);

}

ACTF("Loading extra dictionary from '%s' (level %u)...", dir, dict_level);

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

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

/* This also takes care of . and .. */
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);

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

}

/* Read extras from a file, sort by size. */

static void load_extras_file(u8* fname, u32* min_len, u32* max_len,
u32 dict_level) {

FILE* f;
u8 buf[MAX_LINE];
u8 *lptr;
u32 cur_line = 0;

f = fopen(fname, "r");

if (!f) PFATAL("Unable to open '%s'", fname);

while ((lptr = fgets(buf, MAX_LINE, f))) {

u8 *rptr, *wptr;
u32 klen = 0;

cur_line++;

/* Trim on left and right. */

while (isspace(*lptr)) lptr++;

rptr = lptr + strlen(lptr) - 1;
while (rptr >= lptr && isspace(*rptr)) rptr--;
rptr++;
*rptr = 0;

/* Skip empty lines and comments. */

if (!*lptr || *lptr == '#') continue;

/* All other lines must end with '"', which we can consume. */

rptr--;

if (rptr < lptr || *rptr != '"')
FATAL("Malformed name=\"value\" pair in line %u.", cur_line);

*rptr = 0;

/* Skip alphanumerics and dashes (label). */

while (isalnum(*lptr) || *lptr == '_') lptr++;

/* If @number follows, parse that. */

if (*lptr == '@') {

lptr++;
if (atoi(lptr) > dict_level) continue;
while (isdigit(*lptr)) lptr++;

}

/* Skip whitespace and = signs. */

while (isspace(*lptr) || *lptr == '=') lptr++;

/* Consume opening '"'. */

if (*lptr != '"')
FATAL("Malformed name=\"keyword\" pair in line %u.", cur_line);

lptr++;

if (!*lptr) FATAL("Empty keyword in line %u.", cur_line);

/* Okay, let's allocate memory and copy data between "...", handling
\xNN escaping, \\, and \". */

extras = ck_realloc_block(extras, (extras_cnt + 1) *
sizeof(struct extra_data));

wptr = extras[extras_cnt].data = ck_alloc(rptr - lptr);

while (*lptr) {

char* hexdigits = "0123456789abcdef";

switch (*lptr) {

case 1 ... 31:
case 128 ... 255:
FATAL("Non-printable characters in line %u.", cur_line);

case '\\':

lptr++;

if (*lptr == '\\' || *lptr == '"') {
*(wptr++) = *(lptr++);
klen++;
break;
}

if (*lptr != 'x' || !isxdigit(lptr[1]) || !isxdigit(lptr[2]))
FATAL("Invalid escaping (not \\xNN) in line %u.", cur_line);

*(wptr++) =
((strchr(hexdigits, tolower(lptr[1])) - hexdigits) << 4) |
(strchr(hexdigits, tolower(lptr[2])) - hexdigits);

lptr += 3;
klen++;

break;

default:

*(wptr++) = *(lptr++);
klen++;

}

}

extras[extras_cnt].len = klen;

if (extras[extras_cnt].len > MAX_DICT_FILE)
FATAL("Keyword too big in line %u (%s, limit is %s)", cur_line,
DMS(klen), DMS(MAX_DICT_FILE));

if (*min_len > klen) *min_len = klen;
if (*max_len < klen) *max_len = klen;

extras_cnt++;

}

fclose(f);

}

如果传入的参数有 extras_dir ,则从 extras_dir 中读取额外数据并按照大小排序。同时会要求读取的额外数据都是可打印字符

find_timeout

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
/* The same, but for timeouts. The idea is that when resuming sessions without
-t given, we don't want to keep auto-scaling the timeout over and over
again to prevent it from growing due to random flukes. */

static void find_timeout(void) {

static u8 tmp[4096]; /* Ought to be enough for anybody. */

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

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; /* Ignore errors */
close(fd);

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

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

exec_tmout = ret;
timeout_given = 3;

}

如果没有指定 timeout 时间,则初始化 exec_tmout 和 timeout_given 防止每次都自动调整超时时间

detect_file_args

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
/* Detect @@ in args. */

EXP_ST void detect_file_args(char** argv) {

u32 i = 0;
u8* cwd = getcwd(NULL, 0);

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

while (argv[i]) {

u8* aa_loc = strstr(argv[i], "@@");

if (aa_loc) {

u8 *aa_subst, *n_arg;

/* If we don't have a file name chosen yet, use a safe default. */

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

/* Be sure that we're always using fully-qualified paths. */

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

/* Construct a replacement argv value. */

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

}

识别输入是否有 @@ (占位符),如果有且 out_file 为空则将 out_file 替换为 out_dir/.cur_input

setup_stdio_file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Setup the output file for fuzzed data, if not using -f. */

EXP_ST void setup_stdio_file(void) {

u8* fn = alloc_printf("%s/.cur_input", out_dir);

unlink(fn); /* Ignore errors */

out_fd = open(fn, O_RDWR | O_CREAT | O_EXCL, 0600);

if (out_fd < 0) PFATAL("Unable to create '%s'", fn);

ck_free(fn);

}

如果 out_file 为空则设置 output 文件

check_binary

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

EXP_ST void check_binary(u8* fname) {

u8* env_path = 0;
struct stat st;

s32 fd;
u8* f_data;
u32 f_len = 0;

ACTF("Validating target binary...");

if (strchr(fname, '/') || !(env_path = getenv("PATH"))) {

target_path = ck_strdup(fname);
if (stat(target_path, &st) || !S_ISREG(st.st_mode) ||
!(st.st_mode & 0111) || (f_len = st.st_size) < 4)
FATAL("Program '%s' not found or not executable", fname);

} else {

while (env_path) {

u8 *cur_elem, *delim = strchr(env_path, ':');

if (delim) {

cur_elem = ck_alloc(delim - env_path + 1);
memcpy(cur_elem, env_path, delim - env_path);
delim++;

} else cur_elem = ck_strdup(env_path);

env_path = delim;

if (cur_elem[0])
target_path = alloc_printf("%s/%s", cur_elem, fname);
else
target_path = ck_strdup(fname);

ck_free(cur_elem);

if (!stat(target_path, &st) && S_ISREG(st.st_mode) &&
(st.st_mode & 0111) && (f_len = st.st_size) >= 4) break;

ck_free(target_path);
target_path = 0;

}

if (!target_path) FATAL("Program '%s' not found or not executable", fname);

}

if (getenv("AFL_SKIP_BIN_CHECK")) return;

/* Check for blatant user errors. */

if ((!strncmp(target_path, "/tmp/", 5) && !strchr(target_path + 5, '/')) ||
(!strncmp(target_path, "/var/tmp/", 9) && !strchr(target_path + 9, '/')))
FATAL("Please don't keep binaries in /tmp or /var/tmp");

fd = open(target_path, O_RDONLY);

if (fd < 0) PFATAL("Unable to open '%s'", target_path);

f_data = mmap(0, f_len, PROT_READ, MAP_PRIVATE, fd, 0);

if (f_data == MAP_FAILED) PFATAL("Unable to mmap file '%s'", target_path);

close(fd);

if (f_data[0] == '#' && f_data[1] == '!') {

SAYF("\n" cLRD "[-] " cRST
"Oops, the target binary looks like a shell script. Some build systems will\n"
" sometimes generate shell stubs for dynamically linked programs; try static\n"
" library mode (./configure --disable-shared) if that's the case.\n\n"

" Another possible cause is that you are actually trying to use a shell\n"
" wrapper around the fuzzed component. Invoking shell can slow down the\n"
" fuzzing process by a factor of 20x or more; it's best to write the wrapper\n"
" in a compiled language instead.\n");

FATAL("Program '%s' is a shell script", target_path);

}

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

if (!qemu_mode && !dumb_mode &&
!memmem(f_data, f_len, SHM_ENV_VAR, strlen(SHM_ENV_VAR) + 1)) {

SAYF("\n" cLRD "[-] " cRST
"Looks like the target binary is not instrumented! The fuzzer depends on\n"
" compile-time instrumentation to isolate interesting test cases while\n"
" mutating the input data. For more information, and for tips on how to\n"
" instrument binaries, please see %s/README.\n\n"

" When source code is not available, you may be able to leverage QEMU\n"
" mode support. Consult the README for tips on how to enable this.\n"

" (It is also possible to use afl-fuzz as a traditional, \"dumb\" fuzzer.\n"
" For that, you can use the -n option - but expect much worse results.)\n",
doc_path);

FATAL("No instrumentation detected");

}

if (qemu_mode &&
memmem(f_data, f_len, SHM_ENV_VAR, strlen(SHM_ENV_VAR) + 1)) {

SAYF("\n" cLRD "[-] " cRST
"This program appears to be instrumented with afl-gcc, but is being run in\n"
" QEMU mode (-Q). This is probably not what you want - this setup will be\n"
" slow and offer no practical benefits.\n");

FATAL("Instrumentation found in -Q mode");

}

if (memmem(f_data, f_len, "libasan.so", 10) ||
memmem(f_data, f_len, "__msan_init", 11)) uses_asan = 1;

/* Detect persistent & deferred init signatures in the binary. */

if (memmem(f_data, f_len, PERSIST_SIG, strlen(PERSIST_SIG) + 1)) {

OKF(cPIN "Persistent mode binary detected.");
setenv(PERSIST_ENV_VAR, "1", 1);
persistent_mode = 1;

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

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

}

if (memmem(f_data, f_len, DEFER_SIG, strlen(DEFER_SIG) + 1)) {

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

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

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

}

if (munmap(f_data, f_len)) PFATAL("unmap() failed");

}

进行路径搜索,检查目标二进制文件是否存在且不是 shell 脚本,同时也会检查 ELF 头来确保目标二进制文件是正确格式

首次 Fuzz

start_time

调用 get_cur_time() 函数获取 start_time

get_qemu_argv

1
2
3
4
if (qemu_mode)
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;

如果是 qemu_mode 的话则通过 get_qemu_argv 获取 use_argv

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
/* Rewrite argv for QEMU. */

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

char** new_argv = ck_alloc(sizeof(char*) * (argc + 4));
u8 *tmp, *cp, *rsl, *own_copy;

/* Workaround for a QEMU stability glitch. */

setenv("QEMU_LOG", "nochain", 1);

memcpy(new_argv + 3, argv + 1, sizeof(char*) * argc);

new_argv[2] = target_path;
new_argv[1] = "--";

/* Now we need to actually find the QEMU binary to put in argv[0]. */

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;

}

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

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

}

需要能够找到 afl-qemu-trace 文件

perform_dry_run

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
/* Perform dry run of all test cases to confirm that the app is working as
expected. This is done only for the initial inputs, and only once. */

static void perform_dry_run(char** argv) {

struct queue_entry* q = queue;
u32 cal_failures = 0;
u8* skip_crashes = getenv("AFL_SKIP_CRASHES");

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

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 [...] <testcase )\n\n"
#else
" ( ulimit -Sd $[%llu << 10]; /path/to/binary [...] <testcase )\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. 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 <lcamtuf@coredump.cx> 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 <lcamtuf@coredump.cx> 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;

}

if (q->var_behavior) WARNF("Instrumentation output varies across runs.");

q = q->next;

}

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

}

该函数会执行所有的测试用例并检查是否运行正确:

  • 读取环境变量 AFL_SKIP_CRASHES 到 skip_crashes ,并获取测试用例的队列和设置 cal_failures 为 0
  • 挨个遍历测试用例队列
    • 使用 ck_alloc_nozero 根据测试用例长度分配内存给 use_mem ,然后把测试用例读取到 use_mem 中,其中 ck_alloc_nozero 就是 DFL_ck_alloc_nozero
    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
    #define ck_alloc_nozero   DFL_ck_alloc_nozero
    #define ALLOC_OFF_HEAD 8
    #define ALLOC_OFF_TOTAL (ALLOC_OFF_HEAD + 1)

    /* Allocate a buffer, explicitly not zeroing it. Returns NULL for zero-sized
    requests. */

    static inline void* DFL_ck_alloc_nozero(u32 size) {

    void* ret;

    if (!size) return NULL;

    ALLOC_CHECK_SIZE(size);
    ret = malloc(size + ALLOC_OFF_TOTAL);
    ALLOC_CHECK_RESULT(ret, size);

    ret += ALLOC_OFF_HEAD;

    ALLOC_C1(ret) = ALLOC_MAGIC_C1;
    ALLOC_S(ret) = size;
    ALLOC_C2(ret) = ALLOC_MAGIC_C2;

    return ret;

    }
    • 使用 calibrate_case 函数校准测试用例并返回测试结果,由于 calibrate_case 函数较大后面再介绍
    • 如果有 stop_soon 则返回
    • 如果结果是 crash_mode 或 FAULT_NOBITS 则打印一些信息
    • 随后根据结果进行不同的处理
      • FAULT_NONE
        • 如果是第一个测试用例,调用 check_map_coverage 函数
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        #define MAP_SIZE_POW2       16
        #define MAP_SIZE (1 << MAP_SIZE_POW2)

        #define FF(_b) (0xff << ((_b) << 3))

        /* Count the number of bytes set in the bitmap. Called fairly sporadically,
        mostly to update the status screen or calibrate and examine confirmed
        new paths. */

        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;

        }

        /* Examine map coverage. Called once, for first test case. */

        static void check_map_coverage(void) {

        u32 i;

        if (count_bytes(trace_bits) < 100) return;

        for (i = (1 << (MAP_SIZE_POW2 - 1)); i < MAP_SIZE; i++)
        if (trace_bits[i]) return;

        WARNF("Recompile binary with newer version of afl to improve coverage!");

        }
        • 使用 count_bytes 计算 trace_bits 发现的路径数量,如果小于 100 则直接返回
        • 对于 trace_bits 的后半部分,如果有值就返回
        • 否则抛出警告,提示编译新版本的 afl 提高覆盖率
        • 如果是 crash_mode ,则抛出异常(因为没有 crash )
      • FAULT_TMOUT
        • 判断是否设置了 timeout_given (即是否有 -t 参数)
          • 如果设置了,且 timeout_given 大于 1 ,则抛出警告并设置 q->cal_failed 为 CAL_CHANCES 且递增 cal_failures
      • FAULT_CRASH
        • 如果是 crash_mode ,直接 break 跳出
        • 判断是否设置了 skip_crashes
          • 如果设置了,则抛出警告并设置 q->cal_failed 为 CAL_CHANCES 且递增 cal_failures
        • 判断是否设置了 mem_limit
          • 如果没有设置,则抛出建议增加内存的建议
          • 同时也会打印出一些信息,且抛出测试用例结果 crash 的异常
      • FAULT_ERROR
        • 抛出不能执行指定程序的异常
      • FAULT_NOINST
        • 抛出没有指令的异常
      • FAULT_NOBITS
        • 递增 useless_at_start
        • 如果没有设置 in_bitmap 和 shuffle_queue ,则抛出没有任何新路径的警告
    • 如果有 q->var_behavior ,则代表它多次运行,在同样的输入条件下出现了不同的覆盖信息,就会抛出这个样例的路径输出可变的警告
    • 随后进行 q→next 的测试,直至结束
  • 遍历结束,如果设置了 cal_failures ,会进行判断:
    • 如果 cal_failures 等于 queued_paths ,则抛出所有测试用例都超时或 crash 的异常
    • 如果 cal_failures * 5 大于 queued_paths ,则抛出被拒绝的测试用例比例很高的警告
    • 否则打印该阶段的结果

calibrate_case

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
/* Calibrate a new test case. This is done when processing the input directory
to warn about flaky or otherwise problematic test cases early on; and when
new paths are discovered to detect variable behavior and so on. */

static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem,
u32 handicap, u8 from_queue) {

static u8 first_trace[MAP_SIZE];

u8 fault = 0, new_bits = 0, var_detected = 0, 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;

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

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

q->cal_failed++;

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

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

if (dumb_mode != 1 && !no_forkserver && !forksrv_pid)
init_forkserver(argv);

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

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

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

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

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

cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);

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;

/* OK, let's collect some stats about the performance of this test case.
This is used for fuzzing air time calculations in calculate_score(). */

q->exec_us = (stop_us - start_us) / stage_max;
q->bitmap_size = count_bytes(trace_bits);
q->handicap = handicap;
q->cal_failed = 0;

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

update_bitmap_score(q);

/* If this case didn't result in new output from the instrumentation, tell
parent. This is a non-critical problem, but something to warn the user
about. */

if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;

abort_calibration:

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

/* Mark variable paths. */

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;

}

该函数主要评估测试用例的行为是否异常,并且在发现新路径的时候检测测试用例行为是否可变(也就是同一测试用例出现不同的路径)

  • 申请局部变量 first_trace[MAP_SIZE]
  • 如果 q->exec_cksum 为 0 ,则设置 first_run 为 1
  • 将 stage_cur/stage_max/stage_name 分别存储在 old_sc/old_sm/old_sn 中
  • 设置 use_tmout 为 exec_tmout ,如果 from_queue 为 0 或设置了 resuming_fuzz 则代表在恢复会话或尝试校准已添加的结果时,对超时要更加慷慨一些。会将 use_tmout 设成更大的值
  • 递增 q->cal_failed ,设置 stage_name 为 “calibration” 且通过判断是否设置了 fast_cal 来设置 stage_max 为 3 或 CAL_CYCLES(8)
    • stage_max 代表每个测试用例以及显示出可变行为的测试用例的校准周期
  • 如果不是以 dumb mode 运行且没有 no_forkserver (禁用 fork server)和 forksrv_pid 的时候调用 init_forkserver 初始化 fork server
  • 如果设置了 q->exec_cksum ,则将 trace_bits 拷贝到 first_trace ,并通过 has_new_bits 函数检测 virgin_bits 是否有新的 bits ,如果结果大于 new_bits 则赋值给 new_bits
    • q->exec_cksum 不为空则代表这个 q 不是来自测试用例输入文件夹,而是评估的新的 case
  • 然后开始校准,周期为 stage_max
    • 如果不是第一次运行且不是 stage_cur % stats_update_freq 的时候,调用 show_stats 函数显示当前状态(运行时间啥的)
    • 调用 write_to_testcase 函数将修改后的数据写入文件进行测试,如果设置了 out_file,则取消链接旧文件并创建一个新文件;否则,out_fd 将被倒带并截断。
    • 调用 run_target 函数运行目标程序并将结果返回到 fault
    • 如果设置了 stop_soon 或 fault 不为 crash_mode 则跳转到 abort_calibration
    • 如果不是 dumb mode 且是校准第一次运行且共享内存中没有任何路径(count_bytes 返回结果为 0 )则设置 fault 为 FAULT_NOINST 且跳转到 abort_calibration
    • 调用 hash32 函数传入参数 trace_bits 并将结果返回到 cksum ,并且比较 q→cksum 和 cksum
      • 如果不同,则证明这是第一次运行或同一测试用例出现了不同的路径
        • 通过 has_new_bits 函数检测 virgin_bits 是否有新的 bits ,如果结果大于 new_bits 则赋值给 new_bits
        • 如果 q→cksum 不为 0 ,则证明同一测试用例出现了不同的路径
          • 遍历 0 到 MAP_SIZE ,如果 var_bytes[i] 为空且 first_trace[i] 不等于 trace_bits[i] ,则代表发现了不同的路径,将 var_bytes[i] 设为 1 且将 stage_max 设为 CAL_CYCLES_LONG(40),使得评估循环更久
          • 设置 var_detected 为 1
        • 否则 q→cksum 为 0 ,代表是第一次运行
          • 将 cksum 赋值给 q->exec_cksum
          • 拷贝 trace_bits 到 first_trace
  • 更新 total_cal_us 和 total_cal_cycles
  • 计算一些统计信息
    • q->exec_us :单次执行时间的平均值
    • 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 来为该测试用例寻找一组最小的路径来触发到目前为止在位图中看到的所有位,并专注于以牺牲其余部分为代价来模糊它们。
  • 如果不是 dumb mode 且不是第一次运行且没有设置 fault 且没有设置 new_bits ,代表该测试用例在所有轮次的执行里都没有发现任何新的路径和异常,则设置 fault 为 FAULT_NOBITS
  • 接下来是 abort_calibration 的 label 处
    • 如果 new_bits 为 2 且 q->has_new_cov 为 0 ,则设置 q->has_new_cov 为 1 且递增 queued_with_cov ,代表发现了新的路径
    • 如果设置了 var_detected (该测试用例运行多次有多个路径)
      • 将 count_bytes(var_bytes) 的结果赋值给 var_byte_count ,也就是统计可变路径的数目
      • 如果没有设置 q->var_behavior
        • 调用 mark_as_variable 函数为该测试用例创建符号链接,../../xxx → out_dir//queue/.state/variable_behavior/xxx 的文件,并将 q->var_behavior 设置为 1
        • 递增 queued_variable
  • 从 old_sc/old_sm/old_sn 中恢复 stage_cur/stage_max/stage_name
  • 如果不是第一次运行,调用 show_stats 显示状态
  • 最后返回 fault 值

init_forkserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
/* Spin up fork server (instrumented mode only). The idea is explained here:

http://lcamtuf.blogspot.com/2014/10/fuzzing-binaries-without-execve.html

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

if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL("pipe() failed");

forksrv_pid = fork();

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

if (!forksrv_pid) {

struct rlimit r;

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

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

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

}

if (mem_limit) {

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

#ifdef RLIMIT_AS

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

#else

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

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

#endif /* ^RLIMIT_AS */

}

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

r.rlim_max = r.rlim_cur = 0;

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

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

setsid();

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

if (out_file) {

dup2(dev_null_fd, 0);

} else {

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

}

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

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

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

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

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

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

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

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

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

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

execv(target_path, argv);

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

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

}

/* Close the unneeded endpoints. */

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

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

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

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

setitimer(ITIMER_REAL, &it, NULL);

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

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

setitimer(ITIMER_REAL, &it, NULL);

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

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

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

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

if (WIFSIGNALED(status)) {

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

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

} else if (!mem_limit) {

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

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

#ifdef __APPLE__

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

#endif /* __APPLE__ */

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

} else {

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

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

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

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

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

#ifdef __APPLE__

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

#endif /* __APPLE__ */

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

}

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

}

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

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

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

} else if (!mem_limit) {

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

} else {

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

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

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

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

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

}

FATAL("Fork server handshake failed");

}
  • 初始化 st_pipe 和 ctl_pipe 管道
    • st_pipe 为状态管道
    • ctl_pipe 为控制管道
  • 调用 fork 弄出来子进程并将返回的 pid 赋值给 forksrv_pid
  • 子进程会执行以下操作
    • 设置一些 fd limit
    • 调用 setsid 方法创建新的会话
    • 关闭 stdout 和 stderr
    • 如果定义了 out_file ,关闭 stdin ;否则关闭 out_fd
    • 将 FORKSRV_FD 重定向到 ctl_pipe[0] ,FORKSRV_FD + 1 重定向到 st_pipe[1]
      • 此时子进程只能从控制管道读,向状态管道写
    • 关闭描述符
      • st_pipe/ctl_pipe/out_dir_fd/dev_null_fd/dev_urandom_fd/fileno(plot_file)
    • 从环境变量中读取 LD_BIND_LAZY ,如果没有则设置 LD_BIND_NOW 为 1 ,防止linker在fork之后做额外的工作
    • 设置 ASAN_OPTIONS 和 MSAN_OPTIONS 环境变量
    • 启动 execv(target_path, argv)
    • 如果启动失败,设置 trace_bits 为 EXEC_FAIL_SIG 告诉父进程并退出子进程
  • 父进程会执行以下操作
    • 关闭 ctl_pipe[0] 和 st_pipe[1]
    • 将 ctl_pipe[1] 赋值给 fsrv_ctl_fd ,st_pipe[0] 赋值给 fsrv_st_fd
    • 等待 forkserver 启动
    • 从 fsrv_st_fd 读取 4 字节的状态信息
      • 如果读取到了,直接返回
      • 如果设置 child_timed_out 了,抛出异常告诉用户设置 -t 选项
      • 等待 waitpid(forksrv_pid, &status, 0) 返回,如果小于 0 抛出异常
      • 调用 WIFSIGNALED 判断返回的 status 是否为异常退出的信号
        • 如果是,则
          • 判断是否设置了 mem_limit 且 mem_limit 小于 500 且设置了 uses_asan
            • 告知用户是由于未收到任何 input 就 crash ,可能是由于 asan 和 mem_limit 的原因。查看 notes_for_asan.txt 。
            • 如果没有设置 mem_limit ,告知用户可能的原因
            • 否则告知用户其他的原因
        • 如果否,则
          • 判断 trace_bits 是否等于 EXEC_FAIL_SIG ,如果是则子进程的 execve 没有正常执行
          • 接下来的判断和是的情况一样,也是告知用户可能的原因

has_new_bits

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
/* Check if the current execution path brings anything new to the table.
Update virgin bits to reflect the finds. Returns 1 if the only change is
the hit-count for a particular tuple; 2 if there are new tuples seen.
Updates the map, so subsequent calls will always return 0.

This function is called after every exec() on a fairly large buffer, so
it needs to be fast. We do this in 32-bit and 64-bit flavors. */

static inline u8 has_new_bits(u8* virgin_map) {

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

while (i--) {

/* Optimize for (*current & *virgin) == 0 - i.e., no bits in current bitmap
that have not been already cleared from the virgin map - since this will
almost always be the case. */

if (unlikely(*current) && unlikely(*current & *virgin)) {

if (likely(ret < 2)) {

u8* cur = (u8*)current;
u8* vir = (u8*)virgin;

/* Looks like we have not found any new bytes yet; see if any non-zero
bytes in current[] are pristine in 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++;

}

if (ret && virgin_map == virgin_bits) bitmap_changed = 1;

return ret;

}
  • 设置参数 current 和 virgin 分别为 trace_bits 和 virgin_map ,同时根据 64 还是 32 位设置 i 为 MAP_SIZE >>3 或 MAP_SIZE >> 2 ,并初始化 ret 为 0
  • 8 字节一组挨个遍历 map
    • 如果 *current 不为 0 且 *current & *virgin 不为 0 ,则代表发现了新的路径或路径的执行次数发生了变化
      • 如果 ret < 2
        • 判断 current 和 virgin 中是否存在 cur[i] && vir[i] == 0xff ,0 < i < 7
          • 如果有一个为真,则设置 ret 为 2 ,也就是发现了新的路径
            • vir[i] == 0xff 表示之前都为覆盖该路径,而 cur[i] 则表示这次覆盖到了
          • 否则设置 ret 为 1 ,也就是路径的执行次数发生了变化
      • 设置 virgin &= ~*current
  • 如果 ret 不为 0 且 virgin_map 等于 virgin_bits ,则设置 bitmap_changed 为 1
    • virgin_bits 保存还没有被 Fuzz 覆盖到的 byte ,其初始值每位全被置位 1 ,然后每次按字节置位

run_target

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
/* Execute target application, monitoring for timeouts. Return status
information. The called program will update trace_bits[]. */

static u8 run_target(char** argv, u32 timeout) {

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

int status = 0;
u32 tb4;

child_timed_out = 0;

/* After this memset, trace_bits[] are effectively volatile, so we
must prevent any earlier operations from venturing into that
territory. */

memset(trace_bits, 0, MAP_SIZE);
MEM_BARRIER();

/* If we're running in "dumb" mode, we can't rely on the fork server
logic compiled into the target program, so we will just keep calling
execve(). There is a bit of code duplication between here and
init_forkserver(), but c'est la vie. */

if (dumb_mode == 1 || no_forkserver) {

child_pid = fork();

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

if (!child_pid) {

struct rlimit r;

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

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

}

/* On Linux, would be faster to use O_CLOEXEC. Maybe TODO. */

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

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

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

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

execv(target_path, argv);

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

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

}

} else {

s32 res;

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

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

}

/* Configure timeout, as requested by user, then wait for child to terminate. */

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

setitimer(ITIMER_REAL, &it, NULL);

/* The SIGALRM handler simply kills the child_pid and sets child_timed_out. */

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

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

MEM_BARRIER();

tb4 = *(u32*)trace_bits;

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

/* Report outcome to caller. */

if (WIFSIGNALED(status) && !stop_soon) {

kill_signal = WTERMSIG(status);

if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;

return FAULT_CRASH;

}

/* A somewhat nasty hack for MSAN, which doesn't support abort_on_error and
must use a special exit code. */

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

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

/* It makes sense to account for the slowest units only if the testcase was run
under the user defined timeout. */
if (!(timeout > exec_tmout) && (slowest_exec_ms < exec_ms)) {
slowest_exec_ms = exec_ms;
}

return FAULT_NONE;

}
  • 清空 trace_bits

  • 如果 dumb_mode 为 1 或 no_forkserver 为 1 ,则执行一段与 init_forkserver 中类似的操作,会 fork 出子进程并进行类似的操作

  • 否则向 fsrv_ctl_fd 写入 prev_timed_out 的值,写入失败且设置了 stop_soon 则直接抛出错误

  • 从 fsrv_st_fd 中读取 child_pid 的值,读取失败且设置了 stop_soon 则直接抛出错误

  • 如果 child_pid 小于 0 则直接抛出错误

  • 设置 timer

  • 如果 dumb_mode 为 1 或 no_forkserver 为 1 ,那就 waitpid(child_pid, &status, 0) ,返回为负数则抛出异常

  • 否则再次从 fsrv_st_fd 中读取 child_pid 的值,读取失败且设置了 stop_soon 则直接抛出错误

  • 如果 WIFSTOPPED(status) 为 0 ,则设置 child_pid 为 0

  • 自增 total_execs

  • 调用 classify_counts(trace_bits),对 trace_bits 进行规整化

    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
    #ifdef WORD_SIZE_64

    static inline void classify_counts(u64* mem) {

    u32 i = MAP_SIZE >> 3;

    while (i--) {

    /* Optimize for sparse bitmaps. */

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

    }

    }

    #else

    static inline void classify_counts(u32* mem) {

    u32 i = MAP_SIZE >> 2;

    while (i--) {

    /* Optimize for sparse bitmaps. */

    if (unlikely(*mem)) {

    u16* mem16 = (u16*)mem;

    mem16[0] = count_class_lookup16[mem16[0]];
    mem16[1] = count_class_lookup16[mem16[1]];

    }

    mem++;

    }

    }

    #endif /* ^WORD_SIZE_64 */
  • 设置 prev_timed_out 为 child_timed_out

  • 若 WIFSIGNALED(status) 为真(异常退出)且没有设置 stop_soon

    • 使用 WTERMSIG 获得 status 的 kill_signal
    • 如果设置了 child_timed_out 且 kill_signal 为 SIGKILL 则返回 FAULT_TMOUT
    • 否则返回 FAULT_CRASH
  • 如果设置了 uses_asan 且 WEXITSTATUS(status) 为 MSAN_ERROR

    • 清空 kill_signal
    • 返回 FAULT_CRASH
  • 如果 dumb_mode 为 1 或设置了 no_forkserver 且未规整化之前的 trace_bits 为 EXEC_FAIL_SIG

    • 返回 FAULT_ERROR
  • 如果 timeout 不小于 exec_tmout 且 slowest_exec_ms 小于 exec_ms

    • 设置 slowest_exec_ms 为 exec_ms
  • 最后返回 FAULT_NONE

update_bitmap_score

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/* When we bump into a new path, we call this to see if the path appears
more "favorable" than any of the existing ones. The purpose of the
"favorables" is to have a minimal set of paths that trigger all the bits
seen in the bitmap so far, and focus on fuzzing them at the expense of
the rest.

The first step of the process is to maintain a list of top_rated[] entries
for every byte in the bitmap. We win that slot if there is no previous
contender, or if the contender has a more favorable speed x size factor. */

static void update_bitmap_score(struct queue_entry* q) {

u32 i;
u64 fav_factor = q->exec_us * q->len;

/* For every byte set in trace_bits[], see if there is a previous winner,
and how it compares to us. */

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

if (trace_bits[i]) {

if (top_rated[i]) {

/* Faster-executing or smaller test cases are favored. */

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

/* Looks like we're going to win. Decrease ref count for the
previous winner, discard its trace_bits[] if necessary. */

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

}

/* Insert ourselves as the new winner. */

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;

}

}

每当我们发现新的路径,都会调用该函数来判断其是不是更加 favorable 的,也就是说是否包含最小的路径集合来遍历到所有 bitmap 中的位

  • 首先计算 fav_factor = q->exec_us * q->len
  • 遍历 MAP_SIZE 大小
    • 如果 trace_bits[i] 不为 0 ,则表示这是已经被覆盖到的 path
      • 如果 top_rated[i] 不为 0 ,则
        1
        2
        static struct queue_entry*
        top_rated[MAP_SIZE]; /* Top entries for bitmap bytes */
        • 判断 fav_factor 是否大于 top_rated[i]->exec_us * top_rated[i]->len ,是的话代表 top_rated 更优,就 continue
          • 执行时间和样例大小的乘积
        • 否则 q 更小,则将 top_rated[i]->tc_ref 减一,如果减一之后的值为 0 则 free 掉 top_rated[i]->trace_mini 并清空
      • 将 top_rated[i] 赋值为 q 并递增 q->tc_ref
      • 如果没有初始化 q->trace_mini 则 alloc(MAP_SIZE >> 3) 并调用 minimize_bits(q->trace_mini, trace_bits)
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        /* Compact trace bytes into a smaller bitmap. We effectively just drop the
        count information here. This is called only sporadically, for some
        new paths. */

        static void minimize_bits(u8* dst, u8* src) {

        u32 i = 0;

        while (i < MAP_SIZE) {

        if (*(src++)) dst[i >> 3] |= 1 << (i & 7);
        i++;

        }

        }
        • 将 trace_bits 压缩为较小的位图,byte → bit 的映射
      • 将 score_changed 置为 1

cull_queue

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
/* The second part of the mechanism discussed above is a routine that
goes over top_rated[] entries, and then sequentially grabs winners for
previously-unseen bytes (temp_v) and marks them as favored, at least
until the next run. The favored entries are given more air time during
all fuzzing steps. */

static void cull_queue(void) {

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

for (i = 0; i < MAP_SIZE; i++)
if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {

u32 j = MAP_SIZE >> 3;

/* Remove all bits belonging to the current entry from temp_v. */

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

}

q = queue;

while (q) {
mark_as_redundant(q, !q->favored);
q = q->next;
}

}

精简队列的函数

  • 初始化 queue_entry* q 和 u8 temp_v[MAP_SIZE >> 3] 以及 i
  • 如果设置了 dumb_mode 或没有设置 score_changed 则返回
  • 初始化 score_changed/queued_favored/pending_favored 为 0 且清空 temp_v
  • 将 queue 赋值给 q ,遍历 q 链表将 favored 都置为 0
  • 从 0 开始遍历 i 直到 MAP_SIZE
    • 如果 top_rated[i] 存在且 (temp_v[i >> 3] & (1 << (i & 7))) 为 1(也就是判断该 path 对应的 bit 有没有被置位)
      • 设置 j 为 MAP_SIZE >> 3
      • 递减 j
        • 如果存在 top_rated[i]->trace_mini[j] 则设置 temp_v[j] &= ~top_rated[i]->trace_mini[j]
        • 设置 top_rated[i]->favored 为 1
        • 递增 queued_favored
      • 如果 top_rated[i]->was_fuzzed 为 0 ,则递增 pending_favored
    • 将 queue 赋值给 q
    • 遍历 q 链表,调用 mark_as_redundant(q, !q->favored)
  • 上面的遍历是为了找出一组能够覆盖到所有现在已经覆盖到的路径的 queue entry

mark_as_redundant

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
/* Mark / unmark as redundant (edge-only). This is not used for restoring state,
but may be useful for post-processing datasets. */

static void mark_as_redundant(struct queue_entry* q, u8 state) {

u8* fn;
s32 fd;

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

}
  • 判断 q->fs_redundant 是否等于 state ,是的话则返回
  • 否则设置 q->fs_redundant 为 state
  • 如果 state 为 1 ,尝试创建 out_dir/queue/.state/redundant_edges/fname
  • 否则 state 为 0 ,尝试删除 out_dir/queue/.state/redundant_edges/fname

show_init_stats

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
/* Display quick statistics at the end of processing the input directory,
plus a bunch of warnings. Some calibration stuff also ended up here,
along with several hardcoded constants. Maybe clean up eventually. */

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;

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

if (avg_us > (qemu_mode ? 50000 : 10000))
WARNF(cLRD "The target binary is pretty slow! See %s/perf_tips.txt.",
doc_path);

/* Let's keep things moving with slow binaries. */

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

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

if (!timeout_given) {

/* Figure out the appropriate timeout. The basic idea is: 5x average or
1x max, rounded up to EXEC_TM_ROUND ms and capped at 1 second.

If the program is slow, the multiplier is lowered to 2x or 3x, because
random scheduler jitter is less likely to have any impact, and because
our patience is wearing thin =) */

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

}

/* In dumb mode, re-running every timing out test case with a generous time
limit is very expensive, so let's select a more conservative default. */

if (dumb_mode && !getenv("AFL_HANG_TMOUT"))
hang_tmout = MIN(EXEC_TIMEOUT, exec_tmout * 2 + 100);

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

}

展示初始化的 stats

find_start_position

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
/* When resuming, try to find the queue position to start from. This makes sense
only when resuming, and when we can find the original fuzzer_stats. */

static u32 find_start_position(void) {

static u8 tmp[4096]; /* Ought to be enough for anybody. */

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

if (!resuming_fuzz) return 0;

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;

i = read(fd, tmp, sizeof(tmp) - 1); (void)i; /* Ignore errors */
close(fd);

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

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

}

恢复时尝试找到队列位置开始

  • 如果没设置 resuming_fuzz 则返回
  • 如果设置了 in_place_resume 则 fn 为 out_dir/fuzzer_stats ,否则 fn 为 in_dir/../fuzzer_stats
  • 打开 fn 并读入内容
  • 找寻 “cur_path : “ 的 offset 并设置 ret
  • 如果 ret 大于等于 queued_paths 就返回 0 ,否则返回 ret

write_stats_file

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
/* Update stats file for unattended monitoring. */

static void write_stats_file(double bitmap_cvg, double stability, double eps) {

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

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

/* Keep last values in case we're called from another context
where exec/sec stats and such are not readily available. */

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"
"last_update : %llu\n"
"fuzzer_pid : %u\n"
"cycles_done : %llu\n"
"execs_done : %llu\n"
"execs_per_sec : %0.02f\n"
"paths_total : %u\n"
"paths_favored : %u\n"
"paths_found : %u\n"
"paths_imported : %u\n"
"max_depth : %u\n"
"cur_path : %u\n" /* Must match find_start_position() */
"pending_favs : %u\n"
"pending_total : %u\n"
"variable_paths : %u\n"
"stability : %0.02f%%\n"
"bitmap_cvg : %0.02f%%\n"
"unique_crashes : %llu\n"
"unique_hangs : %llu\n"
"last_path : %llu\n"
"last_crash : %llu\n"
"last_hang : %llu\n"
"execs_since_crash : %llu\n"
"exec_timeout : %u\n" /* Must match find_timeout() */
"afl_banner : %s\n"
"afl_version : " VERSION "\n"
"target_mode : %s%s%s%s%s%s%s\n"
"command_line : %s\n"
"slowest_exec_ms : %llu\n",
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 */

/* Get rss value from the children
We must have killed the forkserver process and called waitpid
before calling getrusage */
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);

}

更新状态信息,将信息写入 stats 文件

save_auto

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
/* Save automatically generated extras. */

static void save_auto(void) {

u32 i;

if (!auto_changed) return;
auto_changed = 0;

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

}

}

保存自动生成的 extras

Fuzz 执行

Fuzz 执行在一个 while(1) 的循环里

主循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
while (1) {

u8 skipped_fuzz;

cull_queue();

if (!queue_cur) {

queue_cycle++;
current_entry = 0;
cur_skipped_paths = 0;
queue_cur = queue;

while (seek_to) {
current_entry++;
seek_to--;
queue_cur = queue_cur->next;
}

show_stats();

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

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

if (queued_paths == prev_queued) {

if (use_splicing) cycles_wo_finds++; else use_splicing = 1;

} else cycles_wo_finds = 0;

prev_queued = queued_paths;

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

}

skipped_fuzz = fuzz_one(use_argv);

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

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

}

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

if (stop_soon) break;

queue_cur = queue_cur->next;
current_entry++;

}
  • 调用 cull_queue 精简队列
  • 如果 queue_cur 为 0
    • 初始化 queue_cur 为 queue ,current_entry 和 cur_skipped_paths 为 0 并自增 queue_cycle ,之后循环递减 seek_to ,并通过 next 指针寻找 queue_cur ,同时递增 current_entry
      • queue_cur 也就是当前 fuzz 的 queue
    • 调用 show_stats 显示 stats
    • 如果设置了 not_on_tty 则打印 queue_cycle 并刷新 stdout
    • 如果 queued_paths 等于 prev_queued ,则代表当前执行的 queue 里 case 的数量和之前一样,也就是没有新的 case
      • 如果设置了 use_splicing 则递增 cycles_wo_finds ,否则讲 use_splicing 置为 1
    • 否则设置 cycles_wo_finds 为 0
    • 将 prev_queued 设置为 queued_paths
    • 如果设置了 sync_id 且 queue_cycle 为 1 且有环境变量 AFL_IMPORT_FIRST
      • 调用 sync_fuzzers(use_argv)
  • 调用 fuzz_one(use_argv) 并将返回值存储到 skipped_fuzz
    • fuzz_one 并不一定会执行当前的 queue_cur ,它具有一定策略,如果不执行则返回 1 ,否则返回 0
  • 如果 stop_soon 为 0 且设置了 sync_id 且 skipped_fuzz 为 0
    • 如果 sync_interval_cnt++ % SYNC_INTERVAL 为 0 ,则执行 sync_fuzzers(use_argv)
  • 如果 stop_soon 为 0 且设置了 exit_1 ,则设置 stop_soon 为 2
  • 如果设置了 stop_soon 则 break
  • 设置 queue_cur 为 queue_cur→next ,也就是使用下一个 queue 进行下一轮 fuzz
  • 递增 current_entry

fuzz_one

1
2
3
4
5
6
7
/* Take the current entry from the queue, fuzz it for a while. This
function is a tad too long... returns 0 if fuzzed successfully, 1 if
skipped or bailed out. */

static u8 fuzz_one(char** argv) {
[...]
}

里面一大段,返回 0 则代表成功,返回 1 则代表 bailed out

queue判断阶段

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
#ifdef IGNORE_FINDS

/* In IGNORE_FINDS mode, skip any entries that weren't in the
initial data set. */

if (queue_cur->depth > 1) return 1;

#else

if (pending_favored) {

/* If we have any favored, non-fuzzed new arrivals in the queue,
possibly skip to them at the expense of already-fuzzed or non-favored
cases. */

if ((queue_cur->was_fuzzed || !queue_cur->favored) &&
UR(100) < SKIP_TO_NEW_PROB) return 1;

} else if (!dumb_mode && !queue_cur->favored && queued_paths > 10) {

/* Otherwise, still possibly skip non-favored cases, albeit less often.
The odds of skipping stuff are higher for already-fuzzed inputs and
lower for never-fuzzed entries. */

if (queue_cycle > 1 && !queue_cur->was_fuzzed) {

if (UR(100) < SKIP_NFAV_NEW_PROB) return 1;

} else {

if (UR(100) < SKIP_NFAV_OLD_PROB) return 1;

}

}

#endif /* ^IGNORE_FINDS */
  • 如果定义了 IGNORE_FINDS ,则跳过所有不在初始数据集中的条目
  • 如果没有定义 IGNORE_FINDS
    • 如果设置了 pending_favored ,则有 99% 的概率跳过以下情况
      • queue_cur->was_fuzzed 被设置的(被 fuzz 过的)
      • queue_cur->favored 没有被设置的(不感兴趣的)
    • 否则如果是 dumb 模式且不是 favored 且 queued_paths 大于 10 的话
      • 如果 queue_cycle 大于 1 且没有被设置 queue_cur->was_fuzzed
        • 有 75% 的概率跳过
        • 否则有 95 的概率跳过

接下来进行一些初始化

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
if (not_on_tty) {
ACTF("Fuzzing test case #%u (%u total, %llu uniq crashes found)...",
current_entry, queued_paths, unique_crashes);
fflush(stdout);
}

/* Map the test case into memory. */

fd = open(queue_cur->fname, O_RDONLY);

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

len = queue_cur->len;

orig_in = in_buf = mmap(0, len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);

if (orig_in == MAP_FAILED) PFATAL("Unable to mmap '%s'", queue_cur->fname);

close(fd);

/* We could mmap() out_buf as MAP_PRIVATE, but we end up clobbering every
single byte anyway, so it wouldn't give us any performance or memory usage
benefits. */

out_buf = ck_alloc_nozero(len);

subseq_tmouts = 0;

cur_depth = queue_cur->depth;

主要就是打开了 queue_cur 所对应的用例文件,然后 mmap 出了 orig_in 和 in_buf ,同时 alloc 出了同样大小的 out_buf

CALIBRATION阶段

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
/*******************************************
* CALIBRATION (only if failed earlier on) *
*******************************************/

if (queue_cur->cal_failed) {

u8 res = FAULT_TMOUT;

if (queue_cur->cal_failed < CAL_CHANCES) {

/* Reset exec_cksum to tell calibrate_case to re-execute the testcase
avoiding the usage of an invalid trace_bits.
For more info: https://github.com/AFLplusplus/AFLplusplus/pull/425 */

queue_cur->exec_cksum = 0;

res = calibrate_case(argv, queue_cur, in_buf, queue_cycle - 1, 0);

if (res == FAULT_ERROR)
FATAL("Unable to execute target application");

}

if (stop_soon || res != crash_mode) {
cur_skipped_paths++;
goto abandon_entry;
}

}
  • 如果设置了 queue_cur->cal_failed (有校准错误)
    • 如果 queue_cur->cal_failed 小于三次,则调用 calibrate_case 进行重新校准
    • 之后如果设置了 stop_soon 或 res 不等于 crash_mode ,则递增 cur_skipped_paths 并跳到 abandon_entry

TRIMMING阶段

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

if (!dumb_mode && !queue_cur->trim_done) {

u8 res = trim_case(argv, queue_cur, in_buf);

if (res == FAULT_ERROR)
FATAL("Unable to execute target application");

if (stop_soon) {
cur_skipped_paths++;
goto abandon_entry;
}

/* Don't retry trimming, even if it failed. */

queue_cur->trim_done = 1;

if (len != queue_cur->len) len = queue_cur->len;

}

memcpy(out_buf, in_buf, len);
  • 如果不是 dumb 模式且 queue_cur->trim_done 为 0 (该 case 没有被修剪过)
    • 调用 trim_case 进行修剪
    • 如果设置了 stop_soon 则递增 cur_skipped_paths 并跳到 abandon_entry
    • 设置 queue_cur->trim_done 为 1
    • 如果 len 不等于 queue_cur->len 则更新 len 为 queue_cur->len
  • 最后将 in_buf 的值拷贝到 out_buf

PERFORMANCE SCORE阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*********************
* PERFORMANCE SCORE *
*********************/

orig_perf = perf_score = calculate_score(queue_cur);

/* Skip right away if -d is given, if we have done deterministic fuzzing on
this entry ourselves (was_fuzzed), or if it has gone through deterministic
testing in earlier, resumed runs (passed_det). */

if (skip_deterministic || queue_cur->was_fuzzed || queue_cur->passed_det)
goto havoc_stage;

/* Skip deterministic fuzzing if exec path checksum puts this out of scope
for this master instance. */

if (master_max && (queue_cur->exec_cksum % master_max) != master_id - 1)
goto havoc_stage;

doing_det = 1;
  • 调用 calculate_score 计算分数并赋值给 orig_perf 和 perf_score
  • 如果设置了 skip_deterministic 或 queue_cur->was_fuzzed 或 queue_cur->passed_det ,则跳到 havoc_stage
  • 如果设置了 master_max 且 (queue_cur->exec_cksum % master_max) != master_id - 1 ,则跳到 havoc_stage
  • 最后设置 doing_det 为 1

SIMPLE BITFLIP (+dictionary construction)阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
/*********************************************
* SIMPLE BITFLIP (+dictionary construction) *
*********************************************/

#define FLIP_BIT(_ar, _b) do { \
u8* _arf = (u8*)(_ar); \
u32 _bf = (_b); \
_arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \
} while (0)

/* Single walking bit. */

stage_short = "flip1";
stage_max = len << 3;
stage_name = "bitflip 1/1";

stage_val_type = STAGE_VAL_NONE;

orig_hit_cnt = queued_paths + unique_crashes;

prev_cksum = queue_cur->exec_cksum;

for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {

stage_cur_byte = stage_cur >> 3;

FLIP_BIT(out_buf, stage_cur);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;

FLIP_BIT(out_buf, stage_cur);

/* While flipping the least significant bit in every byte, pull of an extra
trick to detect possible syntax tokens. In essence, the idea is that if
you have a binary blob like this:

xxxxxxxxIHDRxxxxxxxx

...and changing the leading and trailing bytes causes variable or no
changes in program flow, but touching any character in the "IHDR" string
always produces the same, distinctive path, it's highly likely that
"IHDR" is an atomically-checked magic value of special significance to
the fuzzed format.

We do this here, rather than as a separate stage, because it's a nice
way to keep the operation approximately "free" (i.e., no extra execs).

Empirically, performing the check when flipping the least significant bit
is advantageous, compared to doing it at the time of more disruptive
changes, where the program flow may be affected in more violent ways.

The caveat is that we won't generate dictionaries in the -d mode or -S
mode - but that's probably a fair trade-off.

This won't work particularly well with paths that exhibit variable
behavior, but fails gracefully, so we'll carry out the checks anyway.

*/

if (!dumb_mode && (stage_cur & 7) == 7) {

u32 cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);

if (stage_cur == stage_max - 1 && cksum == prev_cksum) {

/* If at end of file and we are still collecting a string, grab the
final character and force output. */

if (a_len < MAX_AUTO_EXTRA) a_collect[a_len] = out_buf[stage_cur >> 3];
a_len++;

if (a_len >= MIN_AUTO_EXTRA && a_len <= MAX_AUTO_EXTRA)
maybe_add_auto(a_collect, a_len);

} else if (cksum != prev_cksum) {

/* Otherwise, if the checksum has changed, see if we have something
worthwhile queued up, and collect that if the answer is yes. */

if (a_len >= MIN_AUTO_EXTRA && a_len <= MAX_AUTO_EXTRA)
maybe_add_auto(a_collect, a_len);

a_len = 0;
prev_cksum = cksum;

}

/* Continue collecting string, but only if the bit flip actually made
any difference - we don't want no-op tokens. */

if (cksum != queue_cur->exec_cksum) {

if (a_len < MAX_AUTO_EXTRA) a_collect[a_len] = out_buf[stage_cur >> 3];
a_len++;

}

}

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_FLIP1] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_FLIP1] += stage_max;

/* Two walking bits. */

stage_name = "bitflip 2/1";
stage_short = "flip2";
stage_max = (len << 3) - 1;

orig_hit_cnt = new_hit_cnt;

for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {

stage_cur_byte = stage_cur >> 3;

FLIP_BIT(out_buf, stage_cur);
FLIP_BIT(out_buf, stage_cur + 1);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;

FLIP_BIT(out_buf, stage_cur);
FLIP_BIT(out_buf, stage_cur + 1);

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_FLIP2] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_FLIP2] += stage_max;

/* Four walking bits. */

stage_name = "bitflip 4/1";
stage_short = "flip4";
stage_max = (len << 3) - 3;

orig_hit_cnt = new_hit_cnt;

for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {

stage_cur_byte = stage_cur >> 3;

FLIP_BIT(out_buf, stage_cur);
FLIP_BIT(out_buf, stage_cur + 1);
FLIP_BIT(out_buf, stage_cur + 2);
FLIP_BIT(out_buf, stage_cur + 3);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;

FLIP_BIT(out_buf, stage_cur);
FLIP_BIT(out_buf, stage_cur + 1);
FLIP_BIT(out_buf, stage_cur + 2);
FLIP_BIT(out_buf, stage_cur + 3);

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_FLIP4] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_FLIP4] += stage_max;

/* Effector map setup. These macros calculate:

EFF_APOS - position of a particular file offset in the map.
EFF_ALEN - length of a map with a particular number of bytes.
EFF_SPAN_ALEN - map span for a sequence of bytes.

*/

#define EFF_APOS(_p) ((_p) >> EFF_MAP_SCALE2)
#define EFF_REM(_x) ((_x) & ((1 << EFF_MAP_SCALE2) - 1))
#define EFF_ALEN(_l) (EFF_APOS(_l) + !!EFF_REM(_l))
#define EFF_SPAN_ALEN(_p, _l) (EFF_APOS((_p) + (_l) - 1) - EFF_APOS(_p) + 1)

/* Initialize effector map for the next step (see comments below). Always
flag first and last byte as doing something. */

eff_map = ck_alloc(EFF_ALEN(len));
eff_map[0] = 1;

if (EFF_APOS(len - 1) != 0) {
eff_map[EFF_APOS(len - 1)] = 1;
eff_cnt++;
}

/* Walking byte. */

stage_name = "bitflip 8/8";
stage_short = "flip8";
stage_max = len;

orig_hit_cnt = new_hit_cnt;

for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {

stage_cur_byte = stage_cur;

out_buf[stage_cur] ^= 0xFF;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;

/* We also use this stage to pull off a simple trick: we identify
bytes that seem to have no effect on the current execution path
even when fully flipped - and we skip them during more expensive
deterministic stages, such as arithmetics or known ints. */

if (!eff_map[EFF_APOS(stage_cur)]) {

u32 cksum;

/* If in dumb mode or if the file is very short, just flag everything
without wasting time on checksums. */

if (!dumb_mode && len >= EFF_MIN_LEN)
cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
else
cksum = ~queue_cur->exec_cksum;

if (cksum != queue_cur->exec_cksum) {
eff_map[EFF_APOS(stage_cur)] = 1;
eff_cnt++;
}

}

out_buf[stage_cur] ^= 0xFF;

}

/* If the effector map is more than EFF_MAX_PERC dense, just flag the
whole thing as worth fuzzing, since we wouldn't be saving much time
anyway. */

if (eff_cnt != EFF_ALEN(len) &&
eff_cnt * 100 / EFF_ALEN(len) > EFF_MAX_PERC) {

memset(eff_map, 1, EFF_ALEN(len));

blocks_eff_select += EFF_ALEN(len);

} else {

blocks_eff_select += eff_cnt;

}

blocks_eff_total += EFF_ALEN(len);

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_FLIP8] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_FLIP8] += stage_max;

/* Two walking bytes. */

if (len < 2) goto skip_bitflip;

stage_name = "bitflip 16/8";
stage_short = "flip16";
stage_cur = 0;
stage_max = len - 1;

orig_hit_cnt = new_hit_cnt;

for (i = 0; i < len - 1; i++) {

/* Let's consult the effector map... */

if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)]) {
stage_max--;
continue;
}

stage_cur_byte = i;

*(u16*)(out_buf + i) ^= 0xFFFF;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

*(u16*)(out_buf + i) ^= 0xFFFF;

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_FLIP16] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_FLIP16] += stage_max;

if (len < 4) goto skip_bitflip;

/* Four walking bytes. */

stage_name = "bitflip 32/8";
stage_short = "flip32";
stage_cur = 0;
stage_max = len - 3;

orig_hit_cnt = new_hit_cnt;

for (i = 0; i < len - 3; i++) {

/* Let's consult the effector map... */
if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)] &&
!eff_map[EFF_APOS(i + 2)] && !eff_map[EFF_APOS(i + 3)]) {
stage_max--;
continue;
}

stage_cur_byte = i;

*(u32*)(out_buf + i) ^= 0xFFFFFFFF;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

*(u32*)(out_buf + i) ^= 0xFFFFFFFF;

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_FLIP32] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_FLIP32] += stage_max;

skip_bitflip:

if (no_arith) goto skip_arith;
  • 首先定义了 FLIP_BIT 宏,也就是反转比特的操作
  • 然后设置 stage_max 为当前比特数,并通过 stage_cur 进行比特遍历
    • 设置 stage_cur_byte 为 stage_cur >> 3
    • 调用 FLIP_BIT(out_buf, stage_cur) 进行比特翻转
    • 调用 common_fuzz_stuff 进行样例测试,如果返回不为 0 则跳到 abandon_entry
    • 再次调用 FLIP_BIT(out_buf, stage_cur) 进行比特翻转(还原)
    • 当不是 dumb 模式且 stage_cur & 7 等于 7 的时候(也就是该字节的所有比特都进行了翻转)
      • 会进行 token 的判断与捕获,比如说对于 SQL 语句的 Select 关键字,其变化任何一个字符的比特都会导致走到相同的错误路径,且与原始执行路径不一样,那么下面就会去捕获这个 token
      • 计算当前的 cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST)
      • 当 stage_cur 走到最后且 cksum 与 prev_cksum 相同时
        • 如果在文件末尾并且我们仍在收集字符串,则获取最后一个字符并强制输出
        • 如果 a_len 小于 MAX_AUTO_EXTRA(32)
          • a_collect[a_len] = out_buf[stage_cur >> 3]
          • 递增 a_len
        • 如果 a_len 大于等于 MIN_AUTO_EXTRA(3)且小于等于 MAX_AUTO_EXTRA(32)
          • 通过 maybe_add_auto(a_collect, a_len) 将新的 token 添加到 a_extras 数组里
      • 否则当 cksum 与 prev_cksum 不同时
        • 如果 a_len 大于等于 MIN_AUTO_EXTRA(3)且小于等于 MAX_AUTO_EXTRA(32)
          • 通过 maybe_add_auto(a_collect, a_len) 将新的 token 添加到 a_extras 数组里
        • 设置 a_len 为 0 且 prev_cksum 为 cksum
      • 继续收集字符串 token ,如果 cksum 不等于 queue_cur->exec_cksum
        • 如果 a_len 小于 MAX_AUTO_EXTRA(32),则设置 a_collect[a_len] 为 out_buf[stage_cur >> 3]
        • 递增 a_len
  • 设置 new_hit_cnt 为 queued_paths 和 unique_crashes 的和
  • 将 stage_finds[STAGE_FLIP1] 加上 new_hit_cnt - orig_hit_cnt ,stage_cycles[STAGE_FLIP1] 加上 stage_max
  • 之后进入相邻比特的翻转流程,设置 stage_max 为当前比特数,并通过 stage_cur 进行比特遍历
    • 同时调用 FLIP_BIT 翻转 stage_cur 和 stage_cur+1 处的比特
    • 调用 common_fuzz_stuff 进行样例测试,如果返回不为 0 则跳到 abandon_entry
    • 再次调用 FLIP_BIT(out_buf, stage_cur) 进行比特翻转(还原)
  • 更新 new_hit_cnt 以及 tage_finds[STAGE_FLIP2] 和 stage_cycles[STAGE_FLIP2]
  • 之后进入相邻 4 比特的翻转流程,和上面的相邻比特翻转一样
  • 设立 Effector Map ,并进行相邻 8 比特,也就是 1 字节的翻转流程
    • 如果对每个字节进行翻转的时候,其执行路径与原始路径不一致,则在 Effector Map 中的对应位置标志为 1 ,也就是有效的;否则标志为 0 ,也就是无效的
    • 这样做是为了收集信息,如果路径不同则代表这里可能是个 metadata(比如 flags),否则则可能是个数据。在之后的变异中会跳过这些数据内容,从而提高效率。
  • 之后进入相邻 2 字节的翻转流程,其会在 Effector Map 中检查这 2 字节的标志位是否为 0 ,如果是则跳过
  • 最后进入相邻 4 字节的翻转流程,其会在 Effector Map 中检查这 4 字节的标志位是否为 0 ,如果是则跳过
  • 末尾有跳转的 label skip_bitflip ,如果设置了 no_arith ,则跳到 skip_arith

ARITHMETIC INC/DEC阶段

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
/**********************
* ARITHMETIC INC/DEC *
**********************/

/* 8-bit arithmetics. */

stage_name = "arith 8/8";
stage_short = "arith8";
stage_cur = 0;
stage_max = 2 * len * ARITH_MAX;

stage_val_type = STAGE_VAL_LE;

orig_hit_cnt = new_hit_cnt;

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

u8 orig = out_buf[i];

/* Let's consult the effector map... */

if (!eff_map[EFF_APOS(i)]) {
stage_max -= 2 * ARITH_MAX;
continue;
}

stage_cur_byte = i;

for (j = 1; j <= ARITH_MAX; j++) {

u8 r = orig ^ (orig + j);

/* Do arithmetic operations only if the result couldn't be a product
of a bitflip. */

if (!could_be_bitflip(r)) {

stage_cur_val = j;
out_buf[i] = orig + j;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

r = orig ^ (orig - j);

if (!could_be_bitflip(r)) {

stage_cur_val = -j;
out_buf[i] = orig - j;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

out_buf[i] = orig;

}

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_ARITH8] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_ARITH8] += stage_max;

/* 16-bit arithmetics, both endians. */

if (len < 2) goto skip_arith;

stage_name = "arith 16/8";
stage_short = "arith16";
stage_cur = 0;
stage_max = 4 * (len - 1) * ARITH_MAX;

orig_hit_cnt = new_hit_cnt;

for (i = 0; i < len - 1; i++) {

u16 orig = *(u16*)(out_buf + i);

/* Let's consult the effector map... */

if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)]) {
stage_max -= 4 * ARITH_MAX;
continue;
}

stage_cur_byte = i;

for (j = 1; j <= ARITH_MAX; j++) {

u16 r1 = orig ^ (orig + j),
r2 = orig ^ (orig - j),
r3 = orig ^ SWAP16(SWAP16(orig) + j),
r4 = orig ^ SWAP16(SWAP16(orig) - j);

/* Try little endian addition and subtraction first. Do it only
if the operation would affect more than one byte (hence the
& 0xff overflow checks) and if it couldn't be a product of
a bitflip. */

stage_val_type = STAGE_VAL_LE;

if ((orig & 0xff) + j > 0xff && !could_be_bitflip(r1)) {

stage_cur_val = j;
*(u16*)(out_buf + i) = orig + j;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

if ((orig & 0xff) < j && !could_be_bitflip(r2)) {

stage_cur_val = -j;
*(u16*)(out_buf + i) = orig - j;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

/* Big endian comes next. Same deal. */

stage_val_type = STAGE_VAL_BE;

if ((orig >> 8) + j > 0xff && !could_be_bitflip(r3)) {

stage_cur_val = j;
*(u16*)(out_buf + i) = SWAP16(SWAP16(orig) + j);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

if ((orig >> 8) < j && !could_be_bitflip(r4)) {

stage_cur_val = -j;
*(u16*)(out_buf + i) = SWAP16(SWAP16(orig) - j);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

*(u16*)(out_buf + i) = orig;

}

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_ARITH16] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_ARITH16] += stage_max;

/* 32-bit arithmetics, both endians. */

if (len < 4) goto skip_arith;

stage_name = "arith 32/8";
stage_short = "arith32";
stage_cur = 0;
stage_max = 4 * (len - 3) * ARITH_MAX;

orig_hit_cnt = new_hit_cnt;

for (i = 0; i < len - 3; i++) {

u32 orig = *(u32*)(out_buf + i);

/* Let's consult the effector map... */

if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)] &&
!eff_map[EFF_APOS(i + 2)] && !eff_map[EFF_APOS(i + 3)]) {
stage_max -= 4 * ARITH_MAX;
continue;
}

stage_cur_byte = i;

for (j = 1; j <= ARITH_MAX; j++) {

u32 r1 = orig ^ (orig + j),
r2 = orig ^ (orig - j),
r3 = orig ^ SWAP32(SWAP32(orig) + j),
r4 = orig ^ SWAP32(SWAP32(orig) - j);

/* Little endian first. Same deal as with 16-bit: we only want to
try if the operation would have effect on more than two bytes. */

stage_val_type = STAGE_VAL_LE;

if ((orig & 0xffff) + j > 0xffff && !could_be_bitflip(r1)) {

stage_cur_val = j;
*(u32*)(out_buf + i) = orig + j;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

if ((orig & 0xffff) < j && !could_be_bitflip(r2)) {

stage_cur_val = -j;
*(u32*)(out_buf + i) = orig - j;

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

/* Big endian next. */

stage_val_type = STAGE_VAL_BE;

if ((SWAP32(orig) & 0xffff) + j > 0xffff && !could_be_bitflip(r3)) {

stage_cur_val = j;
*(u32*)(out_buf + i) = SWAP32(SWAP32(orig) + j);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

if ((SWAP32(orig) & 0xffff) < j && !could_be_bitflip(r4)) {

stage_cur_val = -j;
*(u32*)(out_buf + i) = SWAP32(SWAP32(orig) - j);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

*(u32*)(out_buf + i) = orig;

}

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_ARITH32] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_ARITH32] += stage_max;

skip_arith:
  • 首先对一个字节进行加减法操作变异,也会通过 Effector Map 跳过无效的字节,并且会检查该操作是否是字节翻转,如果是的话则跳过
    • 变化的例子如下
      • r = orig ^ (orig + j)
      • r = orig ^ (orig - j)
  • 然后对两个字节进行加减法操作变异(每个字节)。如果数据长度不满足变异的长度则跳过该阶段
  • 最后对四个字节进行加减法操作变异(每两个字节为一组,对每组进行操作)。如果数据长度不满足变异的长度则跳过该阶段
  • 加减法变异的上限为 ARITH_MAX(35),也就是 [-35,+35] ,同时也会对大端序小端序进行变异

INTERESTING VALUES阶段

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

stage_name = "interest 8/8";
stage_short = "int8";
stage_cur = 0;
stage_max = len * sizeof(interesting_8);

stage_val_type = STAGE_VAL_LE;

orig_hit_cnt = new_hit_cnt;

/* Setting 8-bit integers. */

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

u8 orig = out_buf[i];

/* Let's consult the effector map... */

if (!eff_map[EFF_APOS(i)]) {
stage_max -= sizeof(interesting_8);
continue;
}

stage_cur_byte = i;

for (j = 0; j < sizeof(interesting_8); j++) {

/* Skip if the value could be a product of bitflips or arithmetics. */

if (could_be_bitflip(orig ^ (u8)interesting_8[j]) ||
could_be_arith(orig, (u8)interesting_8[j], 1)) {
stage_max--;
continue;
}

stage_cur_val = interesting_8[j];
out_buf[i] = interesting_8[j];

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;

out_buf[i] = orig;
stage_cur++;

}

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_INTEREST8] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_INTEREST8] += stage_max;

/* Setting 16-bit integers, both endians. */

if (no_arith || len < 2) goto skip_interest;

stage_name = "interest 16/8";
stage_short = "int16";
stage_cur = 0;
stage_max = 2 * (len - 1) * (sizeof(interesting_16) >> 1);

orig_hit_cnt = new_hit_cnt;

for (i = 0; i < len - 1; i++) {

u16 orig = *(u16*)(out_buf + i);

/* Let's consult the effector map... */

if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)]) {
stage_max -= sizeof(interesting_16);
continue;
}

stage_cur_byte = i;

for (j = 0; j < sizeof(interesting_16) / 2; j++) {

stage_cur_val = interesting_16[j];

/* Skip if this could be a product of a bitflip, arithmetics,
or single-byte interesting value insertion. */

if (!could_be_bitflip(orig ^ (u16)interesting_16[j]) &&
!could_be_arith(orig, (u16)interesting_16[j], 2) &&
!could_be_interest(orig, (u16)interesting_16[j], 2, 0)) {

stage_val_type = STAGE_VAL_LE;

*(u16*)(out_buf + i) = interesting_16[j];

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

if ((u16)interesting_16[j] != SWAP16(interesting_16[j]) &&
!could_be_bitflip(orig ^ SWAP16(interesting_16[j])) &&
!could_be_arith(orig, SWAP16(interesting_16[j]), 2) &&
!could_be_interest(orig, SWAP16(interesting_16[j]), 2, 1)) {

stage_val_type = STAGE_VAL_BE;

*(u16*)(out_buf + i) = SWAP16(interesting_16[j]);
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

}

*(u16*)(out_buf + i) = orig;

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_INTEREST16] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_INTEREST16] += stage_max;

if (len < 4) goto skip_interest;

/* Setting 32-bit integers, both endians. */

stage_name = "interest 32/8";
stage_short = "int32";
stage_cur = 0;
stage_max = 2 * (len - 3) * (sizeof(interesting_32) >> 2);

orig_hit_cnt = new_hit_cnt;

for (i = 0; i < len - 3; i++) {

u32 orig = *(u32*)(out_buf + i);

/* Let's consult the effector map... */

if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)] &&
!eff_map[EFF_APOS(i + 2)] && !eff_map[EFF_APOS(i + 3)]) {
stage_max -= sizeof(interesting_32) >> 1;
continue;
}

stage_cur_byte = i;

for (j = 0; j < sizeof(interesting_32) / 4; j++) {

stage_cur_val = interesting_32[j];

/* Skip if this could be a product of a bitflip, arithmetics,
or word interesting value insertion. */

if (!could_be_bitflip(orig ^ (u32)interesting_32[j]) &&
!could_be_arith(orig, interesting_32[j], 4) &&
!could_be_interest(orig, interesting_32[j], 4, 0)) {

stage_val_type = STAGE_VAL_LE;

*(u32*)(out_buf + i) = interesting_32[j];

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

if ((u32)interesting_32[j] != SWAP32(interesting_32[j]) &&
!could_be_bitflip(orig ^ SWAP32(interesting_32[j])) &&
!could_be_arith(orig, SWAP32(interesting_32[j]), 4) &&
!could_be_interest(orig, SWAP32(interesting_32[j]), 4, 1)) {

stage_val_type = STAGE_VAL_BE;

*(u32*)(out_buf + i) = SWAP32(interesting_32[j]);
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;

} else stage_max--;

}

*(u32*)(out_buf + i) = orig;

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_INTEREST32] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_INTEREST32] += stage_max;

skip_interest:
  • 该阶段会跳过可能变异出前两个阶段的内容,也会跳过该阶段上一次的内容(例如二字节变异会跳过一字节变异可能出现的情况)。同时也会根据 Effector Map 跳过一些阶段

  • 主要就是替换一二四字节的内容为下面的一些特殊边界值

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

DICTIONARY STUFF阶段

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

if (!extras_cnt) goto skip_user_extras;

/* Overwrite with user-supplied extras. */

stage_name = "user extras (over)";
stage_short = "ext_UO";
stage_cur = 0;
stage_max = extras_cnt * len;

stage_val_type = STAGE_VAL_NONE;

orig_hit_cnt = new_hit_cnt;

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

u32 last_len = 0;

stage_cur_byte = i;

/* Extras are sorted by size, from smallest to largest. This means
that we don't have to worry about restoring the buffer in
between writes at a particular offset determined by the outer
loop. */

for (j = 0; j < extras_cnt; j++) {

/* Skip extras probabilistically if extras_cnt > MAX_DET_EXTRAS. Also
skip them if there's no room to insert the payload, if the token
is redundant, or if its entire span has no bytes set in the effector
map. */

if ((extras_cnt > MAX_DET_EXTRAS && UR(extras_cnt) >= MAX_DET_EXTRAS) ||
extras[j].len > len - i ||
!memcmp(extras[j].data, out_buf + i, extras[j].len) ||
!memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, extras[j].len))) {

stage_max--;
continue;

}

last_len = extras[j].len;
memcpy(out_buf + i, extras[j].data, last_len);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;

stage_cur++;

}

/* Restore all the clobbered memory. */
memcpy(out_buf + i, in_buf + i, last_len);

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_EXTRAS_UO] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_EXTRAS_UO] += stage_max;

/* Insertion of user-supplied extras. */

stage_name = "user extras (insert)";
stage_short = "ext_UI";
stage_cur = 0;
stage_max = extras_cnt * (len + 1);

orig_hit_cnt = new_hit_cnt;

ex_tmp = ck_alloc(len + MAX_DICT_FILE);

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

stage_cur_byte = i;

for (j = 0; j < extras_cnt; j++) {

if (len + extras[j].len > MAX_FILE) {
stage_max--;
continue;
}

/* Insert token */
memcpy(ex_tmp + i, extras[j].data, extras[j].len);

/* Copy tail */
memcpy(ex_tmp + i + extras[j].len, out_buf + i, len - i);

if (common_fuzz_stuff(argv, ex_tmp, len + extras[j].len)) {
ck_free(ex_tmp);
goto abandon_entry;
}

stage_cur++;

}

/* Copy head */
ex_tmp[i] = out_buf[i];

}

ck_free(ex_tmp);

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_EXTRAS_UI] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_EXTRAS_UI] += stage_max;

skip_user_extras:

if (!a_extras_cnt) goto skip_extras;

stage_name = "auto extras (over)";
stage_short = "ext_AO";
stage_cur = 0;
stage_max = MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len;

stage_val_type = STAGE_VAL_NONE;

orig_hit_cnt = new_hit_cnt;

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

u32 last_len = 0;

stage_cur_byte = i;

for (j = 0; j < MIN(a_extras_cnt, USE_AUTO_EXTRAS); j++) {

/* See the comment in the earlier code; extras are sorted by size. */

if (a_extras[j].len > len - i ||
!memcmp(a_extras[j].data, out_buf + i, a_extras[j].len) ||
!memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, a_extras[j].len))) {

stage_max--;
continue;

}

last_len = a_extras[j].len;
memcpy(out_buf + i, a_extras[j].data, last_len);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;

stage_cur++;

}

/* Restore all the clobbered memory. */
memcpy(out_buf + i, in_buf + i, last_len);

}

new_hit_cnt = queued_paths + unique_crashes;

stage_finds[STAGE_EXTRAS_AO] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_EXTRAS_AO] += stage_max;

skip_extras:

/* If we made this to here without jumping to havoc_stage or abandon_entry,
we're properly done with deterministic steps and can mark it as such
in the .state/ directory. */

if (!queue_cur->passed_det) mark_as_det_done(queue_cur);
  • 该阶段会将 a_extras 里面的特殊 token 进行替换,主要有以下三类:
    • 替换
      • user extras :stage_max 为 extras_cnt * len
      • auto extras :stage_max 为 MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len
    • 插入
      • user extras:stage_max 为 extras_cnt * len
  • 其中的 user extras 为用户给的,加入 -x 指令便可加入

该阶段也是 deterministic fuzzing 的最后一步

RANDOM HAVOC阶段

对于 dumb 模式来说这是起始阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
/****************
* RANDOM HAVOC *
****************/

havoc_stage:

stage_cur_byte = -1;

/* The havoc stage mutation code is also invoked when splicing files; if the
splice_cycle variable is set, generate different descriptions and such. */

if (!splice_cycle) {

stage_name = "havoc";
stage_short = "havoc";
stage_max = (doing_det ? HAVOC_CYCLES_INIT : HAVOC_CYCLES) *
perf_score / havoc_div / 100;

} else {

static u8 tmp[32];

perf_score = orig_perf;

sprintf(tmp, "splice %u", splice_cycle);
stage_name = tmp;
stage_short = "splice";
stage_max = SPLICE_HAVOC * perf_score / havoc_div / 100;

}

if (stage_max < HAVOC_MIN) stage_max = HAVOC_MIN;

temp_len = len;

orig_hit_cnt = queued_paths + unique_crashes;

havoc_queued = queued_paths;

/* We essentially just do several thousand runs (depending on perf_score)
where we take the input file and make random stacked tweaks. */

for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {

u32 use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2));

stage_cur_val = use_stacking;

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

switch (UR(15 + ((extras_cnt + a_extras_cnt) ? 2 : 0))) {

case 0:

/* Flip a single bit somewhere. Spooky! */

FLIP_BIT(out_buf, UR(temp_len << 3));
break;

case 1:

/* Set byte to interesting value. */

out_buf[UR(temp_len)] = interesting_8[UR(sizeof(interesting_8))];
break;

case 2:

/* Set word to interesting value, randomly choosing endian. */

if (temp_len < 2) break;

if (UR(2)) {

*(u16*)(out_buf + UR(temp_len - 1)) =
interesting_16[UR(sizeof(interesting_16) >> 1)];

} else {

*(u16*)(out_buf + UR(temp_len - 1)) = SWAP16(
interesting_16[UR(sizeof(interesting_16) >> 1)]);

}

break;

case 3:

/* Set dword to interesting value, randomly choosing endian. */

if (temp_len < 4) break;

if (UR(2)) {

*(u32*)(out_buf + UR(temp_len - 3)) =
interesting_32[UR(sizeof(interesting_32) >> 2)];

} else {

*(u32*)(out_buf + UR(temp_len - 3)) = SWAP32(
interesting_32[UR(sizeof(interesting_32) >> 2)]);

}

break;

case 4:

/* Randomly subtract from byte. */

out_buf[UR(temp_len)] -= 1 + UR(ARITH_MAX);
break;

case 5:

/* Randomly add to byte. */

out_buf[UR(temp_len)] += 1 + UR(ARITH_MAX);
break;

case 6:

/* Randomly subtract from word, random endian. */

if (temp_len < 2) break;

if (UR(2)) {

u32 pos = UR(temp_len - 1);

*(u16*)(out_buf + pos) -= 1 + UR(ARITH_MAX);

} else {

u32 pos = UR(temp_len - 1);
u16 num = 1 + UR(ARITH_MAX);

*(u16*)(out_buf + pos) =
SWAP16(SWAP16(*(u16*)(out_buf + pos)) - num);

}

break;

case 7:

/* Randomly add to word, random endian. */

if (temp_len < 2) break;

if (UR(2)) {

u32 pos = UR(temp_len - 1);

*(u16*)(out_buf + pos) += 1 + UR(ARITH_MAX);

} else {

u32 pos = UR(temp_len - 1);
u16 num = 1 + UR(ARITH_MAX);

*(u16*)(out_buf + pos) =
SWAP16(SWAP16(*(u16*)(out_buf + pos)) + num);

}

break;

case 8:

/* Randomly subtract from dword, random endian. */

if (temp_len < 4) break;

if (UR(2)) {

u32 pos = UR(temp_len - 3);

*(u32*)(out_buf + pos) -= 1 + UR(ARITH_MAX);

} else {

u32 pos = UR(temp_len - 3);
u32 num = 1 + UR(ARITH_MAX);

*(u32*)(out_buf + pos) =
SWAP32(SWAP32(*(u32*)(out_buf + pos)) - num);

}

break;

case 9:

/* Randomly add to dword, random endian. */

if (temp_len < 4) break;

if (UR(2)) {

u32 pos = UR(temp_len - 3);

*(u32*)(out_buf + pos) += 1 + UR(ARITH_MAX);

} else {

u32 pos = UR(temp_len - 3);
u32 num = 1 + UR(ARITH_MAX);

*(u32*)(out_buf + pos) =
SWAP32(SWAP32(*(u32*)(out_buf + pos)) + num);

}

break;

case 10:

/* Just set a random byte to a random value. Because,
why not. We use XOR with 1-255 to eliminate the
possibility of a no-op. */

out_buf[UR(temp_len)] ^= 1 + UR(255);
break;

case 11 ... 12: {

/* Delete bytes. We're making this a bit more likely
than insertion (the next option) in hopes of keeping
files reasonably small. */

u32 del_from, del_len;

if (temp_len < 2) break;

/* Don't delete too much. */

del_len = choose_block_len(temp_len - 1);

del_from = UR(temp_len - del_len + 1);

memmove(out_buf + del_from, out_buf + del_from + del_len,
temp_len - del_from - del_len);

temp_len -= del_len;

break;

}

case 13:

if (temp_len + HAVOC_BLK_XL < MAX_FILE) {

/* Clone bytes (75%) or insert a block of constant bytes (25%). */

u8 actually_clone = UR(4);
u32 clone_from, clone_to, clone_len;
u8* new_buf;

if (actually_clone) {

clone_len = choose_block_len(temp_len);
clone_from = UR(temp_len - clone_len + 1);

} else {

clone_len = choose_block_len(HAVOC_BLK_XL);
clone_from = 0;

}

clone_to = UR(temp_len);

new_buf = ck_alloc_nozero(temp_len + clone_len);

/* Head */

memcpy(new_buf, out_buf, clone_to);

/* Inserted part */

if (actually_clone)
memcpy(new_buf + clone_to, out_buf + clone_from, clone_len);
else
memset(new_buf + clone_to,
UR(2) ? UR(256) : out_buf[UR(temp_len)], clone_len);

/* Tail */
memcpy(new_buf + clone_to + clone_len, out_buf + clone_to,
temp_len - clone_to);

ck_free(out_buf);
out_buf = new_buf;
temp_len += clone_len;

}

break;

case 14: {

/* Overwrite bytes with a randomly selected chunk (75%) or fixed
bytes (25%). */

u32 copy_from, copy_to, copy_len;

if (temp_len < 2) break;

copy_len = choose_block_len(temp_len - 1);

copy_from = UR(temp_len - copy_len + 1);
copy_to = UR(temp_len - copy_len + 1);

if (UR(4)) {

if (copy_from != copy_to)
memmove(out_buf + copy_to, out_buf + copy_from, copy_len);

} else memset(out_buf + copy_to,
UR(2) ? UR(256) : out_buf[UR(temp_len)], copy_len);

break;

}

/* Values 15 and 16 can be selected only if there are any extras
present in the dictionaries. */

case 15: {

/* Overwrite bytes with an extra. */

if (!extras_cnt || (a_extras_cnt && UR(2))) {

/* No user-specified extras or odds in our favor. Let's use an
auto-detected one. */

u32 use_extra = UR(a_extras_cnt);
u32 extra_len = a_extras[use_extra].len;
u32 insert_at;

if (extra_len > temp_len) break;

insert_at = UR(temp_len - extra_len + 1);
memcpy(out_buf + insert_at, a_extras[use_extra].data, extra_len);

} else {

/* No auto extras or odds in our favor. Use the dictionary. */

u32 use_extra = UR(extras_cnt);
u32 extra_len = extras[use_extra].len;
u32 insert_at;

if (extra_len > temp_len) break;

insert_at = UR(temp_len - extra_len + 1);
memcpy(out_buf + insert_at, extras[use_extra].data, extra_len);

}

break;

}

case 16: {

u32 use_extra, extra_len, insert_at = UR(temp_len + 1);
u8* new_buf;

/* Insert an extra. Do the same dice-rolling stuff as for the
previous case. */

if (!extras_cnt || (a_extras_cnt && UR(2))) {

use_extra = UR(a_extras_cnt);
extra_len = a_extras[use_extra].len;

if (temp_len + extra_len >= MAX_FILE) break;

new_buf = ck_alloc_nozero(temp_len + extra_len);

/* Head */
memcpy(new_buf, out_buf, insert_at);

/* Inserted part */
memcpy(new_buf + insert_at, a_extras[use_extra].data, extra_len);

} else {

use_extra = UR(extras_cnt);
extra_len = extras[use_extra].len;

if (temp_len + extra_len >= MAX_FILE) break;

new_buf = ck_alloc_nozero(temp_len + extra_len);

/* Head */
memcpy(new_buf, out_buf, insert_at);

/* Inserted part */
memcpy(new_buf + insert_at, extras[use_extra].data, extra_len);

}

/* Tail */
memcpy(new_buf + insert_at + extra_len, out_buf + insert_at,
temp_len - insert_at);

ck_free(out_buf);
out_buf = new_buf;
temp_len += extra_len;

break;

}

}

}

if (common_fuzz_stuff(argv, out_buf, temp_len))
goto abandon_entry;

/* out_buf might have been mangled a bit, so let's restore it to its
original size and shape. */

if (temp_len < len) out_buf = ck_realloc(out_buf, len);
temp_len = len;
memcpy(out_buf, in_buf, len);

/* If we're finding new stuff, let's run for a bit longer, limits
permitting. */

if (queued_paths != havoc_queued) {

if (perf_score <= HAVOC_MAX_MULT * 100) {
stage_max *= 2;
perf_score *= 2;
}

havoc_queued = queued_paths;

}

}

new_hit_cnt = queued_paths + unique_crashes;

if (!splice_cycle) {
stage_finds[STAGE_HAVOC] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_HAVOC] += stage_max;
} else {
stage_finds[STAGE_SPLICE] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_SPLICE] += stage_max;
}

havoc 便是各种随机的变异,每一轮都会将多种变异方式组合(stacked)而成,选择哪些变异方案也是随机的,具体而言如下:

  • case 0 :随机选取某个比特翻转
  • case 1 :随机选取 byte 改成 interesting value
  • case 2 :随机选取 word 改成 interesting value ,并随机选择大小端序
  • case 3 :随机选取 dword 改成 interesting value ,并随机选择大小端序
  • case 4 :随机选取 byte 减去一个随机数
  • case 5 :随机选取 byte 加上一个随机数
  • case 6 :随机选取 word 减去一个随机数 ,并随机选择大小端序
  • case 7 :随机选取 word 加上一个随机数 ,并随机选择大小端序
  • case 8 :随机选取 dword 减去一个随机数 ,并随机选择大小端序
  • case 9 :随机选取 dword 加上一个随机数 ,并随机选择大小端序
  • case 10 :随机选取 byte 将其设置为随机数
  • case 11/12 :随机删除一段 bytes
  • case 13 :随机一个位置,插入一段随机长度的内容
    • 75% 概率为原文中随机位置的内容
    • 25% 概率为随机选取的数
  • case 14 :随机一个位置,替换一段随机长度的内容
    • 75% 概率为原文中随机位置的内容
    • 25% 概率为随机选取的数
  • case 15 :随机一个位置,替换成 extras 内容(用户指定或自动生成的)
  • case 16 :随机一个位置,插入 extras 内容(用户指定或自动生成的)

SPLICING阶段

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
#ifndef IGNORE_FINDS

/************
* SPLICING *
************/

/* This is a last-resort strategy triggered by a full round with no findings.
It takes the current input file, randomly selects another input, and
splices them together at some offset, then relies on the havoc
code to mutate that blob. */

retry_splicing:

if (use_splicing && splice_cycle++ < SPLICE_CYCLES &&
queued_paths > 1 && queue_cur->len > 1) {

struct queue_entry* target;
u32 tid, split_at;
u8* new_buf;
s32 f_diff, l_diff;

/* First of all, if we've modified in_buf for havoc, let's clean that
up... */

if (in_buf != orig_in) {
ck_free(in_buf);
in_buf = orig_in;
len = queue_cur->len;
}

/* Pick a random queue entry and seek to it. Don't splice with yourself. */

do { tid = UR(queued_paths); } while (tid == current_entry);

splicing_with = tid;
target = queue;

while (tid >= 100) { target = target->next_100; tid -= 100; }
while (tid--) target = target->next;

/* Make sure that the target has a reasonable length. */

while (target && (target->len < 2 || target == queue_cur)) {
target = target->next;
splicing_with++;
}

if (!target) goto retry_splicing;

/* Read the testcase into a new buffer. */

fd = open(target->fname, O_RDONLY);

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

new_buf = ck_alloc_nozero(target->len);

ck_read(fd, new_buf, target->len, target->fname);

close(fd);

/* Find a suitable splicing location, somewhere between the first and
the last differing byte. Bail out if the difference is just a single
byte or so. */

locate_diffs(in_buf, new_buf, MIN(len, target->len), &f_diff, &l_diff);

if (f_diff < 0 || l_diff < 2 || f_diff == l_diff) {
ck_free(new_buf);
goto retry_splicing;
}

/* Split somewhere between the first and last differing byte. */

split_at = f_diff + UR(l_diff - f_diff);

/* Do the thing. */

len = target->len;
memcpy(new_buf, in_buf, split_at);
in_buf = new_buf;

ck_free(out_buf);
out_buf = ck_alloc_nozero(len);
memcpy(out_buf, in_buf, len);

goto havoc_stage;

}

#endif /* !IGNORE_FINDS */

ret_val = 0;
  • 如果没有定义 IGNORE_FINDS ,则进入该阶段
  • 具体而言就是获取当前输入文件,然后随机选择另一个输入,并在某个偏移处将它们拼接在一起来进行测试

abandon_entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abandon_entry:

splicing_with = -1;

/* Update pending_not_fuzzed count if we made it through the calibration
cycle and have not seen this entry before. */

if (!stop_soon && !queue_cur->cal_failed && !queue_cur->was_fuzzed) {
queue_cur->was_fuzzed = 1;
pending_not_fuzzed--;
if (queue_cur->favored) pending_favored--;
}

munmap(orig_in, queue_cur->len);

if (in_buf != orig_in) ck_free(in_buf);
ck_free(out_buf);
ck_free(eff_map);

return ret_val;

#undef FLIP_BIT

最后的收尾阶段

common_fuzz_stuff

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
/* Write a modified test case, run program, process results. Handle
error conditions, returning 1 if it's time to bail out. This is
a helper function for fuzz_one(). */

EXP_ST u8 common_fuzz_stuff(char** argv, u8* out_buf, u32 len) {

u8 fault;

if (post_handler) {

out_buf = post_handler(out_buf, &len);
if (!out_buf || !len) return 0;

}

write_to_testcase(out_buf, len);

fault = run_target(argv, exec_tmout);

if (stop_soon) return 1;

if (fault == FAULT_TMOUT) {

if (subseq_tmouts++ > TMOUT_LIMIT) {
cur_skipped_paths++;
return 1;
}

} else subseq_tmouts = 0;

/* Users can hit us with SIGUSR1 to request the current input
to be abandoned. */

if (skip_requested) {

skip_requested = 0;
cur_skipped_paths++;
return 1;

}

/* This handles FAULT_ERROR for us: */

queued_discovered += save_if_interesting(argv, out_buf, len, fault);

if (!(stage_cur % stats_update_freq) || stage_cur + 1 == stage_max)
show_stats();

return 0;

}

编写修改后的测试用例,运行程序,处理结果。 处理错误情况,如果需要退出,则返回 1。 这是 fuzz_one() 的辅助函数

  • post_handler 可以做一层 wrapper 再进行写入

trim_case

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
/* Trim all new test cases to save cycles when doing deterministic checks. The
trimmer uses power-of-two increments somewhere between 1/16 and 1/1024 of
file size, to keep the stage short and sweet. */

static u8 trim_case(char** argv, struct queue_entry* q, u8* in_buf) {

static u8 tmp[64];
static u8 clean_trace[MAP_SIZE];

u8 needs_write = 0, fault = 0;
u32 trim_exec = 0;
u32 remove_len;
u32 len_p2;

/* Although the trimmer will be less useful when variable behavior is
detected, it will still work to some extent, so we don't check for
this. */

if (q->len < 5) return 0;

stage_name = tmp;
bytes_trim_in += q->len;

/* Select initial chunk len, starting with large steps. */

len_p2 = next_p2(q->len);

remove_len = MAX(len_p2 / TRIM_START_STEPS, TRIM_MIN_BYTES);

/* Continue until the number of steps gets too high or the stepover
gets too small. */

while (remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES)) {

u32 remove_pos = remove_len;

sprintf(tmp, "trim %s/%s", DI(remove_len), DI(remove_len));

stage_cur = 0;
stage_max = q->len / remove_len;

while (remove_pos < q->len) {

u32 trim_avail = MIN(remove_len, q->len - remove_pos);
u32 cksum;

write_with_gap(in_buf, q->len, remove_pos, trim_avail);

fault = run_target(argv, exec_tmout);
trim_execs++;

if (stop_soon || fault == FAULT_ERROR) goto abort_trimming;

/* Note that we don't keep track of crashes or hangs here; maybe TODO? */

cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);

/* If the deletion had no impact on the trace, make it permanent. This
isn't perfect for variable-path inputs, but we're just making a
best-effort pass, so it's not a big deal if we end up with false
negatives every now and then. */

if (cksum == q->exec_cksum) {

u32 move_tail = q->len - remove_pos - trim_avail;

q->len -= trim_avail;
len_p2 = next_p2(q->len);

memmove(in_buf + remove_pos, in_buf + remove_pos + trim_avail,
move_tail);

/* Let's save a clean trace, which will be needed by
update_bitmap_score once we're done with the trimming stuff. */

if (!needs_write) {

needs_write = 1;
memcpy(clean_trace, trace_bits, MAP_SIZE);

}

} else remove_pos += remove_len;

/* Since this can be slow, update the screen every now and then. */

if (!(trim_exec++ % stats_update_freq)) show_stats();
stage_cur++;

}

remove_len >>= 1;

}

/* If we have made changes to in_buf, we also need to update the on-disk
version of the test case. */

if (needs_write) {

s32 fd;

unlink(q->fname); /* ignore errors */

fd = open(q->fname, O_WRONLY | O_CREAT | O_EXCL, 0600);

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

ck_write(fd, in_buf, q->len, q->fname);
close(fd);

memcpy(trace_bits, clean_trace, MAP_SIZE);
update_bitmap_score(q);

}

abort_trimming:

bytes_trim_out += q->len;
return fault;

}
  • 当传入的 q->len 小于 5 的时候,直接返回 0

  • 设置 stage_name 为 tmp ,bytes_trim_in 加上 q->len

  • 计算 len_p2 为 next_p2(q->len),也就是比如 3 会得到 4

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /* Find first power of two greater or equal to val (assuming val under
    2^31). */

    static u32 next_p2(u32 val) {

    u32 ret = 1;
    while (val > ret) ret <<= 1;
    return ret;

    }
  • 计算 remove_len 为 MAX(len_p2 / TRIM_START_STEPS, TRIM_MIN_BYTES) ,也就是需要移除的长度

  • 当 remove_len 大于等于 MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES) 的时候,进行循环,每次 remove_len 会除 2

    • 设置 remove_pos 为 remove_len ,stage_cur 为 0 ,stage_max 为 q->len / remove_len
    • 当 remove_pos 小于 q->len 的时候,进行循环,也就是每次前进 remove_len 长度直到文件被遍历完
      • 设置 trim_avail 为 MIN(remove_len, q->len - remove_pos)
      • 调用 write_with_gap(in_buf, q->len, remove_pos, trim_avail)
        • 从 in_buf 的 remove_pos 开始向后跳过 remove_len 长度,写入到 .cur_input 中
      • 调用 run_target(argv, exec_tmout) 并将返回值存储在 fault 中
      • 递增 trim_execs
      • 如果设置了 stop_soon 或 fault 为 FAULT_ERROR ,跳到 abort_trimming
      • 计算 cksum 等于 hash32(trace_bits, MAP_SIZE, HASH_CONST)
      • 将 cksum 和 q->exec_cksum 进行比较,如果相等
        • 设置 move_tail 为 q->len - remove_pos - trim_avail
        • 将 q→len 减去 trim_avail 并重新计算 len_p2
        • 相等的话则从 in_buf + remove_pos + trim_avail 处移动 move_tail 字节到 in_buf + remove_pos
        • 如果没有设置 needs_write ,则设置 needs_write 为 1 并将 trace_bits 拷贝到 clean_trace 中
      • 否则不相等,将 remove_pos 加上 remove_len
    • 定期 show_stats 并递增 stage_cur
  • 如果设置了 needs_write ,删除原来的 q->fname 并创建一个新的,将 in_buf 写进去并将 clean_trace 内容拷贝到 trace_bits ,最后调用 update_bitmap_score(q)

  • 返回 fault

calculate_score

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
/* Calculate case desirability score to adjust the length of havoc fuzzing.
A helper function for fuzz_one(). Maybe some of these constants should
go into config.h. */

static u32 calculate_score(struct queue_entry* q) {

u32 avg_exec_us = total_cal_us / total_cal_cycles;
u32 avg_bitmap_size = total_bitmap_size / total_bitmap_entries;
u32 perf_score = 100;

/* Adjust score based on execution speed of this path, compared to the
global average. Multiplier ranges from 0.1x to 3x. Fast inputs are
less expensive to fuzz, so we're giving them more air time. */

if (q->exec_us * 0.1 > avg_exec_us) perf_score = 10;
else if (q->exec_us * 0.25 > avg_exec_us) perf_score = 25;
else if (q->exec_us * 0.5 > avg_exec_us) perf_score = 50;
else if (q->exec_us * 0.75 > avg_exec_us) perf_score = 75;
else if (q->exec_us * 4 < avg_exec_us) perf_score = 300;
else if (q->exec_us * 3 < avg_exec_us) perf_score = 200;
else if (q->exec_us * 2 < avg_exec_us) perf_score = 150;

/* Adjust score based on bitmap size. The working theory is that better
coverage translates to better targets. Multiplier from 0.25x to 3x. */

if (q->bitmap_size * 0.3 > avg_bitmap_size) perf_score *= 3;
else if (q->bitmap_size * 0.5 > avg_bitmap_size) perf_score *= 2;
else if (q->bitmap_size * 0.75 > avg_bitmap_size) perf_score *= 1.5;
else if (q->bitmap_size * 3 < avg_bitmap_size) perf_score *= 0.25;
else if (q->bitmap_size * 2 < avg_bitmap_size) perf_score *= 0.5;
else if (q->bitmap_size * 1.5 < avg_bitmap_size) perf_score *= 0.75;

/* Adjust score based on handicap. Handicap is proportional to how late
in the game we learned about this path. Latecomers are allowed to run
for a bit longer until they catch up with the rest. */

if (q->handicap >= 4) {

perf_score *= 4;
q->handicap -= 4;

} else if (q->handicap) {

perf_score *= 2;
q->handicap--;

}

/* Final adjustment based on input depth, under the assumption that fuzzing
deeper test cases is more likely to reveal stuff that can't be
discovered with traditional fuzzers. */

switch (q->depth) {

case 0 ... 3: break;
case 4 ... 7: perf_score *= 2; break;
case 8 ... 13: perf_score *= 3; break;
case 14 ... 25: perf_score *= 4; break;
default: perf_score *= 5;

}

/* Make sure that we don't go over limit. */

if (perf_score > HAVOC_MAX_MULT * 100) perf_score = HAVOC_MAX_MULT * 100;

return perf_score;

}

根据 queue_entry 的执行速度、覆盖到的路径数和深度来评估打分,并用作计算 fuzz_one 中后面随机变异部分的最大循环数

save_if_interesting

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
/* Check if the result of an execve() during routine fuzzing is interesting,
save or queue the input test case for further analysis if so. Returns 1 if
entry is saved, 0 otherwise. */

static u8 save_if_interesting(char** argv, void* mem, u32 len, u8 fault) {

u8 *fn = "";
u8 hnb;
s32 fd;
u8 keeping = 0, res;

if (fault == crash_mode) {

/* Keep only if there are new bits in the map, add to queue for
future fuzzing, etc. */

if (!(hnb = has_new_bits(virgin_bits))) {
if (crash_mode) total_crashes++;
return 0;
}

#ifndef SIMPLE_FILES

fn = alloc_printf("%s/queue/id:%06u,%s", out_dir, queued_paths,
describe_op(hnb));

#else

fn = alloc_printf("%s/queue/id_%06u", out_dir, queued_paths);

#endif /* ^!SIMPLE_FILES */

add_to_queue(fn, len, 0);

if (hnb == 2) {
queue_top->has_new_cov = 1;
queued_with_cov++;
}

queue_top->exec_cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);

/* Try to calibrate inline; this also calls update_bitmap_score() when
successful. */

res = calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0);

if (res == FAULT_ERROR)
FATAL("Unable to execute target application");

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

keeping = 1;

}

switch (fault) {

case FAULT_TMOUT:

/* Timeouts are not very interesting, but we're still obliged to keep
a handful of samples. We use the presence of new bits in the
hang-specific bitmap as a signal of uniqueness. In "dumb" mode, we
just keep everything. */

total_tmouts++;

if (unique_hangs >= KEEP_UNIQUE_HANG) return keeping;

if (!dumb_mode) {

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

if (!has_new_bits(virgin_tmout)) return keeping;

}

unique_tmouts++;

/* Before saving, we make sure that it's a genuine hang by re-running
the target with a more generous timeout (unless the default timeout
is already generous). */

if (exec_tmout < hang_tmout) {

u8 new_fault;
write_to_testcase(mem, len);
new_fault = run_target(argv, hang_tmout);

/* A corner case that one user reported bumping into: increasing the
timeout actually uncovers a crash. Make sure we don't discard it if
so. */

if (!stop_soon && new_fault == FAULT_CRASH) goto keep_as_crash;

if (stop_soon || new_fault != FAULT_TMOUT) return keeping;

}

#ifndef SIMPLE_FILES

fn = alloc_printf("%s/hangs/id:%06llu,%s", out_dir,
unique_hangs, describe_op(0));

#else

fn = alloc_printf("%s/hangs/id_%06llu", out_dir,
unique_hangs);

#endif /* ^!SIMPLE_FILES */

unique_hangs++;

last_hang_time = get_cur_time();

break;

case FAULT_CRASH:

keep_as_crash:

/* This is handled in a manner roughly similar to timeouts,
except for slightly different limits and no need to re-run test
cases. */

total_crashes++;

if (unique_crashes >= KEEP_UNIQUE_CRASH) return keeping;

if (!dumb_mode) {

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

if (!has_new_bits(virgin_crash)) return keeping;

}

if (!unique_crashes) write_crash_readme();

#ifndef SIMPLE_FILES

fn = alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,
unique_crashes, kill_signal, describe_op(0));

#else

fn = alloc_printf("%s/crashes/id_%06llu_%02u", out_dir, unique_crashes,
kill_signal);

#endif /* ^!SIMPLE_FILES */

unique_crashes++;

last_crash_time = get_cur_time();
last_crash_execs = total_execs;

break;

case FAULT_ERROR: FATAL("Unable to execute target application");

default: return keeping;

}

/* If we're here, we apparently want to save the crash or hang
test case, too. */

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

ck_free(fn);

return keeping;

}

检查测试用例的执行结果是否有价值,如果有价值返回 1 ,否则返回 0

  • 如果 fault 等于 crash_mode ,没有发现新的 path 则返回 0
  • 否则将 case 保存到 fn = alloc_printf(“%s/queue/id:%06u,%s”, out_dir, queued_paths,
    describe_op(hnb)) ,并将它添加到队列中
  • 如果有新路径,更新 has_new_cov 为 1 并递增 queued_with_cov ,然后计算 exec_cksum
  • 调用 calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0) ,也就是评估该 queue ,并将结果存储到 res
  • 根据 res 的值进行操作
    • FAULT_TMOUT
      • 递增 total_tmouts
      • 如果 unique_hangs 大于等于 KEEP_UNIQUE_HANG(500)则返回 keeping
      • 如果不是 dumb 模式则调用 simplify_trace(trace_bits),之后调用 has_new_bits 发现没有新的路径则返回 keeping
      • 递增 unique_tmouts
      • 如果 exec_tmout 小于 hang_tmout 则
        • 调用 write_to_testcase 写入测试用例并再次执行 run_target ,将结果存储在 new_fault 中
        • 如果 stop_soon 为 0 且 new_fault 为 FAULT_CRASH 则跳到 keep_as_crash
        • 如果定义了 stop_soon 或 new_fault 不等于 FAULT_TMOUT 则返回 keeping
      • 如果没有定义 SIMPLE_FILES 则存储用例在 hangs 目录下
      • 递增 unique_hangs 并获取 last_hang_time
    • FAULT_CRASH
      • 开头便是 keep_as_crash
      • 递增 total_crashes
      • 如果 unique_crashes 大于等于 KEEP_UNIQUE_CRASH(5000)则返回 keeping
      • 如果不是 dumb 模式则调用 simplify_trace(trace_bits),之后调用 has_new_bits 发现没有新的路径则返回 keeping
      • 如果没有定义 unique_crashes 则 write_crash_readme
      • 如果没有定义 SIMPLE_FILES 则存储用例在 crashes 目录下
      • 递增 unique_crashes 并获取 last_hang_time ,同时更新 last_crash_execs 为 total_execs
    • FAULT_ERROR
      • 抛出异常
    • default
      • 返回 keeping
  • 最后写入 fn

sync_fuzzers

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
/* Grab interesting test cases from other fuzzers. */

static void sync_fuzzers(char** argv) {

DIR* sd;
struct dirent* sd_ent;
u32 sync_cnt = 0;

sd = opendir(sync_dir);
if (!sd) PFATAL("Unable to open '%s'", sync_dir);

stage_max = stage_cur = 0;
cur_depth = 0;

/* Look at the entries created for every other fuzzer in the sync directory. */

while ((sd_ent = readdir(sd))) {

static u8 stage_tmp[128];

DIR* qd;
struct dirent* qd_ent;
u8 *qd_path, *qd_synced_path;
u32 min_accept = 0, next_min_accept;

s32 id_fd;

/* Skip dot files and our own output directory. */

if (sd_ent->d_name[0] == '.' || !strcmp(sync_id, sd_ent->d_name)) continue;

/* Skip anything that doesn't have a queue/ subdirectory. */

qd_path = alloc_printf("%s/%s/queue", sync_dir, sd_ent->d_name);

if (!(qd = opendir(qd_path))) {
ck_free(qd_path);
continue;
}

/* Retrieve the ID of the last seen test case. */

qd_synced_path = alloc_printf("%s/.synced/%s", out_dir, sd_ent->d_name);

id_fd = open(qd_synced_path, O_RDWR | O_CREAT, 0600);

if (id_fd < 0) PFATAL("Unable to create '%s'", qd_synced_path);

if (read(id_fd, &min_accept, sizeof(u32)) > 0)
lseek(id_fd, 0, SEEK_SET);

next_min_accept = min_accept;

/* Show stats */

sprintf(stage_tmp, "sync %u", ++sync_cnt);
stage_name = stage_tmp;
stage_cur = 0;
stage_max = 0;

/* For every file queued by this fuzzer, parse ID and see if we have looked at
it before; exec a test case if not. */

while ((qd_ent = readdir(qd))) {

u8* path;
s32 fd;
struct stat st;

if (qd_ent->d_name[0] == '.' ||
sscanf(qd_ent->d_name, CASE_PREFIX "%06u", &syncing_case) != 1 ||
syncing_case < min_accept) continue;

/* OK, sounds like a new one. Let's give it a try. */

if (syncing_case >= next_min_accept)
next_min_accept = syncing_case + 1;

path = alloc_printf("%s/%s", qd_path, qd_ent->d_name);

/* Allow this to fail in case the other fuzzer is resuming or so... */

fd = open(path, O_RDONLY);

if (fd < 0) {
ck_free(path);
continue;
}

if (fstat(fd, &st)) PFATAL("fstat() failed");

/* Ignore zero-sized or oversized files. */

if (st.st_size && st.st_size <= MAX_FILE) {

u8 fault;
u8* mem = mmap(0, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

if (mem == MAP_FAILED) PFATAL("Unable to mmap '%s'", path);

/* See what happens. We rely on save_if_interesting() to catch major
errors and save the test case. */

write_to_testcase(mem, st.st_size);

fault = run_target(argv, exec_tmout);

if (stop_soon) return;

syncing_party = sd_ent->d_name;
queued_imported += save_if_interesting(argv, mem, st.st_size, fault);
syncing_party = 0;

munmap(mem, st.st_size);

if (!(stage_cur++ % stats_update_freq)) show_stats();

}

ck_free(path);
close(fd);

}

ck_write(id_fd, &next_min_accept, sizeof(u32), qd_synced_path);

close(id_fd);
closedir(qd);
ck_free(qd_path);
ck_free(qd_synced_path);

}

closedir(sd);

}

该函数将其他 .synced 文件夹下的 queue 文件读取到自己的队列中并运行测试