0x00 前言

总结师傅们笔记,主要源码分析。

0x01 代码覆盖率

代码覆盖率是fuzz中基本概念,先了解清这个概念后面的插装编译等概念才好理解。

代码覆盖率是一种度量代码的覆盖程度的方式,也就是指源代码中的某行代码是否已执行;对二进制程序,还可将此概念理解为汇编代码中的某条指令是否已执行。对fuzz来说,当然希望每句代码都能被检测到,覆盖率越高越好。

计量方式主要为三种:函数基本块边界

插桩

插桩是为了覆盖率而实行的方法。

afl-gcc.c

afl-gcc是gcc的一个封装(wrapper)

主要三个功能

find_as(argv[0]);                     //找到gcc/clang/llvm编译器
  edit_params(argc, argv);               //处理参数
  execvp(cc_params[0], (char**)cc_params);//执行

打印出cc_params,看看真正的参数是什么

gcc -o test test.c -B /usr/local/lib/afl -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1

看看参数的意思。用了编译优化,指定了编译的标志,最终要的是-B指定了编译器(Assembler)

-funroll-loops      执行循环强度消除并消除在循环内部使用的变量。这是用简单而快速的操作(如加法和减法)替代耗时操作(如乘法和除法)的过程
-B <目录>            将 <目录> 添加到编译器的搜索路径中
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION     /* a flag also shared with libfuzzer) or */
#ifdef __AFL_COMPILER                             /* (this one is just for AFL). */

这一步正是汇编文件通过as进一步编译成二进制文件,这里替换了Assembler,当然为了插桩

afl-as.c和afl-as.h

反汇编刚才生成的test,会发现插了一些插入了额外的汇编指令

这两个文件被单独提出来可以来解释这里是怎么操作的

The sole purpose of this wrapper is to preprocess assembly files generated by GCC / clang and inject the instrumentation bits included from afl-as.h. It is automatically invoked by the toolchain when compiling programs using afl-gcc / afl-clang.

主要是处理不同平台设置标志,处理参数等等.重要函数add_instrumentation

fprintf将插桩用的汇编用fprintf插如合适的地方

static  void  add_instrumentationvoid{
    ......
    while (fgets(line, MAX_LINE, inf)) {//读取每行
    ......
      fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
              R(MAP_SIZE));//插入,注意R(MAP_SIZE)
    ......
        //下面有意思是怎么判断合适的地方插入,选择分支,有兴趣自己看看
    }
}

下面分别是32位和64位的,和调试看的一样

static const u8* trampoline_fmt_32 =

  "\n"
  "/* --- AFL TRAMPOLINE (32-BIT) --- */\n"
  "\n"
  ".align 4\n"
  "\n"
  "leal -16(%%esp), %%esp\n"    //太高栈
  "movl %%edi,  0(%%esp)\n"     //保存寄存器
  "movl %%edx,  4(%%esp)\n"
  "movl %%ecx,  8(%%esp)\n"
  "movl %%eax, 12(%%esp)\n"
  "movl $0x%08x, %%ecx\n"       //保存随机数
  "call __afl_maybe_log\n"      //调用__afl_maybe_log
  "movl 12(%%esp), %%eax\n"
  "movl  8(%%esp), %%ecx\n"
  "movl  4(%%esp), %%edx\n"
  "movl  0(%%esp), %%edi\n"
  "leal 16(%%esp), %%esp\n"
  "\n"
  "/* --- END --- */\n"
  "\n";

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"
  "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";

所以能看到,插桩是为了统计覆盖率。至于具体怎么实现,继续看后面

fork service

这是一种为了不使用execve()函数提高效率想出来的办法,省掉动态链接等过程,在lcamtuf的blog上也有详细的介绍。

afl-fuzz.c

EXP_ST void init_forkserver(char** argv) {

  int st_pipe[2], ctl_pipe[2];//命令管道和状态管道
    ......

 execv(target_path, argv);   //执行fork server
}

有两个重点

  • 怎么高效重复执行测试样例
  • 记录样例的状态

