泻药,自动略过一切apple相关的宏 ,很合理⑧ , XD

afl-gcc.c

find_as

1.获取AFL_PATH 环境变量,然后访问AFL_PATH/as ,将之设置为as_path

2.若argv[0] 存在’/‘ ,即提供了路径,那就通过该路径访问/as,成功则设置为as_path

edit_params

该函数主要用于处理argv,将处理后的argv放入cc_params 中

  • 为cc_params 分配内存,大小为 (argc + 128) * sizeof(u8*)

  • 找到argv[0]路径的程序,放入name中

  • name与afl-clang比较

    • 相同则设置clang_mode以及CLANG_ENV_VAR,然后与afl-clang+比较
      • 相同则设置cc_params[0] = getenv(“AFL_CXX”),若不存在则为clang++
      • 否则设置cc_params[0] = getenv(“AFL_CC”) ,若不存在则为clang
    • 不相同则name与afl-g++比较g
      • 相同则设置cc_params[0] = getenv(“AFL_CXX”),若不存在则为g++
      • 否则设置cc_params[0] = getenv(“AFL_CC”) ,若不存在则为gcc
  • #如果是苹果系统的话则有些别的处理,比如AFL_GCJ ,我没有apple,略过

  • 以argc为循环次数,遍历argv

  • 判断argv

    • 若为 -B ,-integrated-as ,或-pipe 则跳过
    • 若为 -B ,-integrated-as ,或-pipe 则跳过
    • 若为-fsanitize=address或-fsanitize=memory,则asan_set = 1
    • 若为FORTIFY_SOURCE,则fortify_set = 1
  • 循环结束

  • 设置 cc_params[cc_par_cnt++] = “-B” cc_params[cc_par_cnt++] = as_path;

  • 若有clang_mode=1 ,设置-no-integrated-as

  • 若存在AFL_HARDEN环境变量,设置-fstack-protector-all

  • 若fortify_set为0,则设置-D_FORTIFY_SOURCE=2

  • 若asan_set=1,则设置AFL_USE_ASAN为1

    • 否则若有AFL_USE_ASAN环境变量,则设置-U_FORTIFY_SOURCE 与 -fsanitize=address(不能同时指定AFL_USE_MSAN,AFL_HARDEN)
    • 否则若有AFL_USE_MSAN环境变量,则设置-U_FORTIFY_SOURCE 与 -fsanitize=memory(不能同时指定AFL_USE_ASAN,AFL_HARDEN)
  • 若不存在AFL_DONT_OPTIMIZE环境变量,设置-g ,-O3 ,-funroll-loops,-D__AFL_COMPILER=1,-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1

  • 若存在AFL_NO_BUILTIN,则设置

    cc_params[cc_par_cnt++] = “-fno-builtin-strcmp”;

    cc_params[cc_par_cnt++] = “-fno-builtin-strncmp”;

    cc_params[cc_par_cnt++] = “-fno-builtin-strcasecmp”;

    cc_params[cc_par_cnt++] = “-fno-builtin-strncasecmp”;

    cc_params[cc_par_cnt++] = “-fno-builtin-memcmp”;

    cc_params[cc_par_cnt++] = “-fno-builtin-strstr”;

    cc_params[cc_par_cnt++] = “-fno-builtin-strcasestr”;

  • 最后设置cc_params[cc_par_cnt] = NULL

main

  1. 调用find_as(argv[0]) 寻找as
  2. 调用edit_params(argc, argv) 处理编译参数
  3. 执行编译: execvp(cc_params[0], (char**)cc_params)

afl-as.c

主要负责插桩

edit_params

主要负责对参数的处理,文件名为gcc传递的最后一个参数

  • 分配as_params ,ck_alloc((argc + 32) * sizeof(u8*))
  • 检查是否存在TEMP/TMP环境变量,
    • 有的话则设置tmp路径
    • 否则默认为/tmp
  • 判断是否存在AFL_AS环境变量
    • 存在的话,设置as_params[0]为环境变量的值
    • 否则设置as_params[0]为 as
  • 设置参数字符串结尾 as_params[argc] = 0
  • 遍历参数
    • 存在–64,设置use_64bit = 1 (该变量默认值由是否定义WORD_SIZE_64决定)
    • 否则存在–32的话,设置use_64bit = 0
  • 将文件名放入input_file中
    • 如果input_file是–version,直接设置just_version然后输出即可
    • 否则的话将input_file与tmp的路径(tmp_dir,/var/tmp,/tmp/)相比,如果相同则设置pass_thru = 1
  • 设置modified_file,
    • modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(), (u32)time(NULL));
    • 其实就是设置为 tmp_dir/.afl-pid-time.s
  • as_params[as_par_cnt++] = modified_file;
  • as_params[as_par_cnt] = NULL;

add_instrumentation

