大致分析afl++

image 研究一下 afl++ 內部工作原理

Install

我的環境是docker並且在windows wsl2 https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/INSTALL.md

bash
docker pull aflplusplus/aflplusplus:latest
docker run -ti -v [localfolder]:/src aflplusplus/aflplusplus

Build afl++

bash
cp -R /AFLplusplus/ /src

假設你在 wsl2 你要在docker maping 出來的資料夾透過vscode 修改source code 那你必須要在外面對著 container copy出來的 AFLplusplus資料夾在設定權限一次

bash
# in wsl folder
chmod 777 /AFLplusplus

這樣就可以透過vscode 去修改source code 因為某寫檔案已經build 過所以可以很快的build 完整的source code

bash
make
make install

Example

Source Tree

開始建立 example 首先先建立個testfolder

bash
./testfolder

裡面大概要這些資料夾,fuzz_in裡面還要再產生一個testcase

bash
-rwxr-xr-x  1 root root 19248 Apr  4 15:11 fuzzTest
-rwxrwxrwx  1 root root   186 Apr  4 14:02 fuzzTest.c
drwxrwxrwx  2 root root  4096 Apr  4 14:02 fuzz_in

Script

bash
touch fuzzTest.c
echo "1234567892&^$%^$#$@" > fuzz_in/testcase
mkdir fuzz_in

Testcase

fuzzTest.c

c
#include <stdio.h>  // 引入標準I/O庫
int main(int argc, char *argv[])
{
    char buf[100] = {0};  // 定義字符陣列
    gets(buf);   // 讀取輸入,存在栈溢出風險
    printf(buf); // 輸出字符串,存在格式化字符串風險
    return 0;
}

Build

bash
afl-gcc -g -o ./fuzzTest fuzzTest.c

Fuzzy

bash
afl-fuzz -i fuzz_in -o fuzz_out ./fuzzTest

Run

image

compiler instrumentation

在看afl++架構的時候,發先從編譯的時候他會對程式碼進行插樁,也就是埋code,這樣再配合afl-fuzz在runtime的時候可以控制binary 生命流程來達到探索路徑,那最簡單的就是 觀看afl-gcc 做了那些事情

bash
afl-gcc -g -o ./fuzzTest fuzzTest.c

afl-as.c

bash
/home/x213212/afl/AFLplusplus/src/afl-as.c

在這個source code 可以找到 afl-gcc compiler entry point main function

