首页
社区
课程
招聘
[原创]AFL编译插桩部分源码分析
发表于: 2021-2-8 16:49 18130

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

2021-2-8 16:49
18130

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

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

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

首先我们不去看源码,直接先看一下插桩后的样子。
我们使用一个很简单的程序


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

我们将其拉入IDA看一下。


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

main函数主要做了一下几步

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

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

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

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

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

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

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

然后插桩计数ins_lines加一。continue

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

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

而如果只匹配到了line中存在":"但line并非以L开头。那么说明是Function label
此时设置instrument_next = 1进行插桩。

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

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

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

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

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

可以看到这里在用汇编器as来将我们插桩好的.s文件生成可执行文件。
而真正的汇编过程是fork出一个子进程来执行的。

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

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

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

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

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

大体流程如下:

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

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

接下来seto溢出置位。

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

__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

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

首先向FORKSRV_FD+1即199号描述符(即状态管道)中写出__afl_temp中的四个字节,来通知afl我们的fork server已经启动成功。
顺带一提,这里的向状态管道中写的值,在afl-fuzz.c中的这个位置被读出来:

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

接下来进入:__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进行测试。

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

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

3.跳转执行__afl_store

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

而这个a2就是我们在调用_afl_maybe_log时传入的参数rcx
char __usercall _afl_maybe_log@<al>(char a1@<of>, __int64 a2@<rcx>

可以看到这个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->AB->B这样的情况那么异或之后就会都变成0,进而使得无法区分。亦或者考虑:A->BB->A的情况,异或后的key也是一样的,难以区分。

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

 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
 
int vuln(char *str)
{
    int len = strlen(str);
    if(str[0] == 'A' && len == 66)
    {
        raise(SIGSEGV);
        //如果输入的字符串的首字符为A并且长度为66,则异常退出
    }
    else if(str[0] == 'F' && len == 6)
    {
        raise(SIGSEGV);
        //如果输入的字符串的首字符为F并且长度为6,则异常退出
    }
    else
    {
        printf("it is good!\n");
    }
    return 0;
}
 
int main(int argc, char *argv[])
{
    char buf[100]={0};
    gets(buf);//存在栈溢出漏洞
    printf(buf);//存在格式化字符串漏洞
    vuln(buf);
 
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
 
int vuln(char *str)
{
    int len = strlen(str);
    if(str[0] == 'A' && len == 66)
    {
        raise(SIGSEGV);
        //如果输入的字符串的首字符为A并且长度为66,则异常退出
    }
    else if(str[0] == 'F' && len == 6)
    {
        raise(SIGSEGV);
        //如果输入的字符串的首字符为F并且长度为6,则异常退出
    }
    else
    {
        printf("it is good!\n");
    }
    return 0;
}
 
int main(int argc, char *argv[])
{
    char buf[100]={0};
    gets(buf);//存在栈溢出漏洞
    printf(buf);//存在格式化字符串漏洞
    vuln(buf);
 
    return 0;
}
 
if (line[0] == '\t' && line[1] == '.') {
 
    /* OpenBSD puts jump tables directly inline with the code, which is
       a bit annoying. They use a specific format of p2align directives
       around them, so we use that as a signal. */
 
    if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
        isdigit(line[10]) && line[11] == '\n')
        skip_next_label = 1;
 
    if (!strncmp(line + 2, "text\n", 5) ||
        !strncmp(line + 2, "section\t.text", 13) ||
        !strncmp(line + 2, "section\t__TEXT,__text", 21) ||
        !strncmp(line + 2, "section __TEXT,__text", 21)) {
        instr_ok = 1;
        continue;
    }
 
    if (!strncmp(line + 2, "section\t", 8) ||
        !strncmp(line + 2, "section ", 8) ||
        !strncmp(line + 2, "bss\n", 4) ||
        !strncmp(line + 2, "data\n", 5)) {
        instr_ok = 0;
        continue;
    }
 
}
if (line[0] == '\t' && line[1] == '.') {
 
    /* OpenBSD puts jump tables directly inline with the code, which is
       a bit annoying. They use a specific format of p2align directives
       around them, so we use that as a signal. */
 
    if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
        isdigit(line[10]) && line[11] == '\n')
        skip_next_label = 1;
 
    if (!strncmp(line + 2, "text\n", 5) ||
        !strncmp(line + 2, "section\t.text", 13) ||
        !strncmp(line + 2, "section\t__TEXT,__text", 21) ||
        !strncmp(line + 2, "section __TEXT,__text", 21)) {
        instr_ok = 1;
        continue;
    }
 
    if (!strncmp(line + 2, "section\t", 8) ||
        !strncmp(line + 2, "section ", 8) ||
        !strncmp(line + 2, "bss\n", 4) ||
        !strncmp(line + 2, "data\n", 5)) {
        instr_ok = 0;
        continue;
    }
 
}
 
