# 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";
|
- 将当前 rsp 下降 (128+24)
- rdx rcx rax 分别保存在 rsp 上面一丢丢
- 将
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"
|
- 存入两次
__afl_area_ptr
的指针
- STRINGIFY ((FORKSRV_FD + 1)) 就是 # stringify
- 先写信息给 fuzzer 中去(写什么应该关系不大 就代表着来信息了)
- 然后从 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
个人画的总结图,还可以吧?
