AFL原始碼分析(一)

2022-11-23 15:00:59

AFL原始碼分析(一)

文章首發於:ChaMd5公眾號 https://mp.weixin.qq.com/s/E-D_M25xv5gIpRa6k8xOvw

a.alf-gcc.c

1.find_as

這個函數的功能是獲取使用的組合器。首先獲取環境變數AFL_PATH,如果這個環境變數存在的話,接著把他和/as拼接,並判斷次路徑下的as檔案是否存在。如果存在,就使得as_path = afl_path = getenv("AFL_PATH")。如果不存在就通過第二種方式嘗試獲取其路徑。首先判斷是否存在/,並把最後一個/之後的路徑清空,之後為其前面的路徑分配空間,並與/afl-as拼接後判斷這個檔案是否存在,如果存在,則使得as_path = dir = ck_strdup(argv0)。如果這兩種方式都不能找到相應路徑,即會爆出異常。

2.edit_params

這個函數的主要功能是對編譯所用到的引數進行編輯。先為cc_params分配一大塊記憶體空間,然後嘗試獲取argv[0]的最後一個/的位置,如果存在就把它後面的內容設為name,否則name=argv[0]。之後判斷我們預期的編譯是不是afl-clang模式,如果是的話就設定clang_mode = 1,設定環境變數CLANG_ENV_VAR為 1,並新增相應的編譯引數。如果不是clang模式,則判斷name是否等於afl-g++,afl-gcj等選項,並新增相應的引數。接著從argv[1]開始遍歷編譯選項,會跳過-B -integrated-as -pipe這三個選項,因為edit_params會自動新增這三個編譯選項。最後cc_params[cc_par_cnt] = NULL標誌結束對選項的編輯。

3.main

int main(int argc, char** argv) {

  if (isatty(2) && !getenv("AFL_QUIET")) {

    SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <[email protected]>\n");

  } else be_quiet = 1;

  if (argc < 2) {

    SAYF("\n"
         "This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"
         "for gcc or clang, letting you recompile third-party code with the required\n"
         "runtime instrumentation. A common use pattern would be one of the following:\n\n"

         "  CC=%s/afl-gcc ./configure\n"
         "  CXX=%s/afl-g++ ./configure\n\n"

         "You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.\n"
         "Setting AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",
         BIN_PATH, BIN_PATH);

    exit(1);

  }

  find_as(argv[0]);

  edit_params(argc, argv);

  execvp(cc_params[0], (char**)cc_params);

  FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);

  return 0;

}

總體來說就是先呼叫find_as(argv[0])獲取使用的組合器,再呼叫edit_params(argc, argv)對編譯選項進行編輯,再通過execvp去進行編譯。總的來說alf-gcc是對gcc或clang的一個wrapper。而其中強制加上的-B as_path實際上是給其指定組合器,也就是我們下面會提到的afl-as。實際的插樁也就是在afl-as裡進行插樁的。

b.afl-as

1.edit_params

這個函數的主要功能是編輯組合器所用到的引數。首先獲取環境變數TMPDIRAFL_AS。接著根據是否是clang模式並且afl_as是否為空,去判斷是否要重新獲取afl_as的值,直到其不為空。接著獲取tmp_dir的值,直到其不為空。下面就是給as_params分配一大塊空間,並開始對引數進行編輯。首先先設定as_params[0],也即組合器,一般來說這裡都是as。接著從argv[1]遍歷到argv[argc-1],看是否存在--64,如果存在--64就使得use_64bit = 1。如果定義了__APPLE__,那麼如果存在-arch x86_64就使得use_64bit = 1。並且其會忽略-q或者-Q選項。其餘選項引數都會依此加到as_params[as_par_cnt++]中。如果定義了__APPLE__,接下來會判斷是否是clang模式,如果是那麼新增-c -x assembler的選項。緊接著把argv[argc - 1]賦給 input_file,即最後一個引數的值為input_file的值。下面會判斷input_file,是否與--version相等,如果相等,標誌just_version=1,可能是代表查詢版本。如果不等那麼將input_filetmp_dir、/var/tmp/、/tmp/進行比較,如果都不相同,則設定pass_thru = 1。並通過格式化字串設定modified_file = tmp_dir/.afl-getpid()-(u32)time(NULL).s。最後設定as_params[as_par_cnt++] = modified_file,並結束對as_params的編輯。

2.add_instrumentation

這個函數就是進行插樁的關鍵函數了。首先判斷檔案是否存在並且可讀,不滿足就丟擲異常。然後開啟modified_file裡的臨時檔案,獲得其控制程式碼outfd,再通過控制程式碼拿到檔案對應的指標。

  if (input_file) {                                                         // 判斷檔案是否存在並可讀

    inf = fopen(input_file, "r");
    if (!inf) PFATAL("Unable to read '%s'", input_file);

  } else inf = stdin;                                                       // 檔案不存在,則標準輸入作為 input_file

  outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600);           // 開啟這個臨時檔案

  if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file);

  outf = fdopen(outfd, "w");

  if (!outf) PFATAL("fdopen() failed");  

接下來就是插樁的關鍵部分了。

  while (fgets(line, MAX_LINE, inf)) {                                      // 逐行從inf讀取檔案到line陣列裡

    /* 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,       // 根據use_64bit插入相應的插樁程式碼
              R(MAP_SIZE));

      instrument_next = 0;
      ins_lines++;

    }

首先是一個大while迴圈,通過fgets逐行從inf讀取檔案到line陣列裡,最多MAX_LINE也即8192位元組。並且通過幾個標記的值來判斷是否要插入相應的程式碼。並且根據use_64bit的值來確定插入的是trampoline_fmt_64還是trampoline_fmt_32

    fputs(line, outf);                                                       // 把 line 寫到 modified_file 裡

    if (pass_thru) continue;

    /* All right, this is where the actual fun begins. For one, we only want to       // 通過註釋可以知道我們只對.text進行插樁
       instrument the .text section. So, let's keep track of that in processed        // 通過 instr_ok 來標記是否在 .text 段
       files - and let's set instr_ok accordingly. */

    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) ||                                // 如果 line 的值為 \t.text\n
          !strncmp(line + 2, "section\t.text", 13) ||                       // 或 \t.section\t.text
          !strncmp(line + 2, "section\t__TEXT,__text", 21) ||               // 或 \t.section\t__TEXT,__text
          !strncmp(line + 2, "section __TEXT,__text", 21)) {                // 或 \t.section __TEXT,__text
        instr_ok = 1;                                                       // 設定 instr_ok = 1,並跳轉到開頭讀取下一行內容
        continue; 
      }

      if (!strncmp(line + 2, "section\t", 8) ||                             // 如果 line 的值為 \t.section\t
          !strncmp(line + 2, "section ", 8) ||                              // 或 \t.section
          !strncmp(line + 2, "bss\n", 4) ||                                 // 或 \tbss\n
          !strncmp(line + 2, "data\n", 5)) {                                // 或 \tdata\n
        instr_ok = 0;                                                       // 設定 instr_ok = 0,並跳轉到開頭讀取下一行內容
        continue;
      }

    }

我們會把line裡的值寫道outf(即modified_file)裡。根據官方給的註釋可以知道我們只期望對text段進行插樁,並且通過設定instr_ok來標記是否是text段。如果line+2匹配到\t.text\n、\t.section\t.text等就設定instr_ok=1,如果line+2匹配到\t.section\t、\t.section等就設定instr_ok=0。並跳過下面的程式碼,直接跳到迴圈的開頭讀取下一行的內容。

                                                                            // 接下來設定一些其他的標誌
    /* Detect off-flavor assembly (rare, happens in gdb). When this is
       encountered, we set skip_csect until the opposite directive is
       seen, and we do not instrument. */

    if (strstr(line, ".code")) {                                            // 判斷 off-flavor

      if (strstr(line, ".code32")) skip_csect = use_64bit;
      if (strstr(line, ".code64")) skip_csect = !use_64bit;

    }

    /* Detect syntax changes, as could happen with hand-written assembly.
       Skip Intel blocks, resume instrumentation when back to AT&T. */

    if (strstr(line, ".intel_syntax")) skip_intel = 1;                      // 跳過 Intel組合的插樁
    if (strstr(line, ".att_syntax")) skip_intel = 0;

    /* Detect and skip ad-hoc __asm__ blocks, likewise skipping them. */

    if (line[0] == '#' || line[1] == '#') {                                 // 跳過 ad-hoc __asm__(內聯組合) 的插樁

      if (strstr(line, "#APP")) skip_app = 1;
      if (strstr(line, "#NO_APP")) skip_app = 0;

    }

