afl-forkserver-maneuver

# forkserver 的目的

参见 lcamtuf.blogspot
如果每次由 fuzzer 来进行 fork-execve 那么每次目标程序都得进装载器链接器和动态加载库打一套 十分浪费时间。
但是 如果我们劫持了目标程序的_start 让目标程序一开始执行我们写入的 forkserver 代码,这样就能在装载之后通过 COW 实现低成本的 fork。
也就是 forkserver 本身就注入到目标程序里面去了,作为目标程序的起始点不断的进行 fork。

# trampoline

这个被插桩到每个基本块之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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";
  1. 将当前 rsp 下降 (128+24)
  2. rdx rcx rax 分别保存在 rsp 上面一丢丢
  3. R(MAP_SIZE) 这个随机数 (0-65535) 给到 rcx 调用__afl_maybe_log (这个随机数是基本块标号,编译时确定)

# main_payload_64

这个主要是注入大量函数逻辑

# __afl_maybe_log

1
2
3
4
5
6
7
8
"__afl_maybe_log:\n"
" lahf\n"
" seto %al\n"

" movq __afl_area_ptr(%rip), %rdx\n"
" testq %rdx, %rdx\n"
" je __afl_setup\n"
"__afl_store:\n"
  • lahf : load to ah from eflags
  • seto : set to 1 if target byte overflow
  • 如果 __afl_area_ptr 里没有东西 就跳转到 __afl_setup (setup 属于共享内存没被初始化的情况,第一次 if 才会走到里面去)
  • 否则就进行 __afl_store
    其中 __afl_area_ptr 是共享内存的指针 attach 上 fuzzer 创建的。

# __afl_store

1
2
3
4
5
6
7
8
9
10
"__afl_store:\n"
" xorq __afl_prev_loc(%rip), %rcx\n"
" xorq %rcx, __afl_prev_loc(%rip)\n"
" shrq $1, __afl_prev_loc(%rip)\n"
" incb (%rdx, %rcx, 1)\n"

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

为了验证这个原理,上 gdb 调试一下,随便编译一个函数 打断点在 log 那 然后由于通信建立不起来 需要改 prev loc 的内存

1
2
3
4
gef➤  set {int}&__afl_area_ptr=1
gef➤ x/4gx &__afl_area_ptr
0x4040a0 <__afl_area_ptr>: 0x0000000000000001 0x0000000000000001
0x4040b0 <__afl_fork_pid>: 0x0000000000000000 0x0000000000000001

然后

1
2
3
4
→   0x4017a0 <__afl_store+0>  xor    rcx, QWORD PTR [rip+0x2901]        # 0x4040a8 <__afl_prev_loc>
0x4017a7 <__afl_store+7> xor QWORD PTR [rip+0x28fa], rcx # 0x4040a8 <__afl_prev_loc>
0x4017ae <__afl_store+14> shr QWORD PTR [rip+0x28f3], 1 # 0x4040a8 <__afl_prev_loc>
0x4017b5 <__afl_store+21> inc BYTE PTR [rdx+rcx*1]

其实就是 __afl_prev_loc = __afl_prev_loc ^ rcx ^ __afl_prev_loc = rcx 就是一个赋值的 trick
rcx 是这一块地址的标号 在操作之后变成了 上一块地址标号 ^ 当前本块地址标号、
上一块地址标号变成了 本块地址标号 >> 1。
rdx __afl_area_ptr(%rip), %rdx 就是共享内存指针了。

1
2
3
cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;

右移是为了区分 A -> B or B -> A or X -> X.

# __afl_setup

最重要的部分就是这个 shmat 的调用,attach 到 fuzzer 里 setup 的 shm
共享内存指针保存在 __afl_area_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
"\n"
" xorq %rdx, %rdx /* shmat flags */\n"
" xorq %rsi, %rsi /* requested addr */\n"
" movq %rax, %rdi /* SHM ID */\n"
CALL_L64("shmat")
"\n"
" cmpq $-1, %rax\n"
" je __afl_setup_abort\n"
"\n"
" /* Store the address of the SHM region. */\n"
"\n"
" movq %rax, %rdx\n"
" movq %rax, __afl_area_ptr(%rip)\n"

# __afl_forkserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"__afl_forkserver:\n"

" pushq %rdx\n"
" pushq %rdx\n"

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

" cmpq $4, %rax\n"
" jne __afl_fork_resume\n"

"__afl_fork_wait_loop:\n"

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

" cmpq $4, %rax\n"
" jne __afl_die\n"
"\n"
  1. 存入两次 __afl_area_ptr 的指针
  2. STRINGIFY ((FORKSRV_FD + 1)) 就是 # stringify
  3. 先写信息给 fuzzer 中去(写什么应该关系不大 就代表着来信息了)
  4. 然后从 forkserver 中读取 fuzzer 传输来的控制信息到 __afl_temp
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
CALL_L64("fork")
" cmpq $0, %rax\n"
" jl __afl_die\n"
" je __afl_fork_resume\n"

" movl %eax, __afl_fork_pid(%rip)\n"

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

" movq $0, %rdx /* no flags */\n"
" leaq __afl_temp(%rip), %rsi /* status */\n"
" movq __afl_fork_pid(%rip), %rdi /* PID */\n"
CALL_L64("waitpid")

" cmpq $0, %rax\n"
" jle __afl_die\n"

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

" jmp __afl_fork_wait_loop\n"
  • fork 出目标程序的子进程 跳转到 __afl_fork_resume
  • 对于 forkserver 本 f,保存子进程 pid 到 __afl_fork_pid
  • 然后写给 fuzzer
  • waitpid 等待子进程结束
  • 结束后吧 status 写回给 fuzzer,跳转回 __afl_fork_wait_loop

# __afl_fork_resume

这里主要是 由 forkserver fork 出来的 child 是要执行目标进程的,就需要恢复由于执行 forkserver 而压入的上下文以及关掉两个通信的文件描述符。
所以这里就是简单的恢复上下文,关闭,执行正常逻辑代码。

# summary

个人画的总结图,还可以吧?