If we're in the right mood for instrumenting, check for function
names or conditional labels. This is a bit messy, but in essence,
we want to catch:
 
  ^main:      - function entry point (always instrumented)
  ^.L0:       - GCC branch label
  ^.LBB0_0:   - clang branch label (but only in clang mode)
  ^\tjnz foo  - conditional branches
 
...but not:
 
  ^# BB#0:    - clang comments
  ^ # BB#0:   - ditto
  ^.Ltmp0:    - clang non-branch labels
  ^.LC0       - GCC non-branch labels
  ^.LBB0_0:   - ditto (when in GCC mode)
  ^\tjmp foo  - non-conditional jumps
 
Additionally, clang and GCC on MacOS X follow a different convention
with no leading dots on labels, hence the weird maze of #ifdefs
later on.
If we're in the right mood for instrumenting, check for function
names or conditional labels. This is a bit messy, but in essence,
we want to catch:
 
  ^main:      - function entry point (always instrumented)
  ^.L0:       - GCC branch label
  ^.LBB0_0:   - clang branch label (but only in clang mode)
  ^\tjnz foo  - conditional branches
 
...but not:
 
  ^# BB#0:    - clang comments
  ^ # BB#0:   - ditto
  ^.Ltmp0:    - clang non-branch labels
  ^.LC0       - GCC non-branch labels
  ^.LBB0_0:   - ditto (when in GCC mode)
  ^\tjmp foo  - non-conditional jumps
 
Additionally, clang and GCC on MacOS X follow a different convention
with no leading dots on labels, hence the weird maze of #ifdefs
later on.
/* Conditional branch instruction (jnz, etc). We append the instrumentation
           right after the branch (to instrument the not-taken path) and at the
           branch destination label (handled later on). */
 
        if (line[0] == '\t') {
 
            if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) {
 
                fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
                        R(MAP_SIZE));
 
                ins_lines++;
 
            }
 
            continue;
 
        }
/* Conditional branch instruction (jnz, etc). We append the instrumentation
           right after the branch (to instrument the not-taken path) and at the
           branch destination label (handled later on). */
 
        if (line[0] == '\t') {
 
            if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) {
 
                fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
                        R(MAP_SIZE));
 
                ins_lines++;
 
            }
 
            continue;
 
        }
 
 
/* Label of some sort. This may be a branch destination, but we need to
          tread carefully and account for several different formatting
          conventions. */
 
       /* Apple: L<whatever><digit>: */
 
       if ((colon_pos = strstr(line, ":"))) {
 
           if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {
               /* .L0: or LBB0_0: style jump destination */
 
                /* Apple: L<num> / LBB<num> */
 
               if ((isdigit(line[1]) || (clang_mode && !strncmp(line, "LBB", 3)))
                   && R(100) < inst_ratio) {
 
                       if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;
 
               }
 
           } else {
 
               /* Function label (always instrumented, deferred mode). */
 
               instrument_next = 1;
 
           }
/* Label of some sort. This may be a branch destination, but we need to
          tread carefully and account for several different formatting
          conventions. */
 
       /* Apple: L<whatever><digit>: */
 
       if ((colon_pos = strstr(line, ":"))) {
 
           if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {
               /* .L0: or LBB0_0: style jump destination */
 
                /* Apple: L<num> / LBB<num> */
 
               if ((isdigit(line[1]) || (clang_mode && !strncmp(line, "LBB", 3)))
                   && R(100) < inst_ratio) {
 
                       if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;
 
               }
 
           } else {
 
               /* Function label (always instrumented, deferred mode). */
 
               instrument_next = 1;
 
           }
 
/* In some cases, we want to defer writing the instrumentation trampoline
     until after all the labels, macros, comments, etc. If we're in this
     mode, and if the line starts with a tab followed by a character, dump
     the trampoline now. */
 
  if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
      instrument_next && line[0] == '\t' && isalpha(line[1])) {
 
      fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
              R(MAP_SIZE));
 
      instrument_next = 0;
      ins_lines++;
 
  }
/* In some cases, we want to defer writing the instrumentation trampoline
     until after all the labels, macros, comments, etc. If we're in this
     mode, and if the line starts with a tab followed by a character, dump
     the trampoline now. */
 
  if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
      instrument_next && line[0] == '\t' && isalpha(line[1])) {
 
      fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
              R(MAP_SIZE));
 
      instrument_next = 0;
      ins_lines++;
 
  }
 