开始fork service确认创建完毕

/* Close the unneeded endpoints. */
//关闭不需要的通道
  close(ctl_pipe[0]);
  close(st_pipe[1]);

//读取通道状态命令
  fsrv_ctl_fd = ctl_pipe[1];
  fsrv_st_fd  = st_pipe[0];

......

  rlen = read(fsrv_st_fd, &status, 4);//从状态通道读取4个字节


  /* If we have a four-byte "hello" message from the server, we're all set.
     Otherwise, try to figure out what went wrong. */

  if (rlen == 4) {//判断读取是否成功
    OKF("All right - fork server is up.");
    return;
  }

__afl_maybe_log()

这里因为AFL自带的延时检测,所以没法调试看,这里只有看源码

这里先检测是否分配到公共内存,__afl_area_ptr里面就是地址,否则先调用__afl_setup初始化

.text:0000000000000950                 lahf
.text:0000000000000951                 seto    al
.text:0000000000000954                 mov     rdx, cs:__afl_area_ptr
.text:000000000000095B                 test    rdx, rdx
.text:000000000000095E                 jz      short __afl_setup

__afl_forkserver

写4个字节到状态管道st_pipe[0],forkserver告诉fuzzer自己准备好了,而这正好是rlen = read(fsrv_st_fd, &status, 4);中等待的信息

.text:0000000000000ABB __afl_forkserver:
.text:0000000000000ABB                 push    rdx
.text:0000000000000ABC                 push    rdx
.text:0000000000000ABD                 mov     rdx, 4          ; n
.text:0000000000000AC4                 lea     rsi, __afl_temp ; buf
.text:0000000000000ACB                 mov     rdi, 0C7h       ; fd
.text:0000000000000AD2                 call    _write
.text:0000000000000AD7                 cmp     rax, 4
.text:0000000000000ADB                 jnz     __afl_fork_resume

__afl_fork_wait_loop

fork server直到从状态管道read到4个字节表明fuzzer准备好了

text:0000000000000AE1                  mov     rdx, 4          ; nbytes
.text:0000000000000AE8                 lea     rsi, __afl_temp ; buf
.text:0000000000000AEF                 mov     rdi, 0C6h       ; status
.text:0000000000000AF6                 call    _read
.text:0000000000000AFB                 cmp     rax, 4
.text:0000000000000AFF                 jnz     __afl_die
.text:0000000000000B05                 call    _fork
.text:0000000000000B0A                 cmp     rax, 0
.text:0000000000000B0E                 jl      __afl_die
.text:0000000000000B14                 jz      short __afl_fork_resume

记录子进程的pid,一旦子进程执行完了,通过状态管道发送到fuzzer继续执行

.text:0000000000000B16                 mov     cs:__afl_fork_pid, eax
.text:0000000000000B1C                 mov     rdx, 4          ; n
.text:0000000000000B23                 lea     rsi, __afl_fork_pid ; buf
.text:0000000000000B2A                 mov     rdi, 0C7h       ; fd
.text:0000000000000B31                 call    _write
.text:0000000000000B36                 mov     rdx, 0          ; options
.text:0000000000000B3D                 lea     rsi, __afl_temp ; stat_loc
.text:0000000000000B44                 mov     rdi, qword ptr cs:__afl_fork_pid ; pid
.text:0000000000000B4B                 call    _waitpid
.text:0000000000000B50                 cmp     rax, 0
.text:0000000000000B54                 jle     __afl_die
.text:0000000000000B5A                 mov     rdx, 4          ; n
.text:0000000000000B61                 lea     rsi, __afl_temp ; buf
.text:0000000000000B68                 mov     rdi, 0C7h       ; fd
.text:0000000000000B6F                 call    _write
.text:0000000000000B74                 jmp     __afl_fork_wait_loop

用伪代码更能看清楚逻辑

