php插桩 xdebug,[原创]AFL编译插桩部分源码分析

AFL的编译插桩是在afl-as部分完成的。本部分主要介绍afl-as以及相关编译插桩的内容。

开始之前

本篇是afl源码阅读的第二篇,在上一篇我没有主要介绍插桩相关的内容,放在这一章来简单讲一下。

在本篇之后还会有最后一篇第三篇来介绍AFL的 LLVM 优化的相关内容。

一个afl-gcc编译出来的程序是什么样的

首先我们不去看源码,直接先看一下插桩后的样子。

我们使用一个很简单的程序

4771eb383b4a4f04b8932c3d3b97b380.png

可以看到这里已经显示了 Instrumented 10 locations

cbd7f0ab3be6b66137228b510339e742.png

我们将其拉入IDA看一下。

aa57d1bf52d8a09804b7ee8f8136615b.png

0db64b20373d2de7a59062350ae7fe73.png

可以看到afl在代码段进行了插桩,主要是 __afl_maybe_log 函数,用来探测、反馈程序此时的状态。

afl-as.c源码分析

main函数

main函数主要做了一下几步

通过调用edit_params(argc, argv)编辑了参数。

调用add_instrumentation()进行插桩。

fork出一个子进程,在子进程中执行我们编辑好的参数。

等待子进程执行完退出的信号。

退出,exit参数为WEXITSTATUS(status)

edit_params(int argc, char **argv)

首先获取环境变量TMPDIR、AFL_AS

如果设置了clang_mode,且由环境变量获取的afl_as为空设置use_clang_as = 1

afl_as为环境变量"AFL_CC"的值

如果还是没有获取到 afl_as,令afl_as为环境变量"AFL_CXX"的值

如果还是没有获取到 afl_as,令afl_as为"clang"

获取tmp目录的位置,跟上一步类似getenv("TEMP")

getenv("TMP")

若前两个环境变量都没有获取到,直接令其为"/tmp"

给as_params分配空间。

接下来处理as_params[0]如果afl_as不空的话,as_params[0]=afl_as

否则指定为"as"

令as_params[argc]为0.

接下来扫描argv中的参数。如果设置了"--64",令use_64bit = 1

如果是32为,那么use_64bit = 0

如果是macos。若"-arch"指定了"i386",那么abort。

Sorry, 32-bit Apple platforms are not supported.

若在clang mode下,并且当前的argv[i]不是q或Q,那么跳过这个参数,直接continue掉。

否则直接将当前的argv[i]放入as_params[]参数数组中

如果是macos且使用的use_clang_as向参数数组as_params[]中依次添加:-c -x assembler

argv[argc - 1]为input_file

如果input_file以'-'开头如果是"-version"设置just_version = 1

modified_file = input_file

直接跳转到wrap_things_up

如果input_file[1]还有其他值,告知用户使用错误,abort

否则input_file = NULL

否则比较当前input_file是否以tmp_dir或"/var/tmp/"或"/tmp/"开头。若均不是,则令pass_thru = 1

设置modified_file

modified_file = alloc_printf("%s/afl-%u-%u.s", tmp_dir, getpid(),(u32) time(NULL))

最后到达wrap_things_up:令as_params[]最后一个有效参数为modified_file

向as_params[]最后一个位置补NULL,标志结束。

插桩函数 add_instrumentation(void)在编辑完as_params[]参数数组后进入了此插桩函数。

Process input file, generate modified_file. Insert instrumentation in all the appropriate places.

如果设置了input_file只读打开input_file,fd为inf

input_file:/Users/apple/Desktop/AFL/AFL/cmake-build-debug/tmp/test-instr.s

否则inf为stdin

打开modified_file,返回out_fd

接下来通过while循环每次从input_file(test-instr.s)中读取一行到line中(大小为8192)static u8 line[MAX_LINE];

acc8f1aef6b3cf787d9c6c253b3dda0c.png

到了真正插桩的部分了,首先明确,afl只在.text段插桩。所以先要找到.text的位置,并在对应的位置设置instr_ok = 1代表找到了一个位置。

首先我们跳过所有的标签、宏、注释。

在这里我们判断读入的这一行line是否以"\t."开头。(即尝试匹配.s中声明的段)

如果是的话进入更深的判断。首先检查是否是".p2align "指令,如果是的话设置skip_next_label = 1

接下来尝试匹配:text\n "section\t.text" "section\t__TEXT,__text" "section __TEXT,__text"如果匹配到了设置instr_ok = 1,代表我们此时正在.text段。

然后直接continue跳本次循环

尝试匹配:"section\t" "section " "bss\n" "data\n"如果匹配到了说明我们在其他段中。设置instr_ok = 0然后continue

接下来判断一些其他信息,比如att汇编还是intel汇编,设置对应标志位。

AFL尝试抓住一些能标志程序变化的重要的部分:

稍微总结一下就是,AFL试图抓住:_main:(这是必然会插桩的位置)、以及gcc和clang下的分支标记,并且还有条件跳转分支。这几个关键的位置是其着重关注的。

如果是形如:\tj[^m].的指令,即条件跳转指令,并且R(100)产生的随机数小于插桩密度inst_ratio,那么直接使用fprintf将trampoline_fmt_64(插桩部分的指令)写入文件。写入大小为小于MAP_SIZE的随机数。R(MAP_SIZE)

然后插桩计数ins_lines加一。continue

接下来也是对于label的相关评估,有一些label可能是一些分支的目的地,需要自己的评判。

首先判断line中是否有形如类似:^L.*\d(:$)的字符串(比如"Ltext0:")

接下来更进一步的判断L之后是否为为数字 或者 是否满足在clang mode下,line为"LBB"。(L\ / LBB\)如果匹配到了,那么在满足插桩密度以及未设置skip_next_label的情况下。令instrument_next = 1(defer mode)

否则令skip_next_label = 0

而如果只匹配到了line中存在":"但line并非以L开头。那么说明是Function label。

此时设置instrument_next = 1进行插桩。

这一切进行完之后,回到while函数的下一个循环中。而在下一个循环的开头,对于以deferred mode进行插桩的位置进行了真正的插桩处理。

这里关键的两个判断:instr_ok && instrument_next,如果在代码段中,且设置了以deferred mode进行插桩,那么就在这个地方进行插桩,写入trampoline_fmt_64。

插桩完毕后生成的.s文件如图:

5012d142281b67b3df35d0afc93ddc0d.png

可以看到已经被插桩了。这里也就是我们一开始看到的:__afl_maybe_log

收尾工作

在插桩结束后,我们把参数打印一下:

7fef6c905f6b96fa9e515b64d3da92a0.png

可以看到这里在用汇编器as来将我们插桩好的.s文件生成可执行文件。

而真正的汇编过程是fork出一个子进程来执行的。

main函数中等待子进程执行完毕后退出。

至此整个插桩过程就结束了。

instrumentation trampoline究竟是什么?

在上一部分我们已经知道了,64位下AFL将trampoline_fmt_64写入.s文件的指定位置作为插桩。

本部分主要来讨论AFL究竟插进去了什么东西。

trampoline_fmt_64

我们直接看ida中的内容,非常直观,trampoline_fmt_64就是如下汇编:

__afl_maybe_log

大体流程如下:

9105593a92535b13cec499507ceca0ab.png

在这之前我们首先要关注几个位于bss段的变量:

__afl_area_ptr:共享内存的地址。

__afl_prev_loc:上一个插桩位置(R(100)随机数的值)

__afl_fork_pid:由fork产生的子进程的pid

__afl_temp:缓冲区

__afl_setup_failure:标志位,如果置位则直接退出。

__afl_global_area_ptr:一个全局指针。