if (!(pid = fork())) {
    execvp(as_params[0], (char **) as_params);
    FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);
}
if (!(pid = fork())) {
    execvp(as_params[0], (char **) as_params);
    FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);
}
 
 
lea     rsp, [rsp-98h]
mov     [rsp+98h+var_98], rdx
mov     [rsp+98h+var_90], rcx
mov     [rsp+98h+var_88], rax
mov     rcx, 46A1h          ;注意这里46A1h就是在fptintf插桩的时候R(MAP_SIZE)产生的随机数,可以用于区分每个桩
call    __afl_maybe_log
mov     rax, [rsp+98h+var_88]
mov     rcx, [rsp+98h+var_90]
mov     rdx, [rsp+98h+var_98]
lea     rsp, [rsp+98h]
lea     rsp, [rsp-98h]
mov     [rsp+98h+var_98], rdx
mov     [rsp+98h+var_90], rcx
mov     [rsp+98h+var_88], rax
mov     rcx, 46A1h          ;注意这里46A1h就是在fptintf插桩的时候R(MAP_SIZE)产生的随机数,可以用于区分每个桩
call    __afl_maybe_log
mov     rax, [rsp+98h+var_88]
mov     rcx, [rsp+98h+var_90]
mov     rdx, [rsp+98h+var_98]
lea     rsp, [rsp+98h]
 
.AFL_VARS:
 
  .comm   __afl_area_ptr, 8
  .comm   __afl_prev_loc, 8
  .comm   __afl_fork_pid, 4
  .comm   __afl_temp, 4
  .comm   __afl_setup_failure, 1
  .comm    __afl_global_area_ptr, 8, 8
.AFL_VARS:
 
  .comm   __afl_area_ptr, 8
  .comm   __afl_prev_loc, 8
  .comm   __afl_fork_pid, 4
  .comm   __afl_temp, 4
  .comm   __afl_setup_failure, 1
  .comm    __afl_global_area_ptr, 8, 8
__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
__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
 
 
__afl_setup:
 
  /* Do not retry setup if we had previous failures. */
 
  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_setup:
 
  /* Do not retry setup if we had previous failures. */
 
  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_setup_first:
 
  /* Save everything that is not yet saved and that may be touched by
     getenv() and several other libcalls we'll be relying on. */
 
  leaq -352(%rsp), %rsp
 
  movq %rax,   0(%rsp)
  movq %rcx,   8(%rsp)
  movq %rdi,  16(%rsp)
  movq %rsi,  32(%rsp)
  movq %r8,   40(%rsp)
  movq %r9,   48(%rsp)
  movq %r10,  56(%rsp)
  movq %r11,  64(%rsp)
 
  movq %xmm0,  96(%rsp)
  movq %xmm1,  112(%rsp)
  movq %xmm2,  128(%rsp)
  movq %xmm3,  144(%rsp)
  movq %xmm4,  160(%rsp)
  movq %xmm5,  176(%rsp)
  movq %xmm6,  192(%rsp)
  movq %xmm7,  208(%rsp)
  movq %xmm8,  224(%rsp)
  movq %xmm9,  240(%rsp)
  movq %xmm10, 256(%rsp)
  movq %xmm11, 272(%rsp)
  movq %xmm12, 288(%rsp)
  movq %xmm13, 304(%rsp)
  movq %xmm14, 320(%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
 
  leaq .AFL_SHM_ENV(%rip), %rdi
call _getenv
 
  testq %rax, %rax
  je    __afl_setup_abort
 
  movq  %rax, %rdi
call _atoi
 
  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_setup_first:
 
  /* Save everything that is not yet saved and that may be touched by
     getenv() and several other libcalls we'll be relying on. */
 
  leaq -352(%rsp), %rsp
 
  movq %rax,   0(%rsp)
  movq %rcx,   8(%rsp)
  movq %rdi,  16(%rsp)
  movq %rsi,  32(%rsp)
  movq %r8,   40(%rsp)
  movq %r9,   48(%rsp)
  movq %r10,  56(%rsp)
  movq %r11,  64(%rsp)
 
  movq %xmm0,  96(%rsp)
  movq %xmm1,  112(%rsp)
  movq %xmm2,  128(%rsp)
  movq %xmm3,  144(%rsp)
  movq %xmm4,  160(%rsp)
  movq %xmm5,  176(%rsp)
  movq %xmm6,  192(%rsp)
  movq %xmm7,  208(%rsp)
  movq %xmm8,  224(%rsp)
  movq %xmm9,  240(%rsp)
  movq %xmm10, 256(%rsp)
  movq %xmm11, 272(%rsp)
  movq %xmm12, 288(%rsp)

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 2
支持
分享
最新回复 (3)
雪    币: 14653
活跃值: (17749)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
2
感谢分享,请楼主分享下高效的学习方式,我也想高产似母猪
2021-2-8 16:51
0
雪    币: 420
活跃值: (705)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
之前一直误解了,以为创建子进程喂入变异数据的过程是在afl-fuzz中直接通过fork创建目标程序的进程,原来当不使用dump_mode和使用forkserver的时候,创建目标子进程的过程在插桩的地方就做了,非常感谢楼主的分享!
2022-1-14 23:14
0
雪    币: 420
活跃值: (705)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
顺便提一句,如果从汇编上理解插桩和forkserver机制比较晦涩,可以去看一下QEMU模式中的patch,里面对afl_maybe_log、forkserver都是从C代码讲解的,原理上也和这篇文章类似
2022-1-21 20:39
0
游客
登录 | 注册 方可回帖
返回
//