AFL源代码阅读

AFL全名你americal fuzzy lop,是前谷歌安全研究员lcamtuf编写的fuzz工具。一方面是为了对该工具进行二次开发,二来AFL作为优秀的开源C程序,拥有良好的编程风格(C和SHELL)。笔者会阅读当前最新版AFL的代码(2.52b),并将笔记记录下来,希望也能帮到一些对AFL源代码感兴趣的CODER。

AFL的代码真的很简洁,大部分的功能在注释都写的很完整了。

源代码目录

插桩模块

afl-as.c , afl-as.h , afl-gcc (普通插桩)

llvm_mode (llvm模式插桩)

qemu_mode(qemu模式插桩) ->针对无源代码的二进制文件

fuzzer模块

Afl-fuzz.c

其他辅助模块

Libdislocator:简单的内存错误检测工具

libtokencap:语法关键字提取并生成字典文件

afl-analyze.c:对调试用例对字段进行分析

afl-cmin: 对fuzzing中用到的语料库进行精简操作

afl-gotcpu::绘制报告图表

afl-ploit:绘制报告图标

afl-showmap.c:打印目标程序一轮fuzz后的tuple信息

afl-whatsup:各并行例程fuzzing结果总计

alloc-inl.h 定义带检测功能的内存分配和释放操作

config.h 配置信息的定义

debug.h 跟提示信息相关的宏定义

hash.h 哈希函数的实现定义

types.h 部分类型及宏的定义

test-instr.c 用做测试的目标程序

docs: 项目相关的说明文档

experimental:一些新特性的试验研究

插桩实现

afl-gcc相当于在gcc上做了一层封装,使用afl-gcc作为默认编译器之后,afl-as会在assemble的时候将机器将tuple插入每个跳转指令。

llvm mode则得益于llvm的特性,编译好的程序效率会更高,不过原理和afl-gcc相近。

源代码分析:留坑

Fuzzer实现

自定义头文件

qa1m8E

这五个自定义头文件很重要(尤其是中间三个),里面定义AFL的一些宏定义。可以先阅读一下,以减少之后阅读的阻碍。

#include “config.h” :一些AFL相关的宏定义,程序的默认值,例如内存默认限制为50MB
#include “types.h” :定义了一些AFL中的类型比如s8,实际是上是个char类型。
#include “debug.h”:定义Debug相关的一些定义。比如SAYF就是封装printf,OKF则是改了输出颜色,表示执行成功。
#include “alloc-inl.h”:定义内存错误检测(UAF/BufferOverflow/Zero)的一些宏定义。
#include “hash.h” :定义了ROL32/64的hash算法。

预处理部分

运行环境

判断运行环境,如果是Linux,定义AFFINITY(CPU亲和度)

1
2
3
#ifdef __linux__
# define HAVE_AFFINITY 1
#endif /* __linux__ */

在之后的运行中,HAVE_AFFINITY会影响CPU核心数的初始化。使用cpu_aff 标记CPU core。

Kj0tum

静态编译

