AFL源碼淺析
前言AFL是一款著名的模糊測試的工具,最近在閱讀AFL源碼,記錄一下,方便以后查閱。
- 環境? 項目:AFL
- ? 編譯項目:將編譯的優化選項關閉,即改寫成
-O0
1
afl-gcc.c使用gdb加載afl-gcc,并使用set arg -o test test.c設置參數
2
- find_as函數?
find_as函數首先會通過AFL_PATH環境變量的值從而獲得AFL對應的路徑 - ? 若上述環境變量不存在則獲取當前
afl-gcc所在的文件路徑 - ? 判斷該路徑下的
as文件是否具有可執行權限
u8 *afl_path = getenv("AFL_PATH");
...
if (afl_path) {
tmp = alloc_printf("%s/as", afl_path); //將AFL所在路徑與字符as進行拼接
if (!access(tmp, X_OK)) { //函數用來判斷指定的文件或目錄是否有可執行權限,若指定方式有效則返回0,否則返回-1
as_path = afl_path;
ck_free(tmp);
return;
}
ck_free(tmp);
}
slash = strrchr(argv0, '/'); //在參數argv0所指向的字符串中搜索最后一次出現字符'/'
if (slash) {
u8 *dir;
*slash = 0;
dir = ck_strdup(argv0);
*slash = '/';
tmp = alloc_printf("%s/afl-as", dir); //將當前AFL所在的路徑跟afl-as進行拼接
if (!access(tmp, X_OK)) {
as_path = dir;
ck_free(tmp);
return;
}
...
- edit_params函數?
edit_params函數實際就是準備需要傳入編譯器的參數,如編譯器的類型gcc或clang - ? 其次就是是否需要開啟保護如
canary等 - ? 最后就是判斷是否開啟內存泄漏探測的工具,如
ASAN,該工具是針對C/C++ 的快速內存錯誤檢測工具
...
cc_params = ck_alloc((argc + 128) * sizeof(u8*));
name = strrchr(argv[0], '/'); //獲取可執行文件名稱
if (!name) name = argv[0]; else name++; /*跳過路徑符'/' */
if (!strncmp(name, "afl-clang", 9)) { //判斷編譯器是否為clang
...
}
else {
if (!strcmp(name, "afl-g++")) {
u8* alt_cxx = getenv("AFL_CXX");
cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++";
} else if (!strcmp(name, "afl-gcj")) {
u8* alt_cc = getenv("AFL_GCJ");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj";
} else {
u8* alt_cc = getenv("AFL_CC");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc"; //如環境變量沒寫入AFL_CC則默認使用gcc
}
}
while (--argc) {
u8* cur = *(++argv); //讀取下一個參數
if (!strncmp(cur, "-B", 2)) { //若參數是-B
if (!be_quiet) WARNF("-B is already set, overriding"); //用于設置編譯器的搜索路徑
if (!cur[2] && argc > 1) { argc--; argv++; }//繼續讀取下一個參數
continue;
}
if (!strcmp(cur, "-integrated-as")) continue;
if (!strcmp(cur, "-pipe")) continue;
#if defined(__FreeBSD__) && defined(__x86_64__)
if (!strcmp(cur, "-m32")) m32_set = 1;
#endif
if (!strcmp(cur, "-fsanitize=address") ||
!strcmp(cur, "-fsanitize=memory")) asan_set = 1; //內存訪問的錯誤
if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;//緩沖區溢出問題的檢查
cc_params[cc_par_cnt++] = cur; //cc_params用于存放的參數
}
cc_params[cc_par_cnt++] = "-B"; //參數-B
cc_params[cc_par_cnt++] = as_path; //afl-as的路徑
if (clang_mode)
cc_params[cc_par_cnt++] = "-no-integrated-as";
if (getenv("AFL_HARDEN")) {
cc_params[cc_par_cnt++] = "-fstack-protector-all"; //canary保護
if (!fortify_set)
cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2";
}
if (asan_set) {
/* Pass this on to afl-as to adjust map density. */
setenv("AFL_USE_ASAN", "1", 1);
} else if (getenv("AFL_USE_ASAN")) {
if (getenv("AFL_USE_MSAN"))
FATAL("ASAN and MSAN are mutually exclusive");
if (getenv("AFL_HARDEN"))
FATAL("ASAN and AFL_HARDEN are mutually exclusive");
cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
cc_params[cc_par_cnt++] = "-fsanitize=address";
} else if (getenv("AFL_USE_MSAN")) {
if (getenv("AFL_USE_ASAN"))
FATAL("ASAN and MSAN are mutually exclusive");
if (getenv("AFL_HARDEN"))
FATAL("MSAN and AFL_HARDEN are mutually exclusive");
cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
cc_params[cc_par_cnt++] = "-fsanitize=memory";
}
...
cc_params[cc_par_cnt++] = "-g";
...
cc_params[cc_par_cnt++] = "-O3";
cc_params[cc_par_cnt++] = "-funroll-loops";
/* Two indicators that you're building for fuzzing; one of them is
AFL-specific, the other is shared with libfuzzer. */
cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1";
cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1";
}
if (getenv("AFL_NO_BUILTIN")) {
cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";
cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strstr";
cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr";
}
cc_params[cc_par_cnt] = NULL;
}
通過edit_params函數后
4
可以傳遞給編譯器的參數增加了-B . -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1這幾項
- main函數? 首先調用
isatty函數判斷描述符是否為終端機以及是否為靜默模式,即不打印任何信息,SAYF即輸出函數用于輸出提示字符 - ? 接著通過
find_as函數搜索as文件所在的路徑 - ? 接著通過
edit_params函數編輯獲取需要傳入編譯器的參數 - ? 最后通過
execvp函數啟動gcc或其他編譯器
/*
isatty函數用于判斷文件描述詞是否是為終端機
獲取AFL_QUIET的環境變量
*/
if (isatty(2) && !getenv("AFL_QUIET")) { //判斷是否靜默模式
/*
#ifdef MESSAGES_TO_STDOUT
# define SAYF(x...) printf(x)
#else
# define SAYF(x...) fprintf(stderr, x)
#endif
*/
SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <lcamtuf@google.com>\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]); //用于尋找as所在路徑
edit_params(argc, argv);//用于獲取編譯參數
execvp(cc_params[0], (char**)cc_params);//啟動gcc或其他編譯器
大致流程圖
3
afl-gcc可以看作是劫持了gcc的一個程序,從而修改as的路徑(為了后續的插樁做準備),并且添加所有fuzzing所需要的參數再傳入實際的編譯器中去(這里以gcc作為例子)
afl-as.cedit_params函數afl-as.c的edit_params函數比較簡單
- ? 首先是確定
as文件所在的路徑,若沒有設置環境變量則直接使用as作為匯編器所在路徑的參數 - ? 其次是檢測
.s文件是否在臨時目錄下,這里我做了測試如果.s不在臨時目錄則無法插樁成功 - ? 最后隨機生成文件名,將該文件作為插樁后的文件并作為傳輸傳入匯編器
u8 *tmp_dir = getenv("TMPDIR"), *afl_as = getenv("AFL_AS"); //afl-as的地址
...
as_params = ck_alloc((argc + 32) * sizeof(u8*)); //給參數分配空間
as_params[0] = afl_as ? afl_as : (u8*)"as";
as_params[argc] = 0; //截斷符
...
//用于記錄文件是64位還是32位
for (i = 1; i < argc - 1; i++) {
if (!strcmp(argv[i], "--64")) use_64bit = 1;
else if (!strcmp(argv[i], "--32")) use_64bit = 0;
...
if (strncmp(input_file, tmp_dir, strlen(tmp_dir)) &&
strncmp(input_file, "/var/tmp/", 9) &&
strncmp(input_file, "/tmp/", 5)) pass_thru = 1; //匯編文件需要放在臨時目錄下,否則后續無法對文件進行插樁
}
modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),
(u32)time(NULL)); //隨機生成文件名,作為插樁的目標文件
...
as_params[as_par_cnt++] = modified_file; //將待修改的文件名作為匯編器的參數
as_params[as_par_cnt] = NULL;
add_instrumentation函數add_instrumentation函數是插樁的關鍵函數
- ? 首先是分別打開需要編譯的文件以及存放插樁后的文件,并且對需要編譯的文件逐行逐行進行掃描
- ? 其次對于以下情況的代碼塊不進行插樁處理
- ?
pass_thru = 1,這里經調試發現只要.s文件存在于臨時目錄下pass_thru的值就會為0,pass_thru = 1的意思是只傳遞數據不進行插樁 - ?
skip_intel = 1即為跳過intel的匯編語法的代碼 - ? 不在
.text段內 - ? 在
.text段但是不處于函數標簽或者分支標簽 - ?
trampoline_fmt_64與trampoline_fmt_32即為需要插樁的代碼,并會記錄總共插樁了幾處 - ? 若進行了插樁處理,那么則需要在文件末尾插入
main_payload_64,是與afl進行fuzzing相關的函數
...
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, 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)) { //對需要匯編的文件進行一行一行的掃描
/* 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. */
//isalpha是一種函數:判斷字符ch是否為英文字母
//# define R(x) (random() % (x))
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)); //將插樁代碼寫入改寫文件中,trampoline_fmt_64為64位程序的插樁代碼,trampoline_fmt_32為32位程序的插樁代碼
instrument_next = 0;
ins_lines++; //總共插樁了多少處地方
}
...
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.
OpenBSD為一個類unix的操作系統
*/
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; //只要是text段就是我們應該插樁的段
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') {//檢測jnz等分支指令
if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) { //絕對跳轉jmp不進行插樁處理
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE)); //給分支跳轉指令進行插樁
ins_lines++; //插樁的指令數
}
continue; //插樁完直接跳過
}
...
if (strstr(line, ":")) { //檢測標簽
if (line[0] == '.') {
/* Apple: .L<num> / .LBB<num> */
if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "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;//函數標簽都需要進行插樁
}
}
}
if (ins_lines)
fputs(use_64bit ? main_payload_64 : main_payload_32, outf); //若進行插樁處理則需要插入main_payload_64
這里重點關注一下插樁的位置
- ? 情況一:函數入口,例如
main函數
函數標簽處的插樁如下圖所示,插樁的位置是函數第一條指令的上方進行插樁
5
- ? 情況二:分支跳轉,例如
jle指令
掃描到分支跳轉指令,則直接在跳轉指令下方進行插樁處理,如下圖所示
6
- ? 情況三:
.L<num>標簽
.L為本地標簽,afl-as.c也會掃描該標簽并進行插樁處理,可以看到跳轉指令的目的地地址就是以.L<num>,因此.L<num>可以認為分支的起始位置,與函數標簽一樣,會在第一條指令上方進行插樁處理
7
main函數main函數主要經過edit_params函數修改了傳入匯編器的參數,并且對匯編文件進行插樁處理,最后使用execvp函數啟動匯編器進行匯編處理
...
gettimeofday(&tv, &tz);
rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();//隨機種子
srandom(rand_seed);//通過種子生成隨機數
edit_params(argc, argv); //加載參數,并在/tmp/目錄下生成臨時的匯編文件
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;
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]);
...
傳入匯編器的參數情況
8
大致流程圖
9
afl-as相當于劫持了as從而修改匯編的文件名以及對相應的匯編文件進行插樁處理
afl-as.h該文件放置了插樁需要的代碼如trampoline_fmt_64、trampoline_fmt_32、main_payload_64以及main_payload_32,這些代碼結合fuzzing過程有關。
總結afl-gcc與afl-as可以看作是劫持了編譯器,將fuzzing相關的參數設置好并對編譯文件進行相應的插樁后再調用實際的編譯器