c
/* Main entry point */
int main(int argc, char **argv) {

  s32 pid;
  u32 rand_seed, i, j;
  int status;
  u8 *inst_ratio_str = getenv("AFL_INST_RATIO");

  struct timeval  tv;
  struct timezone tz;

  clang_mode = !!getenv(CLANG_ENV_VAR);

  if ((isatty(2) && !getenv("AFL_QUIET")) || getenv("AFL_DEBUG") != NULL) {

    SAYF(cCYA "afl-as" VERSION cRST " by Michal Zalewski\n");

  } else {

    be_quiet = 1;

  }

  if (argc < 2 || (argc == 2 && strcmp(argv[1], "-h") == 0)) {

    fprintf(
        stdout,
        "afl-as" VERSION
        " by Michal Zalewski\n"
        "\n%s [-h]\n\n"
        "This is a helper application for afl-fuzz. It is a wrapper around GNU "
        "'as',\n"
        "executed by the toolchain whenever using afl-gcc or afl-clang. You "
        "probably\n"
        "don't want to run this program directly.\n\n"

        "Rarely, when dealing with extremely complex projects, it may be "
        "advisable\n"
        "to set AFL_INST_RATIO to a value less than 100 in order to reduce "
        "the\n"
        "odds of instrumenting every discovered branch.\n\n"
        "Environment variables used:\n"
        "AFL_AS: path to assembler to use for instrumented files\n"
        "AFL_CC: fall back path to assembler\n"
        "AFL_CXX: fall back path to assembler\n"
        "TMPDIR: directory to use for temporary files\n"
        "TEMP: fall back path to directory for temporary files\n"
        "TMP: fall back path to directory for temporary files\n"
        "AFL_INST_RATIO: user specified instrumentation ratio\n"
        "AFL_QUIET: suppress verbose output\n"
        "AFL_KEEP_ASSEMBLY: leave instrumented assembly files\n"
        "AFL_AS_FORCE_INSTRUMENT: force instrumentation for asm sources\n"
        "AFL_HARDEN, AFL_USE_ASAN, AFL_USE_MSAN, AFL_USE_UBSAN, AFL_USE_LSAN:\n"
        "  used in the instrumentation summary message\n",
        argv[0]);

    exit(1);

  }

  gettimeofday(&tv, &tz);

  rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();
  // in fast systems where pids can repeat in the same seconds we need this
  for (i = 1; (s32)i < argc; i++)
    for (j = 0; j < strlen(argv[i]); j++)
      rand_seed += argv[i][j];

  srandom(rand_seed);

  edit_params(argc, argv);

  if (inst_ratio_str) {

    if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || inst_ratio > 100) {

      FATAL("Bad value of AFL_INST_RATIO (must be between 0 and 100)");

    }

  }

  if (getenv(AS_LOOP_ENV_VAR)) {

    FATAL("Endless loop when calling 'as' (remove '.' from your PATH)");

  }

  setenv(AS_LOOP_ENV_VAR, "1", 1);

  /* When compiling with ASAN, we don't have a particularly elegant way to skip
     ASAN-specific branches. But we can probabilistically compensate for
     that... */

  if (getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) {

    sanitizer = 1;
    if (!getenv("AFL_INST_RATIO")) { inst_ratio /= 3; }

  }

  if (!just_version) { add_instrumentation(); }

  if (!(pid = fork())) {

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

  }

  if (pid < 0) { PFATAL("fork() failed"); }

  if (waitpid(pid, &status, 0) <= 0) { PFATAL("waitpid() failed"); }

  if (!getenv("AFL_KEEP_ASSEMBLY")) { unlink(modified_file); }

  exit(WEXITSTATUS(status));

}

afl-as 首先會先執行 function edit_params() 來調整參數,而後會執行 function add_instrumentation() 做插樁,最後執行 as 做組譯

add_instrumentation

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

