首页
论坛
课程
招聘
[原创]AFL编译插桩部分源码分析
2021-2-8 16:49 5318

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

2021-2-8 16:49
5318

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

目录

开始之前

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

 

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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;
}


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

我们将其拉入IDA看一下。


可以看到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)

  • 首先获取环境变量TMPDIRAFL_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];

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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
    }
 
}

在这里我们判断读入的这一行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尝试抓住一些能标志程序变化的重要的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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;
 
        }

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

 

然后插桩计数ins_lines加一。continue

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 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;
 
           }

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

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

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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 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++;
 
  }

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