【AFL(十一)】AFL 學習筆記


afl-fuzz.c筆記


前言:

本文適合對象:已經對afl的流程有一定了解,自己跑過afl的各個功能;具有一定C編程基礎,瀏覽過afl源碼或者某個模塊的源碼。筆記分為五個大塊對 afl-fuzz.c 進行分析:文件引用、預備工作、fuzzing的整體結構、關鍵函數實現原理、main函數。當然其中涉及到很多其他的頭文件,也會對相關部分進行詳述。


Ⅰ、文件引用

【一】自定義頭文件

這部分幾個頭文件都比較重要,源碼都是需要看的

#include "config.h"  
#include "types.h"  
#include "debug.h"  
#include "alloc-inl.h"  
#include "hash.h"  
  1. 配置文件,包含各種宏定義,屬於通用配置。比如bitflip變異時收集的token的長度和數量會在此文件中進行定義。
  2. 類型重定義,一些在 afl-fuzz.c 中看不太懂的類型,可以在這里看看是不是有相關定義,比如 u8 在源碼中經常出現,實際上在這個頭文件可以看出 typedef uint8_t u8,所以其對應的類型應該是 uint8_t ,對應的是 C99 標准里的無符號字符型
  3. 調試,宏定義各種參數及函數,比如顯示的顏色,還有各種自定義的函數,如果改AFL,這些東西相當於沒有編譯器情況下的 *”高端printf(滑稽臉)”*,比如最常見的 OKF("We're done here. Have a nice day!\n"); 其中的 OKF 就是一個輸出代表成功信息的函數
  4. 內存相關,提供錯誤檢查、內存清零、內存分配等常規操作,“內存器的設計初衷不是為了抵抗惡意攻擊,但是它確實提供了便攜健壯的內存處理方式,可以檢查 use-after-free 等”
  5. 哈希函數,文件中實現一個參數為 const void* key, u32 len, u32 seed 返回為 u32 的靜態內聯函數

【二】標准庫頭文件

標准庫文件,基本跟 Windows 下的 C 編程都差不多,只有個別是Linux C下才會用到的

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <signal.h>
#include <dirent.h>
#include <ctype.h>
#include <fcntl.h>
#include <termios.h>
#include <dlfcn.h>
#include <sched.h>

 

  1. <dirent.h> 文件操作相關頭文件,可以參考在 afl-tmin.c 中的修改,查看其具體用途
  2. <sched.h> 任務調度相關頭文件

【三】Linux C編程特有的頭文件

#include <sys/wait.h>
#include <sys/time.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/resource.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/file.h>

 

這一部分,引用了一些Linux環境下的特殊頭文件,跟上一部分會有一些重疊,但是各司其職,實際編程兩邊的函數都可以,但是偏向於用這部分。對應Linux環境的頭文件位置 /usr/include/sys/

【四】文件引用總結

需要深入了解看源代碼的頭文件是

#include "types.h"  
#include "debug.h"  
#include "alloc-inl.h"  

 

這三個都需要查看源碼,對后續查看、按需修改 afl源碼 一定都會很有幫助。


Ⅱ、預備工作

這一部分,差不多前三百多行左右,到枚舉類型結束。

【一】環境判斷預處理

  1. Linux環境下獨有的宏定義

    #ifdef __linux__
    #  define HAVE_AFFINITY 1
    #endif /* __linux__ */

    在之后代碼中,可以用 ifdef HAVE_AFFINITY 判斷是否是Linux環境,這里的 AFFINITY 是親和性的意思,跟cpu的親和性,與運行性能有關
    在定義之后的源代碼中一共出現了五次 HAVE_AFFINITY,因為是特定條件下的宏定義,所以也可以當是跟cpu操作相關的bool用,都是成對出現,比如:

    #ifdef HAVE_AFFINITY
    static s32 cpu_aff = -1;/* Selected CPU core */
    #endif /* HAVE_AFFINITY */

    只有在啟用這種親和性的情況下,才會定義 cpu_aff 用來標記選擇的cpu核。

  2. AFL_LIB庫處理

    #ifdef AFL_LIB
    #  define EXP_ST
    #else
    #  define EXP_ST static
    #endif /* ^AFL_LIB */

    不常用,當afl被編譯成庫的時候,用來保證變量的輸出。//A toggle to export some variables

【二】全局變量

整個部分代碼塊最開始的注釋塊:
/* Lots of globals, but mostly for the status UI and other things where it really makes no sense to haul them around as function parameters. /
可以看出作者很謙虛的指出,這部分的全局變量大部分是跟 UI 展示的狀態部分有關的,還有一部分是
沒必要*去把他們當作函數參數 (有些情況會造成代碼冗余)。

這部分從四個方面考慮:

1.變量類型

這部分跟頭文件 "types.h" 息息相關,變量類型不清楚的時候就去那個頭文件找答案。比如:

u8: typedef uint8_t u8;
u32: typedef uint32_t u32;
u64: 下面代碼的意思,我猜測是為了兼容32和64不同結構情況下的定義

#ifdef __x86_64__
typedef unsigned long long u64;
#else
typedef uint64_t u64;
#endif /* ^__x86_64__ */  

s32: typedef int16_t s16;

2.global變量

這里作者的變量定義是按代碼塊的結構寫的, 【此部分每塊變量的含義,待補】