当AFL被编译为Lib库时(比如Libfuzzer中,保证输出正常。

1
2
3
4
5
#ifdef AFL_LIB
# define EXP_ST
#else
# define EXP_ST static
#endif /* ^AFL_LIB */

全局变量部分

类型定义

基本定义u8为char类型。

1
typedef uint8_t u8

静态变量宏定义EXP_ST

1
#  define EXP_ST static

变量定义

定义char类型的静态变量指针,存放目录地址,以及bitmap等。(代码不全)

1
2
3
4
5
6
7
8
9
10
EXP_ST u8 *in_dir,                    /* Input directory with test cases  */
*out_file, /* File to fuzz, if any */
*out_dir, /* Working & output directory */
*sync_dir, /* Synchronization directory */
*sync_id, /* Fuzzer ID */
*use_banner, /* Display banner */
*in_bitmap, /* Input bitmap */
*doc_path, /* Path to documentation dir */
*target_path, /* Path to target binary */
*orig_cmdline; /* Original command line */

queue_entry队列结构体

一个队列存放一个testcase对象,AFL会为每一个对象fork一个进程进行FUZZ。队列会存放这个进程的运行状态和fuzz的信息。

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
struct queue_entry {

u8* fname; /* File name for the test case */
u32 len; /* Input length */

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

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

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

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

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

};

初始化tuple。AFL是通过向程序块插入tuple来存储覆盖率信息以及获取目标运行的流程。

1
2
3
4
5
EXP_ST u8* trace_bits;               /* SHM with instrumentation bitmap  *///记录当前tuple信息

EXP_ST u8 virgin_bits[MAP_SIZE], /* Regions yet untouched by fuzzing *///记录总的tuple信息
virgin_tmout[MAP_SIZE], /* Bits we haven't seen in tmouts *///记录fuzz过程中出现的所有timeouts的tuple信息
virgin_crash[MAP_SIZE]; /* Bits we haven't seen in crashes *///记录了fuzz中出现crash的tuple信息

枚举类型

大量的宏定义,用枚举类型比用define漂亮多了(重点错

例如下面定义了FUZZ的几种状态

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

enum {
/* 00 */ STAGE_FLIP1,
/* 01 */ STAGE_FLIP2,
/* 02 */ STAGE_FLIP4,
/* 03 */ STAGE_FLIP8,
/* 04 */ STAGE_FLIP16,
/* 05 */ STAGE_FLIP32,
/* 06 */ STAGE_ARITH8,
/* 07 */ STAGE_ARITH16,
/* 08 */ STAGE_ARITH32,
/* 09 */ STAGE_INTEREST8,
/* 10 */ STAGE_INTEREST16,
/* 11 */ STAGE_INTEREST32,
/* 12 */ STAGE_EXTRAS_UO,
/* 13 */ STAGE_EXTRAS_UI,
/* 14 */ STAGE_EXTRAS_AO,
/* 15 */ STAGE_HAVOC,
/* 16 */ STAGE_SPLICE
};

EVFdwZ

例如这段代码中,stage_finds和stage_cycles是两个数组,用于存放fuzz stage状态下的模式发现。DI也作为状态的输出,在UI界面上的输出格式见下图。

J0qh18

FUZZ流程部分

分析完初始化部分,让从main函数开始,分析流程,理解AFL运行的整体逻辑。

定义变量

初始化一些main函数中需要用到的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  s32 opt;
u64 prev_queued = 0; //初始化队列
u32 sync_interval_cnt = 0, seek_to;
u8 *extras_dir = 0; //初始化目录
u8 mem_limit_given = 0; //初始化内存限制
u8 exit_1 = !!getenv("AFL_BENCH_JUST_ONE"); //获取环境变量
char** use_argv; //用户输入的参数

struct timeval tv;
struct timezone tz;

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

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

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

参数分离

通过while来分离用户输入的参数,例如afl-fuzz -m 99999999 -i corpus/ -o afl-out/ out/Default/d8

内部使用getopt函数用来解析传入main的参数,熟悉afl-fuzz的使用,就知道这部分的功能。初始化种子目录和输出目录,还有fuzz对象等信息。

5uQnAG

根据参数初始化变量

设置信号量处理程序,检查ASAN选项。ASAN能够帮助我们发现程序运行中产生的错误,增加边界检查,类似于windows下开启HEAP PAGE。

1
2
setup_signal_handlers(); //设置信号量处理程序
check_asan_opts(); //检查ASAN

接下来又是设置一些环境变量,暂时不看。直接看一些初始化函数。大部分函数理解作用就行了,之后需要再查询。需要关注的,setup_shm,是初始化 trace_bits 和 virgin_bits 的 bitmap的函数,用于存放当前&总的tuple信息的。

1
2
3
4
5
6
7
8
9
10
11
12
13
save_cmdline(argc, argv); //保存命令行+参数
fix_up_banner(argv[optind]); //修复banner
check_if_tty(); //检查终端,判断是否启用UI
get_core_count(); //检查设定核心数量(运行参数,不是系统核心数)
check_crash_handling();//检测crash的检测句柄
check_cpu_governor(); //检测CPU调速器
setup_post(); //检查postprocess
setup_shm(); //初始化分享内存,用于存放tuple信息(覆盖率)
init_count_class16(); //。。。

setup_dirs_fds(); //初始化输出目录
read_testcases(); //读取样例
load_auto(); //...

read_testcase会读取的testcase样本,调用add_to_queue将样本存储到queue队列中

check_binary()

检查binary。需要符合以下要求

  • 目录和文件存在
  • Bin为ELF格式可执行文件
  • 检查文件是否是AFL structure的,即是否被插桩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pivot_inputs(); 		
if (extras_dir) load_extras(extras_dir);
if (!timeout_given) find_timeout();
detect_file_args(argv + optind + 1);
if (!out_file) setup_stdio_file();
check_binary(argv[optind]); //检查binary
start_time = get_cur_time(); //获取当前时间

if (qemu_mode) //判断是否是qemu模式
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;

perform_dry_run(use_argv);//测试所有的样本

perform_dry_run()

FUZZ时候会碰到如下情况,报错perform_dry_run(),甚至程序还没有进入fuzz界面就退出了。因为在进入主循环之前,程序会运行perform_dry_run对种子文件进行运行检测。

下图中abort的原因是testcase运行花费过多的CPU。

CLcxFO

perform_dry_run()是将所有测试样例都跑一遍,保证没有问题。但是如果一开始的样例就能产生崩溃,程序就不会运行。一般会有以下问题,需要针对性修改testcase。或者改源代码(逃

  • Timeout_given : testcase造成程序timeout的错误,可能来自逻辑错误的语法。
  • Crash :testcase造成程序崩溃,原因有二
    • 样本本身能够造成程序crash
    • 程序运行的内存过小造成crash

在一些测试条件下,我们可能需要输入一些类似的数据。如果要避免这个问题导致无法fuzz,可以设置环境变量AFL_SKIP_CRASHES为1,用来跳过检测。或者-C运行crash_mode,perform_dry_run就会忽略掉crash 的影响。

u8* skip_crashes = getenv("AFL_SKIP_CRASHES");

VaETUJ

perform_dry_run中的 switch (res)的case包含了几种反馈信息的宏定义,进程终止之后返回的status会对应不同的宏定义。(关于进程通信的status
如果出现TIMEOUT/CRASH之类的错误,fuzzer就会停止运行。当然,可以通过修改环境变量来控制这种行为。

1
2
3
4
5
FAULT_NONE
FAULT_TMOUT
FAULT_CRASH
FAULT_ERROR
FAULT_NOBITS

注意的是perform_dry_run中第一次调用了calibrate_case,这个函数中有个关键部分init_forkserver(argv);

forkserver是afl运行的核心部分,为每个用例fork进程,并通过管道进行通信。fork的方式比传统fuzzer加载target效率更高,消耗的内存更少。forkserver只有在instrumented mode下才会使用。

forkserver代码分析:

1
2
3
4
5
6
static struct itimerval it;
int st_pipe[2], ctl_pipe[2]; //与子进程通信的管道
int status; //子进程退出的Signal
s32 rlen;

forksrv_pid = fork(); //fork 子进程

fork函数会返回两个变量,在父进程中fork返回子进程的PID号,而在子进程中fork返回0。

所以这个IF内部是对子进程的操作。if (!forksrv_pid)等价于if (forksrv_pid==0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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
  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. */
//将FORKSEV_FD绑定到ctl_pipe[0]的输入端口
//将FORKSRV_FD + 1绑定到st_pipe[1]的输出端口
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);
//运行目标程序,execve产生的新进程会替代这个fork进程
execv(target_path, argv); //target为testcase,argv存放的是程序路径
/* Use a distinctive bitmap signature to tell the parent about execv()
falling through. */
*(u32*)trace_bits = EXEC_FAIL_SIG;
exit(0);
}

forkserver的核心是借助管道来实现于主进程的通信,下面的代码会在主进程中设置两个句柄,分别作为与子进程的输入和输出通信管道。

1
2
3
4
5
6
7
8
9
 /* Close the unneeded endpoints. */
close(ctl_pipe[0]); //关闭读端口
close(st_pipe[1]); //关闭写端口
//将ctl_pipe作为输入管道,st_pipe作为监听管道。
fsrv_ctl_fd = ctl_pipe[1];//写端口
fsrv_st_fd = st_pipe[0]; //读端口

//例如此处通过read从子进程读取4字节的状态位
rlen = read(fsrv_st_fd, &status, 4);

show_init_stats

初始化阶段的最后一个函数,检查testcase等的初始化状态。加上一些可能的警告信息,完成程序最后的初始化。到这里基本的参数和一些所需内存的分配已经完成,也完成了testcase的读取。

接下来就要进入fuzz的主循环了,程序会不断地进行fuzz直到用户停止软件的运行。

数据/文件变异

FUZZ的主要函数调用关系

1
2
3
4
5
fuzz_one #进入当前测试queue
common_fuzz_stuff #包含主要fuzz功能
write_to_testcase #写入变异后的样本
run_target #运行测试目标进程
save_if_interesting #判断是否有crash

这里还是要提一下init_forkserver函数,afl对于新的testcase采用fork的方式来创建新的进程。通过pipe来让子进程于fuzzer进行通信。

理解forkserver的通信机制

afl fuzzer通过fork进程来运行我们的target,fuzzer通过管道和fork进程进行通信。fork的子进程内部通过execv来运行对象,execv于Windows的CreateProcess不同,并不是创建进程,而是替代该进程。否则这个目标就是运行在孙进程中了,也就无法与fuzzer进行通信。参考

0hI2Vv

fuzzer和子进程的通信过程,如果读者还记得init_forkserver函数中的ctl_pipe和st_pipe,应该就会对这张图有所理解。

nQsPI6

不过这里不得不提一下afl-as.h中对forkserver的实现。

afl-as是afl的汇编器,和afl-gcc对于gcc一样,简单来说是对as的一层封装,afl-as会在生成程序中插入对应的二进制汇编代码(插桩)。afl-as主要有两个功能,一是保存/计算tuple信息,二则是包含了forkserver的功能。

_afl_maybe_log向st_pioe写入4字节,通知父进程已经准备好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  "__afl_maybe_log:\n"
"\n"
#if defined(__OpenBSD__) || (defined(__FreeBSD__) && (__FreeBSD__ < 9))
" .byte 0x9f /* lahf */\n"
#else
" lahf\n"
#endif /* ^__OpenBSD__, etc */
" seto %al\n"
"\n"
" /* Check if SHM region is already mapped. */\n"
"\n"
" movq __afl_area_ptr(%rip), %rdx\n"
" testq %rdx, %rdx\n"
" je __afl_setup\n"

afl_fork_wait_loop从管道FORKSRV_FD,读取来自fuzzer主程式的4字节信息,如果收到则说明通信成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
"__afl_fork_wait_loop:\n"
"\n"
" /* Wait for parent by reading from the pipe. Abort if read fails. */\n"
"\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY(FORKSRV_FD) " /* file desc */\n"
" call read\n"
" addl $12, %esp\n"
"\n"
" cmpl $4, %eax\n"
" jne __afl_die\n"
"\n"

执行fork,将pid存储到__afl_fork_pid中。通过管道,包括PID和waitpid之类的返回方式都是通过这个方式来传递的。也就是为什么我们在fuzzer中能收到status字节来判断进程是否结束,或者以何种方式结束。(Signal值)

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
"  /* Once woken up, create a clone of our process. This is an excellent use\n"
" case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly\n"
" caches getpid() results and offers no way to update the value, breaking\n"
" abort(), raise(), and a bunch of other things :-( */\n"
"\n"
" call fork\n"
"\n"
" cmpl $0, %eax\n"
" jl __afl_die\n"
" je __afl_fork_resume\n"
"\n"
" /* In parent process: write PID to pipe, then wait for child. */\n"
"\n"
" movl %eax, __afl_fork_pid\n"
"\n"
" pushl $4 /* length */\n"
" pushl $__afl_fork_pid /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"
" call write\n"
" addl $12, %esp\n"
"\n"
" pushl $0 /* no flags */\n"
" pushl $__afl_temp /* status */\n"
" pushl __afl_fork_pid /* PID */\n"
" call waitpid\n"
" addl $12, %esp\n"
"\n"
" cmpl $0, %eax\n"
" jle __afl_die\n"
"\n"
" /* Relay wait status to pipe, then loop back. */\n"
"\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"
" call write\n"
" addl $12, %esp\n"
"\n"
" jmp __afl_fork_wait_loop\n"

fuzz_one

让我们回到我们的主程式中来看。fuzz_one会返回0如果fuzzed成功,否则会反馈1,反馈数据到外层的变量skipped_fuzz。

mmap一块内存,将test case文件写入这块内存空间。

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

运行calibrate_case,这个函数内部会运行我们的目标程序,在下文我们会详细分析这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
if (queue_cur->cal_failed) {
u8 res = FAULT_TMOUT;
if (queue_cur->cal_failed < CAL_CHANCES) {
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;
}
}

调用calculate_score函数来为testcase打分

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

calibrate_case

write_to_testcase

将内存中变异好的testcase重新写入文件,用于下一轮模糊测试。代码比较短直接写注释了

1
2
3
4
5
6
7
8
9
10
11
12
13
static void write_to_testcase(void* mem, u32 len) {
s32 fd = out_fd; //获取out_fd文件符号
if (out_file) { //如果out_file存在,则先将其卸载掉
unlink(out_file); /* Ignore errors. */
fd = open(out_file, O_WRONLY | O_CREAT | O_EXCL, 0600);//打开out_file为fd符号
if (fd < 0) PFATAL("Unable to create '%s'", out_file);
} else lseek(fd, 0, SEEK_SET);
ck_write(fd, mem, len, out_file);//将内存中的testcase写入out_filew文件
if (!out_file) {
if (ftruncate(fd, len)) PFATAL("ftruncate() failed");
lseek(fd, 0, SEEK_SET);
} else close(fd);
}

run_target

运行我们的目标程序,并且通过forkserver与目标进程通信,最后会返回对应的反馈信息status,作为save_if_interesting函数的判断依据(例如FAULT_CRASH)。

程序会检测afl运行模式,如果是dumb模式或者forkserver并没有被初始化,将会运行初始化,这部分和前面的init_forkserver相同,不重复分析了。

![image-20200320204927546](/Users/migraine/Library/Application Support/typora-user-images/image-20200320204927546.png)

当我们不是dumb模式的时候,forkserver已经运行,所以直接会进入进程通信部分。

管道读取读取4个字节,检测子进程是否运行正常。

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

定时等待子进程的终端完成初始化

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

从管道中读取forkserver的4字节,作为status

1
2
3
4
5
if ((res = read(fsrv_st_fd, &status, 4)) != 4) {

if (stop_soon) return 0;
RPFATAL(res, "Unable to communicate with fork server (OOM?)");
}

内嵌汇编,用于内存追踪,方便在调试中检测。

a9IA44

当进程运行结束或者被KILL掉,status可以判断进程终止的原因。afl会对这些行为进行分类,按照之前定义的一些FAULT宏定义。Linux 信号signal处理机制

1
2
3
4
5
if (WIFSIGNALED(status) && !stop_soon) {
kill_signal = WTERMSIG(status);
if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;
return FAULT_CRASH;
}

如何让子进程读取testcase

参考

其实看了好几遍代码都不知道是怎么让target读取queue中的testcase。。才疏学浅

一开始就以为是execv的argv传参,读了好多遍代码都没发现输入testcase的代码。

尝试输出了一下,发现传入的argv是一个数组,仅包含target路径。由图可见,target_path和argv存放的都是程序路径

fvKiVQ

abymVw

又猜测是管道传参,但是也没找到具体代码。后来在看write_test_case部分的时候,发现了detect_file_args函数。这个函数将我们的存放testcase的out_file目录写入argv中。运行target的时候,argv则指向.cur_input文件。

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
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);//获取out_file的相对路径,‘.cur_input’为当前fuzz的文件
/* 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);//拼接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 */
}

当然如果不使用-f参数,则会调用setup_stdio_file,效果类似。不过在笔者测试的条件下,argv似乎没有被修改,所以还是不能确定是不是argv传参,也有可能是插桩的模块读取的testcase,之后有空研究一下再回来补充。

1
2
detect_file_args(argv + optind + 1);
if (!out_file) setup_stdio_file();

大师傅说通过dup2修改stdin来进行读取文件,有空再看看源代码。

jE8eL0

以下函数初始化了out_fd,将.cur_input文件作为stdin输入。

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

save_if_interesting

存在问题:afl无法检测出v8自带DebugCheck检测出的崩溃。POC

解决方案设想

  • 增加crash判断,如果出现DebugCheck字样,保存到crash中(看了源代码发现AFL是通过信号量判断CRASH。。所以也不行)
  • 尝试编译非Debug版本的v8,并且使用ASan来进行内存判断(失败。。ASan毕竟不是v8原生。。。)

当然以上方法并不可行,首先,我们需要了解一下AFL判断CRASH的方式。直接看代码

1
2
3
4
5
if (WIFSIGNALED(status) && !stop_soon) {
kill_signal = WTERMSIG(status);
if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;
return FAULT_CRASH;
}

AFL会讲目标进程运行在fork中,如果进程接触,AFL管道读取进程的4字节status参数。status实际上就是进程结束时发送的signal信号量。当status反馈SIGKILL,AFL则判断为FAULT_OUT,如果反馈为SIGSEGV,则判断为CRASH。

接下来让我们看看为什么,DebugCheck发现漏洞之后,AFL却不会发现CRASH。

1
2
3
4
5
//logging.cc 
if (v8::base::g_print_stack_trace) v8::base::g_print_stack_trace();
fflush(stderr);
v8::base::OS::Abort(); //DebugCheck产生一个Abort
}