if ( write(0xC7, &_afl_temp, 4uLL) == 4 )
      {
        while ( 1 )
        {
          v25 = 0xC6;
          if ( read(0xC6, &_afl_temp, 4uLL) != 4 )
            break;
          LODWORD(v26) = fork();
          if ( v26 < 0 )
            break;
          if ( !v26 )
            goto __afl_fork_resume;
          _afl_fork_pid = v26;
          write(0xC7, &_afl_fork_pid, 4uLL);
          v25 = _afl_fork_pid;
          LODWORD(v27) = waitpid(_afl_fork_pid, &_afl_temp, 0);
          if ( v27 <= 0 )
            break;
          write(199, &_afl_temp, 4uLL);
        }
        _exit(v25);
      }

fuzzer这边来看,发出请求,接受状态,根据状态管道判断执行结果......

if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4); //启动fork server
if ((res = read(fsrv_st_fd, &child_pid, 4)) != 4)  
    .......
 /* Report outcome to caller. */
  if (WIFSIGNALED(status) && !stop_soon) {
    kill_signal = WTERMSIG(status);
    if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;
    return FAULT_CRASH;

  }

分支记录

如何判断这条路径(代码)执行过,后面还要根据这些记录对后面变异有帮助。既要节约空间又要有效率,那单链表之类的肯定不能用,AFL用的是二元tuple(跳转的源地址和目标地址)来记录分支信息。

例如:

A->B->C->D->A-B

可以用[A,B] [B,C] [C,D] [D,A]四个二元组表示,只需要记录跳转的源地址和目标地址。并且[A,B]执行了两次,其余执行了一次,这里用hash映射在一张map中。

接下来代码具体讲讲。

之前在__afl_maybe_log后面还有_afl_store这个函数

.text:0000000000000960 __afl_store:                            ; CODE XREF: __afl_maybe_log+4F↓j
.text:0000000000000960                                         ; __afl_maybe_log+309↓j
.text:0000000000000960                 xor     rcx, cs:__afl_prev_loc
.text:0000000000000967                 xor     cs:__afl_prev_loc, rcx
.text:000000000000096E                 shr     cs:__afl_prev_loc, 1
.text:0000000000000975                 inc     byte ptr [rdx+rcx]

对应的伪代码。COMPILE_TIME_RANDOM就是add_instrumentationfprintfR(MAP_SIZE),也是在执行call __afl_maybe_log汇编前rcx中保存的随机数,这个随机数代表分支

cur_location = <COMPILE_TIME_RANDOM>;       //随机数当前分支
shared_mem[cur_location ^ prev_location]++;  //前一分支和当前分支锁表示的随机数异或表示二元tuple映射map
prev_location = cur_location >> 1;          //将当前分支再记录

为什么当前分支最后需要向右移一位?比如A->A或者A->B->A这种不右移异或为0

并且共享内存的MAP_SIZE=64K碰撞概率缩小很多。下面是官方给的

Branch cnt | Colliding tuples | Example targets
------------+------------------+-----------------
      1,000 | 0.75%            | giflib, lzo
      2,000 | 1.5%             | zlib, tar, xz
      5,000 | 3.5%             | libpng, libwebp
     10,000 | 7%               | libxml
     20,000 | 14%              | sqlite
     50,000 | 30%              | -

分支信息处理

共享内存还有个变量trace_bits来记录分支执行次数

classify_counts((u32*)trace_bits);

fuzzer主要将每个分支处理次数归入下面这个表中

static const u8 count_class_lookup8[256] = {

  [0]           = 0, 
  [1]           = 1, 
  [2]           = 2, 
  [3]           = 4, 
  [4 ... 7]     = 8, 
  [8 ... 15]    = 16,
  [16 ... 31]   = 32,
  [32 ... 127]  = 64,
  [128 ... 255] = 128
};

比如执行了4-7次的其计数为8,最后用一个hash还判断新测试用例分支数增加没有

u32 cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);

参考链接

https://paper.seebug.org/496/
http://lcamtuf.coredump.cx/afl/technical_details.txt
https://www.inforsec.org/wp/?p=2678
https://lcamtuf.blogspot.com/2014/10/fuzzing-binaries-without-execve.html

点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