3.結構體 - 測試用例隊列

struct queue_entry {
  u8* fname;//測試用例的文件名  
  u32 len;//輸入長度, 

  u8  cal_failed,// Calibration failed? 
      trim_done,// Trimmed?      
      was_fuzzed,//是否已經經過fuzzing  
      passed_det,// Deterministic stages passed?     
      has_new_cov,// Triggers new coverage?           
      var_behavior,// Variable behavior? 
      favored,     // Currently favored? 
      fs_redundant;// Marked as redundant in the fs?   

  u32 bitmap_size, // Number of bits set in bitmap     
      exec_cksum;  // Checksum of the execution trace  

  u64 exec_us,// Execution time (us)
      handicap,// Number of queue cycles behind    
      depth;// Path depth    

  u8* trace_mini;  // Trace bytes, if kept
  u32 tc_ref;      // Trace bytes ref count            

  struct queue_entry *next,//隊列下一結點 
                     *next_100;       // 100 elements ahead 
};

在之后的整個fuzzing過程中,都會維護一些queue_entry類型的隊列。

4.枚舉

枚舉類型的好處是可讀性強,易於使用,作者定義了三部分枚舉類型,用來表示自己想要展現的狀態。具體用法會分析后舉例說明:

  1. Fuzzing 的狀態,比如有地方的用一個數組,需要區分01234···序列代表的含義,就可以用這個代替。這里涉及了很多種不同的 fuzz 種子變異策略
    /* Fuzzing stages */
    enum {
      /* 00 */ STAGE_FLIP1,
      /* 01 */ STAGE_FLIP2,
      /* 02 */ STAGE_FLIP4,
      /* 03 */ STAGE_FLIP8,
      /* 04 */ STAGE_FLIP16,
      /* 05 */ STAGE_FLIP32,
      /* 06 */ STAGE_ARITH8,
      /* 07 */ STAGE_ARITH16,
      /* 08 */ STAGE_ARITH32,
      /* 09 */ STAGE_INTEREST8,
      /* 10 */ STAGE_INTEREST16,
      /* 11 */ STAGE_INTEREST32,
      /* 12 */ STAGE_EXTRAS_UO,
      /* 13 */ STAGE_EXTRAS_UI,
      /* 14 */ STAGE_EXTRAS_AO,
      /* 15 */ STAGE_HAVOC,
      /* 16 */ STAGE_SPLICE
    };

     

比如 DI(stage_finds[STAGE_FLIP1]), DI(stage_cycles[STAGE_FLIP1]),其中 stage_finds 的定義static u64 stage_finds[32]是一個大數組,用來保存不同的 fuzz stage 狀態下的模式發現。

  1. 狀態的值類型,這里是用來輔助、修飾上一個枚舉fuzzing狀態的,用來識別當前的狀態枚舉對應變異方式的類型

    enum { /* 00 */ STAGE_VAL_NONE, /* 01 */ STAGE_VAL_LE, /* 02 */ STAGE_VAL_BE };
  2. 執行狀態(針對錯誤fault),每個狀態都有其特殊含義 【此部分每個狀態的含義,待補】

    enum { /* 00 */ FAULT_NONE, /* 01 */ FAULT_TMOUT, /* 02 */ FAULT_CRASH, /* 03 */ FAULT_ERROR, /* 04 */ FAULT_NOINST, /* 05 */ FAULT_NOBITS };

【三】預處理總結

這部分最重要的是對於全局變量四部分的理解,afl-fuzz.c文件最常用的變量都在這里面了,特別是測試用例的隊列、枚舉代表的含義。
還有就是要注意宏定義的內容,對afl的環境配置、整體的流程的分析都很有幫助。


Ⅲ、fuzzing 的整體結構

后面的代碼就是真正的 afl fuzz 過程代碼,在開始分析詳細的代碼之前,需要從整體結構上了解掌握 afl-fuzz.c 的主要流程,這也是師兄給的主要任務。這里先不管 main 函數之前的函數是怎么實現原理的,先就通過簡單的 main 函數的分析,結合各函數的原注釋進行主要流程分析。

【一】設置 main 函數內的變量

設置各種main函數需要的主要局部變量,一部分跟要調用函數的參數有關,一部分跟main函數主體相關。

  s32 opt;
  u64 prev_queued = 0;
  u32 sync_interval_cnt = 0, seek_to;
  u8  *extras_dir = 0;
  u8  mem_limit_given = 0;//是否內存限制
  u8  exit_1 = !!getenv("AFL_BENCH_JUST_ONE");
  char** use_argv;//用戶的輸入參數

  struct timeval tv;
  struct timezone tz;

  //顯示,這就是之前提到的自定義的調試頭文件
  SAYF(cCYA "afl-fuzz " cBRI VERSION cRST " by <lcamtuf@google.com>\n");

  //文件路徑
  doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH;

  //時間
  gettimeofday(&tv, &tz);
  srandom(tv.tv_sec ^ tv.tv_usec ^ getpid());

這部分的設置變量,主要是main內使用,跟之外的都無關系,魔改的時候要注意這里。

【二】while循環讀取來自命令行的參數輸入