首先lahf用于将标志寄存器的低八位送入AH,即将标志寄存器FLAGS中的SF、ZF、AF、PF、CF五个标志位分别传送到累加器AH的对应位(八位中有三位是无效的)。

2e419ddf67cf74d719d4b0af72d5d907.png

接下来seto溢出置位。

然后检查共享内存是否已经被设置了。即__afl_area_ptr是否为空?

如果为NULL则说明还没有被设置,跳转到__afl_setup进行设置。

否则继续运行。

__afl_setup

在__afl_setup:中用于初始化__afl_area_ptr,只有在运行到第一个桩时会进行本次初始化。

如果afl_setup_failure不为0的话,直接跳转到afl_return返回。

接下来检查afl_global_area_ptr文件指针是否为NULL,如果为空则跳转到```afl_setup_first```。

否则将afl_global_area_ptr的值赋给afl_area_ptr后跳转到__afl_store

__afl_setup_first

1.在__afl_setup_first中,首先保存寄存器的值(包括xmm寄存器组)

2.接下来进行rsp对齐操作。

3.获取环境变量"__AFL_SHM_ID"的值(共享内存的id)。如果获取失败,那么跳转到__afl_setup_abort,说明获取失败。

4.获取成功后调用shmat启用对共享内存的访问。如果启用失败,跳转到__afl_setup_abort。

5.将shmat返回的共享内存的地址存储在 __afl_area_ptr 与 __afl_global_area_ptr全局变量中。

6.一切顺利的话,接下来运行 __afl_forkserver

__afl_forkserver

首先向FORKSRV_FD+1即199号描述符(即状态管道)中写出__afl_temp中的四个字节,来通知afl我们的fork server已经启动成功。

顺带一提,这里的向状态管道中写的值,在afl-fuzz.c中的这个位置被读出来:

3f1bc27d5dc318003afd1b94f53c795b.png

这样我们整个过程就串连起来了。

接下来进入:__afl_fork_wait_loop:

__afl_fork_wait_loop:

1.首先我们等待parent(fuzz)通过控制管道发来的命令,读入__afl_temp中。

2.如果读取失败,那么跳到 __afl_die,break出循环。

3._fork出一个子进程,子进程跳入执行:__afl_fork_resume

4.将fork出来的子进程pid赋值给__afl_fork_pid

5.向状态管道中写出子进程pid,告知parent。此时__afl_maybe_log中的父进程作为forksrever与我们的fuzz进行通信。

6.等待我们fork出的子进程执行完毕。然后写入状态管道告知fuzz。

7.重新执行下一轮 __afl_fork_wait_loop进行测试。

__afl_fork_resume 与

1.首先关闭子进程中的文件描述符。

2.恢复子进程的寄存器状态。

3.跳转执行__afl_store

__afl_store:

这一部分反编译出来如下:

而这个a2就是我们在调用_afl_maybe_log时传入的参数rcx

char __usercall _afl_maybe_log@(char a1@, __int64 a2@。

可以看到这个rcx实际就是我们此时用于标记当前这个桩的随机数,而_afl_prev_loc就是上一个桩的随机数

两次异或之后_afl_prev_loc=a2,然后将_afl_prev_loc右移1位为新的_afl_prev_loc。

最后在共享内存中存储当前插桩位置的地方计数加一,相当于:share_mem[_afl_prev_loc ^ a2]++,实际上是存入一个64k大小的哈希表,存在碰撞几率,但是问题不大。而这个索引是通过异或得到的。

更进一步的,关于为什么要对_afl_prev_loc = _afl_prev_loc >> 1;进行右移1位。

AFL主要考虑如下情况:如果此分支是A->A和B->B这样的情况那么异或之后就会都变成0,进而使得无法区分。亦或者考虑:A->B与B->A的情况,异或后的key也是一样的,难以区分。

至此,AFL的插桩就基本分析的差不多了。下一篇会着重讲llvm mode

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值