LibFuzzer workshop學習之路
最近做項目開始上手學習libfuzzer,是跟著libfuzzer-workshop學的。寫下自己的心得和收穫。
官方給出的定義
LibFuzzer is in-process, coverage-guided, evolutionary fuzzing engine.LibFuzzer is linked with the library under test, and feeds fuzzed inputs to the library via a specific fuzzing entrypoint (aka 「target function」); the fuzzer then tracks which areas of the code are reached, and generates mutations on the corpus of input data in order to maximize the code coverage. The code coverage information for libFuzzer is provided by LLVM’s SanitizerCoverage instrumentation.簡單來說就是通過與要進行fuzz的庫連接,並將libfuzzer生成的輸入通過模糊測試進入點(fuzz target)餵給要fuzz的庫進行fuzz testing。同時fuzzer會跟蹤哪些區域的代碼已經被測試過的,並且根據種料庫的輸入進行變異來使得代碼覆蓋率最大化。代碼覆蓋率的信息是由LLVM’s SanitizerCoverage插樁提供的
需要注意的是這幾個libfuzzer的特性:in-process指進程內。即libfuzzer在fuzz時並不是產生出多個進程來分別處理不同的輸入,而是將所有的測試數據放入進程的內存空間中。coverage-guided指覆蓋率指導的。即會進行代碼覆蓋率的計算,正如定義所說的使得不斷增大代碼覆蓋率。evolutionary是指libfuzzer是進化型的fuzz,結合了產生和變異兩種形式。
環境搭建跟著https://github.com/Dor1s/libfuzzer-workshop 搭建就好了,主要是build llvm的環境可能要make一會兒。編譯好後拿到libfuzzer.a(靜態連結庫文件),就可以開始上手實踐了。
fuzz testinglibfuzzer已經提供了數據樣本生成和異常檢測功能,我們要做的就是要實現模糊測試進入點(fuzz target),將libfuzzer生成的數據交給目標程序處理。
fuzz target編寫模板:extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { DoSomethingInterestingWithMyAPI(Data, Size); return 0; }需要注意的是LLVMFuzzerTestOneInput函數即使我們要實現的接口函數,他的兩個參數Data(libfuzzer的測試樣本數據),size(樣本數據的大小)。
DoSomethingInterestingWithMyAPI函數即我們實際要進行fuzz的函數。編譯.cc文件:
clang++ -g -O1 -fsanitize=fuzzer,address \fuzz_target.cc ../../libFuzzer/Fuzzer/libFuzzer.a \-o fuzzer_target幾個參數:
-g 可選參數,保留調試符號。
-O1 指定優化等級為1
-fsanitize 指定sanitize。
fuzzer是必須的,用來啟用libfuzzer。還可以附加的其他sanitize有:address(用來檢測內存訪問相關的錯誤,如stack_overflow,heap_overflow,uaf,可以與fuzzer一起使用);memory(檢測未初始化內存的訪問,應單獨使用);undefined(檢測其他的漏洞,如整數溢出,類型混淆等未定義的漏洞)
註:-fsanitize-coverage=trace-pc-guard選項在高版本的clang中已不再適用,代碼的覆蓋率情況默認自動開啟。這一步驟整體過程就是通過clang的-fsanitize=fuzzer選項可以啟用libFuzzer,這個選項在編譯和連結過程中生效,實現了條件判斷語句和分支執行的記錄,並且輔以libFuzzer中的庫函數(libfuzzer.a),通過生成不同的測試樣例然後能夠獲得代碼的覆蓋率情況,最終實現所謂的fuzz testing。
開始fuzz
先來lesson 04,要測試的庫是vulnerable_functions.h:
#ifndef LESSONS_04_VULNERABLE_FUNCTIONS_H_#define LESSONS_04_VULNERABLE_FUNCTIONS_H_
#include <stdint.h>#include <stddef.h>#include <cstring>
#include <array>#include <string>#include <vector>
bool VulnerableFunction1(const uint8_t* data, size_t size) { bool result = false; if (size >= 3) { result = data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' && data[3] == 'Z'; }
return result;}
template<class T>typename T::value_type DummyHash(const T& buffer) { typename T::value_type hash = 0; for (auto value : buffer) hash ^= value;
return hash;}
constexpr auto kMagicHeader = "ZN_2016";constexpr std::size_t kMaxPacketLen = 1024;constexpr std::size_t kMaxBodyLength = 1024 - sizeof(kMagicHeader);
bool VulnerableFunction2(const uint8_t* data, size_t size, bool verify_hash) { if (size < sizeof(kMagicHeader)) return false;
std::string header(reinterpret_cast<const char*>(data), sizeof(kMagicHeader));
std::array<uint8_t, kMaxBodyLength> body;
if (strcmp(kMagicHeader, header.c_str())) return false;
auto target_hash = data[--size];
if (size > kMaxPacketLen) return false;
if (!verify_hash) return true;
std::copy(data, data + size, body.data()); auto real_hash = DummyHash(body); return real_hash == target_hash;}
constexpr std::size_t kZn2016VerifyHashFlag = 0x0001000;
bool VulnerableFunction3(const uint8_t* data, size_t size, std::size_t flags) { bool verify_hash = flags & kZn2016VerifyHashFlag; return VulnerableFunction2(data, size, verify_hash);}
#endif首先看VulnerableFunction1(),有兩個參數data/size,當size>3時會產生數組越界。接下來編寫測試接口:
#include <stdint.h>#include <stddef.h>
#include "vulnerable_functions.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { VulnerableFunction1(data, size); return 0;}可以看到,直接將Libfuzzer生成的測試樣例給到VulnerableFunction1就好。
接下來編譯:
clang++ -g -std=c++11 -fsanitize=fuzzer,address first_fuzzer.cc ../../libFuzzer/Fuzzer/libFuzzer.a -o first_fuzzer
生成可執行程序first_fuzzer。mkdir corpus1./first_fuzz corpus1corpus1是我們提供的語料庫。理想情況下,該語料庫應該為被測代碼提供各種有效和無效的輸入,模糊器基於當前語料庫中的樣本輸入生成隨機突變。如果突變觸發了測試代碼中先前未覆蓋的路徑的執行,則該突變將保存到語料庫中以供將來變更。當然LibFuzzer也可以沒有任何初始種子的情況下工作(因為上面提到他是evolutionary型的fuzzer),但如果受測試的庫接受複雜的結構化輸入,則會因為隨機產生的樣例不易符合導致效率降低。
另外,如果我們有太多的樣例並希望能夠精簡一下,則可以:mkdir corpus1_min./first_fuzzer -merge=1 corpus1_min corpus1這樣,corpus1_min將會存放精簡後的輸入樣例。
運行後得到crash,很快啊!!!
➜ 04 git:(master) ✗ ./first_fuzzer corpus1 INFO: Seed: 2222548757INFO: Loaded 1 modules (35 inline 8-bit counters): 35 [0x7f7120, 0x7f7143), INFO: Loaded 1 PC tables (35 PCs): 35 [0x5b7a68,0x5b7c98), INFO: 0 files found in corpus1INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytesINFO: A corpus is not provided, starting from an empty corpus#2 INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 27Mb#3 NEW cov: 4 ft: 4 corp: 2/4b lim: 4 exec/s: 0 rss: 27Mb L: 3/3 MS: 1 CMP- DE: "\x00\x00"-#1190 NEW cov: 5 ft: 5 corp: 3/7b lim: 14 exec/s: 0 rss: 27Mb L: 3/3 MS: 2 ChangeBinInt-CMP- DE: "F\x00"-#12191 NEW cov: 6 ft: 6 corp: 4/11b lim: 122 exec/s: 0 rss: 28Mb L: 4/4 MS: 1 InsertByte-#12297 REDUCE cov: 6 ft: 6 corp: 4/10b lim: 122 exec/s: 0 rss: 28Mb L: 3/3 MS: 1 EraseBytes-#17213 REDUCE cov: 7 ft: 7 corp: 5/40b lim: 170 exec/s: 0 rss: 29Mb L: 30/30 MS: 1 InsertRepeatedBytes-#17274 REDUCE cov: 7 ft: 7 corp: 5/27b lim: 170 exec/s: 0 rss: 29Mb L: 17/17 MS: 1 EraseBytes-#17356 REDUCE cov: 7 ft: 7 corp: 5/24b lim: 170 exec/s: 0 rss: 29Mb L: 14/14 MS: 2 ChangeBit-EraseBytes-#17437 REDUCE cov: 7 ft: 7 corp: 5/18b lim: 170 exec/s: 0 rss: 29Mb L: 8/8 MS: 1 EraseBytes-#17458 REDUCE cov: 7 ft: 7 corp: 5/14b lim: 170 exec/s: 0 rss: 29Mb L: 4/4 MS: 1 EraseBytes-===================================================================9875==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200005a1d3 at pc 0x00000059b461 bp 0x7ffd79c84880 sp 0x7ffd79c84878READ of size 1 at 0x60200005a1d3 thread T0 #0 0x59b460 in VulnerableFunction1(unsigned char const*, unsigned long) /home/admin/libfuzzer-workshop/lessons/04/./vulnerable_functions.h:22:14 #1 0x59bde4 in LLVMFuzzerTestOneInput /home/admin/libfuzzer-workshop/lessons/04/first_fuzzer.cc:10:3 #2 0x466186 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:556 #3 0x46b7e9 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:470 #4 0x46b7e9 in fuzzer::Fuzzer::MutateAndTestOne() /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:699 #5 0x46e80f in fuzzer::Fuzzer::Loop(std::Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:830 #6 0x456b99 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:824 #7 0x41f522 in main /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerMain.cpp:19 #8 0x7fbfaac5abf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6) #9 0x41f599 in _start (/home/admin/libfuzzer-workshop/lessons/04/first_fuzzer+0x41f599)
0x60200005a1d3 is located 0 bytes to the right of 3-byte region [0x60200005a1d0,0x60200005a1d3)allocated by thread T0 here: #0 0x597b58 in operator new[](unsigned long) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/asan/asan_new_delete.cpp:102 #1 0x466092 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:541 #2 0x46b7e9 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:470 #3 0x46b7e9 in fuzzer::Fuzzer::MutateAndTestOne() /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:699 #4 0x46e80f in fuzzer::Fuzzer::Loop(std::Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:830 #5 0x456b99 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:824 #6 0x41f522 in main /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerMain.cpp:19 #7 0x7fbfaac5abf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/admin/libfuzzer-workshop/lessons/04/./vulnerable_functions.h:22:14 in VulnerableFunction1(unsigned char const*, unsigned long)Shadow bytes around the buggy address: 0x0c04800033e0: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fd 0x0c04800033f0: fa fa fd fd fa fa fd fa fa fa fd fa fa fa fd fd 0x0c0480003400: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa 0x0c0480003410: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa 0x0c0480003420: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa=>0x0c0480003430: fa fa fd fa fa fa fd fa fa fa[03]fa fa fa fa fa 0x0c0480003440: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c0480003450: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c0480003460: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c0480003470: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c0480003480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa faShadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb Shadow gap: cc==9875==ABORTINGMS: 1 EraseBytes-; base unit: aea2e3923af219a8956f626558ef32f30a914ebc0x46,0x55,0x5a,FUZartifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60Base64: RlVa需要注意的地方有點多:
前面的幾行輸出fuzzer相關的選項和配置信息。seed=2222548757是生成的隨機數種子,可以利用./first_fuzzer -seed=2222548757指定隨機種子。-max_len為測試輸入的最大長度以#開頭的表示在fuzz的過程中覆蓋的路徑信息。
INITED fuzzer已完成初始化,其中包括通過被測代碼運行每個初始輸入樣本。
READ fuzzer已從語料庫目錄中讀取了所有提供的輸入樣本。
NEW fuzzer創建了一個測試輸入,該輸入涵蓋了被測代碼的新區域。此輸入將保存到主要語料庫目錄。
pulse fuzzer已生成 2的n次方個輸入(定期生成以使用戶確信fuzzer仍在工作)。
REDUCE fuzzer發現了一個更好(更小)的輸入,可以觸發先前發現的特徵(設置-reduce_inputs=0以禁用)。
cov 執行當前語料庫所覆蓋的代碼塊或邊的總數。
ft libFuzzer使用不同的信號來評估代碼覆蓋率:邊緣覆蓋率,邊緣計數器,值配置文件,間接調用方/被調用方對等。這些組合的信號稱為功能(ft:)。
corp 當前內存中測試語料庫中的條目數及其大小(以字節為單位)。
exec/s 每秒模糊器迭代的次數。
rss 當前的內存消耗。
L 新輸入的大小(以字節為單位)。
MS:<n> <操作> 計數和用於生成輸入的變異操作列表#17458 REDUCE cov: 7 ft: 7 corp: 5/14b lim: 170 exec/s: 0 rss: 29Mb L: 4/4 MS: 1 EraseBytes-
指嘗試了17458個輸入,成功發現了5個樣本(放入語料庫)大小為14b,共覆蓋了7個代碼塊,佔用內存29mb,變異操作為EraseBytes-。接下來就是異常檢測相關的信息:
==9875==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200005a1d3 at pc 0x00000059b461 bp 0x7ffd79c84880 sp 0x7ffd79c84878
READ of size 1 at 0x60200005a1d3 thread T0可以看到AddressSanitizer檢測到其中的一個輸入觸發了堆溢出(heap-buffer-overflow)的漏洞。
更據錯誤信息中的#0 0x59b460 in VulnerableFunction1(unsigned char const*, unsigned long) /home/admin/libfuzzer-workshop/lessons/04/./vulnerable_functions.h:22:14可以看到錯誤點在vulnerable_functions.h:22:14,對應data[3] == 'Z';即數組越界的位置。下面的SUMMARY也與之對應。倒數第二行給出了造成crash的輸入,並將其寫入了crash-0eb8e4ed029b774d80f2b66408203801cb982a60。
復現crash可執行./first_fuzzer ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60。這樣fuzz tesing基本上已經完成了,我們得到了一個造成程序crash的輸入,並得知存在堆溢出的漏洞。這樣我們就可以有針對性的對程序進行動態調試,利用造成crash的輸入回溯出漏洞的細節。
繼續fuzz函數VulnerableFunction2
constexpr auto kMagicHeader = "ZN_2016";constexpr std::size_t kMaxPacketLen = 1024;constexpr std::size_t kMaxBodyLength = 1024 - sizeof(kMagicHeader);
bool VulnerableFunction2(const uint8_t* data, size_t size, bool verify_hash) { if (size < sizeof(kMagicHeader)) return false;
std::string header(reinterpret_cast<const char*>(data), sizeof(kMagicHeader));
std::array<uint8_t, kMaxBodyLength> body;
if (strcmp(kMagicHeader, header.c_str())) return false;
auto target_hash = data[--size];
if (size > kMaxPacketLen) return false;
if (!verify_hash) return true;
std::copy(data, data + size, body.data()); auto real_hash = DummyHash(body); return real_hash == target_hash;}該函數多了一個bool參數,因此我們的的接口函數要有所改動:
#include <stdint.h>#include <stddef.h>
#include "vulnerable_functions.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { bool verify_hash_flags[] = {true, false}; for(auto flag : verify_hash_flags) VulnerableFunction2(data, size, flag); return 0;}為了提高fuzz出crash的概率,我們要分別fuzz flag為true和false的請況,而不應該把flag寫死。
接著編譯並執行得到:INFO: Seed: 296692635INFO: Loaded 1 modules (37 inline 8-bit counters): 37 [0x7f8160, 0x7f8185), INFO: Loaded 1 PC tables (37 PCs): 37 [0x5b7d00,0x5b7f50), INFO: 0 files found in corpus2INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytesINFO: A corpus is not provided, starting from an empty corpus#2 INITED cov: 5 ft: 6 corp: 1/1b exec/s: 0 rss: 27Mb#414 NEW cov: 6 ft: 7 corp: 2/9b lim: 8 exec/s: 0 rss: 27Mb L: 8/8 MS: 2 ChangeByte-CrossOver- NEW_FUNC[1/13]: 0x59bab0 in unsigned char* std::copy<unsigned char const*, unsigned char*>(unsigned char const*, unsigned char const*, unsigned char*) /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/c++/7.5.0/bits/stl_algobase.h:447 NEW_FUNC[2/13]: 0x59bb40 in std::array<unsigned char, 1016ul>::data() /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/c++/7.5.0/array:235#584 NEW cov: 24 ft: 26 corp: 3/17b lim: 8 exec/s: 0 rss: 28Mb L: 8/8 MS: 5 CopyPart-CopyPart-ShuffleBytes-CrossOver-CMP- DE: "ZN_2016"-#105505 NEW cov: 25 ft: 27 corp: 4/1067b lim: 1050 exec/s: 0 rss: 47Mb L: 1050/1050 MS: 1 CrossOver-#106508 REDUCE cov: 25 ft: 27 corp: 4/1046b lim: 1050 exec/s: 0 rss: 48Mb L: 1029/1029 MS: 3 EraseBytes-ChangeBit-CopyPart-#108257 REDUCE cov: 25 ft: 27 corp: 4/1043b lim: 1060 exec/s: 0 rss: 49Mb L: 1026/1026 MS: 4 ChangeByte-ChangeByte-CrossOver-InsertRepeatedBytes-===================================================================10468==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff31422548 at pc 0x00000055286d bp 0x7fff31421f90 sp 0x7fff31421740WRITE of size 1023 at 0x7fff31422548 thread T0 #0 0x55286c in __asan_memmove /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:30 #1 0x59c238 in unsigned char* std::__copy_move<false, true, std::random_access_iterator_tag>::__copy_m<unsigned char>(unsigned char const*, unsigned char const*, unsigned char*) /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/c++/7.5.0/bits/stl_algobase.h:368:6 #2 0x59c158 in unsigned char* std::__copy_move_a<false, unsigned char const*, unsigned char*>(unsigned char const*, unsigned char const*, unsigned char*) /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/c++/7.5.0/bits/stl_algobase.h:385:14 #3 0x59c0b6 in unsigned char* std::__copy_move_a2<false, unsigned char const*, unsigned char*>(unsigned char const*, unsigned char const*, unsigned char*) /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/c++/7.5.0/bits/stl_algobase.h:422:18 #4 0x59bb39 in unsigned char* std::copy<unsigned char const*, unsigned char*>(unsigned char const*, unsigned char const*, unsigned char*) /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/c++/7.5.0/bits/stl_algobase.h:454:15 #5 0x59b8b5 in VulnerableFunction2(unsigned char const*, unsigned long, bool) /home/admin/libfuzzer-workshop/lessons/04/./vulnerable_functions.h:61:3 #6 0x59bf63 in LLVMFuzzerTestOneInput /home/admin/libfuzzer-workshop/lessons/04/second_fuzzer.cc:12:2 #7 0x466186 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:556 #8 0x46b7e9 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:470 #9 0x46b7e9 in fuzzer::Fuzzer::MutateAndTestOne() /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:699 #10 0x46e80f in fuzzer::Fuzzer::Loop(std::Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:830 #11 0x456b99 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:824 #12 0x41f522 in main /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerMain.cpp:19 #13 0x7fd1b4bf6bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6) #14 0x41f599 in _start (/home/admin/libfuzzer-workshop/lessons/04/second_fuzzer+0x41f599)
Address 0x7fff31422548 is located in stack of thread T0 at offset 1128 in frame #0 0x59b4af in VulnerableFunction2(unsigned char const*, unsigned long, bool) /home/admin/libfuzzer-workshop/lessons/04/./vulnerable_functions.h:42
This frame has 3 object(s): [32, 64) 'header' (line 46) [96, 97) 'ref.tmp' (line 46) [112, 1128) 'body' (line 48) <== Memory access at offset 1128 overflows this variableHINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported)SUMMARY: AddressSanitizer: stack-buffer-overflow /home/admin/libfuzzer-workshop/src/llvm/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:30 in __asan_memmoveShadow bytes around the buggy address: 0x10006627c450: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10006627c460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10006627c470: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10006627c480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10006627c490: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00=>0x10006627c4a0: 00 00 00 00 00 00 00 00 00[f3]f3 f3 f3 f3 f3 f3 0x10006627c4b0: f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 00 00 00 00 0x10006627c4c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10006627c4d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10006627c4e0: 00 00 00 00 f1 f1 f1 f1 02 f3 f3 f3 00 00 00 00 0x10006627c4f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb Shadow gap: cc==10468==ABORTINGMS: 4 ChangeBinInt-CrossOver-ChangeByte-EraseBytes-; base unit: e63bf1b07c6950248bb14fe74c3a84f06c711b15artifact_prefix='./'; Test unit written to ./crash-a42cbe2ff7331f281ef213e54919e8cd932883bd相信大家已經不再陌生了,定位錯誤位於#5 0x59b8b5 in VulnerableFunction2(unsigned char const*, unsigned long, bool) /home/admin/libfuzzer-workshop/lessons/04/./vulnerable_functions.h:61:3
即std::copy(data, data + size, body.data());,該句造成了stack_overflow,原因在於vector類型的body的大小為1024 – sizeof(kMagicHeader),而在copy時的data的size的限制條件是size > kMaxPacketLen(1024),從而造成了緩衝區溢出。如果我們寫死flag為false的話就可能會跑很久,也跑不出來crash。因此,我們在套用模板時要結合函數的邏輯,使得fuzz的接口設計的更加合理,從而增加fuzz的效率。
繼續繼續VulnerableFunction3:
constexpr std::size_t kZn2016VerifyHashFlag = 0x0001000;
bool VulnerableFunction3(const uint8_t* data, size_t size, std::size_t flags) { bool verify_hash = flags & kZn2016VerifyHashFlag; return VulnerableFunction2(data, size, verify_hash);}可以看到,與2不同的地方就是對flag進行了& kZn2016VerifyHashFlag的計算,這種其實我們可以計算出使得flags & kZn2016VerifyHashFlag為true/false的輸入,並模仿second_fuzzer那樣寫個循環,這樣就和2沒差了。
但這裡workshop提供了一個不同的方法:In this case, we can get some randomization of flags values using data provided by libFuzzer:#include <stdint.h>#include <stddef.h>
#include "vulnerable_functions.h"
#include <functional>#include <string>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { std::string data_string(reinterpret_cast<const char*>(data), size); auto data_hash = std::hash<std::string>()(data_string);
std::size_t flags = static_cast<size_t>(data_hash); VulnerableFunction3(data, size, flags); return 0;}採用了libfuzzer提供的隨機化方法使得我們的輸入flag為隨機數,從而flags & kZn2016VerifyHashFlag的結果也隨機化。在次數足夠多的情況下false和true的情況將趨於同為50%。
接下來看lesson05:
這次的目標和lesson04有所不同,lesson04中我們針對.h函數庫中的函數進行fuzz,而libfuzzer的威力遠不止於此,它還可以對大型開源庫進行模糊測試。在源碼編譯開源庫時選擇合適的選項以及將libfuzzer與開源庫連結在一起以進行fuzz,這些細節將在該lesson05體現。
首先解包tar xzf openssl1.0.1f.tgz。接著執行./config生成makefile。之後:
make cleanmake CC="clang -O2 -fno-omit-frame-pointer -g -fsanitize=address" -j$(nproc)第二條指令其實並不太規範,將編輯器以外的其他參數也一股腦寫到CC變量裡了。clang後的參數本應該是由CFLAGS或CXXFLAGS指定的。解釋一下選項:
-02 指定優先級別
-g 帶有調試符號來編譯
-fno-omit-frame-pointer 對於不需要棧指針的函數就不在寄存器中保存指針,因此可以忽略存儲和檢索地址的代碼,同時對許多函數提供一個額外的寄存器。所有」-O」級別都打開它,但僅在調試器可以不依靠棧指針運行時才有效。在AMD64平臺上此選項默認打開,但是在x86平臺上則默認關閉。建議顯式的設置它。
-fsanitize 指定sanitize這些參數的指定是至關重要的,它會影響到之後開源庫與libfuzzer的連結以及fuzz的效率。如果不設置這些編譯選項直接make的話fuzz的效率如下:
➜ 05 git:(master) ✗ ./openssl_fuzzer2INFO: Seed: 1865248494INFO: Loaded 1 modules (10 inline 8-bit counters): 10 [0x96c950, 0x96c95a), INFO: Loaded 1 PC tables (10 PCs): 10 [0x6d56d0,0x6d5770), INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytesINFO: A corpus is not provided, starting from an empty corpus#2 INITED cov: 2 ft: 3 corp: 1/1b exec/s: 0 rss: 29Mb#131072 pulse cov: 2 ft: 3 corp: 1/1b lim: 1300 exec/s: 43690 rss: 388Mb#262144 pulse cov: 2 ft: 3 corp: 1/1b lim: 2611 exec/s: 43690 rss: 397Mb#524288 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 37449 rss: 411Mb#1048576 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 34952 rss: 411Mb#2097152 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 32768 rss: 411Mb這路徑覆蓋率太感人,其中的重要原因就在於編譯開源庫時的選項設置。
設置編譯選項後的fuzz效果:INFO: Seed: 2565779026INFO: Loaded 1 modules (35878 inline 8-bit counters): 35878 [0xcd8590, 0xce11b6), INFO: Loaded 1 PC tables (35878 PCs): 35878 [0x954da8,0x9e1008), INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytesINFO: A corpus is not provided, starting from an empty corpus#2 INITED cov: 410 ft: 411 corp: 1/1b exec/s: 0 rss: 36Mb#112 NEW cov: 413 ft: 416 corp: 2/2b lim: 4 exec/s: 0 rss: 42Mb L: 1/1 MS: 5 ChangeBit-ChangeBinInt-ChangeByte-InsertByte-EraseBytes- NEW_FUNC[1/1]: 0x65f0a0 in ERR_put_error /home/admin/libfuzzer-workshop/lessons/05/openssl1.0.1f/crypto/err/err.c:708#319 NEW cov: 420 ft: 448 corp: 3/8b lim: 6 exec/s: 0 rss: 51Mb L: 6/6 MS: 2 ShuffleBytes-CrossOver-#327 REDUCE cov: 420 ft: 448 corp: 3/7b lim: 6 exec/s: 0 rss: 51Mb L: 5/5 MS: 3 ChangeByte-EraseBytes-CrossOver- NEW_FUNC[1/2]: 0x562a40 in tls1_enc /home/admin/libfuzzer-workshop/lessons/05/openssl1.0.1f/ssl/t1_enc.c:685 NEW_FUNC[2/2]: 0x67e1d0 in EVP_MD_CTX_md /home/admin/libfuzzer-workshop/lessons/05/openssl1.0.1f/crypto/evp/evp_lib.c:282#454 REDUCE cov: 431 ft: 467 corp: 4/12b lim: 6 exec/s: 0 rss: 57Mb L: 5/5 MS: 2 ShuffleBytes-CMP- DE: "\xfd\x03\x00\x00"- NEW_FUNC[1/6]: 0x5663d0 in tls1_alert_code /home/admin/libfuzzer-workshop/lessons/05/openssl1.0.1f/ssl/t1_enc.c:1214 NEW_FUNC[2/6]: 0x5c6560 in do_ssl3_write /home/admin/libfuzzer-workshop/lessons/05/openssl1.0.1f/ssl/s3_pkt.c:634#470 NEW cov: 465 ft: 514 corp: 5/17b lim: 6 exec/s: 0 rss: 58Mb L: 5/5 MS: 1 ChangeByte-#472 REDUCE cov: 465 ft: 521 corp: 6/23b lim: 6 exec/s: 0 rss: 58Mb L: 6/6 MS: 2 CrossOver-CrossOver-#475 NEW cov: 467 ft: 523 corp: 7/28b lim: 6 exec/s: 0 rss: 58Mb L: 5/6 MS: 3 PersAutoDict-ChangeByte-ShuffleBytes- DE: "\xfd\x03\x00\x00"-#1025 NEW cov: 467 ft: 526 corp: 8/39b lim: 11 exec/s: 0 rss: 81Mb L: 11/11 MS: 5 CopyPart-ShuffleBytes-CrossOver-ShuffleBytes-ChangeBinInt-#1097 NEW cov: 474 ft: 549 corp: 9/49b lim: 11 exec/s: 0 rss: 84Mb L: 10/11 MS: 2 ChangeBit-CopyPart-#1293 REDUCE cov: 474 ft: 549 corp: 9/48b lim: 11 exec/s: 0 rss:92Mb L: 10/10 MS: 1 EraseBytes-選擇合適的編譯器和編譯選項,完成對該庫的源碼編譯,生成.a文件。接下來就要研究編寫fuzzer接口函數了。
workshop提供了openssl_fuzzer.cc:#include <openssl/ssl.h>#include <openssl/err.h>#include <assert.h>#include <stdint.h>#include <stddef.h>
#ifndef CERT_PATH# define CERT_PATH#endif
SSL_CTX *Init() { SSL_library_init(); SSL_load_error_strings(); ERR_load_BIO_strings(); OpenSSL_add_all_algorithms(); SSL_CTX *sctx; assert (sctx = SSL_CTX_new(TLSv1_method())); assert(SSL_CTX_use_certificate_file(sctx, CERT_PATH "server.pem", SSL_FILETYPE_PEM)); assert(SSL_CTX_use_PrivateKey_file(sctx, CERT_PATH "server.key", SSL_FILETYPE_PEM)); return sctx;}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { static SSL_CTX *sctx = Init(); SSL *server = SSL_new(sctx); BIO *sinbio = BIO_new(BIO_s_mem()); BIO *soutbio = BIO_new(BIO_s_mem()); SSL_set_bio(server, sinbio, soutbio); SSL_set_accept_state(server); BIO_write(sinbio, Data, Size); SSL_do_handshake(server); SSL_free(server); return 0;}這裡就涉及到openssl庫提供的相關方法了,本篇主要講解fuzz相關,就不細講openssl了。總之就是要先搞清楚openssl的用法,再通過include openssl提供的函數來對openssl進行fuzz。編譯如下:
clang++ -g openssl_fuzzer.cc -O2 -fno-omit-frame-pointer -fsanitize=address,fuzzer \ -I openssl1.0.1f/include openssl1.0.1f/libssl.a openssl1.0.1f/libcrypto.a \ ../../libFuzzer/Fuzz/libFuzzer.a -o openssl_fuzzer-I指定inlcude的搜索路徑,同時連結靜態庫libcrypto.a和libFuzzer.a以使用庫中的函數。
運行跑出crash:
===================================================================2133==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x629000009748 at pc 0x00000051eaba bp 0x7ffd255dae50 sp 0x7ffd255da618READ of size 21050 at 0x629000009748 thread T0
0x629000009748 is located 0 bytes to the right of 17736-byte region [0x629000005200,0x629000009748)allocated by thread T0 here:
SUMMARY: AddressSanitizer: heap-buffer-overflow /local/mnt/workspace/bcain_clang_bcain-ubuntu_23113/llvm/utils/release/final/llvm.src/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cc:22:3 in __asan_memcpyShadow bytes around the buggy address: 0x0c527fff9290: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c527fff92a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c527fff92b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c527fff92c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c527fff92d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00=>0x0c527fff92e0: 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa 0x0c527fff92f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c527fff9300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c527fff9310: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c527fff9320: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c527fff9330: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa faShadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb Shadow gap: cc==2133==ABORTINGMS: 2 ChangeBinInt-InsertByte-; base unit: 7bfa057a7ae6a7bc6ea487749407e4e7d4e9bf840x15,0x3,0xd4,0x0,0x7,0x1,0x3a,0x1,0x3a,0x1,0xd3,0xb3,0x18,0x3,0xd4,0x0,0x7,0x1,0x52,0x3a,0x1,0x3a,0x9,0xd3,0xb3,0x18,0x2c,0x0,0x7,0x2,0x7,0x1,0x3a,0x1,0x0,\x15\x03\xd4\x00\x07\x01:\x01:\x01\xd3\xb3\x18\x03\xd4\x00\x07\x01R:\x01:\x09\xd3\xb3\x18,\x00\x07\x02\x07\x01:\x01\x00artifact_prefix='./'; Test unit written to ./crash-4bfb53055de3fed318590b20ddd4cda0d95e8ed8Base64: FQPUAAcBOgE6AdOzGAPUAAcBUjoBOgnTsxgsAAcCBwE6AQA=通過SUMMARY容易找到造成heap_overflow的漏洞點在於:/local/mnt/workspace/bcain_clang_bcain-ubuntu_23113/llvm/utils/release/final/llvm.src/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cc:22:3 in __asan_memcpy這種目錄一看就是很底層的目錄,不易定位。再往找一層:#1 0x55e323 in tls1_process_heartbeat /home/admin/libfuzzer-workshop/lessons/05/openssl1.0.1f/ssl/t1_lib.c:2586:3這就很容易定位了,接下來就是漏洞溯源了。
定位到了代碼:tls1_process_heartbeat(SSL *s) { unsigned char *p = &s->s3->rrec.data[0], *pl; unsigned short hbtype; unsigned int payload; unsigned int padding = 16;
hbtype = *p++; n2s(p, payload); pl = p;
if (s->msg_callback) s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT, &s->s3->rrec.data[0], s->s3->rrec.length, s, s->msg_callback_arg);
if (hbtype == TLS1_HB_REQUEST) { unsigned char *buffer, *bp; int r;
buffer = OPENSSL_malloc(1 + 2 + payload + padding); bp = buffer;
*bp++ = TLS1_HB_RESPONSE; s2n(payload, bp); memcpy(bp, pl, payload); bp += payload; RAND_pseudo_bytes(bp, padding);
r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);
if (r >= 0 && s->msg_callback) s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding, s, s->msg_callback_arg);
OPENSSL_free(buffer);
if (r < 0) return r; } else if (hbtype == TLS1_HB_RESPONSE) { unsigned int seq;
n2s(pl, seq);
if (payload == 18 && seq == s->tlsext_hb_seq) { s->tlsext_hb_seq++; s->tlsext_hb_pending = 0; } }
return 0; }漏洞點再memcpy(bp, pl, payload);根據crash原因為over_flow說明payload的長度有問題。往上分析發現payload是通過n2s函數從p指向的結構體(用戶數據包)內容得到的。該結構體為:
typedef struct ssl3_record_st { int type; / type of record / unsigned int length; / How many bytes available / unsigned int off; / read/write offset into 'buf' / unsigned char data; / pointer to the record data / unsigned char input; / where the decode bytes are / unsigned char comp; / only used with decompression - malloc()ed / unsigned long epoch; / epoch number, needed by DTLS1 / unsigned char seq_num[8]; / sequence number, needed by DTLS1 / } SSL3_RECORD;而程序並沒有對用戶可控的length做檢查,從而導致memcpy溢出(有可能將server端的數據寫入到返回數據包中返回給用戶)。
初學libfuzzer,如有紕漏錯誤之後還煩請師傅們指正。