while ((opt = getopt(argc, argv, "+i:o:f:m:t:T:dnCB:S:M:x:Q")) > 0)` 

while的判斷條件是“命令行”里是否還有輸入的參數,while內的命令判斷是通過 switch - case 實現的:

i:輸入文件夾,包含所有的測試用例 testcase
o:輸出文件夾,用來存儲所有的中間結果和最終結果
M:設置主(Master)Fuzzer
S:設置從屬(Slave)Fuzzer
f:testcase的內容會作為afl_test的stdin
x:設置用戶提供的tokens
t:設置程序運行超時的時間,單位為ms
m:設置分配的內存空間
d:
B:
C:
n:
T:
Q:
default: 如果輸入錯誤,提示使用手冊(當進行魔改的時候,可以在這個函數 usage(argv[0]); 里進行修改)
對afl進行魔改的時候,需要外界輸入參數的話,可以從這里先入手,然后倒推到開頭,整理出afl實現過程中的參數變化過程。

【三】初始化,環境設置和檢查,為fuzz做准備

1. tuple

首先應該知道的是,AFL是根據二元tuple(跳轉的源地址和目標地址)來記錄分支信息,從而獲取target的執行流程和代碼覆蓋情況。起始階段 fuzzer 會進行一系列的准備工作,為記錄插樁得到的目標程序執行路徑,即 tuple 信息

/* SHM with instrumentation bitmap  */
EXP_ST u8* trace_bits;
EXP_ST u8  virgin_bits[MAP_SIZE],//Regions yet untouched by fuzzing
           virgin_tmout[MAP_SIZE],//Bits we haven't seen in tmouts
           virgin_crash[MAP_SIZE];//Bits we haven't seen in crashes

 

其中 trace_bits 記錄當前的tuple信息;
virgin_bits 用來記錄總的tuple信息;
virgin_tmout 記錄fuzz過程中出現的所有目標程序的timeout時的tuple信息;
virgin_crash 記錄fuzz過程中出現的crash時的tuple信息;

2. forkserve

初始化階段有個重要的函數調用不得不提 init_forkserver(關於此函數的詳細解讀在之后的函數實現原理part會詳細說明),因為這個函數操作就是初始化 forkserver。下面說一下forkserver為什么重要,以及到底是用來干嘛的。
fuzzing的大致思路是,對輸入的文件不斷變異,然后輸入喂給target執行,檢查是否會造成崩潰,這其中會涉及大量的fork和執行target過程。(在“[文件/數據]變異”部分就會見識到有多多了)
AFL實現的這套 fork server 機制就是為了提高效率,把 fork 相關的操作集成起來,fuzzer不需要對fork負責,只需要與fork server交互即可。關於這部分的具體運行原理在之后會有詳細說明。

3. 目錄處理 && 環境檢查

對輸入目錄輸出目錄進行檢查,檢查cpu,檢查內核數量等等。

【四】[文件/數據]變異

正好這里涉及了對輸入文件的變異策略,可以單獨先提一下。在AFL的fuzz過程中,維護了一個 testcase 隊列,每次把隊列里的文件取出來之后,對其進行變異,下面就講一下各個階段的變異是怎樣的。
注: bitflip、arithmetic、interest、dictionary 是 deterministic fuzzing 過程,屬於dumb mode(-d) 和主 fuzzer(-M) 會進行的操作; havoc、splice 與前面不同是存在隨機性,是所有fuzz都會進行的變異操作。文件變異是具有啟發性判斷的,應注意“避免浪費,減少消耗”的原則,即之前變異應該盡可能產生更大的效果,比如 eff_map 數組的設計;同時減少不必要的資源消耗,變異可能沒啥好效果的話要及時止損。

1.bitflip,位反轉,顧名思義按位進行翻轉,0變1,1變0。

STAGE_FLIP1 每次翻轉一位(1 bit),按一位步長從頭開始。
STAGE_FLIP2 每次翻轉相鄰兩位(2 bit),按一位步長從頭開始。
STAGE_FLIP4 每次翻轉相鄰四位(4 bit),按一位步長從頭開始。
STAGE_FLIP8 每次翻轉相鄰八位(8 bit),按八位步長從頭開始,也就是說,每次對一個byte做翻轉變化。
STAGE_FLIP16每次翻轉相鄰十六位(16 bit),按八位步長從頭開始,每次對一個word做翻轉變化。
STAGE_FLIP32每次翻轉相鄰三十二位(32 bit),按八位步長從頭開始,每次對一個dword做翻轉變化。

這一部分在 5135 行的 #define FLIP_BIT(_ar, _b) do {***} 有詳細的代碼實現。

<token - 自動檢測>

源碼中有一段關於這部分的注釋,意思是說在進行為翻轉的時候,程序會隨時注意翻轉之后的變化。比如說,對於一段 xxxxxxxxIHDRxxxxxxxx 的文件字符串,當改變 IHDR 任意一個都會導致奇怪的變化,這個時候,程序就會認為 IHDR 是一個可以讓fuzzer很激動的“神仙值”–token。

/* While flipping the least significant bit in every byte, pull of an extra trick to detect possible syntax tokens. In essence, the idea is that if you have a binary blob like this:
xxxxxxxxIHDRxxxxxxxx
…and changing the leading and trailing bytes causes variable or no changes in program flow, but touching any character in the “IHDR” string always produces the same, distinctive path, it’s highly likely that “IHDR” is an atomically-checked magic value of special significance to the fuzzed format.
We do this here, rather than as a separate stage, because it’s a nice way to keep the operation approximately “free” (i.e., no extra execs).
Empirically, performing the check when flipping the least significant bit is advantageous, compared to doing it at the time of more disruptive changes, where the program flow may be affected in more violent ways.
The caveat is that we won’t generate dictionaries in the -d mode or -S mode - but that’s probably a fair trade-off.
This won’t work particularly well with paths that exhibit variable behavior, but fails gracefully, so we’ll carry out the checks anyway.
*/

其實token的長度和數量都是可以控制的,在 config.h 中有定義,但是因為是在頭文件宏定義的,修改之后需要重新編譯使用。

/* Maximum number of auto-extracted dictionary tokens to actually use in fuzzing (first value), and to keep in memory as candidates. The latter should be much higher than the former. */

 #define USE_AUTO_EXTRAS 50 #define MAX_AUTO_EXTRAS (USE_AUTO_EXTRAS * 10) 
<effector map - 生成>

在這里值得一提的是 effector map,在看源碼數據變異這一部分的時候,一定會注意的是在 bitflip 8/8 的時候遇到一個叫 eff_map 的數組,這個數組的大小是 EFF_ALEN(len) (也就是【??】),數組元素只有 0/1 兩種值,很明顯是標記的意思,到底是標記什么呢?
要想明白 effector map 的原理需要了解三個點:

  1. 為什么是 8/8 的時候出現?因為 8bit(比特)的時候是 1byte(字節),如果一個字節的翻轉都無法帶來路徑變化,此byte極有可能是不會導致crash的數據,所以之后應該用一種思路避開無效byte。
  2. 標記是干什么用的?根據上面的分析,就很好理解了,標記好的數組可以為之后的變異服務,相當於提前“踩雷(踩掉無效byte的雷)”,相當於進行了啟發式的判斷。無效為0,有效為1。
  3. 達到了怎樣的效果?要知道判斷的時間開銷,對不停循環的fuzzing過程來說是致命的,所以 eff_map 利用在這一次8/8的判斷中,通過不大的空間開銷,換取了可觀的時間開銷。(暫時是這樣分析的,具體是否真的節約很多,不得而知)

2.arithmetic,算術,實際操作就是加加減減。

bitflip 結束之后,就進入 arithmetic 階段,目標大小和階段與 bitflip 非常類似:

arith 8/8,每次8bit進行加減運算,8bit步長從頭開始,即對每個byte進行整數加減變異;
arith 16/8,每次16bit進行加減運算,8bit步長從頭開始,即對每個word進行整數加減變異;
arith 32/8,每次32bit進行加減運算,8bit步長從頭開始,即對每個dword進行整數加減變異;

其中對於加減變異的上限,在 config.h 中有所定義:

/* Maximum offset for integer addition / subtraction stages: */

#define ARITH_MAX 35

注:跟bitflip相同的,如果需要修改此值,在頭文件中修改完之后,要進行編譯才會生效。

在這里對整數目標進行+1,+2,+3…+35,-1,-2,-3…-35的變異。由於整數存在大端序和小端序兩種表示,AFL會對這兩種表示方式都進行變異。
前面也提到過AFL設計的巧妙之處,AFL盡力不浪費每一個變異,也會盡力讓變異不冗余,從而達到快速高效的目標。AFL會跳過某些arithmetic變異:

  1. 在 eff_map 數組中對byte進行了 0/1 標記,如果一個整數的所有 bytes 都被判為無效,那么就認為整數無效,跳過此數的變異;
  2. 【??】如果加減某數之后效果與之前某bitflip效果相同,認為此次變異在上一階段已經執行過,此次不再執行;

3.interest,把一些“有意思”的特殊內容替換到原文件中。

interest的三個步驟跟arithmetic相同:

interest 8/8,每次8bit進行加減運算,8bit步長從頭開始,即對每個byte進行替換;
interest 16/8,每次16bit進行加減運算,8bit步長從頭開始,即對每個word進行替換;
interest 32/8,每次32bit進行加減運算,8bit步長從頭開始,即對每個dword進行替換;

用於替換的叫做 interesting values ,是AFL預設的特殊數:

/* Interesting values, as per config.h */

static s8 interesting_8[] = { INTERESTING_8 }; 
static s16 interesting_16[] = { INTERESTING_8,INTERESTING_16 };
static s32 interesting_32[] = { INTERESTING_8, INTERESTING_16, INTERESTING_32 };

其中 interesting values 在 config.h 中的設定:

/* List of interesting values to use in fuzzing. */ #define INTERESTING_8 \ -128, /* Overflow signed 8-bit when decremented */ \ -1, /* */ \ 0, /* */ \ 1, /* */ \ 16, /* One-off with common buffer size */ \ 32, /* One-off with common buffer size */ \ 64, /* One-off with common buffer size */ \ 100, /* One-off with common buffer size */ \ 127 /* Overflow signed 8-bit when incremented */ #define INTERESTING_16 \ -32768, /* Overflow signed 16-bit when decremented */ \ -129, /* Overflow signed 8-bit */ \ 128, /* Overflow signed 8-bit */ \ 255, /* Overflow unsig 8-bit when incremented */ \ 256, /* Overflow unsig 8-bit */ \ 512, /* One-off with common buffer size */ \ 1000, /* One-off with common buffer size */ \ 1024, /* One-off with common buffer size */ \ 4096, /* One-off with common buffer size */ \ 32767 /* Overflow signed 16-bit when incremented */ #define INTERESTING_32 \ -2147483648LL, /* Overflow signed 32-bit when decremented */ \ -100663046, /* Large negative number (endian-agnostic) */ \ -32769, /* Overflow signed 16-bit */ \ 32768, /* Overflow signed 16-bit */ \ 65535, /* Overflow unsig 16-bit when incremented */ \ 65536, /* Overflow unsig 16 bit */ \ 100663045, /* Large positive number (endian-agnostic) */ \ 2147483647 /* Overflow signed 32-bit when incremented */

可以看到,基本是些會造成溢出的數值。與前面的思想相同的,本着“避免浪費,減少消耗”的原則,eff_map數組中已經判定無效的就此輪跳過;如果 interesting value 達到的效果跟 bitflip 或者 arithmetic 效果相同,也被認為是重復消耗,跳過。

4.distionary,字典,會把自動生成或者用戶提供的token替換、插入到原文件中。

此階段已經是確定性變異 deterministic fuzzing 的結尾:

user extras (over),從頭開始,將用戶提供的tokens依次替換到原文件中
user extras (insert),從頭開始,將用戶提供的tokens依次插入到原文件中
auto extras (over),從頭開始,將自動檢測的tokens依次替換到原文件中

其中 “用戶提供的tokens” 是一開始通過 -x 選項指定的,如果沒有則跳過對應的子階段;“自動檢測的tokens” 是第一個階段 bitflip 生成的。

5.havoc,“大破壞”,對原文件進行大量變異。

havoc 意味着隨機的開始,與后面的 splice 是任何模式下都要進行的變異,具體來說 havoc 包含了多輪變異,每一輪都是組合拳:

隨機選取某個bit進行翻轉
隨機選取某個byte,將其設置為隨機的interesting value
隨機選取某個word,並隨機選取大、小端序,將其設置為隨機的interesting value
隨機選取某個dword,並隨機選取大、小端序,將其設置為隨機的interesting value
隨機選取某個byte,對其減去一個隨機數
隨機選取某個byte,對其加上一個隨機數
隨機選取某個word,並隨機選取大、小端序,對其減去一個隨機數
隨機選取某個word,並隨機選取大、小端序,對其加上一個隨機數
隨機選取某個dword,並隨機選取大、小端序,對其減去一個隨機數
隨機選取某個dword,並隨機選取大、小端序,對其加上一個隨機數
隨機選取某個byte,將其設置為隨機數
隨機刪除一段bytes
隨機選取一個位置,插入一段隨機長度的內容,其中75%的概率是插入原文中隨機位置的內容,25%的概率是插入一段隨機選取的數
隨機選取一個位置,替換為一段隨機長度的內容,其中75%的概率是替換成原文中隨機位置的內容,25%的概率是替換成一段隨機選取的數
隨機選取一個位置,用隨機選取的token(用戶提供的或自動生成的)替換
隨機選取一個位置,用隨機選取的token(用戶提供的或自動生成的)插入

之后AFL會生成一個隨機數,作為變異組合的數量,每次從上面隨機選取作用於當前文件。

6.splice,“拼接”,兩個文件拼接到一起得到一個新文件。

具體來說,AFL會在文件隊列中隨機選擇一個文件與當前文件進行對比,如果差別不大就重新再選;如果差異明顯,就隨機選取位置兩個文件都一切兩半。最后將當前文件的頭與隨機文件的尾拼接起來得到新文件【為什么不是當前的尾拼接隨機文件的頭【??】】。當然了本着“減少消耗”的原則拼接后的文件應該與上一個文件對比,如果未發生變化應該過濾掉。

7.cycle,循環往復,一波變異結束后的文件,會在隊列結束后下一輪中繼續變異下去

AFL狀態欄右上角的 cycles done 意味着完成的循環數,每次循環是對整個隊列的再一次變異,不過只有第一次 cycle 才會進行 deterministic fuzzing,之后的只有隨機性變異了。

【五】fuzzing策略

這部分跟 mutation 相關,會多寫一點(不斷更新),將會分為兩部分進行展開:現有AFL的fuzzing的策略論文相關魔改AFL的fuzzing策略

1.AFL現在的fuzzing策略

在上一節中已經說完了文件/數據的各種變異類型,其實還有一個點沒說,就是一次變異操作到底是怎樣的流程:(一次變異到底是變一個比特還是從一比特到32比特都結束了才算事【??】)

fuzz_one :進行一次測試用例變異過程
common_fuzz_stuff :變異完成后的一般操作處理
write_to_testcase :變異后的內容寫入測試文件
run_target :運行目標進程
save_is_insteresting :判斷是否保存這個測試用例
has_new_bits :判斷測試用例是否產生新狀態

【六】語料庫更新

隨着fuzzing的不斷進行,cycle一輪又一輪,勢必會產生更多的變異測試用例,這些用例並不是一股腦全都放進隊列里的,對於沒有新狀態(由tuple信息可得)的測試用例應該拋棄;而且對於較優的用例應該更有可能被利用(這也是啟發性的體現)。

更優的測試用例

其實在循環的fuzzing過程中,到現在還有一個問題未解決,我們現在知道了什么樣的文件應該留下,但是有沒有可能扔掉的文件也很有用呢?有沒有一個系統的標准來判定,兩個文件到底哪個更優?

 

下篇前言:

上一篇已經將前三部分寫完了,但是留有一些疑問點,所以下篇將分三部分:對上篇遺留問題的解決關鍵函數實現原理介紹main函數注釋(逐行貼出代碼塊,因為main是整個alf的核心流程,所以需要一行行的詳解),這一篇可能對代碼的分析偏多一點,因為以后我的論文應該也是要在afl的基礎上進行改進,所以就需要對代碼多進行分析。其實像mopt-afl對代碼的改進就主要是在fuzz_one函數上也就是mutation階段。
最后是附錄,寫有我看過的一些實用的相關文章、博客、帖子之類的,多是實用總結型的。


接上篇遺留的問題

先來說說上一篇未解決的問題(上篇 ),這幾個問題主要是在代碼細節的理解上,有錯誤之處還望斧正):

1. effector map

關於effector map,在看源碼數據變異這一部分的時候,一定會注意的是在 bitflip 8/8 的時候遇到一個叫eff_map的數組,這個數組的大小是EFF_ALEN(len),這個數是多大?

當時沒想出來這么寫是個什么意思,后來轉過彎來,其實那不是變量也不是普通的函數,EFF_ALEN是一個宏定義的函數:

#define EFF_APOS(_p)          ((_p) >> EFF_MAP_SCALE2)
#define EFF_REM(_x)           ((_x) & ((1 << EFF_MAP_SCALE2) - 1))
#define EFF_ALEN(_l)          (EFF_APOS(_l) + !!EFF_REM(_l))
#define EFF_SPAN_ALEN(_p, _l) (EFF_APOS((_p) + (_l) - 1) - EFF_APOS(_p) + 1)
//為eff_map分配大小為EFF_ALEN(len)的內存
eff_map = ck_alloc(EFF_ALEN(len));

如果把這一塊的宏定義拆開,變成函數的形式就是這樣:

/*此部分我寫了下偽代碼,宏定義雖然寫代碼的時候好用,但是不得不說全是大寫字母有點難理解,所以我又給拆分成了通俗的代碼,
這部分跟上下文關系不大,看懂這部分的代碼,其他類似的也很好懂了。*/
type(_p) EFF_APOS(_p){
    /* >> 是位移操作,把參數的二進制形式進行右移,每移動一位就減小2倍,所以這個函數的意思就是:
    返回傳入參數_p除以(2的EFF_MAP_SCALE2次方)。
    同理另一個方向就是左移,是放大2的冪的倍數。*/
    return (_p / 8);
    // EFF_MAP_SCALE2 在文件config.h中出現,值為3,所以這里就是除以8的意思。
}
type(_x) EFF_REM(_x){
    //這里 & 是按位與,所以求的是 _x 與 ((1 << EFF_MAP_SCALE2) - 1)進行按位與,實際上就是跟 7 以二進制形式按位與
    return (_x & 7)
}

 

宏函數本質知道了,但是分別代表的什么意思呢?(代碼中給出的注釋如下)

EFF_APOS - position of a particular file offset in the map. 在map中的特定文件偏移位置。
EFF_ALEN - length of a map with a particular number of bytes. 根據特定數量的字節數,計算得到的文件長度。
EFF_SPAN_ALEN - map span for a sequence of bytes. 跳過一塊bytes

type(_l) EFF_ALEN(_l){
    /*這里的 !! 是一個兩次否,目的是歸一化(又是一個騷操作,這個作者寫代碼真的是凈整些這種,主要還是自己菜,菜是原罪)
    比如 r = !!a,如果a是整數0,則r=0,如果a是整數非0,則r=1。

    在a不是整數的情況下一般不這么用,但這里都是默認_l為整數的,畢竟字符型轉成ascii碼那不也是整數嗎。*/
    return (EFF_APOS(_l) + !!EFF_REM(_l))
}
type(_p) EFF_SPAN_ALEN(_p, _l){
    return (EFF_APOS((_p) + (_l) - 1) - EFF_APOS(_p) + 1)
}

現在重新看eff_map = ck_alloc(EFF_ALEN(len));,len來自於隊列當前結點queue_cur的成員len,是input length(輸入長度),所以這里分配給eff_map的大小是 (文件大小/8) 向下取整,這里的 8 = 2^EFF_MAP_SCALE2。比如文件17bytes,那么這里的EFF_ALEN(_l)就是3。

2. ARITHMETIC

如果加減某數之后效果與之前某bitflip效果相同,認為此次變異在上一階段已經執行過,此次不再執行?

這里當時標記出來是因為沒有理解透徹,沒怎么搞懂作者是怎么實現的,后來經過仔細分析幾個變量的關系,才縷清楚,這里以 ARITHMETIC 的8/8變異這一段為例(16和32的變異大同小異),把重要部分做解釋:

  stage_name  = "arith 8/8";//當前進行的狀態,這個在fuzz的時候用來在狀態欄展示
  stage_short = "arith8";//同上,是簡短的狀態名
  stage_cur   = 0;
  stage_max   = 2 * len * ARITH_MAX;
  /*ARITH_MAX就是加減變異的最大值限制35,
  文件大小len bytes,
  然后進行 +/- 操作乘以2,
  每個byte要進行的 +/- 操作各35次,
  所以這個stage_max意思就是將要進行多少次變異,但是之后要是沒有進行有效變異就要給減去*/
  stage_val_type = STAGE_VAL_LE;
  orig_hit_cnt = new_hit_cnt;//暫存用於最后的計算
  for (i = 0; i < len; i++) {//循環len次
    u8 orig = out_buf[i];
    /* Let's consult the effector map... */
    //如果當前i byte在eff_map對應位置是0,就跳過此次循環,進入for循環的下一次
    //並且此byte對應的變異無效,所以要減 2*ARITH_MAX
    if (!eff_map[EFF_APOS(i)]) {
      stage_max -= 2 * ARITH_MAX;
      continue;
    }
    stage_cur_byte = i;//當前byte
    for (j = 1; j <= ARITH_MAX; j++) {
      //分別進行 +/- 操作 j = 1~35
      u8 r = orig ^ (orig + j);
      /* Do arithmetic operations only if the result couldn't be a product of a bitflip. */
      //只有當arithmetic變異跟bitflip變異不重合時才會進行
      if (!could_be_bitflip(r)) {//判斷函數就是對是否重合進行判斷的
        stage_cur_val = j;
        out_buf[i] = orig + j;
        if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
        stage_cur++;
      } else stage_max--;//如果沒有進行變異,stage_max減一,因為這里屬於無效操作
      r =  orig ^ (orig - j);
      if (!could_be_bitflip(r)) {
        stage_cur_val = -j;
        out_buf[i] = orig - j;
        if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
        stage_cur++;
      } else stage_max--;
      out_buf[i] = orig;
    }
  }
  new_hit_cnt = queued_paths + unique_crashes;//如果8/8期間有新crash的話會加到這里
  stage_finds[STAGE_ARITH8]  += new_hit_cnt - orig_hit_cnt;//這期間增加了的
  stage_cycles[STAGE_ARITH8] += stage_max;//如果之前沒有有效變異的話stage_max這里就已經變成0了

3. 文件拼接

為什么不是當前的尾拼接隨機文件的頭

這個其實想想當時挺蠢的,一堆文件,拿出一個一分為二,把當前文件的頭和其他文件的尾拼接,這是原來的策略,這樣多次之后,自然會有之前的那個拿了尾的文件,用這個的頭跟其他的尾拼接了。所以沒必要此次拼接的時候,非得生成兩個文件,因為這樣的話后面還會出現,就有點重復了。

4. fuzz流程

一次變異到底是變一個比特還是從一比特到32比特都結束了才算事

這個看理解,我跟同學總結的說法是:一次變異是指一個比特的改變也算變異,根據編譯策略,經過很多次變異之后,fuzz_one結束叫做一輪變異。


Ⅳ、關鍵函數實現原理

這部分詳提一下三個重要函數  fuzz_one、init_forkserver、show_stats 

1. fuzz_one函數

這個函數的主要功能就是變異,每一階段變異的原理,在上一篇已經分析過,這里着重說一下作者加的 eff_map 這個變量,看代碼的話,在變異fuzz_one階段,這是個貫穿始終的數組,剛開始第一遍看代碼其實不是理解的很深刻,現在理解的比較多了,也能理解作者為什么要加這個數組了:
fuzz_one函數將近兩千行,每一個變異階段都有自己的功能,怎么把上一階段的信息用於下一階段,需要一個在函數內通用的局部變量,可以看到fuzz_one一開始的局部變量有很多,有很多類型:

  s32 len, fd, temp_len, i, j;
  u8  *in_buf, *out_buf, *orig_in, *ex_tmp, *eff_map = 0;
  u64 havoc_queued,  orig_hit_cnt, new_hit_cnt;
  u32 splice_cycle = 0, perf_score = 100, orig_perf, prev_cksum, eff_cnt = 1;
  u8  ret_val = 1, doing_det = 0;
  u8  a_collect[MAX_AUTO_EXTRA];
  u32 a_len = 0;

 

eff_map的類型是u8(上篇解釋過這種看不懂的,可以去types.h里找,這是一個uint8_t = unsigned char 形,8比特(0~255)),並且u8類型只有這一個是map命名形式的,在后面的注釋中如果出現Effector map,說的就是這個變量。主要的作用就是標記,在初始化數組的地方有這么一段注釋Initialize effector map for the next step (see comments below). Always flag first and last byte as doing something.把第一個和最后一個字節單獨標記出來用作其他用途,這里其實就說明了,這個map是標記作用,那是標記什么呢,標記當前byte對應的map塊是否需要進行階段變異。如果是0意味着不需要變異,非0(比如1)就需要變異,比如一開始分析的 arithmetic 8/8 階段就用過這個eff_map,此時已經它在bitflip階段經過了改變了。

2. init_forkserver函數

用於fork server進行的初始化,這部分跟插樁息息相關,只有插樁模式才會運行這部分,可以配合afl-as.h文件一起看。
這里的整體流程是這樣的:

  1. 局部變量,用於設置管道記錄信息,以及讀取進程記錄狀態
  2. fork一個進程
  3. execv執行fork severe
  4. 關閉不需要的通道
  5. 讀取第一步設置的兩個通道的狀態
  6. 從狀態通道讀取4個字節,並且監測是否成功,如果成功會輸出“All right - fork server is up.”

3. show_stats函數

這個函數技術上沒什么好說的,就是用來展示那個fuzz界面的,每次有數據更新的時候就會調用這個函數刷新頁面,如果說在AFL運行的界面,看到哪個不懂的變量,或者想單獨看那個參數是怎么來的,就從這個函數入手就對了,對應着界面的字符串定位到函數的位置,然后從變量再定位到相應位置,就可以很輕松的理解參數的含義和求法。


Ⅴ、main函數

main函數逐塊分析,這部分可能會有點長,大家隨意看看就好,算是我自己看代碼的碎碎念吧,因為不少在前面已經解釋過了,這里算是總結了,有很多總結文章對源代碼的解讀比我更好,形式也比我的好看,我這算是獻丑了233333)main函數代碼太長,我放在這里:afl-fuzz.c文件里的main函數代碼注釋版了,可以點進去看。


Ⅵ、附錄

AFL教程類

  1. 持續更新的AFL相關小細節

    這個是我從去年年底一開始接觸AFL一直到現在記錄的平時遇到的一些小細節,一般情況下就很機錄在這個博客里,王婆賣瓜自賣自誇哈哈哈哈哈

  2. 利用AFL對upx從頭開始模糊測試

    這是一篇比較簡單的實踐類,非常適合剛開始的新手對AFL進行初探。但是我最早的時候跟着這個帖子進行測試,發現並沒有達到作者的效果,所以大家就跟着練練手就好啦,只要跑出來的界面🆗就行。

  3. 從頭開始的fuzzing流程

    很詳細的教程,配合2.可以加快對afl工作原理的理解,我是這么覺得的。如果對英文吃力,可以看這篇翻譯

  4. afl-fuzz.c源碼講解

    這位前輩寫的源碼分析,對我而言收獲頗多,希望能幫到大家。

  5. AFL整體結構

    版主寫的一篇對AFL從整體結構上進行剖析的文章,寫的也很好,很有收獲。

  6. 256多線程運行AFL

    😳一個對AFL進行多線程操作的文章,算是從工程上進行性能改進的類型,有機會可以實現一下。

  7. Awesome AFL

    一個搜集的項目,作者收集了很多 不同的AFL變種,或者AFL啟發的超級魔改,這個項目的好處是從沒停更,一直整的挺好的。

  8. AFL生態圈

    這位師傅寫的翻譯,對很多AFL類,以及其他fuzzer工具的介紹,比較仔細全面

  9. AFL插樁講解

    可以看到上下篇只對afl-fuzz.c進行講解,插樁部分並沒有講解,因為我也還沒仔細研究。有這方面需要的可以看一下這篇文章,對插樁進行了詳細解釋

Fuzz相關有用收藏

  1. Fuzzer tools

    一些fuzzer工具集合,內容是前幾年比較經典的一些fuzzer,不包括最近頂會的那些fuzzer

  2. fuzz總結性優秀文章分類

    這是看雪上的一篇質量挺高的總結性的文章

  3. 頂會fuzz論文收集與總結

    作者收集的一些文章是比較經典的頂會文章,而且做了思維導圖,可以說是很用心了,~ 但是貌似停更很久了,不知道還會不會繼續更新。 ~ 跟作者聊過,現在是不更新了,不過還有一個項目在維護🔜

  4. 頂會fuzzing論文分類

    這是 3. 的作者寫的另一個一直在維護的論文分類project,后面我跟同學也會參與其中一起維護,后面對論文的理解相關的東西也會同步更新到這里。我跟郭老板的想法是把近十年的論文abstract都過一遍然后進行分類,希望能從中找到啟發。

  5. 大手子rk700

    寫的關於fuzz的文章都挺好的,最早的幾篇對afl的理解也很有幫助。他的這篇分析寫的很好

寫在最后

下篇本來是跟着上篇寫的,但是中間被安排了一些雜七雜八的事情,就一直拖到現在,還好現在已經進入正軌了,期間看雪上也有發私信一起討論的,如果大家也想一起討論,或者對文章內容有異議歡迎留言,或者發郵件 wayne-tao(at)Outlook.com,我也是上學期末對時候下定決心搞這方面的,再加上本科不是搞安全的,所以進步很慢,大家共同進步。
文章和注釋源碼我放在git上了地址: https://github.com/WayneDevMaze/Chinese_noted_AFL 歡迎大家star,除了上下兩篇對 afl.c 文件對分析,還有兩篇對cmin和tmin的分析,這兩個因為沒多少技術含量就不放看雪了,感興趣可以看一下我對博客(對兩個工具對分析和修改tmincmin),目前一方面搞fuzz,一方面也在搞web安全(畢竟新入門的水平比較菜,這個好入門),后面也會深入一下二進制,因為經常是發現一些崩潰情況,但是不知道怎么入手分析就很煩,感覺入門還早,繼續努力把吧。
最后,謝謝大家看我的文章,希望大家都能有所收獲,早發文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM