这篇文章只分析64位的情况,32位大同小异,这里就不多阐述
trampoline_fmt_64
首先看到的是trampoline_fmt_64
这段汇编是在汇编器编译汇编到二进制时插入到汇编代码中的。
1 | static const u8* trampoline_fmt_64 = |
首先先在栈上开辟一段空间,然后将rdx,rcx,rax这三个寄存器的值保存到栈上面,将rcx的值赋值会一个随机数,这个随机数是在插入这段汇编的时候动态传进来的。
然后调用__afl_maybe_log,调用完之后,把栈上保存的值恢复回去,再把栈恢复
main_payload_64
接下来看的是main_payload_64,这里我就先删去一些对特定系统进行的patch,因为我们只是分析功能,实现的细节不用过于关注
__afl_maybe_log
首先看到的是__afl_maybe_log,也就是插入代码调用的部分
一开始看到两条指令是
1 | lahf |
这两条指令大概就是将标志寄存器FLAGS,溢出进位 保存到AH上面
1 | " movq __afl_area_ptr(%rip), %rdx\n" |
这里检查共享内存是否已经加载,如果加载了的话,__afl_area_ptr保存了共享内存的指针,否则就是NULL
这里默认共享内存已经加载,先看后面的部分,__afl_setup的部分后面再分析
__afl_store
1 | "__afl_store:\n" |
这部分是计算并储存代码命中位置,当前代码的位置在寄存器rcx中
假如没有定义COVERAGE_ONLY,那么前两条xor,是将__afl_prev_loc的值与rcx的值进行交换
然后将__afl_prev_loc的值右移一下,
下面是,假如定义了SKIP_COUNTS,那么就会执行
1 | or byte ptr[rdx+rcx], 1 |
如果没有定义的话,那么就会变成
1 | inc byte ptr[rdx+rcx] |
这里rdx的值存的是共享内存的地址
__afl_return
1 | "__afl_return:\n" |
这里首先是将 al+0x7f,然后再把标志寄存器FLAGS的值从AH中恢复回去,这里al+0x7f并不太了解是什么意思,但估计也是恢复标志寄存器,溢出进位的步骤吧
注意,这里调用afl_maybe_log,其实是执行到afl_return才返回的
__afl_setup
1 | " /* Do not retry setup if we had previous failures. */\n" |
首先判断之前有没有错误,有的话,直接就返回
1 | " /* Check out if we have a global pointer on file. */\n" |
第一个首先是判断我们是否有一个文件全局指针
即__afl_global_area_ptr是否为NULL
如果存在的话,就把afl_area_ptr的值放到rdx,调用afl_store,这里__afl_store就是我们上面分析过的
不存在的话,就继续到__afl_setup_first
__afl_setup_first
1 | " /* Save everything that is not yet saved and that may be touched by\n" |
这段代码的意思就是将剩下所有会被libc库函数影响的寄存器保存到栈上面
1 | " /* Map SHM, jumping to __afl_setup_abort if something goes wrong. */\n" |
这里是先保存r12,然后将栈指针保存到r12那里,再开一段栈空间,进行对齐
1 | " leaq .AFL_SHM_ENV(%rip), %rdi\n" |
这里就是调用getenv去拿存在环境变量中的共享内存标志符,拿不到的话,就会跳到__afl_setup_abort
1 | " movq %rax, %rdi\n" |
这里调用atoi将字符串转为数字,然后调用shmat拿到共享内存,然后判断一下shamat的结果,假如拿不到,也会跳到__afl_setup_abort
1 | " /* Store the address of the SHM region. */\n" |
这里是把共享内存的地址存到afl_area_ptr和afl_global_area_ptr指向的内存
__afl_forkserver
到这里就是fork server的逻辑
1 | "__afl_forkserver:\n" |
首先是push 两次rdx来使得栈整齐一点?emmmm,这里就不管了
然后是将__afl_temp中的4个字节写到提前开好的管道中,这里开管道的过程在afl-fuzz的代码中,后面再慢慢分析
再判断下write的返回值,假如不为4,就会跳到__afl_fork_resume,这个后面到了再分析
__afl_fork_wait_loop
1 | "__afl_fork_wait_loop:\n" |
这里是不断地从管道中读取内容,假如读取到的字节数不为4就会跳到__afl_die
如果正常读取,就会到下面的代码
1 | " /* Once woken up, create a clone of our process. This is an excellent use\n" |
这里首先fork了,然后判断fork是否成功,如果成功,就会跳到__afl_fork_resume
失败的话,就会跳到__afl_die
之后把fork出来的pid存到__afl_fork_pid中,再写到与fuzzer通信的管道中
1 | " movq $0, %rdx /* no flags */\n" |
这里是父进程等待子进程,如果waitpid返回的结果小于等于0,就会跳到afl_die,waitpid也会把子进程的状态写到afl_temp中
1 | " /* Relay wait status to pipe, then loop back. */\n" |
然后把子进程的状态通过管道写回到fuzzer中,跳回到__afl_fork_wait_loop,继续等待fuzzer的fork请求
__afl_fork_resume
1 | "__afl_fork_resume:\n" |
这里是把两个管道给关掉
1 | " popq %rdx\n" |
然后把各种寄存器恢复,跳到__afl_store
__afl_die
1 | "__afl_die:\n" |
这里就是简单的exit
__afl_setup_abort
1 | __afl_setup_abort:\n" |
这里就是设置__afl_setup_failure为1,然后恢复下寄存器,直接返回
总结
这里大概说下被fuzz的程序运行的过程和fork_server的过程
首先alf-fuzz这个程序会创建两个管道,然后利用afl-gcc或者afl-clang编译的程序,就会被执行
之前在afl-as.c中也分析到了,main函数肯定会被插桩的,也就是肯定会调用__afl_maybe_log
而对于第一次运行的进程,就会作为fork-server,后面的由fork-server fork出来的才是真正被fuzz的程序
然后fork-server 不断地等待fuzzer的指令去fork子进程,用waitpid去拿到子进程的结束状态,写回给fuzzer
不过这里也有个疑问,那些read(0,xxx,xxx)是怎么hook掉的? 感觉应该是fuzzer改掉了,使得0是某个特定的文件吧(这里只是猜测,详细的后面再去分析下afl-fuzz)