插桩的主要函数(雾

主要就是处理我们输出的文件,然后生成插装后的,放在modified_file

  • 用line[MAX_LINE] 来实现文件的读取与输出
  • 打开input_file,作为输入文件
  • 打开modified_file ,作为输出文件
  • 使用fgets从输入中循环读取到line,我们插桩是对代码插装,需要找到.text段

注意,我们是对汇编代码进行插桩的,

我们可以先观察一下gcc -S 生成的汇编代码,这里我以quickstart的代码为例

image-20221128230034539

在代码段之前,都会有.text 或者.code的相关伪指令描述,我们匹配也是根据伪指令进行判断的,但我们并不知道生成的是什么样的汇编,所以程序对不同架构都做了判断处理

具体的判断流程

  • 判断 (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&

    instrument_next && line[0] == '\t' && isalpha(line[1])) ,也就是是否是制表符开头,且后面第一个字节是字母.以及判断instrument_next,instr_ok,skip_app,pass_thru是否都为1

    • 是的话,向output 输出trampoline_fmt, instrument_next = 0,插桩计数器ins_lines++
    • 否则直接将line原封不动地输出进output中
  • 如果pass_thru=1, continue

  • 如果制表符开头,且其后第一个字符为 ‘ . ‘ 的话,判断其后是否是 text/ section\t.text /section\t__TEXT,__text/ section __TEXT,__text

    • 是的话则设置instr_ok = 1,代表处理到代码段,continue
    • 否则判断是否是 section\t ,section,bss,data,是的话设置instr_ok = 0 , continue
  • 如果存在code

    • code32,设置skip_csect = use_64bit
    • code64,设置skip_csect = !use_64bit
  • 如果 skip_intel || skip_app || skip_csect || !instr_ok || line[0] == '#' || line[0] == ' ',continue

  • 如果是制表符开头,且R(100) < inst_ratio,且后面是’j’ ,且不是jmp跳转的,如jnz,etc,则插桩,向output 输出trampoline_fmt, instrument_next = 0,插桩计数器ins_lines++

    这里R(100)用于产生碰撞生成随机值,可以用来区分桩

  • 如果是存在’:’,判断第一个字符是否为’ . ‘

    • 如果是,判断skip_next_label
      • 如果为0,则设置instrument_next = 1
      • 否则设置instrument_next = 0
    • 如果不是,instrument_next = 1 (代表这是个函数label)
  • while结束,判断ins_lines,如果不为0则再一次插桩

  • 关闭输入输出文件

main

  • 读取AFL_INST_RATIO环境变量到inst_ratio_str
  • 设置随机数种子
  • 调用edit_params设置参数
  • 设置AS_LOOP_ENV_VAR环境变量为1
  • 读取AFL_USE_ASAN和AFL_USE_MSAN环境变量
  • 设置sanitizer = 1
  • 设置inst_ratio /= 3
  • 调用add_instrumentation进行插桩
  • 调用fork
    • 子进程通过 execvp(as_params[0], (char**)as_params) 执行as插桩
    • 父进程等待子进程退出
      • 如果没有设置AFL_KEEP_ASSEMBLY环境变量,就unlink(modified_file)

总的来说,就是对函数入口处,条件跳转处,以及各种分支处进行插桩

插桩

插桩的代码trampoline_fmt 定义于afl-as.h中

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

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

static const u8* trampoline_fmt_64 =

"\n"
"/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leaq -(128+24)(%%rsp), %%rsp\n"
"movq %%rdx, 0(%%rsp)\n"
"movq %%rcx, 8(%%rsp)\n"
"movq %%rax, 16(%%rsp)\n"
"movq $0x%08x, %%rcx\n"
"call __afl_maybe_log\n"
"movq 16(%%rsp), %%rax\n"
"movq 8(%%rsp), %%rcx\n"
"movq 0(%%rsp), %%rdx\n"
"leaq (128+24)(%%rsp), %%rsp\n"
"\n"
"/* --- END --- */\n"
"\n";

分别使用afl-gcc与原生gcc编译代码,在IDA中查看

原生gcc

image-20221129141053608

image-20221129141143831

afl之后的

image-20221129141158981

image-20221129141032589

我们可以看出,插入的汇编就是调用了_afl_maybe_log() 以及一些寄存器,栈的设置

具体的就是下面这几句

1
2
3
4
5
6
7
8
9
10
lea     rsp, [rsp-98h]
mov [rsp+98h+var_98], rdx
mov [rsp+98h+var_90], rcx
mov qword ptr [rsp+98h+input], rax
mov rcx, 0DBA6h #插桩随机数
call __afl_maybe_log
mov rax, qword ptr [rsp+98h+var_88] #这里ida识别成input乐,但也就是var_88
mov rcx, [rsp+98h+var_90]
mov rdx, [rsp+98h+var_98]
lea rsp, [rsp+98h]

保存了当前的rax,rdx,rcx,rsp寄存器到栈上,调用函数后再恢复对应寄存器的值

__afl_maybe_log流程

这里流程图等引用乐@Roland_ 师傅做的图

img

函数的实现也定义于afl-as.h中,被定义为main_payload

源码中直接写的是汇编代码~~~我这里本来选择用IDA查看其反汇编方便理解,发现反汇编出来的C也不是很好看(~~~

以下源码均来自main_payload_64,函数顺序基本也按汇编代码的顺序来

先看源码内定义的静态变量

1
2
3
4
5
6
7
8
9
10
11
  "  .lcomm   __afl_area_ptr, 8\n"
#ifndef COVERAGE_ONLY
" .lcomm __afl_prev_loc, 8\n"
#endif /* !COVERAGE_ONLY */
" .lcomm __afl_fork_pid, 4\n"
" .lcomm __afl_temp, 4\n"
" .lcomm __afl_setup_failure, 1\n"

#endif /* ^__APPLE__ */

" .comm __afl_global_area_ptr, 8, 8\n"
  • __afl_area_ptr 指向共享内存
  • __afl_prev_loc 代表上一次插桩(插桩随机数)
  • __afl_fork_pid 为fork的子进程pid
  • __afl_temp 为临时的缓冲区
  • __afl_setup_failure 代表设置标志位
  • __afl_global_area_ptr 指向一个全局域

__afl_maybe_log

1
2
3
4
5
6
7
8
9
10
11
12
13
#if defined(__OpenBSD__)  || (defined(__FreeBSD__) && (__FreeBSD__ < 9))
.byte 0x9f /* lahf */
#else
lahf
#endif /* ^__OpenBSD__, etc */
seto %al

/* Check if SHM region is already mapped. */

movq __afl_area_ptr(%rip), %rdx\n
testq %rdx, %rdx\n
je __afl_setup\n

  • lah用于将标志寄存器的低八位送入AH,即将标志寄存器FLAGS中的SF、ZF、AF、PF、CF五个标志位分别传送到累加器AH的对应位,八位中有三位是无效的;
  • SF(符号标志位),ZF(零标志位),AF(辅助进位标志位),PF(奇偶标志位),CF(进位标志位)
  • img
  • seto指令:将溢出置位。
  • 然后检查是否有共享内存,没有的话就进入__afl_setup 进行设置

__afl_setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__afl_setup:
/* Do not retry setup if we had previous failures. */

cmpb $0, __afl_setup_failure(%rip)
jne __afl_return\n"

" /* Check out if we have a global pointer on file. */

#ifndef __APPLE__
movq __afl_global_area_ptr@GOTPCREL(%rip), %rdx
movq (%rdx), %rdx
#else
movq __afl_global_area_ptr(%rip), %rdx
#endif /* !^__APPLE__ */
testq %rdx, %rdx
je __afl_setup_first
movq %rdx, __afl_area_ptr(%rip)
jmp __afl_store
  • 如果已经设置过并且失败了,直接return
  • 否则检查__afl_global_area_ptr
    • 若为NULL,则跳转至__afl_setup_first
    • 否则设置 ___afl_area_ptr=__afl_global_area_ptr,跳转至 __afl_store

__afl_setup_first

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
__afl_setup_first:

/* Save everything that is not yet saved and that may be touched by
getenv() and several other libcalls we'll be relying on. */

leaq -352(%rsp), %rsp

movq %rax, 0(%rsp)
movq %rcx, 8(%rsp)
movq %rdi, 16(%rsp)
movq %rsi, 32(%rsp)
movq %r8, 40(%rsp)
movq %r9, 48(%rsp)
movq %r10, 56(%rsp)
movq %r11, 64(%rsp)

movq %xmm0, 96(%rsp)
movq %xmm1, 112(%rsp)
movq %xmm2, 128(%rsp)
movq %xmm3, 144(%rsp)
movq %xmm4, 160(%rsp)
movq %xmm5, 176(%rsp)
movq %xmm6, 192(%rsp)
movq %xmm7, 208(%rsp)
movq %xmm8, 224(%rsp)
movq %xmm9, 240(%rsp)
movq %xmm10, 256(%rsp)
movq %xmm11, 272(%rsp)
movq %xmm12, 288(%rsp)
movq %xmm13, 304(%rsp)
movq %xmm14, 320(%rsp)
movq %xmm15, 336(%rsp)

/* Map SHM, jumping to __afl_setup_abort if something goes wrong. */

/* The 64-bit ABI requires 16-byte stack alignment. We'll keep the
original stack ptr in the callee-saved r12. */

pushq %r12
movq %rsp, %r12
subq $16, %rsp
andq $0xfffffffffffffff0, %rsp

leaq .AFL_SHM_ENV(%rip), %rdi
call _getenv

testq %rax, %rax
je __afl_setup_abort

movq %rax, %rdi
call _atoi

xorq %rdx, %rdx /* shmat flags */
xorq %rsi, %rsi /* requested addr */
movq %rax, %rdi /* SHM ID */
call _shmat

cmpq $-1, %rax
je __afl_setup_abort

/* Store the address of the SHM region. */

movq %rax, %rdx
movq %rax, __afl_area_ptr(%rip)

movq %rax, __afl_global_area_ptr(%rip)
movq %rax, %rdx
__afl_forkserver:
  • 前面一大坨是保存所有的寄存器至栈上,然后对栈进行对齐(16字节)

  • 调用getenv( AFL_SHM_ENV) 获取环境变量__AFL_SHM_ID,该值是共享内存的shmid

    • 获取失败 ,跳至__afl_setup_abort

    • 获取成功,设置参数,调用shmat,将共享内存映射至当前进程的地址空间,该函数会返回映射后共享内存的地址

      • 调用失败 ,跳至__afl_setup_abort
      • 调用成功,设置__afl_area_ptr指向共享内存,__afl_global_area_ptr 也指向共享内存

__afl_forkserver:

紧接 __afl_setup_first

rdx为上面的rax,也就是共享内存的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__afl_forkserver:

/* Enter the fork server mode to avoid the overhead of execve() calls. We
push rdx (area ptr) twice to keep stack alignment neat. */

pushq %rdx
pushq %rdx

/* Phone home and tell the parent that we're OK. (Note that signals with
no SA_RESTART will mess it up). If this fails, assume that the fd is
closed because we were execve()d from an instrumented binary, or because
the parent doesn't want to use the fork server. */

movq $4, %rdx /* length */
leaq __afl_temp(%rip), %rsi /* data */
movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */
CALL_L64("write")

cmpq $4, %rax
jne __afl_fork_resume
  • FORKSRV_FD + 1 为状态管道,向该管道写入4个字节,用于通知fork server 已经成功启动
  • 成功写入四个字节的话则继续,否则跳转至__afl_fork_resume

__afl_fork_wait_loop

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
__afl_fork_wait_loop:

/* Wait for parent by reading from the pipe. Abort if read fails. */

movq $4, %rdx /* length */
leaq __afl_temp(%rip), %rsi /* data */
movq $" STRINGIFY(FORKSRV_FD) ", %rdi /* file desc */
CALL_L64("read")
cmpq $4, %rax
jne __afl_die

/* Once woken up, create a clone of our process. This is an excellent use
case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly
caches getpid() results and offers no way to update the value, breaking
abort(), raise(), and a bunch of other things :-( */

CALL_L64("fork")
cmpq $0, %rax
jl __afl_die
je __afl_fork_resume

/* In parent process: write PID to pipe, then wait for child. */

movl %eax, __afl_fork_pid(%rip)

movq $4, %rdx /* length */
leaq __afl_fork_pid(%rip), %rsi /* data */
movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */
CALL_L64("write")

movq $0, %rdx /* no flags */
leaq __afl_temp(%rip), %rsi /* status */
movq __afl_fork_pid(%rip), %rdi /* PID */
CALL_L64("waitpid")
cmpq $0, %rax
jle __afl_die

/* Relay wait status to pipe, then loop back. */

movq $4, %rdx /* length */
leaq __afl_temp(%rip), %rsi /* data */
movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */
CALL_L64("write")

jmp __afl_fork_wait_loop
  • 读取控制管道
    • 读到4字节信息则成功
      • 将所读信息放入__afl_temp 中
      • 继续
    • 读取失败,跳转至__afl_die
  • 调用fork
    • 子进程执行__afl_fork_resume
    • 父进程将子进程pid放入__afl_fork_pid,并向状态管道写入该pid值
      • 等子进程执行完,再次向状态管道写入其status 状态
      • 执行下一次__afl_fork_wait_loop

__afl_fork_resume

这是上面fork出来的子进程干的事

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
__afl_fork_resume:

/* In child process: close fds, resume execution. */

movq $" STRINGIFY(FORKSRV_FD) ", %rdi
CALL_L64("close")

movq $(" STRINGIFY(FORKSRV_FD) " + 1), %rdi
CALL_L64("close")

popq %rdx
popq %rdx

movq %r12, %rsp
popq %r12

movq 0(%rsp), %rax
movq 8(%rsp), %rcx
movq 16(%rsp), %rdi
movq 32(%rsp), %rsi
movq 40(%rsp), %r8
movq 48(%rsp), %r9
movq 56(%rsp), %r10
movq 64(%rsp), %r11

movq 96(%rsp), %xmm0
movq 112(%rsp), %xmm1
movq 128(%rsp), %xmm2
movq 144(%rsp), %xmm3
movq 160(%rsp), %xmm4
movq 176(%rsp), %xmm5
movq 192(%rsp), %xmm6
movq 208(%rsp), %xmm7
movq 224(%rsp), %xmm8
movq 240(%rsp), %xmm9
movq 256(%rsp), %xmm10
movq 272(%rsp), %xmm11
movq 288(%rsp), %xmm12
movq 304(%rsp), %xmm13
movq 320(%rsp), %xmm14
movq 336(%rsp), %xmm15

leaq 352(%rsp), %rsp

jmp __afl_store
  • 关闭该进程的FORKSRV_FD和FORKSRV_FD+1
  • 恢复了寄存器状态(__afl_setup_first时的状态)
  • 跳转至__afl_store

__afl_store

1
2
3
4
5
6
7
8
9
__afl_store:

/* Calculate and store hit for the code location specified in rcx. */

xorq __afl_prev_loc(%rip), %rcx
xorq %rcx, __afl_prev_loc(%rip)
shrq $1, __afl_prev_loc(%rip)

incb (%rdx, %rcx, 1)
  • rcx为调用_afl_maybe_log前所传的,值为插桩随机数id
  • __afl_prev_loc为上一次桩的id

简单来说,就是__afl_prev_loc去异或本次桩的id, 得到一个新的值x

再将_ afl_prev_loc与该新的值异或,得到新的_ afl_prev_loc

再将 _ _afl_prev_loc右移动1位,得到最后全新的 __afl_prev_loc

最后将共享内存中存储当前插桩位置的值+1

参考@Roland_ 的解释,这里右移是为了避免桩id无法区分的情况,尤其是在上次桩与本次是同一个id,即

A->A,这种情况,第一次异或得到0,第二次异或得到自己,id不变,难以区分

A->B,B->A也是一样,第一次 A ^( A ^B) =B , 对B来说就是相同id乐

看一眼伪代码

1
2
3
4
v7 = _afl_prev_loc ^ a4;
_afl_prev_loc ^= v7;
_afl_prev_loc = (unsigned __int64)_afl_prev_loc >> 1;
v8 = __CFADD__((*(_BYTE *)(v6 + v7))++, 1);

__afl_die

1
2
xorq %rax, %rax
CALL_L64("_exit")

调用乐exit退出

__afl_setup_abort:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
__afl_setup_abort:

/* Record setup failure so that we don't keep calling
shmget() / shmat() over and over again. */

incb __afl_setup_failure(%rip)

movq %r12, %rsp
popq %r12

movq 0(%rsp), %rax
movq 8(%rsp), %rcx
movq 16(%rsp), %rdi
movq 32(%rsp), %rsi
movq 40(%rsp), %r8
movq 48(%rsp), %r9
movq 56(%rsp), %r10
movq 64(%rsp), %r11

movq 96(%rsp), %xmm0
movq 112(%rsp), %xmm1
movq 128(%rsp), %xmm2
movq 144(%rsp), %xmm3
movq 160(%rsp), %xmm4
movq 176(%rsp), %xmm5
movq 192(%rsp), %xmm6
movq 208(%rsp), %xmm7
movq 224(%rsp), %xmm8
movq 240(%rsp), %xmm9
movq 256(%rsp), %xmm10
movq 272(%rsp), %xmm11
movq 288(%rsp), %xmm12
movq 304(%rsp), %xmm13
movq 320(%rsp), %xmm14
movq 336(%rsp), %xmm15

leaq 352(%rsp), %rsp

jmp __afl_return

与__afl_fork_resume差不多,区别就是这里直接return乐

__afl_return

1
2
3
4
__afl_return:

addb $127, %al
ret

暂时不知道这127加乐是图啥,可能用来区别正常exit退出和return退出的吧

afl-fuzz.c —-fuzzer

该函数主要实现了fuzzer,就是它来不断变异我们的样例来得到改变执行路径的效果,源码8000行(顺带一提,glibc实现的malloc在5000行左右,里面还有很多是宏和注释0.0

最近下载了understand玩玩,刚好拿这个来试试,生成的CFG

image-20221129232414704

虽然很强大,但这么多我能看明白个√⑧

在阅读源码之前,先有个整体的大概了解,方便我们深入阅读,

这里还是引用了@Roland_ 做的图

img

结合该图与上文分析的插桩,就能大概明白插桩那些函数具体怎么用的了

由fork出的子进程来执行我们的程序实例,通过运行插桩代码中的函数与fuzzer进行交互,从而提取状态信息

函数很多,这里我就不选择逐一分析乐,而是通过main函数的流程来分析各个函数

关于fork server

我们需要监控的程序由fork server进行fork,而不由fuzzer进行fork。一般来说,我们要执行一个新的进程示例,会调用fork之后执行execve替换子进程,我们的fuzzer需要大量的进程创建,若都采取这种方式,每次调用execve都会耗费大量时间用于载入目标文件以及相关库、解析符号地址等操作,且还是重复性的。

作者Jann Horn 提出了一个方法,也就是afl插桩的由来:注入桩代码,当程序运行至桩处,收到管道命令时,调用fork来加载当前程序的克隆,由于都是同一个程序,就不用再调用exceve(而且还有cow机制,速度很快),节省了大量的时间耗费,而fuzzer不再是测试进程的父进程了,而是祖父进程,只需监视管道信息即可

感兴趣的可以看看这篇文章:

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

main(1) 配置与输出化

  • *以下均为main流程以及相关函数调用

  • 调用gettimeofday获取当前时间

  • 用当前时间与当前进程pid做异或运行作为seed

  • while循环,调用getopt,关于该函数的详解可以看https://blog.csdn.net/c1523456/article/details/79173776,主要用于处理命令行参数

    • -i代表input目录,将参数赋值给in_dir
    • -o代表output目录,将参数赋值给out_dir
    • -M,-s指定 master_id,sync_id
    • -f代表outputfile,为输出的目标文件
    • -t设置timeout
    • -m设置memery
    • -b 绑定cpu
    • -Q代表qemu mode
    • -C代表crash mode
    • -n代表dumb mode
  • 调用setup_signal_handlers与check_asan_opts

setup_signal_handlers

该函数为注册信号处理函数,设置句柄

信号 作用
SIGHUP/SIGINT/SIGTERM handle_stop_sig,处理各种“stop”情况
SIGALRM handle_timeout,处理超时的情况
SIGWINCH handle_resize,处理窗口大小
SIGUSER1 handle_skipreq,用户自定义信号,这里定义为skip request
SIGSTP/SIGPIPE SIG_IGN,不是很重要的一些信号,可以不用关心

check_asan_opts

获取MSAN_OPTIONS和ASAN_OPTIONS环境变量,做一些检查

ASAN(Address Sanitizer)是针对 C/C++ 的快速内存错误检测工具,在运行时检测 C/C++ 代码中的多种内存错误。

  • 若sync_id被设置,调用fix_up_sync

fix_up_sync

设置sync_dir为out_dir,outdir为out_dir/sync_id

  • 判断in_dir和out_dir是否相等,它们不应该相等
  • 判断dumb_mode
    • 如果有dumb_mode,就不能有crash_mode或qemu_mode
  • 获取环境变量AFL_NO_FORKSRV,AFL_NO_CPU_RED,AFL_NO_ARITH,AFL_SHUFFLE_QUEUE,AFL_FAST_CAL,若存在,则将相关变量置为1
  • 获取AFL_HANG_TMOUT,存访进hang_tmout
  • 调用save_cmdline,做了一份命令行参数的拷贝,放在orig_cmdline
  • 调用fix_up_banner,不是很重要,先跳过
  • 调用check_if_tty,如果我们是在tty上运行,就会设置tty相关变量,读取窗口大小
  • 调用get_core_count,计算cpu核数
  • 调用check_crash_handling,确保核心转储不会进入该程序
  • 调用check_cpu_governor,检查cpu的管理者
  • 调用setup_post,加载post process
  • 调用setup_shm

setup_shm

这里涉及到了in_bitmap,这是输入的bitmap,有8位

u8 virgin_bits[MAP_SIZE],代表的是尚未被fuzzing的区域

u8 virgin_tmout[MAP_SIZE],记录超时信息

u8 virgin_crash[MAP_SIZE],记录崩溃信息

如果没有in_bitmap,就初始化virgin_bits为255(全是1)

初始化virgin_tmout与virgin_crash为255(全是1)

调用shmget分配MAP_SIZE的共享内存,记录为shm_id,字符串记录为shm_str

如果没有设置dumb_mode,就设置SHM_ENV_VAR为shr_str

调用shmat,将共享内存映射到进程中,返回值即共享内存地址,存入trace_bits

  • 调用init_count_class16

init_count_class16

trace_bits存放于共享内存中,用一个字节来记录该路径是否被到达,以及该路径被命中的次数

该函数主要用于初始化count_class_lookup16[65536] 数组,该数组会被初始化为类似这样

1
2
3
4
5
6
7
8
9
10
11
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
};

当每次命中新路径时,就是利用该数组去对路径命中数进行规整处理,比如如果是9的话就计算为16这样子。

这里引用@sakura 师傅的解释:

为什么这么做呢,主要是为了让一些循环不被当成不同的路径,比如说一个很多次的循环,循环第n次和第n+1次是完全一样的,这样子可以尽可能减少命中次数导致的区别

  • 调用setup_dirs_fds

setup_dirs_fds

该函数主要用于准备输出目录与文件

  • 存在sync_id的话,建立sync_dir目录

  • 建立out_dir目录

  • 以只读模式打开out_dir文件

  • 建立out_dir/queue目录,然后在该目录下

    • 建立.state目录,该目录保存queue metadata used for session

      resume and related tasks

      • 建立.state/deterministic_done目录,该目录保存过去经历过deterministic fuzzing的queue entries
      • 建立.state/auto_extras目录,该目录保存auto-selected dictionary entries
      • 建立.state/redundant_edges目录,该目录保存当前认为的多余路径
      • 建立.state/variable_behavior目录,该目录保存showing variable behavior路径的集合
  • 如果sync_id存在,建立out_dir/.synced目录,用于keeping track of cooperating fuzzers,也就是跟踪同步

  • 建立out_dir/crashes目录,保存crashes

  • 建立out_dir/hangs目录,保存hangs

  • 打开/dev/null以及/dev/urandom设备

  • 打开out_dir/plot_data文件,gnuplot为命令行的交互式绘图工具

    • 向该文件写入”# unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec\n”

到main

  • 调用read_testcases

read_testcases

该函数主要用于处理输入

  • 访问in_dir/queue

    • 如果存在,设置in_dir为in_dir/queue
  • 调用scandir,扫描in_dir

  • 遍历该文件夹下的所有文件名字符串

    • 跳过 . 和 ..

    • 判断文件的size是否大于 MAX_FILE

    • 判断是否是已经被确定性变异(deterministic fuzzing)的input,如果是的话就条过

    • 代码里的注释:检查指示该条目的确定性模糊测试已完成的元数据。 我们不想在恢复中止扫描时重复确定性模糊测试,因为这毫无意义,并且可能非常耗时

    • 对文件调用add_to_queue

add_to_queue

定义乐一个queue_entry结构体,以链表的形式串起来

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

};

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;

如果有队列头且(queued_paths - 1) % 100 == 0 && queued_paths > 1,就设置

1
2
q_prev100->next_100 = q;
q_prev100 = q;

一般的话设置

1
q_prev100 = queue = queue_top = q;

没有的话,就设置这个q为队列头

1
2
queue_top->next = q;
queue_top = q;

queued_paths++ (queue路径计数)

pending_not_fuzzed++ (待fuzz样例计数)

last_path_time更新为当前时间。

  • 回到main ,调用load_auto

load_auto

load自动生成的提取出来的词典token

  • 遍历 USE_AUTO_EXTRAS次,尝试打开 %s/.state/auto_extras/auto_%06u", in_dir, i
    • 失败直接结束
    • 成功就读取MAX_AUTO_EXTRA + 1个字节放入tmp中
      • 然后调用maybe_add_auto(tmp, len)

maybe_add_auto

  • !MAX_AUTO_EXTRAS || !USE_AUTO_EXTRAS ,直接return

  • 循环遍历,将tmp[i]异或tmp[i+1] ,相同则结束,遍历次数为len

  • len==2

    • 和interesting_16数组里的元素比较,如果和其中某一个相同,就直接return
  • len==4

    • 和interesting_32数组里的元素比较,如果和其中某一个相同,就直接return
  • 与extras数组元素进行比较,到其元素值>=len即return

  • 源码注释:

    拒绝任何与现有extras内容相匹配的内容。 进行不区分大小写的匹配。 我们利用 extras[] 按大小排序这一事实进行优化。

  • auto_changed = 1

  • 从0开始遍历到a_extras_cnt

    • 如果a_extras[i].len == len && !memcmp_nocase(a_extras[i].data, mem, len
      • 就将a_extras[i].hit_cnt++,这个值代表在语料中被使用的次数
      • 跳转至sort_a_extras
  • 执行到这里,我们看起来就是在处理一个新的entry乐,如果有空间的话,我们会加上这个entry,否则会随机删去一些entry,具体的:

    • 如果a_extras_cnt < MAX_AUTO_EXTRAS,直接添加即可
    • 否则执行
      • i = MAX_AUTO_EXTRAS / 2 + UR((MAX_AUTO_EXTRAS + 1) / 2);
        ck_free(a_extras[i].data);
      • 也就是free掉后半部分中的随机一个

回到main,调用pivot_inputs

pivot_inputs

在输出目录中为输入测试用例创建硬链接,选择好的名称并相应地进行旋转

也就是为in_dir中的test cases,在out_dir中创建hard link

  • 遍历queue entry链表

    • 找到fname中最后一个‘/’
    • 一些字符串比较操作,就是用来找到test case的
    • 创建相关hard link到out_dir/queue/rsl
    • fname改为指向这个hard link路径
  • 如果设置了q->passed_det,就调用mark_as_det_done(q),主要就是打开out_dir/queue/.state/deterministic_done/use_name,,设置passed_det=1,确保passed_det 值有被覆盖

  • 如果设置了in_place_resume,就是调用nuke_resume_dir,该函数主要删除out_dir/_resume/下所有id:为前缀的文件

回到main

  • 如果存在extras_dir,就调用load_extras(extras_dir),该函数就是从该路径中读取extras到extras数组里,按size大小排序。
  • 如果没有设置timeout_given,调用 find_timeout
    • 该函数主要就是在out_dir/fuzzer_stats或者in_dir/../fuzzer_stats中找exec_timeout字符串,找到了就读取数值,小于等于4就直接return,否则设置timeout_given = 3
  • 调用detect_file_args(argv + optind + 1),
    • 该函数处理命令行参数,替换‘@@’ in args,替换为out_dir/.cur_input
  • 如果没有设置out_file,就调用setup_stdio_file
    • 删除原本的out_dir/.cur_input
    • 创建新的out_dir/.cur_input,fd保存为out_fd
  • 调用check_binary(argv[optind]),确保我们要执行的程序存在,且不能是shell script
  • 调用get_cur_time,获取当前时间保存为start_time
  • 判断是否设置了qemu_mode
    • 设置了的话,调用use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind)
    • 否则use_argv = argv + optind
  • 调用perform_dry_run(use_argv)

在此之前,讲一些与各种结构体,变量处理相关的函数,下面会调用到

has_new_bits

参数为(u8* virgin_map)

用于检测有没有新路径,或者是某一路径的执行次数有所不同

virgin_bits保存还没有被Fuzz覆盖到的byte,其初始值每位全被置位1,然后每次按字节置位

  • 初始化current,virgin为trace_bits,virgin_map的首地址

  • 每次从共享内存(trace_bits)中取8字节

    • 如果current不为0,且*current & *virgin 也不为0,则代表发现了新路径,或某次路径执行次数和有变。

      • 如果ret<2

        • 取current首字节地址为cur,virgin的首字节地址为vir

        • 比较cur[i] && vir[i] == 0xff,如果有一个为真,则设置ret为2 (i取值为0-7)

        • 在源码中直接是这么写的

        • ```c
          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

          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

          - 这代表了发现了新的tuple,设置ret=2

          - 否则 ret=1

          - 代表仅仅只是改变了某个tuple的hit-count

          - \*virgin &= ~*curren

          - current++ , virgin++ ,一直循环直至遍历完MAPSIZE

          - 如果ret && virgin_map == virgin_bits

          - bitmap_changed=1



          ### classify_counts

          参数为u64 *mem

          - 每次从mem中读取8字节,直至读完

          - 每次取两个字节,u16* mem16 = (u16*)mem

          - 然后进行这样的操作

          - ```c
          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]];
    • 在count_class_lookup16数组中找到对应的值并复制给mem

