淺談AFL++ fuzzing:如何進(jìn)行有效且規(guī)整的fuzzing
input corpus
收集語料庫
對于模糊測試工具而言,我們需要為其準(zhǔn)備一個或多個起始的輸入案例,這些案例通常能夠很好的測試目標(biāo)程序的預(yù)期功能,這樣我們就可以盡可能多的覆蓋目標(biāo)程序。
收集語料的來源多種多樣。通常目標(biāo)程序會包含一些測試用例,我們可以將其做位我們初始語料的一部分,此外互聯(lián)網(wǎng)上也有些公開的語料庫你可以收集他們做為你的需要。
關(guān)于語料庫的主動性選擇,這個更多需要你對fuzzing 目標(biāo)內(nèi)部結(jié)構(gòu)的了解。例如你當(dāng)你要fuzzing的目標(biāo)對隨著輸入的規(guī)模內(nèi)存變化非常敏感,那么制作一批很大的文件與較小的文件可能是一個策略,具體是否是否有效取決于你經(jīng)驗(yàn)、以及對目標(biāo)的理解。
此外,需要注意控制語料庫的規(guī)模,太過龐大的語料庫并不是好的選擇,太過潘達(dá)的語料庫會拖慢fuzzing的效率,盡可能用相對較小的語料覆蓋更多目標(biāo)代碼的預(yù)期功能即可。
語料庫唯一化
我們在上一小節(jié)最后提到一點(diǎn),太過龐大的語料庫會因?yàn)橛刑嗟臏y試用例重復(fù)相同的路徑覆蓋,這會減慢fuzzing的效率。因此人們制作了一個工具,能夠使語料庫覆蓋的路徑唯一化,簡單的說就算去除重復(fù)的種子輸入,縮減語料庫的規(guī)模,同時保持相當(dāng)?shù)臏y試路徑效果。
在AFL++中可以使用工具afl-cmin
從語料庫中去除不會產(chǎn)生新路徑和覆蓋氛圍的重復(fù)輸入,并且AFL++官方提示強(qiáng)烈建議我們對語料庫唯一化,這是一個幾乎不會產(chǎn)生壞處的友誼操作。
具體的使用如下:
- 將收集到的所有種子文件放入一個目錄中,例如 INPUTS
- 運(yùn)行 afl-cmin:
字典
其實(shí)將字典放到這一個大節(jié)下面不是很合適,因?yàn)樽值淇梢詺w類為一種輔助技巧,不過因?yàn)樽值溆绊戄斎耄晕揖蛯⑵鋭澋竭@里了。
關(guān)于是否使用字典,取決于fuzzing的目的與目標(biāo)。例如fuzzing的目標(biāo)是ftp服務(wù)器,我們fuzzing的目的是站在用戶的視角僅能輸入命令,因此我們的輸入其中很大一部分可以規(guī)范到ftp提供的命令,我們更多的是通過重復(fù)測試各種命令的組合來測試目標(biāo)ftp服務(wù)器在各種場景都能正確運(yùn)行。
又比如,當(dāng)你fuzzing一個很復(fù)雜的目標(biāo)時,它通常提供一個非常非常豐富的命令行參數(shù),每一次運(yùn)行時組合不同的參數(shù)可能會有更好的覆蓋效果,因此可以將你需要啟用的參數(shù)標(biāo)記為字典添加進(jìn)命令行參數(shù)列表中。
最后,目標(biāo)程序可能經(jīng)常有常量的比較和驗(yàn)證,而這些環(huán)節(jié)通常會使得fuzzing停滯在此,因?yàn)槟:鞯淖儺惒呗酝ǔ?yīng)常量的猜測是非常低效的。我們可以收集目標(biāo)程序中使用到的常量,定義為一個字典提供給模糊器。但目前對于AFL++來說有更好的方法解決這種需求,而無需定義字典,后面我們會介紹這些方法。
# 模糊器默認(rèn)的變異策略通常難以命中if分支為true的情況,因?yàn)閕nput做為64位,其值的空間太大了,根本難以猜測。
if (input = 0x1122336644587) {
crash();
}
else {
OK();
}
編譯前的準(zhǔn)備
選擇最佳的編譯器
如我們上一節(jié)中談到收集程序常量定義字典時,事實(shí)上收集常量并生成字典這個事情,在編譯時完全可以順便將其解決。沒錯,功能強(qiáng)大的編譯器可以使我們在編譯期間獲得非常多有用的功能。對于AFL++的編譯器選擇,官方提供了一個簡單的選擇流程,如下
+--------------------------------+
| clang/clang++ 11+ is available | --> use LTO mode (afl-clang-lto/afl-clang-lto++)
+--------------------------------+ see [https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.lto.md](https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.lto.md)
|
| if not, or if the target fails with LTO afl-clang-lto/++
|
v
+---------------------------------+
| clang/clang++ 3.8+ is available | --> use LLVM mode (afl-clang-fast/afl-clang-fast++)
+---------------------------------+ see [https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.llvm.md](https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.llvm.md)
|
| if not, or if the target fails with LLVM afl-clang-fast/++
|
v
+--------------------------------+
| gcc 5+ is available | -> use GCC_PLUGIN mode (afl-gcc-fast/afl-g++-fast)
+--------------------------------+ see [https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.gcc_plugin.md](https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.gcc_plugin.md) and
[https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.instrument_list.md](https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.instrument_list.md)
|
| if not, or if you do not have a gcc with plugin support
|
v
use GCC mode (afl-gcc/afl-g++) (or afl-clang/afl-clang++ for clang)
若你的LLVM和clang版本大于等于11,那么你可以啟用LLVM LTO模式,使用afl-clang-lto/afl-clang-lto++,該模式通常是最佳的。隨后依次是afl-clang-fast/afl-clang-fast++和afl-gcc-fast/afl-g++-fast。
關(guān)于為什么LTO模式通常是最佳的,其中一個原因是它解決了原版AFL中邊碰撞的情況,提供了無碰撞的邊(edge)檢測。在原本AFL中,因?yàn)槠鋵?edge)的標(biāo)識是隨機(jī)的,對于AFL默認(rèn)2^16容量來說,一旦程序足夠大,邊的標(biāo)識會重復(fù),這種現(xiàn)象就算邊碰撞,它會降低模糊測試的效率。此外LTO模式會自動收集目標(biāo)代碼中的常量制作成為一個字典并自動啟用,并且社區(qū)提供的一些有用的插件和功能很多時候是要求LLVM模式(clang-fast)甚至是LTO模式(clang-lto)。
NOTE:此處涉及一點(diǎn)AFL度量覆蓋率的工作原理,可以參考我注意的另一篇文章《基于覆蓋率的Fuzzer和AFL》,寫的很一般(逃
關(guān)于編譯器的選擇,如果可能直接選LTO模式即可。但你需要注意,LTO模式編譯代碼非常的吃內(nèi)存,編譯時間也會很久,尤其是啟用某些Sanitizer的時候。
NOTE:你的計(jì)算機(jī)配置最好至少由8核心,內(nèi)存最好不低于16G。請注意8核心,16G仍然不是很夠用,最好32G,16核或以上,核心越多越好。因?yàn)榈綍r候你會編譯很多不同版本的程序,不同的插件、不同的sanitizer、不同策略等等,這些不同的選項(xiàng)往往不能兼并到一個程序上,往往需要編譯多分不同配置的程序,并你會經(jīng)常patch程序再編譯測試patch的效果。簡言之,你會編譯很多次程序,你需要足夠大的內(nèi)存和核心來編譯目標(biāo),使得你不必經(jīng)常阻塞等待編譯隊(duì)列和結(jié)果。
編譯的選項(xiàng)
AFL++是一個非常活躍的社區(qū),AFL++會集成社區(qū)中、互聯(lián)網(wǎng)上一些強(qiáng)大的第三方插件,這些集成的插件有一些我們可以通過設(shè)置對應(yīng)的編譯選項(xiàng)啟用。
對于LTO模式(afl-clang-fast/afl-clang-lto)進(jìn)行編譯插樁時,可以啟用下面兩項(xiàng)比較通用的特性,主要用于優(yōu)化一些固定值的比較和校驗(yàn)。
- Laf-Intel:能夠拆分程序中整數(shù)、字符串、浮點(diǎn)數(shù)等固定常量的比較和檢測??紤]下面一個情況
assert x == 0x11223344
,Laf-Intel會拆分為assert (x & 0xff) == 0x44 && ((x >> 8) & 0xff) == 0x33 ....
這樣形式,每一次只會進(jìn)行單字節(jié)的比較,這樣AFL就可以逐個字節(jié)的猜測,每當(dāng)確定一個字節(jié)時,就會發(fā)現(xiàn)一個新的路徑,進(jìn)而繼續(xù)在第一個字節(jié)的基礎(chǔ)上猜測第二個字節(jié),如此使得模糊器可以快速猜出0x11223344
。如果你沒有自己制作好的字典、豐富的語料庫,這個功能會非常有用,通常建議至少有一個AFL++實(shí)例運(yùn)行Laf-Intel插件。在編譯前設(shè)置如下環(huán)境使用:export AFL_LLVM_LAF_ALL=1
- CmpLog:這個插件會提取程序中的比較的固定值,這些值會被用于變異算法中。功能與Laf-Intel類似,但效果通常比Laf-Intel。使用該插件需要單獨(dú)編譯一份cmplog版本的程序,在fuzzing時指定該cmplog版本加入到fuzzing中。具體的用法如下:
# 編譯一份常規(guī)常規(guī)版本
cd /target/path
CC=afl-clang-lto make -j4
cp ./program/path/target ./target/target.afl
# 編譯cmplog版本
make clean
export AFL_LLVM_CMPLOG=1
CC=afl-clang-lto make -j4
cp ./program/path/target ./target/target.cmplog
unset AFL_LLVM_CMPLOG
# 使用cmplog, 用-c參數(shù)指定cmplog版本目標(biāo),因?yàn)閏mplog回申請很多內(nèi)存做映射因此我們設(shè)置
# -m none,表示不限制afl-fuzz的內(nèi)存使用。你也可以指定一個值例如 -m 1024,即1GB。
afl-fuzz -i input -o output -c ./target.cmplog -m none -- ./target.afl @@
NOTE:需要注意,兩個插件并不是說誰替代誰,往往在實(shí)際fuzzing中兩者都會用至少一個afl實(shí)例啟用。
考慮下面兩種場景。
有時候你想要fuzzing的目標(biāo)中,他自動的集成了很多第三方的庫代碼,他們會在編譯中一并編譯,而你并不想fuzzing這些第三方庫來,你只想高效、快速的fuzzing目標(biāo)的代碼,額外的fuzzing第三方代碼只會拖慢你fuzzing的效率。
有時候你的目標(biāo)會非常龐大和復(fù)雜,他們的構(gòu)建往往是模塊化的,有時候你只想fuzzing某幾個模塊。
這上面兩種情況都是我們fuzzing中很常遇見的,所幸AFL++提供了部分插樁編譯的功能,即"partial instrumentation",它允許我們指定應(yīng)該檢測那些內(nèi)容以及不應(yīng)該檢測那些內(nèi)容,這個檢測的顆粒是代碼源文件、函數(shù)級兩級。具體用法如下:
- 檢測指定部分。創(chuàng)建一個文件(
allowlist.txt
,文件名沒有要求),需要在其中指定應(yīng)包含檢測的源代碼文件或者函數(shù)。
- 1.在文件中每行輸入一個文件名或函數(shù)
foo.cpp # 將會匹配所有命名為foo.cpp的文件,注意是所有命名為foo.cpp的文件
path/foo.cpp # 將會只確定的包含該路徑的foo.cpp文件,不會造成意外的包含
fun:foo_fun # 將會包含所有foo_fun函數(shù)
- 設(shè)置export AFL_LLVM_ALLOWLIST=allowlist.txt 啟用選擇性檢測
- 排除某些部分。與指定某些部分類似,編寫一個文件然后設(shè)置環(huán)境變量
export AFL_LLVM_DENYLIST=denylist.txt
以啟用,這會跳過我們文件中指定的內(nèi)容。
Note:有些小函數(shù)可能在編譯期間被優(yōu)化,內(nèi)聯(lián)到上級調(diào)用者,即類似于宏函數(shù)展開。這時將會導(dǎo)致指定失效!如果不想受此影響,禁用內(nèi)聯(lián)函數(shù)優(yōu)化即可。
此外,對于C++由于函數(shù)命名粉碎機(jī)制,你需要特別的提取粉碎后的函數(shù)名。例如函數(shù)名為test
的函數(shù)可能會被粉碎重命名為_Z4testv
??梢杂胣m提取函數(shù)名,創(chuàng)建一個腳本篩選出來。
添加Sanitizer檢測更多BUG
Sanitizer最初是Google的一個開源項(xiàng)目,它們是一組檢測工具。例如AddressSanitizer是一個內(nèi)存錯誤檢測器,可以檢測諸如OOB、UAF、Double-free等到內(nèi)存錯誤的場景?,F(xiàn)在該項(xiàng)目以及成為LLVM的一部分,相對較高的gcc和clang都默認(rèn)包含Sanitizer功能。
由于AFL++基本只會檢測到導(dǎo)致Crash的BUG,因此啟用一些Sanitizer可以使得我們檢測一些并不會導(dǎo)致Crash的錯誤,例如內(nèi)存泄露。
AFL++內(nèi)置支持下面幾種Sanitizer:
- ASAN:AddressSanitizer,用于發(fā)現(xiàn)內(nèi)存錯誤的bug,如
use-after-free
、空指針解引用(NULL pointer dereference)
、緩沖區(qū)溢出(buffer overruns)
、Stack And Heap Overflow
、Double Free/ Wild Free
、Stack use outside scope
等。若要使用請?jiān)?strong>編譯前設(shè)置環(huán)境變量export AFL_USE_MSAN=1
。更多關(guān)于ASAN的信息參與LLVM官網(wǎng)對ASAN:AddressSanitizer的描述(https://clang.llvm.org/docs/AddressSanitizer.html)。 - MSAN:MemorySanitizer,用于檢測對未初始化內(nèi)存的訪問。若要啟用,在編譯前設(shè)置
export AFL_USE_MSAN=1
以啟用。 - UBSAN:UndefinedBehavior Sanitizer,如其名字一般用于檢測和查找C和C++語言標(biāo)的未定義行為。未定義行為是語言標(biāo)準(zhǔn)沒有定義的行為,編譯器在編譯時可能不會報(bào)錯,然而這些行為導(dǎo)致的結(jié)果是不可預(yù)測的,對于程序而言是一個極大的隱患。請?jiān)?strong>編譯前,設(shè)置
export AFL_USE_UBSAN=1
環(huán)境變量以啟用。 - CFISAN:Control Flow Integrity Sanitizer,CFI的實(shí)現(xiàn)有多種,它們是為了在程序出現(xiàn)未知的危險行為時終止程序,這些危險行為可能導(dǎo)致控制流劫持或破壞,用于預(yù)防ROP。在Fuzzing中,CFISAN主要用于檢測類型混淆。請?jiān)?strong>編譯前,設(shè)置
export AFL_USE_CFISAN=1
環(huán)境變量以啟用。 - TSAN:Thread Sanitizer, 用于多線程環(huán)境下數(shù)據(jù)競爭檢測。在目前,計(jì)算機(jī)通常都是多核,一個進(jìn)程中通常包含多個進(jìn)程,常常導(dǎo)致一個問題,即數(shù)據(jù)競爭。此類錯誤通常很難通過調(diào)試發(fā)現(xiàn),出現(xiàn)也不穩(wěn)定。當(dāng)至少兩個線程訪問同一個變量,并且同時存在讀取和寫入的行為時,即發(fā)送了數(shù)據(jù)競爭,若讀取在寫入之后,線程可能讀取到非預(yù)期的數(shù)據(jù),可能導(dǎo)致嚴(yán)重的錯誤。請?jiān)?strong>編譯前,設(shè)置
export AFL_USE_TSAN=1
環(huán)境變量以啟用。 - LSAN,Leak Sanitizer,用于檢測程序中的內(nèi)存泄露。內(nèi)存泄露通常并不會導(dǎo)致程序crash,但它是一個不穩(wěn)定的因素,可能會被利用、也可能沒辦法被利用,這不是一個嚴(yán)格意義上的漏洞。與其他Sanitizer的使用不同,需要將
__AFL_LEAK_CHECK();
添加到你想要進(jìn)行內(nèi)存泄露檢查的目標(biāo)源代碼的所有區(qū)域。在編譯之前啟用 ,export AFL_USE_LSAN=1
。要忽略某些分配的內(nèi)存泄漏檢查,__AFL_LSAN_OFF();
可以在分配內(nèi)存之前和__AFL_LSAN_ON();
之后使用,LSAN不會檢查這兩個宏之間區(qū)域。
Note:
- 一些Sanitizer不能混用,而即使有些可以同時允許的Santizier也可能導(dǎo)致意想不到的行為影響fuzzing,這需要結(jié)合你fuzzing的目標(biāo)情況而定。如果你不熟悉Sanitizer的原理,最好一個編譯實(shí)例中只啟用一個Sanitizer,這樣通常不會出問題,而且組合Sanitizer不見得會有好效果,基于對目標(biāo)的了解正確的使用Sanitizer才是最佳的實(shí)踐。
- 有些Sanitizer提供了參數(shù)設(shè)置的環(huán)境變量,如
ASAN_OPTIONS
,如果你有很明確的需求可以設(shè)置該變量進(jìn)一步限制Sanitizer的檢測行為,這可能會提高你fuzzing的效率。如果你不熟悉、也沒有明確的需求,那么保持默認(rèn)即可,這通常是最實(shí)用的。- 啟用CFISAN的實(shí)例,可能會檢測出很多crash(成百上千),這是正常的,但大多數(shù)是無用的,甚至全是無用的,你需要注意甄別。
- 如果你對目標(biāo)內(nèi)部結(jié)構(gòu)足夠熟悉,你確定那些區(qū)域是線程并發(fā)的高發(fā)區(qū)域,那么你可以結(jié)合TSAN與partial instrumentation功能提高TSAN的檢測效率,因?yàn)閱⒂肨SAN的實(shí)例通常fuzzing速度會大幅減慢。
- 通常啟Sanitizer后,會大幅減慢fuzzing的速度,CPU每秒執(zhí)行次數(shù)會減少,內(nèi)存也會被大量消耗(AddressSanitizer會大量消耗內(nèi)存,甚至可能導(dǎo)致計(jì)算機(jī)內(nèi)存耗盡)。如果你的計(jì)算機(jī)配置不行,請斟酌一個合理的搭配。
- 一種Sanitizer只應(yīng)該允許一個實(shí)例 。在兩個實(shí)例上允許兩個同樣的Sanitizer是一種浪費(fèi),因?yàn)锳FL++會同步所有實(shí)例的testcase,其他實(shí)例的testcase無論如何都會被該實(shí)例上的Sanitizer檢測一遍,不應(yīng)該啟用兩個相同的Sanitizer檢測兩遍,這會減慢效率。
暫時只想到這些,以后想到了再補(bǔ)充。
LLVM Persistent Mode
In-process fuzzing是一個強(qiáng)大功能,通常比默認(rèn)常規(guī)編譯fuzzing的速度快得多,大概快10-20倍,并且基本沒有任何缺點(diǎn)。如果可以,請毫不猶豫的使用Persistent mode。
眾所周知,AFL使用ForkServer來進(jìn)行每次fuzzing,然而即便不用execve這種巨大的開銷,但fork仍然是一筆不小的開。而Persistent fuzzing即一次fork進(jìn)程種進(jìn)行多次fuzzing,而無需每次都fork。
Persistent mode提供一組AFL++的函數(shù)和宏,我們使用下面的形式,用一個while包含我們要進(jìn)行Persistent fuzzing的區(qū)域。請注意,該區(qū)域的代碼必須要是無狀態(tài)的,要么是可以手動可靠的重置為初始狀態(tài)!這樣我們才能再每次fuzzing時重置進(jìn)而再次fuzzing。
afl-clang-fast/lto編譯的情況下,只需要使用下面的形式即可,但若不是,則復(fù)雜一些。
AFL++官方的倉庫對Persistent Mode花了不小的篇幅講訴,講的也比較全面,請?jiān)诖颂?a >Persistent Mode中查閱,我就不做過多描述了。
#include "what_you_need_for_your_target.h"
__AFL_FUZZ_INIT();
int main() {
// anything else here, e.g. command line arguments, initialization, etc.
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF; // must be after __AFL_INIT
// and before __AFL_LOOP!
while (__AFL_LOOP(10000)) {
int len = __AFL_FUZZ_TESTCASE_LEN; // don't use the macro directly in a
// call!
if (len < 8) continue; // check for a required/useful minimum input length
/* Setup function call, e.g. struct target *tmp = libtarget_init() */
/* Call function to be fuzzed, e.g.: */
target_function(buf, len);
/* Reset state. e.g. libtarget_free(tmp) */
}
return 0;
}
Patch
大多數(shù)時候,我們fuzzing一個目標(biāo)想要其達(dá)到我們預(yù)期的效果,都需要Patch。并且我們在后續(xù)fuzzing流程的持續(xù)改進(jìn)中可能還會發(fā)現(xiàn)一些影響fuzzing效率的地方,我們又會倒回來patch,編譯重新啟動fuzzing。
此外,有時候一些校驗(yàn)、檢查,它們往往對于fuzzing的結(jié)果沒有什么影響,但是卻嚴(yán)重影響fuzzing的效率。此時我們通常會審查目標(biāo)內(nèi)部代碼,將這些嚴(yán)重性fuzzing效率的地方Patch,或者是刪除。
我們都知道Persistent Mode收益十分巨大,但卻要求Persistent循環(huán)區(qū)域內(nèi)的代碼是無狀態(tài)的,有時候區(qū)域會有一些有狀態(tài)的函數(shù),但他們卻并不重要,這時你可以Patch它們,使它們返回諸如硬編碼之類可以,這樣就變成無狀態(tài)的,我們就可以使用Persistent Mode了。
例如一個區(qū)域的輸入可能依賴于socket IO讀入,而處理socket IO是很麻煩的,因此我們可以考慮將socket fd替換為文件 fd,并patch那些受socket fd受影響區(qū)域,以便我們fuzzing正確運(yùn)行。
簡言之,Patch最好有明確的理由,隨意的Patch對模糊測試來說可能會導(dǎo)致很糟糕的現(xiàn)象,要么你對此處的Patch是基于改進(jìn)fuzzing效率,要么是為了啟用某些有益的fuzzing功能....總之,最好清楚自己的Patch是為了什么。
但請注意,對于一次模糊測試來說Patch只是可選的,如果你對自己的工具、目標(biāo)不甚了解,那么Patch對你而言可能不重要。如果你清楚目標(biāo)的內(nèi)部結(jié)構(gòu),并且明確知道要改進(jìn)fuzzing的流程和目的,那么Patch可能是你定制化自己fuzzing的一個重要手段。
后續(xù)
目前就先寫到著,后面的內(nèi)容,包括build、fuzzing、評估、流程改進(jìn)等等就放到下篇,最近的工作可能要忙一些其他的。
本文作者:Cheney辰星, 轉(zhuǎn)載請注明來自FreeBuf.COM