0x00 前言
为了解决使用AFL++进行Fuzzing的部分问题,个人决定对AFL的代码进行阅读,此为第一部分。
0x01 目录结构
- 插桩模块
- afl-as.c,afl-gcc.c:一般插桩模式,针对源码插桩,编译器可以用gcc,clang
- llvm_mode:llvm插桩模式,针对源码插桩,编译器是哦那个clang
- qemu_mode:qemu插桩模式,针对二进制文件插桩
- fuzzer模块
- afl-fuzzer.c:fuzzer实现的核心代码,AFL主体
- 其他辅助模块
- afl-analyze:对测试用例进行分析,通过分析给定的用例,确定是否可以发现用例中有意义的字段
- afl-plot:生成测试任务的状态图
- afl-tmin:对测试用例进行最小化
- afl-cmin:对语料库进行精简操作
- afl-showmap:对单个测试用例进行执行路径跟踪
- afl-whatsup:各并行历程fuzzing结果统计
- afl-gotcpu:查看当前cpu状态
- 头文件
- afl-as.h:定义带检测功能的内存分配和释放操作
- config.h:定义配置信息
- debug.h:与提示信息相关的宏定义
- hash.h:哈希函数的实现定义
- types.h:部分类型及宏定义
0x02 文件分析
2.1 afl-gcc.c
2.1.1 简介
该程序是GCC或者clang的包装,最常用的方法是在调用./configure的时候通过CC将afl-gcc或afl-clang的路径传递过去。(如果是C++代码,通过CXX传递afl-g++,afl-clang++的路径)
该包装需要知道afl-as的路径,如果AFL_HARDEN设置了,可以使用各种加固选项编译目标应用,并且可以通过设置AFL_USE_ASAN开启ASAN。
如果想调用非默认编译器作为链的下一步,可以通过AFL_CC或AFL_CXX指定其位置。
2.1.2 关键变量
tatic u8``*` `as_path; ``/``*` `Path to the AFL ``'as'` `wrapper,AFL的as的路径 ``*``/
static u8``*``*` `cc_params; ``/``*` `Parameters passed to the real CC,CC实际使用的编译器参数 ``*``/
static u32 cc_par_cnt ``=` `1``; ``/``*` `Param count, including argv0 ,参数计数 ``*``/
static u8 be_quiet, ``/``*` `Quiet mode,静默模式 ``*``/
``clang_mode; ``/``*` `Invoked as afl``-``clang``*``? ,是否使用afl``-``clang``*``模式 ``*``/
# 数据类型说明
# typedef uint8_t u8;
# typedef uint16_t u16;
# typedef uint32_t u32;
2.1.3 函数
1.find_as函数
从AFL_PATH找汇编器或者从argv[0]的路径找到汇编器。
2.edit_params
复制参数到cc_params,并做一些需要的修改。
如果是clang,会将clang_mode置为1。然后根据第一个参数选择编译器。
随后处理一些编译选项
- -B选项用于设置编译器的搜索路径,直接跳过 因为会使用变量as_path。
- -integrated-as 选项跳过
- -pipe 选项跳过
- -fsanitize=address或-fsanitize=memory选项 asan_set被设置为1,这里按理说address选项启用了 AddressSanitizer,是一种用于检测内存错误(如越界访问、使用已释放的内存等)的工具。它会在编译时插入额外的代码,以便在程序运行时对内存访问进行检测。如果发现了内存错误,程序会打印相关信息,并退出。memory选项 MemorySanitizer,是一种检测未初始化内存使用的工具。它会在编译时插入额外的代码,以便在程序运行时跟踪内存的初始化状态。如果程序尝试使用未初始化的内存,MemorySanitizer 会报告这种情况。
- FORTIFY_SOURCE 选项fortify_set 被设置为 1
如果clang_mode为1,会设置选项-no-integrated-as;如果有AFL_HARDEN环境变量,设置编译选项-fstack-protector-all;如果fortify_set为0设置选项-D_FORTIFY_SOURCE=2。
如果asan_set被设置,设置环境变量AFL_USE_ASAN为1,如果 asan_set 不为1且存在 AFL_USE_ASAN 环境变量,则设置选项-U_FORTIFY_SOURCE -fsanitize=address;如果 asan_set 不为1且不存在AFL_USE_ASAN 环境变量,但是存在AFL_USE_MSAN就设置选项-U_FORTIFY_SOURCE -fsanitize=memory;
默认会开启-O3 -funroll-loops并设置宏定义-D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
如果环境变量存在AFL_NO_BUILTIN=1,默认不用gcc内建的函数替代标准库中的函数以供优化。
3.main
main函数首先调用find_as查找使用的汇编器,其次处理传入的传输,将确定好的参数传入edit_params数组,最后调用 execvp(cc_params[0], (cahr**)cc_params)执行afl-gcc。
2.2 afl-as.c
2.2.1 简介
afl-as是GNU as的wrapper包装,其目的是预处理GCC/Clang生成的汇编文件,注入来自afl-as.h文件的代码,在使用afl-gcc/afl-clang编译程序时,它会被工具链自动调用。这里和有毒师傅的文章有点冲突,我认为是不对手写的汇编代码进行插桩,而有毒师傅的理解是
后续可以通过一个简单的实验来进行验证。
2.2.2 关键变量
static u8** as_params; /* Parameters passed to the real 'as' */
static u8* input_file; /* Originally specified input file */
static u8* modified_file; /* Instrumented file for the real 'as' */
static u8 be_quiet, /* Quiet mode (no stderr output) */
clang_mode, /* Running in clang mode? */
pass_thru, /* Just pass data through? */
just_version, /* Just show version? */
sanitizer; /* Using ASAN / MSAN */
static u32 inst_ratio = 100, /* Instrumentation probability (%) */
as_par_cnt = 1; /* Number of params to 'as' */
2.2.3 函数
1.edit_params
处理调用afl-as的参数,modified_file = tmp_dir/afl-pid-tim.s
2.add_instrumentation
根据input的.s文件,选择插桩的地方(只向.text段进行插桩)
需要插桩的情况:
- main::函数入口点,始终进行插桩。
- ^L0::GCC分支标签。
- ^LBB0_0::Clang分支标签(仅在Clang模式下)。
- ^ jnz foo:条件分支。
不需要插桩的情况包括:
- # BB#0::Clang注释。
- # BB#0::Clang注释(不同格式)。
- ^Ltmp0::Clang非分支标签。
- ^LC0:GCC非分支标签。
- ^LBB0_0::Clang非分支标签(在GCC模式下)。
- ^ jmp foo:非条件跳转。
插桩代码trampoline_fmt_32如下:
section .text
align 16
push edi
push edx
push ecx
push eax
mov ecx, 0x%08x
call __afl_maybe_log
pop eax
pop ecx
pop edx
pop edi
此代码压入四个参数,调用__afl_maybe_log。
64位payload中__afl_maybe_log用到变量的值:
.AFL_VARS:
.comm __afl_area_ptr, 8 ;共享内存地址
.comm __afl_prev_loc, 8 ;上一个插桩为止(id为R(100)的值)
.comm __afl_fork_pid, 4 ;由fork产生的子进程的pid
.comm __afl_temp, 4 ;缓冲区
.comm __afl_setup_failure, 1 ;标志位,如果置位直接退出
.comm __afl_global_area_ptr, 8,8 ;全局指针
;数字为分配空间大小,第二个数字代表对齐方式
__afl_maybe_log函数:
__afl_maybe_log: /* 源码删除无关内容后 */
lahf
seto %al
/* Check if SHM region is already mapped. */
movq __afl_area_ptr(%rip), %rdx
testq %rdx, %rdx
je __afl_setup
该代码块将lahf将标志寄存器复制到了AH,然后根据OF对AL置1或者置0。随后对共享内存地址进行检测,如果为NULL就进行__afl_setup,否则继续进行。
__afl_setup:
__afl_setup:
/* Do not retry setup is we had previous failues. */
cmpb $0, __afl_setup_failure(%rip)
jne __afl_return
/* Check out if we have a global pointer on file. */
movq __afl_global_area_ptr(%rip), %rdx
testq %rdx, %rdx
je __afl_setup_first
movq %rdx, __afl_area_ptr(%rip)
jmp __afl_store
该代码块主要作用为初始化__afl_area_ptr,只在第一个桩时进行本次初始化。如果__afl_setup_failure不为0,代表之前出错过,跳转到__afl_return返回,然后检查__afl_global_area_ptr是否为null,如果为null跳转到__afl_setup_first,否则将其值赋给__afl_area_ptr,然后跳转到__afl_store。
__afl_return:
__afl_return:
addb $127, %al
sahf
ret
该代码段从AH恢复寄存器,返回。
__afl_setup_abort:
__afl_setup_abort:
" /* Record setup failure so that we don't keep calling\n"
" shmget() / shmat() over and over again. */\n"
incb __afl_setup_failure
movq %r12, %rsp
popq %r12
movq 0(%rsp), %rax
...;从栈中恢复寄存器
movq 336(%rsp), %xmm15
leaq 352(%rsp), %rsp
jmp __afl_return
该代码段让__afl_setup_failure+1,并跳转到__afl_return。
__afl_setup_first:
;首先抬高栈顶,将没有存储并且可能被getenv()或其他libcalls用到的寄存器存储在栈中。
__afl_setup_first:
leaq -352(%rsp), %rsp
movq %rax, 0(%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 ;对齐16位。
leaq .AFL_SHM_ENV(%rip), %rdi ;这里获取的是__AFL_SHM_ID,该环境变量保存的是共享内存的ID
call _getenv
testq %rax, %rax
je __afl_setup_abort
movq %rax, %rdi
call _atoi ;将环境变量__AFL_SHM_ID字符串变为整数
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_area_ptr 和__afl_global_area_ptr。
然后开始执行__afl_forkserver。
__afl_forkserver:
__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
该代码段是将__afl_temp的四字节数据,写入到FORKSRV_FD + 1(198 + 1),告诉forkserver已经成功启动。写入不完全就跳到__afl_fork_resume。
__afl_fork_resume:
__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 336(%rsp), %xmm15
leaq 352(%rsp), %rsp
jmp __afl_store
; 重新开始执行
__afl_store:
__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)
; 将rcx的值给__afl_prev_loc
shrq $1, __afl_prev_loc(%rip)
; __afl_prev_loc >> 1
incb (%rdx, %rcx, 1)
; rdx:_afl_area_ptr rcx:_afl_prev_loc^random_mapsize
__afl_prev_loc是上一个桩的随机id,上一个和现在的异或计算共享内存位置然后+1,随后将现在的id右移一位作为新的__afl_prev_loc。
__afl_fork_wait_loop:
__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
; 从文件描述符198读取4字节数据到__afl_temp
; __afl_die 调用exit调用
/* 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
; __afl_die调用exit系统调用
/* 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
该代码段首先将控制管道发送的命令读入到__afl_temp中。随后fork一个子进程,子进程执行__afl_fork_resume。在这之后将子进程的pid赋给__afl_fork_pid。最后将子进程的status写入199,重新开始循环。
插桩代码的流程图:
图中有两个问题,一个是__afl_store的指向问题,其执行完到__afl_return,应该直接向下指我多画了一个图,然后是有毒师傅说__afl_pre_loc的数据为上一个插桩位置,这个我觉得没什么问题但是id为R(100)随机数的值,我认为是根据MAP_SIZE的大小而更改的。后续看afl-fuzz应该能解惑。
3.main
这里会执行插桩,有一个有意思的运算符用法!!,clang_mode = !!getenv(CLANG_ENV_VAR);,只在CLANG_ENV_VAR有值且为True的情况下clang_mode=1。
第一部分到此结束。
主要分析了文件afl-gcc.c和afl-as.c。
0xFF 参考文献
https://bbs.kanxue.com/thread-269534.htm
文章评论