count_bytes

计算bitmap中 被 set的bytes的数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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;
}

源码很短很清晰,这里直接引用@sakura师傅的

  • 初始化计数器ret的值为0,循环读取mem里的值,每次读取4个字节到u32变量v中

  • 如果v为0,则代表这四个字节都是0,直接跳过,进入下一次循环

  • 如果v不为0,则依次计算 v & FF(0),v & FF(1),v & FF(2),v&FF(3)

    的结果,如果不为0,则计数器ret加一。

    • #define FF(_b) (0xff << ((_b) << 3))
      • (_b) << 3)_b * 8
      • 0x000000ff左移(_b * 8)
      • 最终结果可以是0x000000ff,0x0000ff00,0x00ff0000,0xff000000其中之一

**update_bitmap_score

perform_dry_run

执行所有测试用例以确认应用程序按预期工作。 这仅针对初始输入完成,并且仅完成一次。

  • 设置cal_failures = 0,获取skip_crashes,然后开始一个遍历queue_entry链表的主循环

    • 打开q->fname文件,读取到use_mem中,
    • 调用res = calibrate_case(argv, q, use_mem, 0, 1)
    • 函数描述:校准新的测试用例。 这是在处理输入目录时完成的,以尽早警告不稳定或其他有问题的测试用例; 以及何时发现新路径以检测变量行为等。
    • 如果stop_soon=1,直接return
    • 否则根据返回值res判断错误类型
      • FAULT_NONE
        • q是queue_top的话,也就是第一个测试样例,调用check_map_coverage,以评估map覆盖率
        • 如果设置了crash_mode,抛出异常
      • FAULT_TMOUT
        • 如果设置了timeout_given
          • 如果timeout_given>1
            • q->cal_failed = CAL_CHANCES
            • cal_failures++
          • 否则输出了个 FATAL(“Test case ‘%s’ results in a timeout”, fn)
        • 输出了个 FATAL(“Test case ‘%s’ results in a timeout”, fn)
      • FAULT_CRASH
        • 如果设置了crash_mode,直接break
        • 如果设置了skip_crashes
          • 跳过,q->cal_failed = CAL_CHANCES,al_failures++
        • 如果指定了mem_limit,可能会建议摩多摩多
      • FAULT_ERROR
        • 没法运行程序,错误了,狗叫一下异常
      • FAULT_NOINST
        • 没有任何路径信息,也是狗叫一下异常
      • FAULT_NOBITS
        • 没有任何新的路径,抛出警告,认为这是无用路径
        • useless_at_start++
      • q->var_behavior为真,表示多次运行乐
        • 抛出警告,Instrumentation output varies across runs.,表示该样例路径输出可变。
        • useless_at_start++
      • q=q->next
    • 循环结束,判断cal_failures
      • 如果cal_failures == queued_paths,输出异常,crash或time out
      • 如果cal_failures * 5 > queued_paths,抛出警告,被拒绝的test cases占比过高了