我们可以看到,当DebugCheck触发时,v8会发出一个Abort并且终止进程。而AFL对Abort并不会反馈crash信息。所以如果要测试Debug版本的v8,AFL会漏掉一些潜在漏洞。

那我们如何去修复这个问题呢?

  • v8在DebugCheck时,singal都为4,所以只需要对症下药就好了。我们尝试修改afl,让singal判断处增加对SIGABRT&&SIGILL的检测。这是可行的,测试时需要注意,在afl中poc需要删掉函数间的空行,否则可能解析错误无法crash。(因为afl是先运行v8再逐行输入样本,所以要保证一个函数在同一行内

    1
    2
    3
    4
    5
    6
    if (WIFSIGNALED(status) && !stop_soon) {
    kill_signal = WTERMSIG(status);
    if (kill_signal ==SIGILL) return FAULT_CRASH;//V8 DEBUG CHECK //signal 4
    if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;
    return FAULT_CRASH;
    }
  • 修改v8源代码,让DebugCheck后发送signal信号量为11(默认DCHECK发的是abort)。

1
2
3
4
5
//logging.cc 
if (v8::base::g_print_stack_trace) v8::base::g_print_stack_trace();
fflush(stderr);
v8::base::OS::Abort(); //DebugCheck产生一个Abort
}

