AFL使用指南


AFL使用指南

0x00 前言

二進制分析方面主要利用技術包括:動態分析(Dynamic Analysis)、靜態分析(Static Analysis)、符號化執行(Symbolic Execution)、Constraint Solving、資訊流追蹤技術(Data Flow Tracking)以及自動化測試(Fuzz Testing)

0x01 AFL的基本使用

1. 使用afl-gcc

1.1 使用AFL插樁程序

目標程序

#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;
}

使用afl-gcc進行插樁編譯

afl-gcc -g -o ./zerotest/vuln ./zerotest/vuln.c

PS:
如果目標程序中有Makefile,那么分兩種情況:

  1. 程序是用autoconf構建,那么此時只需要執行如下即可
./configure CC="afl-gcc" CXX="afl-g++"

此外,還可以執行如下語句設置LD_LIBRARY_PATH讓程序加載經過AFL插樁的.so文件,進行靜態構建而不是動態鏈接

./configure --disable-shared CC="afl-gcc" CXX="afl-g++"
  1. 程序不是用autoconf構建,那么直接修改Makefile文件中的編譯器為afl-gcc/g++

為了后期更好的分析crash,在此處可以開啟Address Sanitizer(ASAN)這個內存檢測工具,此工具可以更好的檢測出緩存區溢出、UAF 等內存漏洞,開啟方法如下:

AFL_USE_ASAN=1 ./configure CC=afl-gcc CXX=afl-g++ LD=afl-gcc--disable-shared
AFL_USE_ASAN=1 make

不使用 AFL 編譯插樁時,可使用以下方式開啟 Address Sanitizer。

./configure CC=gcc CXX=g++ CFLAGS="-g -fsanitize=address"
make

1.2 開始fuzz

fuzz的語法一般情況是兩種:

  1. 直接從stdin讀取輸入的目標程序
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program […params…]
  1. 從文件讀取輸入的目標程序,@@就是占位符,表示輸入替換的位置
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

此處我采用第一種方式

afl-fuzz -m 300 -i ./zerotest/fuzz_in -o ./zerotest/fuzz_out ./zerotest/vuln -f

PS: 常見參數的含義如下

  • -f參數表示:testcase的內容會作為afl_test的stdin
  • -m參數表示分配的內存空間
  • -i 指定測試樣本的路徑
  • -o 指定輸出結果的路徑
  • /dev/null 使錯誤信息不輸出到屏幕
  • -t:設置程序運行超時值,單位為 ms
  • -M:運行主(Master) Fuzzer
  • -S:運行從屬(Slave) Fuzzer

1.3 fuzz的結果

60889809-4D22-4C14-AF3C-E98D2C5EB60D.png

從界面上主要注意以下幾點:

  1. last new path 如果報錯那么要及時修正命令行參數,不然繼續fuzz也是徒勞(因為路徑是不會改變的);
  2. cycles done 如果變綠就說明后面及時繼續fuzz,出現crash的幾率也很低了,可以選擇在這個時候停止
  3. uniq crashes 代表的是crash的數量

1.4 crash分析

PS: xxd命令的作用就是將一個文件以十六進制的形式顯示出來

1FF9D91F-F823-4DF7-B716-C68B58134D65.png

可以看到已經得到的幾個crash文件,那么分析的話只需要將其作為之前vuln文件的輸入,使用gdb調試分析就可以得到詳細結果了,但是在這之前可以使用xxd看一下其中數據的內容做一個初步的判斷。

分別看一下這幾個crash的信息

  1. 可以看到應該是滿足了開頭是F且字符串長度為6的異常退出情況
    63005FA6-59BD-48BD-8B19-F5EF08C84D4E.png
  2. 看這個數據情況可能是棧溢出
    D74734D1-F13E-4256-B47C-260817BE102E.png
  3. 棧溢出
    B909DBCF-66FF-4C37-BF39-1ED413A6B0C3.png
  4. 符合首字符為A且棧溢出
    8213201E-7B35-45D7-9057-5493978CCABB.png
  5. 格式化字符串?可能
    C7F22A69-F1E6-4863-B9BD-08D88A82C789.png
  6. 符合首字符為A且字符串長度為66的異常退出情況
    41BAF060-7874-4D52-93CC-49FD86AF26D3.png

主要參考:
《初探Fuzz-AFL》

1.5 語料庫蒸餾(Corpus Distillation)

一般來說在進行fuzz之前構建一份有效的語料庫是十分有必要的,這將作為程序開始時的種子。

語料庫的信息來源主要如下:

  • 使用項目自身提供的測試用例
  • 目標程序bug提交頁面
  • 使用格式轉換器,用從現有的文件格式生成一些不容易找到的文件格式:
  • afl源碼的testcases目錄下提供了一些測試用例
  • 其他開源的語料庫

收集完后可以使用afl提供的工具來對語料庫進行進一步的處理:

  1. afl-cmin: 移除執行相同代碼的輸入文件
    afl-cmin的核心思想是: 嘗試找到與語料庫全集具有相同覆蓋范圍的最小子集。
    它一般的兩種執行模式是:
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params]
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@
  1. afl-tmin: 減小單個輸入文件的大小
    它有兩種工作模式: instrumented mode和crash mode。默認的工作方式是instrumented mode
# instrumented mode
afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@
# crash mode 將會剔除導致crash的文件
afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@

由於只能針對單個目標進行使用,因此使用如下shell腳本進行批量處理

for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done;

或者修改如下的Python腳本進行預處理

import os
import sys
import shutil
def cmin():
    command = ' -m 300 -t 5000 ./utilities/magick convert @@ /dev/null' 
    os.system('afl-cmin -i seeds/tmin -o seeds/cmin ' + command)
def tmin():
    command = ' -m 300 -t 5000 ./utilities/magick convert @@ /dev/null' 
    seed_list = os.listdir('seeds/all_format')
    for seed in seed_list:
        in_file = os.path.join('seeds/all_format', seed)
        out_file = os.path.join('seeds/tmin', seed)
        if os.path.getsize(in_file) > 1024*1:
            if os.path.getsize(in_file) < 1024*3 and not seed.endswith('.txt'):
                os.system('afl-tmin -i ' + in_file + ' -o ' + out_file + command)
                print('afl-tmin -i ' + in_file + ' -o ' + out_file + command)
            else:
                pass
        elif os.path.getsize(in_file) > 0:
            shutil.copyfile(in_file,out_file)
        else:
            pass
def convert(origin_seeds):
    seed_list = os.listdir(origin_seeds)
    for seed in seed_list:
        seed_in = os.path.join(origin_seeds, seed)
        file_name = (os.path.splitext(seed)[0])
        coder_list = os.listdir('coders')
        for cfile in coder_list:
            if cfile.endswith('.c'):
                extern = cfile[:cfile.find('.c')]
                seed_out = 'seeds/all_format/' + file_name + '.' + extern
                os.system('utilities/magick convert ' + seed_in + ' ' + seed_out)
if __name__ == '__main__':
    if len(sys.argv) < 2:
        print 'Usage: ' + sys.argv[0] + ' origin_seeds_dir'
    else:
        origin_seeds_dir = sys.argv[1]
        try:
            os.mkdir('seeds')
            seeds_path = os.path.join(os.path.abspath('.'),'seeds')
            os.mkdir(os.path.join(seeds_path,'all_format'))
            os.mkdir(os.path.join(seeds_path,'cmin'))
            os.mkdir(os.path.join(seeds_path,'tmin'))
        except:
            print 'make dir fail!'
        convert(origin_seeds_dir)
        tmin()
        cmin()

預處理腳本來自:《使用 AFL 進行模糊測試》

2. LLVM Mode模式

2.1 啟用llvm

LLVM Mode模式編譯程序可以獲得更快的Fuzzing速度,因此針對大型項目可以考慮啟用。

下載必要的安裝包

wget http://releases.llvm.org/8.0.0/llvm-8.0.0.src.tar.xz
wget http://releases.llvm.org/8.0.0/compiler-rt-8.0.0.src.tar.xz
wget http://releases.llvm.org/8.0.0/clang-tools-extra-8.0.0.src.tar.xz
wget http://releases.llvm.org/8.0.0/cfe-8.0.0.src.tar.xz

解壓縮

xz -d ./*
tar xvf cfe-8.0.0.src.tar
tar xvf clang-tools-extra-8.0.0.src.tar
tar xvf llvm-8.0.0.src.tar
tar xvf compiler-rt-8.0.0.src.tar

源碼合並

mv cfe-8.0.0.src clang
mv clang llvm-8.0.0.src/tools
mv clang-tools-extra-8.0.0.src extra
mv extra llvm-8.0.0.src/tools/clang
mv compiler-rt-8.0.0.src compiler-rt
mv compiler-rt llvm-8.0.0.src/projects

編譯安裝

mkdir build-8.0
cmake ../llvm-8.0.0.src/
cmake --build .
cmake --build . --target install
cmake -DCMAKE_INSTALL_PREFIX=/tmp/llvm -P cmake_install.cmake

上面的編譯安裝對硬件配置和硬盤的空間要求比較高,所以你可以直接使用源進行安裝,比如:

apt install llvm clang

編譯安裝afl的llvm模塊
(我的使用的是kali linux 2019.1進行編譯的,clang版本過高會失敗,使用clang++也會失敗,所以最終發現下面方法可行)

cd afl/llvm_mode
export CXX=/usr/bin/g++
export CC=/usr/bin/clang-6.0
make

因為clang沒有辦法使用update-alternatives,因此我直接修改軟連接

ln -s /usr/bin/clang-6.0 /usr/bin/clang
ln -sb /usr/bin/clang++-6.0 /usr/bin/clang++

之后就可以正常使用afl-clang-fast了
9B83B620-E1A2-420F-81CA-FC6C4777F4EB.png

其實以上均太費勁,還有更簡單的方法,kali linux的源中包含了afl,所以可以直接apt進行安裝,裝好之后afl-clang-fast也就有了

apt install afl

2.2 使用LLVM Mode模式進行fuzz

編譯插樁

root@kali-z ~/Desktop/fuzz/afl$ ./afl-clang-fast -g -o ./zerotest/vuln-fast ./zerotest/vuln.c

之后重復上面的方式進行fuzz即可,接下來展示一個使用此模式fuzz php內核代碼的例子。

1. 下載目標代碼
wget https://github.com/php/php-src/archive/php-7.2.11.tar.gz && tar xf php-7.2.11.tar.gz
2. 進行編譯插樁
cd php-src-php-7.2.11
./buildconf --force
CC=afl-clang-fast CXX=afl-clang-fast++ ./configure
AFL_USE_ASAN=1 make

PS: 如果報錯缺失libconv,則在Makefile中的EXTRA_LIBS =添加-liconv

3. 進行源代碼的修改

未修改之前 sapi/cli/php_cli.c
BB750539-176B-4A52-A437-D370814C7CCE.png
修改之后 sapi/cli/php_cli.c
C21FA188-6FFE-440A-B0AD-BA0F8C60312C.png
修改完之后執行如下進行rebuild

AFL_USE_ASAN=1 make

PS: 之所以進行這樣的修改,是因為我們使用php -r來eval php string,因此定位到sapi/cli/php_cli.c進行代碼的修改離開提升后期fuzz的效率。

4. 構造一個輸入點

我們想在fuzz的時候從stdin進行數據的輸入,因此構造如下輸入點

unserialize(file_get_contents(“php://stdin”));
5. 根據上述的構造點構造輸入數據

此處賬戶要考慮構造不同類類型的輸入數據,構造如下

mkdir serialized_data && cd serialized_data
../sapi/cli/php -r 'echo serialize("a");' > string
../sapi/cli/php -r 'echo serialize(1);' > number
../sapi/cli/php -r 'echo serialize([1,2]);' > array_of_num
../sapi/cli/php -r 'echo serialize(["1","2"]);' > array_of_str
../sapi/cli/php -r 'echo serialize([["1","2"],["3","4"],[1,2]]);' > array_of_array
echo 'O:6:"zeroyu":1:{s:4:"test";O:7:"npusec2":1:{s:5:"test2";s:10:"phpinfo();";}}' > class
6. 開始fuzz

為了從地址清理(ASAN)中獲得有用的結果,有必要設置一個環境變量,以便PHP禁用其自定義內存分配器,從而使內存安全問題對ASAN可見。

USE_ZEND_ALLOC=0 screen -S zeroyu

使用screen可以隨時進入查看fuzz的結果

screen -r zeroyu

使用如下命令開始fuzz

cd..
afl-fuzz -i serialized_data -o basic_fuzz -m none -- ./sapi/cli/php -r 'unserialize(file_get_contents("php://stdin"));'

11B4692E-E516-4743-9FAC-B39AB1C03ED0.png

7. 分析crash

用是使用如下bash腳本來尋找可能是bug的crash,因為有些是良性的crash,是由於ASAN無法分配足夠的內存。這是因為ASAN需要額外的內存來跟蹤所有分配,而精心編制的序列化對象可能會觸發大內存分配。

for FILE in $(ls id*); do cat $FILE | ../../sapi/cli/php -r "unserialize(file_get_contents('php://stdin'));" 2>&1 | grep -E "SUMMARY|ERROR" | grep -v "LargeMmap" && echo $FILE; done

參考: 《Fuzzing PHP for Fun and Profit》

3. 黑盒測試

參考:《AFL漏洞挖掘技術漫談(一):用AFL開始你的第一次Fuzzing》

4. 並行測試

4.1 單系統並行

查看系統核心數

cat /proc/cpuinfo| grep "cpu cores"| uniq

afl-fuzz並行Fuzzing,一般的做法是通過-M參數指定一個主Fuzzer(Master Fuzzer)、通過-S參數指定多個從Fuzzer(Slave Fuzzer)。

$ screen afl-fuzz -i testcases/ -o sync_dir/ -M fuzzer1 -- ./program
$ screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer2 -- ./program
$ screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer3 -- ./program

PS: -o指定的是一個同步目錄,並行測試中,所有的Fuzzer將相互協作,在找到新的代碼路徑時,相互傳遞新的測試用例。所以不用擔心重復的問題

兩個輔助工具:

  • afl-whatsup工具可以查看每個fuzzer的運行狀態和總體運行概況,加上-s選項只顯示概況,其中的數據都是所有fuzzer的總和。
  • afl-gotcpu工具可以查看每個核心使用狀態。

4.2 多系統並行

壓縮每個fuzzer實例目錄中queue下的文件,通過如下SSH腳本同步分發到其他機器上解壓。

#!/bin/sh

# authorized_keys的方式進行ssh認證
# 所有要同步的主機
FUZZ_HOSTS='172.21.5.101 172.21.5.102'
# SSH user
FUZZ_USER=root
# 同步目錄
SYNC_DIR='/root/syncdir'
# 同步間隔時間
SYNC_INTERVAL=$((30 * 60))

if [ "$AFL_ALLOW_TMP" = "" ]; then
  if [ "$PWD" = "/tmp" -o "$PWD" = "/var/tmp" ]; then
    echo "[-] Error: do not use shared /tmp or /var/tmp directories with this script." 1>&2
    exit 1
  fi
fi

rm -rf .sync_tmp 2>/dev/null
mkdir .sync_tmp || exit 1

while :; do

  # 打包所有機器上的數據
  for host in $FUZZ_HOSTS; do
    echo "[*] Retrieving data from ${host}..."
    ssh -o 'passwordauthentication no' ${FUZZ_USER}@${host} \
      "cd '$SYNC_DIR' && tar -czf - SESSION*" >".sync_tmp/${host}.tgz"
  done

  # 分發數據

  for dst_host in $FUZZ_HOSTS; do
    echo "[*] Distributing data to ${dst_host}..."
    for src_host in $FUZZ_HOSTS; do
      test "$src_host" = "$dst_host" && continue
      echo "    Sending fuzzer data from ${src_host}..."
      ssh -o 'passwordauthentication no' ${FUZZ_USER}@$dst_host \
        "cd '$SYNC_DIR' && tar -xkzf - &>/dev/null" <".sync_tmp/${src_host}.tgz"
    done
  done

  echo "[+] Done. Sleeping for $SYNC_INTERVAL seconds (Ctrl-C to quit)."
  sleep $SYNC_INTERVAL
  
done

0x02 Fuzz結果分析和代碼覆蓋率

1. 工作狀態

afl-fuzz永遠不會停止,所以何時停止測試很多時候就是依靠afl-fuzz提供的狀態來決定的。具體的幾種方式如下所示:

  • 狀態窗口的cycles done變為綠色;
  • afl-whatsup查看afl-fuzz狀態;
  • afl-stat得到類似於afl-whatsup的輸出結果;
  • 定制afl-whatsup->在所有代碼外面加個循環就好;
  • afl-plot繪制各種狀態指標的直觀變化趨勢;
  • pythia估算發現新crash和path概率。

2. fuzz結束判斷

  • 狀態窗口中”cycles done”字段顏色變為綠色該字段的顏色可以作為何時停止測試的參考;
  • 距上一次發現新路徑(或者崩潰)已經過去很長時間了,至於具體多少時間還是需要自己把握;
  • 目標程序的代碼幾乎被測試用例完全覆蓋,這種情況好像很少見;
  • pythia提供的各種數據中,path covera達到99或者correctness的值達到1e-08(含義: 從上次發現path/uniq crash到下一次發現之間大約需要1億次執行)

3. 輸出結果說明

queue:存放所有具有獨特執行路徑的測試用例。

crashes:導致目標接收致命signal而崩潰的獨特測試用例。

crashes/README.txt:保存了目標執行這些crash文件的命令行參數。

hangs:導致目標超時的獨特測試用例。

fuzzer_stats:afl-fuzz的運行狀態。

plot_data:用於afl-plot繪圖。

4. 對crash結果的簡單分析和分類

  1. crash exploration mode
    afl-fuzz的一種運行模式,也稱為peruvian rabbit mode,用於確定bug的可利用性,其輸入的是crash的信息,之后使用-C啟用這種模式,afl會自動探索並創造與之相關的crash來幫助你進行分析,比如判斷能夠控制某塊內存地址的長度。
afl-fuzz -m none -C -i ./fuzz_out/crashes -o ./peruvian-were-rabbit_out -- ./vuln -f
  1. triage_crashes.sh
    AFL源碼的experimental目錄中有一個名為triage_crashes.sh的腳本,可以幫助我們觸發收集到的crashes。

直接使用腳本跟參數的話,我們可以看到相關crash情況的寄存器等信息,但是如果只是大致分類的話,可以使用如下命令

/root/Desktop/fuzz/afl/experimental/crash_triage/triage_crashes.sh ./fuzz_out ./vuln 2>&1 | grep SIGNAL

效果如下,11代表了SIGSEGV信號,有可能是因為緩沖區溢出導致進程引用了無效的內存
0C1CB900-EA6C-4814-A67E-1FBE5994AB87.png

  1. crashwalk
    優點:可以顯示更為詳細的信息
    項目地址: https://github.com/bnagy/crashwalk
# 手動模式
~/go/bin/cwtriage -root ./fuzz_out/crashes -match id -- ./vuln -f
# afl自動化模式
~/go/bin/cwtriage -root ./fuzz_out/crashes -afl

6DD9C8EA-E494-4633-9F84-3F6B7959CD74.png

  1. afl-collect

項目地址: https://github.com/rc0r/afl-utils
afl-collect基於exploitable來檢查crashes的可利用性。它可以自動刪除無效的crash樣本、刪除重復樣本以及自動化樣本分類。

afl-collect -j 8 -d crashes.db -e gdb_script ./fuzz_out ./fuzz_in --  ./vuln --target-opts

效果如下

D9712AE7-55FB-4A82-B974-0D418D4C85D7.png

5. 代碼覆蓋率

原理部分參考:
《AFL漏洞挖掘技術漫談(二):Fuzz結果分析和代碼覆蓋率》

afl-cov的使用說明如下:
首先使用gcov重新編譯源碼

gcc -fprofile-arcs -ftest-coverage vuln.c -o vuln_cov

如果遇到需要make進行編譯的文件,執行如下:

$ make clean
$ ./configure --prefix=/root/tiff-4.0.10/build-cov CC="gcc" CXX="g++" CFLAGS="-fprofile-arcs -ftest-coverage" --disable-shared
$ make
$ make install

之后使用afl-cov來計算覆蓋率

afl-cov -d ./fuzz_out --live --enable-branch-coverage -c . -e "cat AFL_FILE | ./vuln_cov AFL_FILE"

同時進行對插樁過的vuln的fuzz

afl-fuzz -i ./fuzz_in -o ./fuzz_out ./vuln -f

最終效果如下
60FD6E10-65F2-4CC1-B270-218F679D59B0.png

生成的報告會保存在/path/to/afl-fuzz-output/cov/web/lcov-web-final路徑下。


轉載自http://zeroyu.xyz/2019/05/15/how-to-use-afl-fuzz/


免責聲明!

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



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