在往下就是設定一些其他的標誌來判斷是否跳過插樁。主要是跳過與設定架構不同的架構的組合,跳過Intel組合,跳過內聯組合的插樁。

    /* 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 (skip_intel || skip_app || skip_csect || !instr_ok ||
        line[0] == '#' || line[0] == ' ') 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,          // 通過 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. */

#ifdef __APPLE__

    /* Apple: L<whatever><digit>: */

    if ((colon_pos = strstr(line, ":"))) {

      if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {

#else

    /* Everybody else: .L<whatever>: */

    if (strstr(line, ":")) {                                                     // 檢查 line 裡是否有 :

      if (line[0] == '.') {                                                      // 判斷 line 是否以 . 開始

#endif /* __APPLE__ */

        /* .L0: or LBB0_0: style jump destination */

#ifdef __APPLE__

        /* Apple: L<num> / LBB<num> */

        if ((isdigit(line[1]) || (clang_mode && !strncmp(line, "LBB", 3)))
            && R(100) < inst_ratio) {

#else

        /* Apple: .L<num> / .LBB<num> */

        if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3)))   // 如果 line[2] 是數位,或者在 clang 模式下,line = .LBB
            && R(100) < inst_ratio) {

#endif /* __APPLE__ */

          /* An optimization is possible here by adding the code only if the
             label is mentioned in the code in contexts other than call / jmp.
             That said, this complicates the code by requiring two-pass
             processing (messy with stdin), and results in a speed gain
             typically under 10%, because compilers are generally pretty good
             about not generating spurious intra-function jumps.

             We use deferred output chiefly to avoid disrupting
             .Lfunc_begin0-style exception handling calculations (a problem on
             MacOS X). */

          if (!skip_next_label) instrument_next = 1; else skip_next_label = 0; // 如果 skip_next_label == 0

        }

      } else {                                                                 // 否則就是函數(function),給 function 直接設定 instrument_next = 1

        /* Function label (always instrumented, deferred mode). */

        instrument_next = 1;
    
      }

    }

  }

接下來是對其他的標誌進行設定,可以從註釋中看出我們想對main、.L0、.LBB0_0(clang mode)、\tjnz foo或者function等地方設定instrument_next = 1。其他部分看我對原始碼加的註釋。

迴圈結束後,接下來如果ins_lines不為空,那麼通過use_64bit,判斷向 outf 裡寫入main_payload_64還是main_payload_32。並且關閉兩個檔案。

3.main

主函數就比較簡單了。首先獲取環境變數AFL_INST_RATIO,並檢測其是否合法(在0-100之間)。通過當前時間和程序號來獲取並設定srandom的隨機種子。獲取環境變數AS_LOOP_ENV_VAR,如果存在就丟擲異常。

呼叫edit_params設定相關引數。獲取環境變數AFL_USE_ASANAFL_USE_MSAN,如果有一個存在就設定 sanitizer = 1,inst_ratio /= 3,這是因為在進行ASAN的編譯時,AFL無法識別出ASAN特定的分支,導致插入很多無意義的樁程式碼,所以直接暴力地將插樁概率/3。最後fork出一個子程序,執行 execvp(as_params[0], (char**)as_params)

有註釋的原始碼也放在了我的github專案裡:https://github.com/fxc233/my-afl-interpret