DebugCheck会反馈一个Abort(即signal 4)

sZ1HuY

但是afl不会对signal4有反馈。(图中笔者修改了AFL的源代码,让子进程结束时能够输出signal值)

8F8fwh

换一个能产生singal 11的漏洞,不需要修改代码afl就能跑出来。。。类似下图这种。验证环境的配置参数见附录。

04rhEb

其他

在v8编译中要使用use_afl=true,编译afl_fuzz需要添加参数

make CFLAGS="-std=c11 -D_GNU_SOURCE"

扩展阅读

https://bbs.pediy.com/thread-249912.htm

https://bbs.pediy.com/thread-257399.htm

https://www.wxnmh.com/thread-5948378.htm

https://gtoad.github.io/2019/07/26/V8-CVE-2016-5198/

https://www.cnblogs.com/justin-y-lin/p/11257472.html

附录

通过afl跑出v8的crash实例

vTCuRX

修改V8版本,将afl源代码和Build.gn分别放入v8/third_party/afl/src和v8/third_party/afl/目录,并且编译

1
2
3
4
5
$ git checkout a7a350012c05f644f3f373fb48d7ac72f7f60542
$ gclient sync

$ gn gen out/Default --args='is_clang=true is_debug=true use_sanitizer_coverage=true sanitizer_coverage_flags="trace-pc-guard" use_afl=true' # 参数会写入gn.args中
$ ninja -C out/Default d8

