fuzz系列之libfuzzer
前言
本文以 libfuzzer-workshop 為基礎(chǔ) 介紹 libFuzzer 的使用。
libFuzzer簡(jiǎn)介
libFuzzer 是一個(gè)in-process,coverage-guided,evolutionary 的 fuzz 引擎,是 LLVM 項(xiàng)目的一部分。
libFuzzer 和 要被測(cè)試的庫(kù) 鏈接在一起,通過(guò)一個(gè)模糊測(cè)試入口點(diǎn)(目標(biāo)函數(shù)),把測(cè)試用例喂給要被測(cè)試的庫(kù)。
fuzzer會(huì)跟蹤哪些代碼區(qū)域已經(jīng)測(cè)試過(guò),然后在輸入數(shù)據(jù)的語(yǔ)料庫(kù)上進(jìn)行變異,來(lái)使代碼覆蓋率最大化。代碼覆蓋率的信息由 LLVM 的 SanitizerCoverage 插樁提供。
一些概念
fuzz 的種類
- Generation Based :通過(guò)對(duì)目標(biāo)協(xié)議或文件格式建模的方法,從零開始產(chǎn)生測(cè)試用例,沒(méi)有先前的狀態(tài)
- Mutation Based :基于一些規(guī)則,從已有的數(shù)據(jù)樣本或存在的狀態(tài)變異而來(lái)
- Evolutionary :包含了上述兩種,同時(shí)會(huì)根據(jù)代碼覆蓋率的回饋進(jìn)行變異。
target (被 fuzz 的目標(biāo))
基本上所有的程序的主要功能都是對(duì)一些 字節(jié)序列 進(jìn)行操作,libfuzzer 就是基于這一個(gè)事實(shí)(libfuzzer 生成 隨機(jī)的 字節(jié)序列 ,扔給 待 fuzz 的程序,然后檢測(cè)是否有異常出現(xiàn)) 所以在 libfuzzer 看來(lái),fuzz 的目標(biāo) 其實(shí)就是一個(gè) 以 字節(jié)序列 為輸入的 函數(shù)。
fuzzer
一個(gè) 生成 測(cè)試用例, 交給目標(biāo)程序測(cè)試,然后檢測(cè)程序是否出現(xiàn)異常 的程序
corpus(語(yǔ)料庫(kù))
給目標(biāo)程序的各種各樣的輸入
以圖片處理程序?yàn)槔?#xff1a;
語(yǔ)料庫(kù)就是各種各樣的圖片文件,這些圖片文件可以是正常圖片也可以不是。
傳統(tǒng)Fuzz
介紹
傳統(tǒng)的 fuzz 大多通過(guò)對(duì)已有的樣本 按照預(yù)先設(shè)置好的規(guī)則 進(jìn)行變異產(chǎn)生測(cè)試用例,然后喂給 目標(biāo)程序同時(shí)監(jiān)控目標(biāo)程序的運(yùn)行狀態(tài)。
這類 fuzz 有很多,比如: peach , FileFuzz 等
實(shí)戰(zhàn)
生成測(cè)試用例
本節(jié)使用 radamsa 作為 變異樣本生成引擎,對(duì) pdfium 進(jìn)行 fuzz 。
相關(guān)文件位于
https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/02radamsa 是一個(gè) 測(cè)試用例生成引擎,它是通過(guò)對(duì)已有的樣本進(jìn)行變異來(lái)生成新的測(cè)試用例。
首先看看 測(cè)試樣本的生成
generate_testcases.py
#!/usr/bin/env python2 import os import randomWORK_DIR = 'work'# Create work `directory` and `corpus` subdirectory. if not os.path.exists(WORK_DIR):os.mkdir(WORK_DIR)corpus_dir = os.path.join(WORK_DIR, 'corpus') if not os.path.exists(corpus_dir):os.mkdir(corpus_dir)seed_corpus_filenames = os.listdir('seed_corpus')for i in xrange(1000):random_seed_filename = random.choice(seed_corpus_filenames)random_seed_filename = os.path.join('seed_corpus', random_seed_filename)output_filename = os.path.join(WORK_DIR, 'corpus', 'testcase-%06d' % i)cmd = 'bin/radamsa "%s" > "%s"' % (random_seed_filename, output_filename)os.popen(cmd)就是調(diào)用 radamsa ,然后隨機(jī)選取 seed_corpus 目錄中的文件名作為參數(shù),傳遞給 radamsa 進(jìn)行變異,然后把生成的測(cè)試用例,放到 work/corpus 。
開始fuzz
這樣測(cè)試樣本就生成好了,下面看看 用于 fuzz 的腳本
run_fuzzing.py
#!/usr/bin/env python2 import os import subprocessWORK_DIR = 'work'def checkOutput(s):if 'Segmentation fault' in s or 'error' in s.lower():return Falseelse:return Truecorpus_dir = os.path.join(WORK_DIR, 'corpus') corpus_filenames = os.listdir(corpus_dir) for f in corpus_filenames:testcase_path = os.path.join(corpus_dir, f)cmd = ['bin/asan/pdfium_test', testcase_path]process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.STDOUT)output = process.communicate()[0]if not checkOutput(output):print testcase_pathprint outputprint '-' * 80就是不斷調(diào)用 程序 處理剛剛生成的測(cè)試用例,根據(jù)執(zhí)行的輸出結(jié)果中 是否有 Segmentation fault 和 error來(lái)判斷是否觸發(fā)了漏洞。
ps: 由于用于變異樣本的選取 和 樣本的變異方式是隨機(jī)的,可能需要重復(fù)多次 樣本生成 && fuzz 才能找到 crash
寫個(gè) bash 腳本,不斷重復(fù)即可
#!/bin/bash while [ "0" -lt "1" ] dorm -rf ./work/./generate_testcases.py./run_fuzzing.py doneHelloworld-For-libFuzzer
安裝
本節(jié)相關(guān)資源文件位于:
https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/04首先先把 libFuzzer 安裝一下
首先
git clone https://github.com/Dor1s/libfuzzer-workshop.git sudo ln -s /usr/include/asm-generic /usr/include/asm apt-get install gcc-multilib然后進(jìn)入 libfuzzer-workshop/ , 執(zhí)行 checkout_build_install_llvm.sh 安裝好 llvm.
然后進(jìn)入 libfuzzer-workshop/libFuzzer/Fuzzer/ ,執(zhí)行 build.sh 編譯好 libFuzzer。
如果編譯成功,會(huì)生成 libfuzzer-workshop/libFuzzer/Fuzzer/libFuzzer.a
實(shí)戰(zhàn)
這一節(jié)中主要使用 libFuzzer 對(duì) vulnerable_functions.h 中實(shí)現(xiàn)的幾個(gè)有漏洞的 函數(shù) 進(jìn)行 fuzz
VulnerableFunction1
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; }這個(gè)函數(shù)有兩個(gè)參數(shù),第一個(gè)參數(shù) data 是 uint8_t* 類型的,說(shuō)明 data 應(yīng)該是指向了一個(gè)緩沖區(qū), size 應(yīng)該是緩沖區(qū)的大小,如果 size >=3 , 會(huì)訪問(wèn) data[3] , 越界訪問(wèn)了。
進(jìn)行 fuzz 的第一步是 實(shí)現(xiàn)一個(gè) 入口點(diǎn),用來(lái)接收 libFuzzer 生成的 測(cè)試用例(比特序列)。
示例
// fuzz_target.cc extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {DoSomethingInterestingWithMyAPI(Data, Size);return 0; // Non-zero return values are reserved for future use. }對(duì)于 LLVMFuzzerTestOneInput 有一些要注意的 tips
data 是 libFuzzer 生成的 測(cè)試數(shù)據(jù), size 是數(shù)據(jù)的長(zhǎng)度
- fuzz 引擎會(huì)在一個(gè)進(jìn)程中進(jìn)行多次 fuzz, 所以其效率非常高
- 要能處理各種各樣的輸入 (空數(shù)據(jù), 大量的 或者 畸形的數(shù)據(jù)...)
- 內(nèi)部不會(huì)調(diào)用 exit()
如果使用多線程的話,在函數(shù)末尾要把 線程 join
對(duì)于 VulnerableFunction1 , 直接把 libFuzzer 傳過(guò)來(lái)的數(shù)據(jù),傳給 VulnerableFunction1 即可
first_fuzzer.cc
// Copyright 2016 Google Inc. All Rights Reserved. // Licensed under the Apache License, Version 2.0 (the "License");#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; }然后用 clang++ 編譯
clang++ -g -std=c++11 -fsanitize=address -fsanitize-coverage=trace-pc-guard \first_fuzzer.cc ../../libFuzzer/Fuzzer/libFuzzer.a \-o first_fuzzer- -fsanitize=address: 表示使用 AddressSanitizer
- -fsanitize-coverage=trace-pc-guard: 為 libfuzzer 提供代碼覆蓋率信息
然后運(yùn)行
haclh@ubuntu:~/vmdk_kernel/libfuzzer-workshop-master/lessons/04$ ./first_fuzzer INFO: Seed: 1608565063 INFO: Loaded 1 modules (37 guards): [0x788ec0, 0x788f54), INFO: -max_len is not provided, using 64 INFO: A corpus is not provided, starting from an empty corpus #0 READ units: 1 #1 INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 11Mb #3 NEW cov: 4 ft: 4 corp: 2/4b exec/s: 0 rss: 12Mb L: 3 MS: 2 InsertByte-InsertByte- #3348 NEW cov: 5 ft: 5 corp: 3/65b exec/s: 0 rss: 12Mb L: 61 MS: 2 ChangeByte-InsertRepeatedBytes- #468765 NEW cov: 6 ft: 6 corp: 4/78b exec/s: 0 rss: 49Mb L: 13 MS: 4 CrossOver-ChangeBit-EraseBytes-ChangeByte- #564131 NEW cov: 7 ft: 7 corp: 5/97b exec/s: 0 rss: 56Mb L: 19 MS: 5 InsertRepeatedBytes-InsertByte-ChangeByte-InsertByte-InsertByte- ================================================================= ==32049==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200072bb93 at pc 0x000000528540 bp 0x7ffdb3439100 sp 0x7ffdb34390f8 READ of size 1 at 0x60200072bb93 thread T0..................................................................................................................................................................0x60200072bb93 is located 0 bytes to the right of 3-byte region [0x60200072bb90,0x60200072bb93) allocated by thread T0 here:..................................................................................................................................................................SUMMARY: AddressSanitizer: heap-buffer-overflow /home/haclh/vmdk_kernel/libfuzzer-workshop-master/lessons/04/./vulnerable_functions.h:22:14 in VulnerableFunction1(unsigned char const*, unsigned long) Shadow bytes around the buggy address:0x0c04800dd720: fa fa fd fd fa fa fd fa fa fa fd fa fa fa fd fa0x0c04800dd730: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd0x0c04800dd740: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa0x0c04800dd750: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa0x0c04800dd760: fa fa fd fa fa fa fd fa fa fa fd fd fa fa fd fd =>0x0c04800dd770: fa fa[03]fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c04800dd780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c04800dd790: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c04800dd7a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c04800dd7b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c04800dd7c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa ...................................................... ...................................................... ==32049==ABORTING MS: 1 CrossOver-; base unit: 38a223b0988bd9576fb17f5947af80b80203f0ef 0x46,0x55,0x5a, FUZ artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60 Base64: RlVa正常的話應(yīng)該可以看到類似上面的輸出,這里對(duì)其中的一些信息解析一下
- Seed: 1608565063 說(shuō)明這次的種子數(shù)據(jù)
- -max_len is not provided, using 64 , -max_len 用于設(shè)置最大的數(shù)據(jù)長(zhǎng)度,默認(rèn)為 64
- 接下來(lái) # 開頭的行是 fuzz 過(guò)程中找到的路徑信息
- 倒數(shù)第二行是觸發(fā)漏洞的測(cè)試用例
使用
ASAN_OPTIONS=symbolize=1 ./first_fuzzer ./crash-0eb8e4ed029b774d80f2b66408203801 # ASAN_OPTIONS=symbolize=1 用于顯示 棧的符號(hào)信息重現(xiàn) crash.
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; }代碼量比第一個(gè)要復(fù)雜了一些,不管那么多先 fuzz , 首先看看這個(gè)函數(shù)的參數(shù), 比 VulnerableFunction1 多了一個(gè) bool 類型的參數(shù),所以 fuzz 腳本比 VulnerableFunction1 中的加一點(diǎn)修改即可。
#include "vulnerable_functions.h"extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {bool verify_hash_flags[] = { false, true };for (auto flag : verify_hash_flags)VulnerableFunction2(data, size, flag);return 0; }fuzz 測(cè)試的目標(biāo)就是盡可能的測(cè)試所有代碼路徑,又 VulnerableFunction2 的第 3 個(gè)參數(shù)是 bool 型的(有兩種可能), 那我們就對(duì)每一個(gè)測(cè)試用例都用 {false, true} 測(cè)試一遍以獲取更多的測(cè)試路徑。
首先編譯一下
clang++ -g -std=c++11 -fsanitize=address -fsanitize-coverage=trace-pc-guard \second_fuzzer.cc ../../libFuzzer/Fuzzer/libFuzzer.a \-o second_fuzzer直接執(zhí)行會(huì)得到類似下面的結(jié)果
haclh@ubuntu:~/vmdk_kernel/libfuzzer-workshop-master/lessons/04$ ./second_fuzzer INFO: Seed: 4141499174 INFO: Loaded 1 modules (39 guards): [0x788f00, 0x788f9c), INFO: -max_len is not provided, using 64 INFO: A corpus is not provided, starting from an empty corpus #0 READ units: 1 #1 INITED cov: 5 ft: 5 corp: 1/1b exec/s: 0 rss: 11Mb #26 NEW cov: 6 ft: 6 corp: 2/29b exec/s: 0 rss: 12Mb L: 28 MS: 5 ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-InsertRepeatedBytes- #1840 NEW cov: 26 ft: 26 corp: 3/70b exec/s: 0 rss: 12Mb L: 41 MS: 4 InsertRepeatedBytes-ShuffleBytes-CMP-CMP- DE: "\x00\x00\x00\x00\x00\x00\x00\x00"-"ZN_2016"- #1048576 pulse cov: 26 ft: 26 corp: 3/70b exec/s: 174762 rss: 103Mb #2097152 pulse cov: 26 ft: 26 corp: 3/70b exec/s: 149796 rss: 194Mb ^C==32554== libFuzzer: run interrupted; exiting可以看到 libfuzzer 到后面基本找不到新的路徑了,一直 pulse 。回到 VulnerableFunction2 我們發(fā)現(xiàn)函數(shù)可以處理的最大數(shù)據(jù)長(zhǎng)度是 1024 , 所以給 fuzzer 加個(gè)參數(shù)設(shè)置一下最大數(shù)據(jù)長(zhǎng)度。
./second_fuzzer -max_len=1024可以看到檢測(cè)到了 棧溢出,觸發(fā)漏洞的指令位于
vulnerable_functions.h:61:3跟到該文件內(nèi)查看,發(fā)現(xiàn)是
std::copy(data, data + size, body.data());觸發(fā)了漏洞,漏洞產(chǎn)生的原因在于, body 的 buf 的大小為 kMaxBodyLength , 而這里可以往 body 的 buf 里面寫入最多 kMaxPacketLen 字節(jié)。
constexpr std::size_t kMaxPacketLen = 1024; constexpr std::size_t kMaxBodyLength = 1024 - sizeof(kMagicHeader);溢出。
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); }就是動(dòng)態(tài)生成了 verify_hash 的值,然后傳給 了 VulnerableFunction2。
我們也照著來(lái)就行
#include "vulnerable_functions.h"#include <functional> #include <string>extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {VulnerableFunction3(data, size, 0x0001000); // 觸發(fā) flag = trueVulnerableFunction3(data, size, 0x1000000); // 觸發(fā) flag = falsereturn 0; }編譯
clang++ -g -std=c++11 -fsanitize=address -fsanitize-coverage=trace-pc-guard \third_fuzzer.cc ../../libFuzzer/Fuzzer/libFuzzer.a \-o third_fuzzer運(yùn)行
總結(jié)
簡(jiǎn)單理解 libfuzzer 。如果我們要 fuzz 一個(gè)程序,找到一個(gè)入口函數(shù),然后利用
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {.............. }接口,我們可以拿到 libfuzzer 生成的 測(cè)試數(shù)據(jù)以及測(cè)試數(shù)據(jù)的長(zhǎng)度,我們的任務(wù)就是把這些生成的測(cè)試數(shù)據(jù) 傳入到目標(biāo)程序中 讓程序來(lái)處理 測(cè)試數(shù)據(jù), 同時(shí)要盡可能的觸發(fā)更多的代碼邏輯。
實(shí)戰(zhàn)兩個(gè)簡(jiǎn)單的CVE
CVE-2014-0160 (openssl 心臟滴血漏洞 )
libfuzzer 是用于 fuzz 某個(gè)函數(shù)的,所以我們先編譯好目標(biāo)代碼庫(kù), 然后在根據(jù)要 fuzz 的功能編寫 fuzzer 函數(shù)。
本節(jié)資源文件位于
https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/05首先用 clang 編譯 openssl.
./config make clean make CC="clang -O2 -fno-omit-frame-pointer -g -fsanitize=address -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div" -j$(nproc)主要是為了加上 AddressSanitizer ,用于檢測(cè)程序中出現(xiàn)的異常(uaf, 堆溢出,棧溢出等漏洞)
常用內(nèi)存錯(cuò)誤檢測(cè)工具
AddressSanitizer: 檢測(cè) uaf, 緩沖區(qū)溢出,stack-use-after-return, container-overflow
MemorySanitizer: 檢測(cè)未初始化內(nèi)存的訪問(wèn)
UndefinedBehaviorSanitizer: 檢測(cè)一些其他的漏洞,整數(shù)溢出,類型混淆等
然后寫 fuzzer 的邏輯
#include <openssl/ssl.h> #include <openssl/err.h> #include <assert.h> #include <stdint.h> #include <stddef.h>#ifndef CERT_PATH # define CERT_PATH #endifSSL_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()));/* These two file were created with this command:openssl req -x509 -newkey rsa:512 -keyout server.key \-out server.pem -days 9999 -nodes -subj /CN=a/*/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; }感覺(jué)用 libfuzzer 的話,我們需要做的工作就是根據(jù)目標(biāo)程序的邏輯,把 libfuzzer 生成的 測(cè)試數(shù)據(jù) 傳遞 給 目標(biāo)程序去處理, 然后在編譯時(shí)采取合適的 Sanitizer 用于檢測(cè)運(yùn)行時(shí)出現(xiàn)的內(nèi)存錯(cuò)誤。
比如上面就是模擬了 SSL 握手的邏輯,然后把 libfuzzer 生成的 測(cè)試數(shù)據(jù)作為握手包傳遞給 openssl 。
編譯之
clang++ -g openssl_fuzzer.cc -O2 -fno-omit-frame-pointer -fsanitize=address -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div -Iopenssl1.0.1f/include openssl1.0.1f/libssl.a openssl1.0.1f/libcrypto.a ../../libFuzzer/Fuzzer/libFuzzer.a -o openssl_fuzzer運(yùn)行然后就會(huì)出現(xiàn) crash 信息了
haclh@ubuntu:~/vmdk_kernel/libfuzzer-workshop-master/lessons/05$ ./openssl_fuzzer ./corpus/ INFO: Seed: 1472290074 INFO: Loaded 1 modules (33464 guards): [0xc459b0, 0xc66490), Loading corpus dir: ./corpus/ INFO: -max_len is not provided, using 64 INFO: A corpus is not provided, starting from an empty corpus #0 READ units: 1 #1 INITED cov: 1513 ft: 396 corp: 1/1b exec/s: 0 rss: 22Mb ................................ ................................ ................................ #68027 NEW cov: 1592 ft: 703 corp: 28/1208b exec/s: 13605 rss: 363Mb L: 35 MS: 1 EraseBytes- ================================================================= ==35462==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x629000009748 at pc 0x0000004e8f7d bp 0x7ffd58180520 sp 0x7ffd5817fcd0 READ of size 65535 at 0x629000009748 thread T0#0 0x4e8f7c in __asan_memcpy /home/haclh/vmdk_kernel/libfuzzer-workshop-master/src/llvm/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cc:23#1 0x5353f6 in tls1_process_heartbeat /home/haclh/vmdk_kernel/libfuzzer-workshop-master/lessons/05/openssl1.0.1f/ssl/t1_lib.c:2586:3可以看到在 tls1_process_heartbeat 中 觸發(fā)了堆溢出 (heap-buffer-overflow )
CVE-2016-5180 (c-ares 堆溢出)
本節(jié)相關(guān)文件位于
https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/06和前面一樣,首先編譯一下這個(gè)庫(kù)。
tar xzvf c-ares.tgz cd c-ares./buildconf ./configure CC="clang -O2 -fno-omit-frame-pointer -g -fsanitize=address -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div" make CFLAGS=然后 fuzzer 的邏輯就非常簡(jiǎn)單了
#include <ares.h>extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {unsigned char *buf;int buflen;std::string s(reinterpret_cast<const char *>(data), size);ares_create_query(s.c_str(), ns_c_in, ns_t_a, 0x1234, 0, &buf, &buflen, 0);ares_free_string(buf);return 0; }把 libfuzzer 傳過(guò)來(lái)的數(shù)據(jù),轉(zhuǎn)成 char * , 然后扔給 ares_create_query 進(jìn)行處理。
運(yùn)行就有 crash 了。
總結(jié)
感覺(jué)libfuzzer 已經(jīng)把 一個(gè) fuzzer 的核心(樣本生成引擎和異常檢測(cè)系統(tǒng)) 給做好了, 我們需要做的是根據(jù)目標(biāo)程序的邏輯,把 libfuzzer 生成的數(shù)據(jù),交給目標(biāo)程序處理。
libFuzzer進(jìn)階
前面介紹了 libFuzzer 的一些簡(jiǎn)單的使用方法,下面以 fuzz libxml2 為例,介紹一些 libFuzzer 的高級(jí)用法。
相關(guān)文件位于
https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/08Start fuzz
首先把 libxml2 用 clang 編譯
tar xzf libxml2.tgz cd libxml2./autogen.shexport FUZZ_CXXFLAGS="-O2 -fno-omit-frame-pointer -g -fsanitize=address \-fsanitize-coverage=edge,indirect-calls,trace-cmp,trace-div,trace-gep,trace-pc-guard"CXX="clang++ $FUZZ_CXXFLAGS" CC="clang $FUZZ_CXXFLAGS" \CCLD="clang++ $FUZZ_CXXFLAGS" ./configure make -j$(nproc)然后寫個(gè) fuzzer, 這里選擇 測(cè)試 xmlReadMemory
#include "libxml/parser.h"void ignore (void* ctx, const char* msg, ...) {// Error handler to avoid spam of error messages from libxml parser. }extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {xmlSetGenericErrorFunc(NULL, &ignore);if (auto doc = xmlReadMemory(reinterpret_cast<const char*>(data),static_cast<int>(size), "noname.xml", NULL, 0)) {xmlFreeDoc(doc);}return 0; }然后編譯之。
export FUZZ_CXXFLAGS="-O2 -fno-omit-frame-pointer -g -fsanitize=address \-fsanitize-coverage=edge,indirect-calls,trace-cmp,trace-div,trace-gep,trace-pc-guard" clang++ -std=c++11 xml_read_memory_fuzzer.cc $FUZZ_CXXFLAGS -Ilibxml2/include \libxml2/.libs/libxml2.a /usr/lib/x86_64-linux-gnu/liblzma.a ../../libFuzzer/Fuzzer/libFuzzer.a -lz \-o xml_read_memory_fuzzerps: /usr/lib/x86_64-linux-gnu/liblzma.a 是 liblzma.a 的路徑,需要安裝 liblzma-dev:
apt-get install liblzma-dev編譯好 fuzzer 后,我們運(yùn)行它:
mkdir corpus1 ./xml_read_memory_fuzzer -max_total_time=300 -print_final_stats=1 corpus1其中的一些參數(shù)做個(gè)解釋
- -max_total_time : 設(shè)置最長(zhǎng)的運(yùn)行時(shí)間, 單位是 秒, 這里是 300s , 也就是 5 分鐘
- -print_final_stats:執(zhí)行完 fuzz 后 打印統(tǒng)計(jì)信息
- corpus1: fuzzer 程序可以有多個(gè)目錄作為參數(shù),此時(shí) fuzzer 會(huì)遞歸遍歷所有目錄,把目錄中的文件讀入最為樣本數(shù)據(jù)傳給測(cè)試函數(shù),同時(shí)會(huì)把那些可以產(chǎn)生新的的代碼路徑的樣本保存到第一個(gè)目錄里面。
運(yùn)行完后會(huì)得到類似下面的結(jié)果
###### Recommended dictionary. ###### "X\x00\x00\x00\x00\x00\x00\x00" # Uses: 1228 "prin" # Uses: 1353 ........................... ........................... ........................... "U</UTrri\x09</UTD" # Uses: 61 ###### End of recommended dictionary. ###### Done 1464491 runs in 301 second(s) stat::number_of_executed_units: 1464491 stat::average_exec_per_sec: 4865 stat::new_units_added: 1407 stat::slowest_unit_time_sec: 0 stat::peak_rss_mb: 407開始由 #### 夾著的是 libfuzzer 在 fuzz 過(guò)程中挑選出來(lái)的 dictionary, 同時(shí)還給出了使用的次數(shù),這些 dictionary 可以在以后 fuzz 同類型程序時(shí) 節(jié)省 fuzz 的時(shí)間。
然后以 stat: 開頭的是一些 fuzz 的統(tǒng)計(jì)信息, 主要看 stat::new_units_added 表示整個(gè) fuzz 過(guò)程中觸發(fā)了多少個(gè)代碼單元。
可以看到直接 fuzz , 5分鐘 觸發(fā)了 1407 個(gè)代碼單元
使用 Dictionary 提升性能
Dictionary 貌似是 afl 中提出的 ,具體可以看下面這篇文章
https://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html我們知道基本上所有的程序都是處理具有一定格式的數(shù)據(jù),比如 xml 文檔, png圖片等等。 這些數(shù)據(jù)中會(huì)有一些特殊字符序列 (或者說(shuō)關(guān)鍵字), 比如 在 xml 文檔中 就有 CDATA, <!ATTLIST 等,png圖片 就有 png 圖片頭。
如果我們事先就把這些 字符序列 列舉出來(lái), fuzz 直接使用這些關(guān)鍵字去 組合,就會(huì)就可以減少很多沒(méi)有意義的 嘗試,同時(shí)還有可能會(huì)走到更深的程序分支中去。
Dictionary 就是實(shí)現(xiàn)了這種思路。 libfuzzer 和 afl 使用的 dictionary 文件的語(yǔ)法是一樣的, 所以可以直接拿 afl 里面的 dictionary 文件來(lái)給 libfuzzer 使用。
下面這個(gè)網(wǎng)址里面就有一些 afl 的 dictionary 文件
https://github.com/rc0r/afl-fuzz/tree/master/dictionaries以 libfuzzer 官網(wǎng) 的示例介紹一下語(yǔ)法
# Lines starting with '#' and empty lines are ignored. # Adds "blah" (w/o quotes) to the dictionary. kw1="blah" # Use \\ for backslash and \" for quotes. kw2="\"ac\\dc\"" # Use \xAB for hex values kw3="\xF7\xF8" # the name of the keyword followed by '=' may be omitted: "foo\x0Abar"- # 開頭的行 和 空行會(huì)被忽略
- kw1= 這些就類似于注釋, 沒(méi)有意義
- 真正有用的是由 " 包裹的字串,這些 字串 就會(huì)作為一個(gè)個(gè)的關(guān)鍵字, libfuzzer 會(huì)用它們進(jìn)行組合來(lái)生成樣本。
libfuzzer 使用 -dict 指定 dict 文件,下面使用 xml.dict 為 dictionary 文件,進(jìn)行 fuzz。
./xml_read_memory_fuzzer -dict=./xml.dict -max_total_time=300 -print_final_stats=1 corpus2最終這種方式可以探測(cè)到 2200+ 的代碼單元, 效率提升還是很明顯的。
###### End of recommended dictionary. ###### Done 1379646 runs in 301 second(s) stat::number_of_executed_units: 1379646 stat::average_exec_per_sec: 4583 stat::new_units_added: 2215 stat::slowest_unit_time_sec: 0 stat::peak_rss_mb: 415精簡(jiǎn)樣本集
在上一節(jié)里面我們獲得了很多的樣本,其中有很多其實(shí)是重復(fù)的,可以使用 libfuzzer 把樣本集進(jìn)行精簡(jiǎn)。
mkdir corpus1_min ./xml_read_memory_fuzzer -merge=1 corpus1_min corpus1corpus1_min: 精簡(jiǎn)后的樣本集存放的位置
corpus1: 原始樣本集存放的位置
可以得到類似的結(jié)果
03:20 haclh@ubuntu:08 $ ls corpus1 | wc -l 1403 03:20 haclh@ubuntu:08 $ ./xml_read_memory_fuzzer -merge=1 corpus1_min corpus1 INFO: Seed: 3775041129 .................... .................... .................... #1024 pulse cov: 1569 exec/s: 0 rss: 108Mb MERGE-OUTER: succesfull in 1 attempt(s) MERGE-OUTER: the control file has 4150228 bytes MERGE-OUTER: consumed 2Mb (24Mb rss) to parse the control file MERGE-OUTER: 928 new files with 5307 new features added可以看到 1403 個(gè)樣本被精簡(jiǎn)成了 928 個(gè)樣本
生成代碼覆蓋率報(bào)告
使用 -dump_coverage 參數(shù)。
03:28 haclh@ubuntu:08 $ ./xml_read_memory_fuzzer corpus1_min -runs=0 -dump_coverage=1 INFO: Seed: 2354910000 .............. .............. ./xml_read_memory_fuzzer.69494.sancov: 1603 PCs written然后會(huì)生成一個(gè) *.sancov 文件
03:26 haclh@ubuntu:08 $ ls *.sancov xml_read_memory_fuzzer.69494.sancov然后把它轉(zhuǎn)換成 .symcov
sancov -symbolize xml_read_memory_fuzzer ./xml_read_memory_fuzzer.69494.sancov > xml_read_memory_fuzzer.symcov然后使用 coverage-report-server 解析這個(gè)文件。
03:29 haclh@ubuntu:08 $ python3 coverage-report-server.py --symcov xml_read_memory_fuzzer.symcov --srcpath libxml2 Loading coverage... Serving at 127.0.0.1:8001用瀏覽器訪問(wèn)之
通過(guò)這個(gè)功能,我們可以非常直觀的看到每個(gè)源文件的覆蓋率。
Fuzz xmlRegexpCompile
前面已經(jīng) fuzz 了 xmlReadMemory,這里 fuzz 另外一個(gè)函數(shù) xmlRegexpCompile
看看 fuzz 代碼
#include "libxml/parser.h" #include "libxml/tree.h" #include "libxml/xmlversion.h"void ignore (void * ctx, const char * msg, ...) {// Error handler to avoid spam of error messages from libxml parser. } // Entry point for LibFuzzer. extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {xmlSetGenericErrorFunc(NULL, &ignore);std::vector<uint8_t> buffer(size + 1, 0);std::copy(data, data + size, buffer.data());xmlRegexpPtr x = xmlRegexpCompile(buffer.data());if (x)xmlRegFreeRegexp(x);return 0; }就是把 數(shù)據(jù)用 std::vector 做了個(gè)中轉(zhuǎn),喂給 xmlRegexpCompile 函數(shù)。
編譯之
export FUZZ_CXXFLAGS="-O2 -fno-omit-frame-pointer -g -fsanitize=address \-fsanitize-coverage=edge,indirect-calls,trace-cmp,trace-div,trace-gep,trace-pc-guard" clang++ -std=c++11 xml_compile_regexp_fuzzer.cc $FUZZ_CXXFLAGS -Ilibxml2/include \libxml2/.libs/libxml2.a /usr/lib/x86_64-linux-gnu/liblzma.a ../../libFuzzer/Fuzzer/libFuzzer.a -lz \-o xml_compile_regexp_fuzzer運(yùn)行
mkdir corpus3 ./xml_compile_regexp_fuzzer -dict=./xml.dict corpus3然后過(guò)一會(huì)就會(huì)出現(xiàn) crash 了。
參考
https://github.com/Dor1s/libfuzzer-workshop/
轉(zhuǎn)載于:https://www.cnblogs.com/hac425/p/9416907.html
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的fuzz系列之libfuzzer的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 原价买了二手机,我是如何做到的?
- 下一篇: struct 模块 把一个类型,如数字,