感谢大佬们的文章:
http://rk700.github.io/2017/12/28/afl-internals/
https://bbs.pediy.com/thread-265973.htm
这篇记录 afl-as.h 的代码解读,这个文件中是要插入的桩代码,基本上都是汇编码,但是并不难理解,可读性比较高。插入的桩代码是分了32位和64位的,但是差别不大,这里只分析64位的情况。
afl-as.h
插桩代码的流程
以64位为例,提前介绍几个变量,这些变量位于bss段,即存放的是未初始化的全局变量或是局部静态变量,并且只存放变量的大小。
具体关于bss段的解释请参考:https://www.jianshu.com/p/ddfb284c1f7a
".AFL_VARS:\n"
" .lcomm __afl_area_ptr, 8\n" // 共享内存地址
#ifndef COVERAGE_ONLY
" .lcomm __afl_prev_loc, 8\n" // 前一个块ID
#endif /* !COVERAGE_ONLY */
" .lcomm __afl_fork_pid, 4\n" // fork出的子进程的PID
" .lcomm __afl_temp, 4\n" // 存放fork server 状态的缓冲区
" .lcomm __afl_setup_failure, 1\n" // 标志位,是否已经调用过__afl_setup
#endif /* ^__APPLE__ */
" .comm __afl_global_area_ptr, 8, 8\n" // 全局指针,指向共享内存地址
"\n"
".AFL_SHM_ENV:\n"
" .asciz \"" SHM_ENV_VAR "\"\n" // 这个变量位于.text段,获取环境变量的字符串
1. trampoline_fmt_64
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" // 将随机数random() % MAP_SIZE 存入rcx寄存器,我称其为块ID
"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";
2. __afl_maybe_log
#if defined(__OpenBSD__) || (defined(__FreeBSD__) && (__FreeBSD__ < 9))
" .byte 0x9f /* lahf */\n"
#else
" lahf\n" // 将标志寄存器的低八位送入AH
#endif /* ^__OpenBSD__, etc */
" seto %al\n" // seto溢出置位指令,如果溢出,操作数置1
"\n" // 用lahf + seto 代替pop,因为pop很慢。但是会污染到al,不过问题不大
" /* Check if SHM region is already mapped. */\n" // 检查共享内存地址是否映射完毕
"\n"
" movq __afl_area_ptr(%rip), %rdx\n" // __afl_area_ptr 共享内存的地址存至rdx
" testq %rdx, %rdx\n" // 判断变量是否为null
" je __afl_setup\n" // ZF标志位为1时进行跳转,__afl_area_ptr 为null,即没有设置共享内存地址时,跳转
"\n"
"__afl_store:\n" // 共享内存地址已设置,则继续往下执行__afl_store
3. __afl_setup 获取共享内存地址
"__afl_setup:\n"
"\n"
" /* Do not retry setup if we had previous failures. */\n" // 不管成功与否,只设置一次,不重复设置,所以会有下面这个标志位
"\n"
" cmpb $0, __afl_setup_failure(%rip)\n" // 判断__afl_setup_failure 是否为0
" jne __afl_return\n" // 不为0,说明已经重新设置过了,跳转至返回代码
"\n"
" /* Check out if we have a global pointer on file. */\n" // 检查全局指针是否指向共享内存的地址
"\n"
#ifndef __APPLE__
" movq __afl_global_area_ptr@GOTPCREL(%rip), %rdx\n" // 先取出这个指针
" movq (%rdx), %rdx\n" // 因为是指针,也就是一个地址,所以需要再寻址一次
#else
" movq __afl_global_area_ptr(%rip), %rdx\n" // 不执行
#endif /* !^__APPLE__ */
" testq %rdx, %rdx\n" // 判断全局指针是否为null
" je __afl_setup_first\n" // 如果为null,跳转
"\n"
" movq %rdx, __afl_area_ptr(%rip)\n" // 如果不为null,将值赋给__afl_area_ptr
" jmp __afl_store\n" // 跳转至 __afl_store
"\n"
4. **__afl_setup_first **设置指向共享内存的全局变量
"__afl_setup_first:\n"
"\n"
" /* Save everything that is not yet saved and that may be touched by\n" // 保存尚未保存的所有内容,
" getenv() and several other libcalls we'll be relying on. */\n" // 因为可能会被getenv()和接下来要调用的函数使用
"\n"
" leaq -352(%rsp), %rsp\n" // 预留空间,用于保存寄存器的状态。这里是不是可以直接用push?但是push很慢,所以用lea+mov替代?
"\n"
" movq %rax, 0(%rsp)\n" // 保存寄存器的状态,包括xmm寄存器组
" movq %rcx, 8(%rsp)\n"
" movq %rdi, 16(%rsp)\n"
" movq %rsi, 32(%rsp)\n"
" movq %r8, 40(%rsp)\n"
" movq %r9, 48(%rsp)\n"
" movq %r10, 56(%rsp)\n"
" movq %r11, 64(%rsp)\n"
"\n"
" movq %xmm0, 96(%rsp)\n"
" movq %xmm1, 112(%rsp)\n"
" movq %xmm2, 128(%rsp)\n"
" movq %xmm3, 144(%rsp)\n"
" movq %xmm4, 160(%rsp)\n"
" movq %xmm5, 176(%rsp)\n"
" movq %xmm6, 192(%rsp)\n"
" movq %xmm7, 208(%rsp)\n"
" movq %xmm8, 224(%rsp)\n"
" movq %xmm9, 240(%rsp)\n"
" movq %xmm10, 256(%rsp)\n"
" movq %xmm11, 272(%rsp)\n"
" movq %xmm12, 288(%rsp)\n"
" movq %xmm13, 304(%rsp)\n"
" movq %xmm14, 320(%rsp)\n"
" movq %xmm15, 336(%rsp)\n" // 最后一个保存数据的位置351
"\n"
" /* Map SHM, jumping to __afl_setup_abort if something goes wrong. */\n" // 映射共享内存,如果出错则跳转至__afl_setup_abort
"\n"
" /* The 64-bit ABI requires 16-byte stack alignment. We'll keep the\n" // 64 位 ABI 需要 16 字节堆栈对齐
" original stack ptr in the callee-saved r12. */\n" // 在被调用者的 r12 寄存器中保存调用者的堆栈指针rsp
"\n"
" pushq %r12\n" // 这里最后一个push而不是mov,应该是要更新rsp栈顶指针
" movq %rsp, %r12\n" // 将父函数的rsp保存至r12寄存器中
" subq $16, %rsp\n" //
" andq $0xfffffffffffffff0, %rsp\n" // sub + and 这两步是对齐操作,最低位对齐为0
"\n"
" leaq .AFL_SHM_ENV(%rip), %rdi\n" // 这好像是在将"SHM_ENV_VAR"这个字符串的地址放入rdi
CALL_L64("getenv") // 调用getenv函数,获取共享内存地址
"\n"
" testq %rax, %rax\n" // 判断是否获取成功
" je __afl_setup_abort\n" // 如果为null,跳转
"\n"
" movq %rax, %rdi\n" // 将获取到的字符串地址放入rdi
CALL_L64("atoi") // 调用atoi,将字符串转换为int型
"\n"
" xorq %rdx, %rdx /* shmat flags */\n" // shmat函数的参数 标志位,通常为0
" xorq %rsi, %rsi /* requested addr */\n" // shmat函数的参数 让系统来选择共享内存的链接地址
" movq %rax, %rdi /* SHM ID */\n" // shmat函数的参数 共享内存ID 即转换后的整数存入
CALL_L64("shmat") // 调用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" // 将共享内存地址存至rdx
" movq %rax, __afl_area_ptr(%rip)\n" // 将共享内存地址存至__afl_area_ptr
"\n"
#ifdef __APPLE__
" movq %rax, __afl_global_area_ptr(%rip)\n" // 不执行
#else
" movq __afl_global_area_ptr@GOTPCREL(%rip), %rdx\n" // 全局指针存至rdx
" movq %rax, (%rdx)\n" // 将共享内存地址存入全局指针指向的位置
#endif /* ^__APPLE__ */
" movq %rax, %rdx\n" // 将共享内存地址存至rdx
"\n"
"__afl_forkserver:\n" // 继续往下执行__afl_forkserver
5. __afl_forkserver 通知fuzzer, forkserver准备就绪
"__afl_forkserver:\n"
"\n"
" /* Enter the fork server mode to avoid the overhead of execve() calls. We\n" // 进入fork server模式以避免 execve()调用开销。
" push rdx (area ptr) twice to keep stack alignment neat. */\n" // 两次将 rdx 压栈以保持堆栈对齐
"\n"
" pushq %rdx\n"
" pushq %rdx\n"
"\n"
" /* Phone home and tell the parent that we're OK. (Note that signals with\n" // 告诉父进程我们正常运行。
" no SA_RESTART will mess it up). If this fails, assume that the fd is\n" // (请注意,没有 SA_RESTART 的信号会搞砸)
" closed because we were execve()d from an instrumented binary, or because\n" // 如果失败,假设 fd 已关闭
" the parent doesn't want to use the fork server. */\n" // 因为执行了插桩的二进制文件,或者因为父级不想使用 fork server。
"\n"
" movq $4, %rdx /* length */\n" // write函数的参数 要写入的数据长度为4
" leaq __afl_temp(%rip), %rsi /* data */\n" // write函数的参数 存放要写入数据的缓冲区
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n" // write函数的参数 要写入文件的文件描述符
CALL_L64("write") // 调用write
"\n"
" cmpq $4, %rax\n" // 判断是否成功写入
" jne __afl_fork_resume\n" // forkserver 启动失败,跳转到__afl_fork_resume恢复寄存器状态,结束进程
"\n" // 成功写入,已通知fuzzer forkserver准备就绪
"__afl_fork_wait_loop:\n" // fork server 进入等待状态
6. __afl_fork_wait_loop 等待指令并fork子进程
"__afl_fork_wait_loop:\n" // fork server 进入等待状态
"\n"
" /* Wait for parent by reading from the pipe. Abort if read fails. */\n" // 等待fuzzer从控制管道传过来的命令
"\n"
" movq $4, %rdx /* length */\n" // read函数的参数 要读取的数据长度为4
" leaq __afl_temp(%rip), %rsi /* data */\n" // read函数的参数 保存读取数据的缓冲区地址
" movq $" STRINGIFY(FORKSRV_FD) ", %rdi /* file desc */\n" // read函数的参数 要读取文件的文件描述符
CALL_L64("read") // 调用read函数
" cmpq $4, %rax\n" // 判断是否成功读取
" jne __afl_die\n" // 读取失败 跳转
"\n"
" /* Once woken up, create a clone of our process. This is an excellent use\n" // 收到fuzzer的指令后,开始fork子进程
" 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" // 但是 glibc 笨拙地缓存 getpid() 结果
" abort(), raise(), and a bunch of other things :-( */\n" // 并且无法更新值、破坏 abort()、raise() 和一堆其他东西
"\n"
CALL_L64("fork") // 调用fork,创建子进程
" cmpq $0, %rax\n" // 判断进程id是否为0
" jl __afl_die\n" // 如果fork返回值小于0,即fork失败,跳转
" je __afl_fork_resume\n" // 如果返回值为0,即是子进程,跳转至子进程执行的代码 __afl_fork_resume
"\n" // 否则,是父进程,继续往下执行。父进程将子进程的PID通过管道发送给fuzzer,并等待子进程执行完毕
" /* In parent process: write PID to pipe, then wait for child. */\n"
"\n"
" movl %eax, __afl_fork_pid(%rip)\n" // 将子进程的PID存至__afl_fork_pid
"\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") // 调用write向状态管道写入子进程PID
"\n"
" movq $0, %rdx /* no flags */\n" // waitpid的参数 为0表示阻塞父进程等待子进程退出
" leaq __afl_temp(%rip), %rsi /* status */\n" // waitpid的参数 指针,用于获得子进程的退出状态
" movq __afl_fork_pid(%rip), %rdi /* PID */\n" // waitpid的参数 等待的子进程的PID
CALL_L64("waitpid") // 调用waitpid
" cmpq $0, %rax\n" // 判断是否执行成功
" jle __afl_die\n" // 执行失败 则跳转
"\n"
" /* Relay wait status to pipe, then loop back. */\n" // 将结束状态写入管道发送给fuzzer,之后再次进入等待状态
"\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") // 调用write向状态管道写入子进程结束状态
"\n"
" jmp __afl_fork_wait_loop\n" // 再次进入等待状态
"\n"
7. __afl_fork_resume 关闭资源,恢复寄存器状态
"__afl_fork_resume:\n"
"\n"
" /* In child process: close fds, resume execution. */\n" // 在子进程中,关闭不需要的文件描述符,继续向下执行
"\n"
" movq $" STRINGIFY(FORKSRV_FD) ", %rdi\n" // 关闭控制管道
CALL_L64("close")
"\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi\n" // 关闭状态管道
CALL_L64("close")
"\n" // 子进程恢复执行状态
" popq %rdx\n"
" popq %rdx\n"
"\n"
" movq %r12, %rsp\n" // 恢复寄存器状态
" popq %r12\n"
"\n"
" movq 0(%rsp), %rax\n"
" movq 8(%rsp), %rcx\n"
" movq 16(%rsp), %rdi\n"
" movq 32(%rsp), %rsi\n"
" movq 40(%rsp), %r8\n"
" movq 48(%rsp), %r9\n"
" movq 56(%rsp), %r10\n"
" movq 64(%rsp), %r11\n"
"\n"
" movq 96(%rsp), %xmm0\n"
" movq 112(%rsp), %xmm1\n"
" movq 128(%rsp), %xmm2\n"
" movq 144(%rsp), %xmm3\n"
" movq 160(%rsp), %xmm4\n"
" movq 176(%rsp), %xmm5\n"
" movq 192(%rsp), %xmm6\n"
" movq 208(%rsp), %xmm7\n"
" movq 224(%rsp), %xmm8\n"
" movq 240(%rsp), %xmm9\n"
" movq 256(%rsp), %xmm10\n"
" movq 272(%rsp), %xmm11\n"
" movq 288(%rsp), %xmm12\n"
" movq 304(%rsp), %xmm13\n"
" movq 320(%rsp), %xmm14\n"
" movq 336(%rsp), %xmm15\n"
"\n"
" leaq 352(%rsp), %rsp\n"
"\n"
" jmp __afl_store\n" // 跳转至__afl_store
"\n"
8. __afl_store 计算边命中次数
"__afl_store:\n"
"\n" // 计算并存储 rcx 中指定的代码位置的命中次数
" /* Calculate and store hit for the code location specified in rcx. */\n"
"\n"
#ifndef COVERAGE_ONLY
" xorq __afl_prev_loc(%rip), %rcx\n" // prev ^ cur 存至 rcx
" xorq %rcx, __afl_prev_loc(%rip)\n" // prev ^ cur ^ prev = cur 这两步更新prev
" shrq $1, __afl_prev_loc(%rip)\n" // prev >> 1
#endif /* ^!COVERAGE_ONLY */
"\n"
#ifdef SKIP_COUNTS
" orb $1, (%rdx, %rcx, 1)\n"
#else
" incb (%rdx, %rcx, 1)\n" // 命中次数加1
#endif /* ^SKIP_COUNTS */
"\n"