使用crash模式运行,否则刚开始运行就会crash。

1
$ /home/p0kerface/Documents/Browser/v8/v8/third_party/afl/src/afl-fuzz -m1000000 -C -i afl-in/ -o afl-out/  out/Default/d8

poc.js

注意一个函数/循环内不能有换行,否则无法跑出crash(应该与afl读取文件的方式有关,换行会被当作无关语句拆分开

1
2
3
4
5
6
function Ctor() {n = new Set();}
function Check() {n.xyz = 0x826852f4;parseInt();}
for(var i=0; i<2000; ++i) {Ctor();}
for(var i=0; i<2000; ++i) {Check();}
Ctor();
Check();

BUILD.gn

放在v8/third_party/afl/目录,否则use_afl将无法使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

group("afl") {
deps = [
":afl-cmin",
":afl-fuzz",
":afl-showmap",
":afl-tmin",
":afl_docs",
":afl_runtime",
]
}

source_set("afl_runtime") {
# AFL needs this flag to be built with -Werror. This is because it uses u8*
# and char* types interchangeably in its source code. The AFL Makefiles use
# this flag.
cflags = [ "-Wno-pointer-sign" ]

configs -= [
# These functions should not be compiled with sanitizers since they
# are used by the sanitizers.
"//build/config/sanitizers:default_sanitizer_flags",

# Every function in this library should have "default" visibility.
# Thus we turn off flags which make visibility "hidden" for functions
# that do not specify visibility.
# The functions in this library will not conflict with others elsewhere
# because they begin with a double underscore and/or are static.
"//build/config/gcc:symbol_visibility_hidden",
]

sources = [ "src/llvm_mode/afl-llvm-rt.o.c" ]
}

afl_headers = [
"src/alloc-inl.h",
"src/config.h",
"src/debug.h",
"src/types.h",
"src/hash.h",
]

config("afl-tool") {
cflags = [
# Include flags from afl's Makefile.
"-O3",
"-funroll-loops",
"-D_FORTIFY_SOURCE=2",

# These flags are necessary to build with -Werror.
"-Wno-sign-compare",
"-Wno-pointer-sign",

# afl_docs copies docs/ to this location.
"-DDOC_PATH=\"$root_build_dir/afl/docs/\"",

# This flag is needed for compilation but is only used for QEMU mode which
# we do not use. Therefore its value is unimportant.
"-DBIN_PATH=\"$root_build_dir\"",
]
}

copy("afl-cmin") {
# afl-cmin is a bash script used to minimize the corpus, therefore we can just
# copy it over.
sources = [ "src/afl-cmin" ]
outputs = [ "$root_build_dir/{{source_file_part}}" ]
deps = [ ":afl-showmap" ]
}

copy("afl_docs") {
# Copy the docs folder. This is so that we can use a real value for for
# -DDOC_PATH when compiling.
sources = [ "src/docs" ]
outputs = [ "$root_build_dir/afl/{{source_file_part}}" ]
}

executable("afl-fuzz") {
# Used to fuzz programs.
configs -= [ "//build/config/sanitizers:default_sanitizer_flags" ]
configs += [ ":afl-tool" ]

#no_default_deps = true

sources = [ "src/afl-fuzz.c" ]
sources += afl_headers
}

executable("afl-tmin") {
configs -= [ "//build/config/sanitizers:default_sanitizer_flags" ]
configs += [ ":afl-tool" ]

#no_default_deps = true

sources = [ "src/afl-tmin.c" ]
sources += afl_headers
}

executable("afl-showmap") {
configs -= [ "//build/config/sanitizers:default_sanitizer_flags" ]
configs += [ ":afl-tool" ]

#no_default_deps = true

sources = [ "src/afl-showmap.c" ]
sources += afl_headers
}