calibrate_case

校准新的测试用例。 这是在处理输入目录时完成的,以尽早警告不稳定或其他有问题的测试用例; 以及何时发现新路径以检测变量行为等。

  • 创建first_trace[MAP_SIZE]

  • first_run = (q->exec_cksum == 0),如果为0代表第一次运行,first_run设置为1

  • 保存stage_max,exec_tmout,stage_name

  • 如果是resuming_fuzz=1,或者from_queue为0,代表正在resuming sessions或者不来自queue,将use_tmout扩大。

  • q->cal_failed++

  • 如果有fast_cal,stage_max为3,否则为CAL_CYCLES(8)

  • stage_name = “calibration”

  • 如果不是dumb_mode,且no_forkserver=1,且forksrv_pid没被设置

    • 调用init_forkserver(argv),启动fork server
  • 如果q->exec_cksum被设置

    • hnb = has_new_bits(virgin_bits)
    • 如果hnb > new_bits, new_bits = hnb
  • 获取cur_time到start_us

  • 执行stage_max次calibration stage

    • 如果不是第一次跑且!(stage_cur % stats_update_freq)(queue不来自input,而是评估新的case),输出状态信息,用来展示此次执行结果

    • 调用write_to_testcase(use_mem, q->len)

      • q->fname的文件内容写入.cur_input中

      • 调用fault = run_target(argv, use_tmout),执行程序,然后判断fault返回值

      • stop_soon或者fault != crash_mode,就abort(stop_soon 就是按下了ctrl c触发中断那种)

      • 如果这是calibration stage第一次运行,且不在dumb_mode,且共享内存里没有任何路径(即没有任何byte被置位),设置fault为FAULT_NOINST,然后goto abort_calibration。

      • 计算共享内存里有多少字节被置位了,通过count_bytes函数

        • u32 count_bytes(u8 *mem)
      • 计算hash32(trace_bits, MAP_SIZE, HASH_CONST)的结果,其值为一个32位uint值,保存到cksum中

      • 如果q->exec_cksum != cksum,即第一次运行,亦或是相同参数下的多次执行,但是cksum却不同,单表路径可变

        • hnb = has_new_bits(virgin_bits)
        • 如果hnb>new_bits,设置hnb为new_bits
        • 如果q->exec_cksum被设置,则我们来判断该queue是否可变
          • i从0到MAP_SIZE
            • first_trace[i] != trace_bits[i],同时var_bytes[i]为0,代表变化了,说明确实是可变的
              • 设置var_bytes[i]=1
              • 设置stage_max = CAL_CYCLES_LONG
            • 设置var_detected = 1
        • 否则即第一次执行
          • 设置q->exec_cksum = cksum,
          • 复制trace_bits到first_trace
      • 获取当前cur_time为stop_us

      • 计算total_cal_us += stop_us - start_us,total_cal_cycles += stage_max

      • 接下来是收集关于这个测试样例性能的统计数据,这将会被在calculate_score中被用来计算。

      • q->exec_us 保存单次queue执行时间(通过平均值计算)

      • q->bitmap_size存贮count_bytes(trace_bits),也就是最后一次执行所覆盖的路径数

      • q->handicap = handicap

      • q->cal_failed = 0

      • total_bitmap_size += q->bitmap_size,也就是加上了这个queue所覆盖到的路径数

      • total_bitmap_entries++

      • update_bitmap_score(q),更新该queue

      • ```
        update_bitmap_score:当我们遇到一条新路径时,我们调用它来查看该路径是否比任何现有路径看起来更“有利”。 “favorables”的目的是拥有一组最小的路径来触发到目前为止在位图中看到的所有位,并以牺牲其余部分为代价专注于模糊它们。
        该过程的第一步是为位图中的每个字节维护一个 top_rated[] 条目列表。 如果之前没有竞争者,或者如果竞争者具有更有利的速度 x 大小因子,我们将赢得该位置。

        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

        - 如果fault为`FAULT_NONE`,且该queue是第一次执行,且不属于dumb_mode,而且new_bits为0,代表在这个样例所有轮次的执行里,都没有发现任何新路径和出现异常,设置fault为`FAULT_NOBITS`

        - 如果new_bits为2,且`q->has_new_cov`为0,设置其值为1,并将queued_with_cov加一,代表有一个queue发现了新路径。()

        - 如果该queue为可变路径,var_detected会为1

        - 调用count_bytes计算被置位的tuple,保存到var_byte_count,代表这些tuple可变
        - 调用 mark_as_variable(q),标记该queue位variable
        - queued_variable++

        - 恢复stagename,cur,max

        - 如果不是第一次运行该queue,调用show_stats()

        - 返回fault的值

        ### init_forkserver

        代码中有注释描述:启动 fork 服务器(仅限检测模式)。 这个想法在这里解释:
        http://lcamtuf.blogspot.com/2014/10/fuzzing-binaries-without-execve.html
        本质上,检测允许我们跳过 execve(),并继续clone一个停止的child process。 所以,我们只执行一次,然后通过管道发送命令。 该逻辑的另一部分在 afl-as.h 中。

        ​ 我看了一下这篇文章,主要就是讲了向二进制文件注入代码(也就是我们afl-as干的事)的想法,减少了每次fork 然后exec的开销,而fuzzer不在是process的直接父进程,而是祖父进程,它不能直接使用*waitpid()*;也没有其他简单、可移植的 API 来获取有关进程退出状态的通知。而是通过共享内存的方法解决这些问题。这在当时让fuzz的效率提升了两倍以上。~~我还没完全看懂,等完全看懂了再来改改~~

        - 创建两个管道,st_pipe状态管道和ctl_pipe控制管道
        - 调用fork ,返回forksrv_pid
        - 子进程
        - 设置了一下r.rlim_cur,r.rlim_max
        - 调用setsid(),把子进程变成新的会话组的领头进程,与父进程脱离乐
        - 重定向文件描述符1,2到dev_null_fd
        - 如果指定了out_file
        - 重定向文件描述符0到dev_null_fd
        - 否则
        - 重定向文件描述符0到out_fd
        - 关闭out_fd
        - 重定向FORKSRV_FD到ctl_pipe控制管道读端
        - 重定向FORKSRV_FD+1到st_pipe状态管道写端
        - 获取一些环境变量
        - 设置ASAN_OPTIONS,MSAN_OPTIONS环境变量
        - 调用execv(target_path, argv)执行目标程序
        - 如果execv失败了,设置*(u32*)trace_bits = EXEC_FAIL_SIG 并退出
        - 父进程
        - fsrv_ctl_fd到ctl_pipe控制管道写端
        - fsrv_st_fd到st_pipe状态管道读端
        - 等待fork server启动,但是不能等太久
        - 从管道里读取4个字节到status里(也就是我们桩代码向状态管道写的那四个字节),如果读取成功,则代表fork server成功启动,就结束这个函数并返回。
        - 如果超时,就抛出异常
        - 剩下的就是一些check乐,毕竟没有读取成功

        总的来说 ,可以用该图简单概括

        ![未命名绘图.drawio](https://images.seebug.org/content/images/2021/10/08/1633680605000-28hriil.png-w331s)





        **回到main**

        调用cull_queue()

        ### cull_queue

        调整queue,我们会有函数遍历 top_rated[] entries,并将曾经没有见过的 bytes(temp_v)设置为更优先的,在整个fuzz过程中,优先的entries应该被给予更多的时间

        - 如果设置了dumb_mode或score_changed为0,直接return
        - score_changed=0
        - 初始化temp_v每个字节为0xff
        - 遍历queue,让每个queue->favored=0
        - 循环MAP_SIZE次,索引为i
        - 这个循环是为了找出一组queue entry,他们能覆盖到现在已经覆盖到的所有路径(有点类似最小生成树的感觉),由于是顺序找的,所以是贪心算法,并没有去筛选“最小的queue集合组”
        - `if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7))))`
        - 其实就是取temp_v的path那一位
        - 如果top_rated[i],且该path又被置位
        - 就从temp_v中清除掉所有`top_rated[i]`覆盖到的path,将对应的bit置为0
        - 代表当前集合已经覆盖乐这些了
        - top_rated[i]->favored = 1
        - queued_favored++
        - 如果!top_rated[i]->was_fuzzed,也就是该路径还没被fuzz过,就pending_favored++
        - 遍历queue
        - mark_as_redundant(q, !q->favored)
        - 如果不是favored的queue,就被标记成redundant_edges

        **回到main**

        - 调用show_init_stats()
        - 显示统计信息的,略过
        - 调用seek_to = find_start_position()

        ### find_start_position

        恢复时,尝试找到开始的队列位置。 这只有在resume时,并且当我们可以找到初始的 fuzzer_stats 时 才有意义

        - 如果不是resuming_fuzz,就直接返回
        - 如果是in_place_resume,就打开`out_dir/fuzzer_stats`文件,否则打开`in_dir/../fuzzer_stats`文件
        - 读取文件内容到tmp,关闭文件
        - 找到“cur_path : ”,读取其中的cur_path值到ret
        - 若值大于queued_paths
        - ret=0
        - 返回ret

        **回到main**

        调用write_stats_file(0, 0, 0)

        调用save_auto()

        ### write_stats_file

        负责更新统计信息文件,以及监视

        这个直接看源码就好,里面对各种变量进行了一些处理再输出到out_dir/fuzzer_stats中



        ### save_auto

        保存自动生成的extras

        - auto_changed=0的话
        - return
        - auto_changed=0
        - 向("%s/queue/.state/auto_extras/auto_%06u", out_dir, i )文件中写入a_extras的内容



        **回到main**

        - 如果stop_soon被设置,直接停止fuzz,跳转到stop_fuzzing
        - 如果not_on_tty=0,也就是在tty设备上的话
        - sleep(4)
        - start_time += 4000
        - 如果stop_soon被设置,直接停止fuzz,跳转到stop_fuzzing

        ## main(2) fuzzer主循环

        - while(1)

        - 调用cull_queue(),精简queue
        - 如果queue_cur=0,代表所有queue已经被执行过一轮乐
        - queue_cycle++,记录执行轮次
        - current_entry = 0,cur_skipped_paths = 0
        - queue_cur = queue,准备开始新的一轮
        - 如果seek_to被设置,则循环(seek_to就是我们一开始找的起始位置,find_start_position的返回值)
        - current_entry++
        - seek_to--
        - queue_cur = queue_cur->next
        - 循环结束,也就是从seek_to所指定的queue开始执行
        - 调用show_stats(),刷新展示页面
        - 如果不在tty上
        - ACTF("Entering queue cycle %llu.", queue_cycle),刷新stdout
        - 判断queued_paths == prev_queued,
        - 若成立,也就是没有新的case被发现
        - (queued_paths为test cases的总数)
        - 判断use_splicing
        - 不为0,则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)
        - 该函数对queue_cur进行一次test
        - 调用fuzz_one(use_argv),返回值存入skipped_fuzz
        - fuzz_one(use_argv)不一定会执行当前queue_cur进行测试,内有一定判断
        - skipped_fuzz为0,sync_id被设置,且stop_soon为0
        - 若sync_interval_cnt为SYNC_INTERVAL倍数 且,sync_interval_cnt++
        - 调用 sync_fuzzers(use_argv)
        - 若stop_soon为0,exit_1被设置
        - 设置stop_soon=2
        - 如果stop_soon被设置
        - break
        - queue_cur = queue_cur->next,迭代到下一个queue
        - current_entry++(当前queue的entry id)
        - while(1)循环结束

        这是循环之外的乐

        - 如果仍有queue_cur,就调用show_stats()再次刷新展示
        - 然后如果是正常break的,也就是stopsoon=2
        - 杀掉父进程和子进程(forksrv_pid与child_pid)
        - 调用write_bitmap
        - 此函数向out_dir/fuzz_bitmap 写入virgin_bits内容
        - 调用write_stats_file
        - 更新统计信息文件
        - 调用save_auto
        - save_auto
        - stopfuzzing的操作
        - 一些文件的关闭,释放等,还有一点统计输出

        ### fuzz_one

        - 若定义了 IGNORE_FINDS
        - 如果 queue_cur->depth > 1,直接return1

        - 判断pending_favored是否为0
        - 如果不为0
        - 如果当前queue被fuzz过,或者当前queue不是favored的queue,有99%的概率直接返回1
        - 否则如果当前不是dumb__mode,且当前queue不是favored,且queued_paths也就是test cases的总数>10
        - 如果queue_cycle>1 也就是轮过一次,且当前queue没被fuzz过,说明这是个新的queue(吧?)
        - 有75%概率返回1
        - 否则有95%的概率返回1

        - 判断是否在tty上,略

        - 打开当前queue->fname文件

        - 调用mmap私有映射该文件内容,返回到in_buf和orig_in

        - 定义长为len的out_buf

        - **CALIBRATION** (only if failed earlier on)

        - 如果当前queue有校准错误,且小于3次,就调用calibrate_case再次校准,这是为了避免使用不合法的trace_bits
        - 更多相关的可以查看https://github.com/AFLplusplus/AFLplusplus/pull/425

        - **TRIMMING**

        - 如果不是dumb_mode,且该queue没有被trim过(!trim_done)
        - 调用trim_case(argv, queue_cur, in_buf)进行修剪
        - 设置queue的trim_done=1
        - 拷贝in_buf内容到out_buf

        - **PERFORMANCE SCORE**

        - 调用 calculate_score(queue_cur)获取当前queue score,存入perf_score,这是个计算时会用到的得分
        - 如果有skip_deterministic,或者当前queue被fuzz过,或者当前queue设置了passed_det
        - 跳转至havoc_stage
        - 如果master_max不为0,且(queue_cur->exec_cksum % master_max) != master_id - 1
        - 跳转至havoc_stage
        - doing_det = 1

        - **SIMPLE BITFLIP (+dictionary construction)**

        - ```c
        #define FLIP_BIT(_ar, _b) do { \
        u8* _arf = (u8*)(_ar); \
        u32 _bf = (_b); \
        _arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \
        } while (0)
        /* 源码里的注释
        在翻转每个字节中的最低有效位时,使用额外的技巧来检测可能的语法标记。本质上,这个想法是,如果你有一个像这样的二进制 blob:

        xxxxxxxxIHDRxxxxxxxx
        ...并且改变前导和尾随字节会导致程序流发生变化或没有变化,但是触摸“IHDR”字符串中的任何字符总是会产生相同的,独特的路径,“IHDR”很可能是一个原子检查的魔法对模糊格式具有特殊意义的值。

        我们在这里而不是作为一个单独的阶段执行此操作,因为这是使操作大致“免费”(即没有额外的执行程序)的好方法。

        根据经验,与在更具破坏性的更改时执行检查相比,在翻转最低有效位时执行检查是有利的,程序流可能会以更剧烈的方式受到影响。

        需要注意的是,我们不会在 -d 模式或 -S 模式下生成字典——但这可能是一个公平的权衡。

        对于表现出可变行为的路径,这不会特别有效,但会优雅地失败,所以我们无论如何都会进行检查。

        */

        我的理解就是,为了保证得到正确的路径,有些key word 之类的词是不能变的,当翻转了那些key word的话,路径就会完全不一样,这样的key word 我们当成token
        以下的操作就是AFL通过flip识别了这些token 并进行保存
        token长度默认最小是3,最大是32
        每次发现新token时,通过maybe_add_auto添加到a_extras数组里。

    做了一个位翻转的运算,参数为out_buf,stage_cur

    _arf每次取一字节,8位

    先看右边,128为二进制100000000,7为二进制111,_bf&7就是取后三位,128进行右移这个值,也就是右移0-7之间的值

    再看左边,_bf>>3 就是取除了后三位以外的值,也相当于/8

    stage_cur最大值为stage_max = len << 3,_bf>>3最大值就是 (len<<3)>>3=len

    _bf是逐一递增的,也就是说每8次,对应的_bf>>3是相同的,也就是每循环8次,_arf[i] 的i((_bf) >> 3)就会增加1

    而以每8次循环为一组,里面的每一次,128都将分别右移0-7之间的值(这样就是每一位都有一次为1,比如10000000,01000000,00100000这样子),但前面的_arf[i] 又是不变的,也就是对_arf[i]的值,每一位都进行了一次对1异或,1^1=0, 0^1=1,这样就刚好实现了翻转。

    理解起来还是不难的。

  • 这里的for循环还是直接贴源码

  • 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); //执行完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



    - 如果不是dumb_mode,且(stage_cur & 7) == 7,也就是刚好是一个字节循环(按8位为一组算)的最后一次翻转

    - 计算当前trace_bits的hash32为cksum
    - 如果当前到达最后一次循环并且cksum == prev_cksum
    - 若a_len小于MAX_AUTO_EXTRA
    - a_collect[a_len] = out_buf[stage_cur >> 3]
    - a_len++
    - 若a_len >= MIN_AUTO_EXTRA 且 a_len <= MAX_AUTO_EXTRA
    - 调用maybe_add_auto(a_collect, a_len),将新发现的token加入a_extra[]
    - a_len = 0
    - prev_cksum = cksum

    - 若cksum不等于当前queue的exec_cksum

    - 若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 (也就是加上新发现的路径的crahses)

    - stage_cycles[STAGE_FLIP1] += stage_max (循环执行的次数)

    - 接下来是类似之前的操作,但现在是连续翻转两位

    - bitflip 2/1

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

    }
    然后还有连续翻转四位的 bitflip 4/1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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);

    }
  • 生成Effector map

  •   /* 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_APcOS((_p) + (_l) - 1) - EFF_APOS(_p) + 1)
    
  • eff_map = ck_alloc(EFF_ALEN(len));

    eff_map[0] = 1;

  • 进行bitflip 8/8翻转,也就是对一个byte进行翻转,简单的异或0xFF 即可

  • 大部分操作与上面类似

  • 不同点:在对每个byte进行翻转时,如果其造成执行路径与原始路径不一致,就将该byte在effector map中标记为1,即“有效”的,否则标记为0,即“无效”的

  • 这样我们后续变异的时候,就会跳过那些不能带来变化的byte

  • 进行bitflip 16/8翻转,也就是对两个byte进行翻转

    • 如果这连续两个字节之前effector map已经置0了,就跳过此位
  • 进行bitflip 32/8翻转,对连续四个byte进行翻转,步骤与上面相同,也有effector map检查

  • 如果no_arith

    • 跳至skip_arith
  • ARITHMETIC INC/DEC

**sync_fuzzers

该函数主要读取sync文件夹下的queue,将有效的queue保存

calculate_score

通过覆盖的路径数量以及路径深度,执行速度等判断queue的得分

common_fuzz_stuff

就是按位翻转后执行的操作:写一个修该后的test case,执行程序,通过其结果判断该case是否为interesting,若该case被保存即返回1

  • 若post_hander不为0
    • out_buf=post_handeler(out_buf,&len),我们可以在里面对变异完的queue进行一定处理
    • 如果out_buf/len为0 直接return 0
  • write_to_testcase(out_buf, len)
  • fault = run_target(argv, exec_tmout) 执行
  • falut返回FAULT_TMOUT的话
    • 若subseq_tmouts++ > TMOUT_LIMIT,cur_skipped_paths++,直接返回1
  • 否则subseq_tmouts = 0
  • 若skip_requested为1
    • 重置skip_requested,cur_skipped_paths++,直接返回1
  • queued_discovered += save_if_interesting(argv, out_buf, len, fault)
  • show stats

save_if_interesting

用于判断该case是否为interesting,如果返回是1的话,就是interesting,该case会被保存

主要就是通过是否有 新的path/不同的path命中次数 出现来判断

而对于不同falut情况的处理

  • FAULT_TMOUT
    • 若发现新的超时路径,unique_tmouts++
    • hang_tmout>exec_tmout的话,则修改timeout为hang_tmout再run一次判断是否超时,仍然超时就记录,否则就同无新超时路径情况处理(直接返回)。
  • FAULT_CRASH
    • 判断是否产生新的crash路径
  • FAULT_ERROR
    • 抛出异常

参考资料

https://www.cnblogs.com/wsg1100/p/14290340.html

https://f0cus7.github.io/2022/05/14/fuzz-%E9%80%9A%E8%BF%87afl-training%E5%AD%A6%E4%B9%A0afl/

https://blog.csdn.net/bcbobo21cn/article/details/121940674

https://paper.seebug.org/1732/#afl-afl-asc

https://eternalsakura13.com/2020/08/23/afl/

https://bbs.pediy.com/thread-265936.htm#msg_header_h1_3

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