static void add_instrumentation(void) {

  static u8 line[MAX_LINE];

  FILE *inf;
  FILE *outf;
  s32   outfd;
  u32   ins_lines = 0;

  u8 instr_ok = 0, skip_csect = 0, skip_next_label = 0, skip_intel = 0,
     skip_app = 0, instrument_next = 0;

#ifdef __APPLE__

  u8 *colon_pos;

#endif                                                         /* __APPLE__ */

  if (input_file) {

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

  } else {

    inf = stdin;

  }

  outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, DEFAULT_PERMISSION);

  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)) {

    /* 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++;

    }

    /* Output the actual line, call it a day in pass-thru mode. */

    fputs(line, outf);

    if (pass_thru) { continue; }

    /* All right, this is where the actual fun begins. For one, we only want to
       instrument the .text section. So, let's keep track of that in processed
       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) ||
          !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;

      }

    }

    /* 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")) {

      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; }
    if (strstr(line, ".att_syntax")) { skip_intel = 0; }

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

    if (line[0] == '#' || line[1] == '#') {

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

    }

    /* 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) < (long)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
       read 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, ":")) {

      if (line[0] == '.') {

#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) < (long)inst_ratio) {

#else

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

        if ((isdigit(line[2]) ||
             (clang_mode && !strncmp(line + 1, "LBB", 3))) &&
            R(100) < (long)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;

          }

        }

      } else {

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

        instrument_next = 1;

      }

    }

  }

  if (ins_lines) { fputs(use_64bit ? main_payload_64 : main_payload_32, outf); }

  if (input_file) { fclose(inf); }
  fclose(outf);

  if (!be_quiet) {

    if (!ins_lines) {

      WARNF("No instrumentation targets found%s.",
            pass_thru ? " (pass-thru mode)" : "");

    } else {

      char modeline[100];
      snprintf(modeline, sizeof(modeline), "%s%s%s%s%s%s",
               getenv("AFL_HARDEN") ? "hardened" : "non-hardened",
               getenv("AFL_USE_ASAN") ? ", ASAN" : "",
               getenv("AFL_USE_MSAN") ? ", MSAN" : "",
               getenv("AFL_USE_TSAN") ? ", TSAN" : "",
               getenv("AFL_USE_UBSAN") ? ", UBSAN" : "",
               getenv("AFL_USE_LSAN") ? ", LSAN" : "");

      OKF("Instrumented %u locations (%s-bit, %s mode, ratio %u%%).", ins_lines,
          use_64bit ? "64" : "32", modeline, inst_ratio);

    }

  }

}

那這些插樁的asm 到底寫在哪個資料夾 可以在afl-as.h 看的到

bash
/home/x213212/afl/AFLplusplus/include/afl-as.h

image 像在這篇文章就有提到跳轉規則 https://zhuanlan.zhihu.com/p/583178410

  • 插樁的模式:
    1. ^main - 函式入口點
    2. ^..L0 - GCC跳轉標籤
    3. ^..LBB0_0 - clang跳轉標籤
    4. ^\tjnz foo - 條件跳轉標籤
  • 不希望捕獲的模式:
    1. ^# BB#0 - clang注釋
    2. ^ # BB#0 - clang注釋
    3. ^..Ltmp0 - clang非分支標籤
    4. ^..LC0 - GCC非分支標籤
    5. ^..LBB0_0 - GCC非分支標籤(這條與想要捕獲的clang跳轉標籤相同,需確定context以做區分)
    6. ^\tjmp foo - 非條件跳轉

https://www.secpulse.com/archives/166956.html 在這邊文章也有敘述到 在路徑中 他會寫入到bitmap來記錄探索的路徑資料寫到shard memory 也就是後續要大概說一下fork_server運作原理

fork_server

透過剛剛插樁,其實就是在控制程式的生命流程 https://ch4r1l3.github.io/2019/03/08/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%903%E2%80%94%E2%80%94afl-as-h%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/ 這篇文章有對插入的asm進行詳細解釋了我們直接看 image image

c
#include <stdio.h>
int main(void){

    //instrumentation A, 函数入口
    int a;
    scanf("%d", &a);

    if(a==0xdeadbeef)
        //instrumentation B, jnz 不執行
        *((char *)0)=1;

    //instrumentation C, jnz 執行
    return 0;
}

也就是說fork_server 透過 fork 先停在main function ,由於我們又用compiler 進行插樁,所以在遇到新的路徑的時候都會記錄在shard memory 來避免重複探索路徑,那透過這樣的方式 假設fork 出來的 child process 發生 timeout 或者 crash ,子父程序都可以藉由pipe 去傳遞彼此之間的狀態,透過這樣的方式不用每次從頭開始執行,假設發生crash 也只是fork 出來的child process ,fuzzer 隨時可以從上一個中斷點開始運行

fuzzy

後續就是透過演算法不斷的探索新路徑,然後紀錄發生crash 再去看在修復程式碼,分析較為淺不過大致流程應該是這樣 https://bbs.kanxue.com/thread-254705.htm

ref

https://tttang.com/user/f1tao https://ch4r1l3.github.io/2019/03/05/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%901%E2%80%94%E2%80%94afl-gcc-c%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/ https://ch4r1l3.github.io/2019/03/06/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%902%E2%80%94%E2%80%94afl-as-c%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/ https://ch4r1l3.github.io/2019/03/08/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%903%E2%80%94%E2%80%94afl-as-h%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/ https://ch4r1l3.github.io/2019/03/09/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%904%E2%80%94%E2%80%94afl-fuzz-c%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%901/ https://ch4r1l3.github.io/2019/03/10/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%905%E2%80%94%E2%80%94afl-fuzz-c%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%902/ https://ithelp.ithome.com.tw/users/20151153/ironman/5164?page=1 https://ithelp.ithome.com.tw/articles/10288409