fuzz實戰之libfuzzer

2021-03-02 SecPulse安全脈搏
安全脈搏SecPulse.Com獨家發文,如需轉載,請先聯繫授權。前言

本文以 libfuzzer-workshop 為基礎 介紹 libFuzzer 的使用。

libFuzzer簡介

libFuzzer 是一個in-process,coverage-guided,evolutionary 的 fuzz 引擎,是 LLVM 項目的一部分。

libFuzzer 和 要被測試的庫 連結在一起,通過一個模糊測試入口點(目標函數),把測試用例餵給要被測試的庫。

fuzzer會跟蹤哪些代碼區域已經測試過,然後在輸入數據的語料庫上進行變異,來使代碼覆蓋率最大化。代碼覆蓋率的信息由 LLVM 的SanitizerCoverage 插樁提供。

一些概念

fuzz 的種類

Generation Based :通過對目標協議或文件格式建模的方法,從零開始產生測試用例,沒有先前的狀態

Mutation Based :基於一些規則,從已有的數據樣本或存在的狀態變異而來

Evolutionary :包含了上述兩種,同時會根據代碼覆蓋率的回饋進行變異。

target (被 fuzz 的目標)

基本上所有的程序的主要功能都是對一些 字節序列 進行操作,libfuzzer 就是基於這一個事實(libfuzzer 生成 隨機的 字節序列 ,扔給 待fuzz 的程序,然後檢測是否有異常出現) 所以在 libfuzzer 看來,fuzz 的目標 其實就是一個 以 字節序列 為輸入的 函數

fuzzer

一個 生成 測試用例, 交給目標程序測試,然後檢測程序是否出現異常 的程序

corpus(語料庫)

給目標程序的各種各樣的輸入

以圖片處理程序為例:

語料庫就是各種各樣的圖片文件,這些圖片文件可以是正常圖片也可以不是。

傳統Fuzz介紹

傳統的 fuzz 大多通過對已有的樣本 按照預先設置好的規則 進行變異產生測試用例,然後餵給 目標程序同時監控目標程序的運行狀態。

這類 fuzz 有很多,比如: peach , FileFuzz 等

實戰生成測試用例

本節使用 radamsa 作為 變異樣本生成引擎,對 pdfium 進行 fuzz 。

相關文件位於

https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/02

radamsa 是一個 測試用例生成引擎,它是通過對已有的樣本進行變異來生成新的測試用例。

首先看看 測試樣本的生成

generate_testcases.py

#!/usr/bin/env python2import osimport 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)

就是調用 radamsa ,然後隨機選取 seed_corpus 目錄中的文件名作為參數,傳遞給 radamsa 進行變異,然後把生成的測試用例,放到 work/corpus。

開始fuzz

這樣測試樣本就生成好了,下面看看 用於 fuzz 的腳本

run_fuzzing.py

#!/usr/bin/env python2import osimport subprocessWORK_DIR = 'work'def checkOutput(s):  if 'Segmentation fault' in s or 'error' in s.lower():    return False  else:    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_path    print output    print '-' * 80

就是不斷調用 程序 處理剛剛生成的測試用例,根據執行的輸出結果中 是否有 Segmentation fault 和 error來判斷是否觸發了漏洞。

ps: 由於用於變異樣本的選取 和 樣本的變異方式是隨機的,可能需要重複多次 樣本生成 && fuzz 才能找到 crash

寫個 bash 腳本,不斷重複即可

#!/bin/bashwhile [ "0" -lt "1" ]do  rm -rf ./work/  ./generate_testcases.py  ./run_fuzzing.pydone

Helloworld-For-libFuzzer安裝

本節相關資源文件位於:

https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/04

首先先把 libFuzzer 安裝一下

首先

git clone https://github.com/Dor1s/libfuzzer-workshop.gitsudo ln -s /usr/include/asm-generic /usr/include/asmapt-get install gcc-multilib

然後進入 libfuzzer-workshop/ , 執行 checkout_build_install_llvm.sh 安裝好 llvm.

然後進入 libfuzzer-workshop/libFuzzer/Fuzzer/ ,執行 build.sh 編譯好 libFuzzer。

如果編譯成功,會生成 libfuzzer-workshop/libFuzzer/Fuzzer/libFuzzer.a

實戰

這一節中主要使用 libFuzzer 對 vulnerable_functions.h 中實現的幾個有漏洞的 函數 進行 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;}

這個函數有兩個參數,第一個參數 data 是 uint8_t* 類型的,說明 data 應該是指向了一個緩衝區, size 應該是緩衝區的大小,如果 size >=3 , 會訪問 data[3] , 越界訪問了。

進行 fuzz 的第一步是 實現一個 入口點,用來接收 libFuzzer 生成的 測試用例(比特序列)。

示例

// fuzz_target.ccextern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {  DoSomethingInterestingWithMyAPI(Data, Size);  return 0;  // Non-zero return values are reserved for future use.}

對於 LLVMFuzzerTestOneInput 有一些要注意的 tips

fuzz 引擎會在一個進程中進行多次 fuzz, 所以其效率非常高

要能處理各種各樣的輸入 (空數據, 大量的 或者 畸形的數據...)

內部不會調用 exit()

如果使用多線程的話,在函數末尾要把 線程 join

對於 VulnerableFunction1 , 直接把 libFuzzer 傳過來的數據,傳給 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

然後運行

haclh@ubuntu:~/vmdk_kernel/libfuzzer-workshop-master/lessons/04$ ./first_fuzzer INFO: Seed: 1608565063INFO: Loaded 1 modules (37 guards): [0x788ec0, 0x788f54), INFO: -max_len is not provided, using 64INFO: 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 0x7ffdb34390f8READ 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 fa  0x0c04800dd730: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd  0x0c04800dd740: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa  0x0c04800dd750: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa  0x0c04800dd760: 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 fa  0x0c04800dd780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa  0x0c04800dd790: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa  0x0c04800dd7a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa  0x0c04800dd7b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa  0x0c04800dd7c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa...==32049==ABORTINGMS: 1 CrossOver-; base unit: 38a223b0988bd9576fb17f5947af80b80203f0ef0x46,0x55,0x5a,FUZartifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60Base64: RlVa

正常的話應該可以看到類似上面的輸出,這裡對其中的一些信息解析一下

Seed: 1608565063 說明這次的種子數據

-max_len is not provided, using 64 , -max_len 用於設置最大的數據長度,默認為 64

接下來 # 開頭的行是 fuzz 過程中找到的路徑信息

倒數第二行是觸發漏洞的測試用例

使用

ASAN_OPTIONS=symbolize=1 ./first_fuzzer ./crash-0eb8e4ed029b774d80f2b66408203801# ASAN_OPTIONS=symbolize=1 用於顯示 棧的符號信息

重現 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;}

代碼量比第一個要複雜了一些,不管那麼多先 fuzz , 首先看看這個函數的參數, 比 VulnerableFunction1 多了一個 bool 類型的參數,所以 fuzz 腳本比 VulnerableFunction1 中的加一點修改即可。

#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 測試的目標就是儘可能的測試所有代碼路徑,又 VulnerableFunction2 的第 3 個參數是 bool 型的(有兩種可能), 那我們就對每一個測試用例都用 {false, true} 測試一遍以獲取更多的測試路徑。

首先編譯一下

clang++ -g -std=c++11 -fsanitize=address -fsanitize-coverage=trace-pc-guard \    second_fuzzer.cc ../../libFuzzer/Fuzzer/libFuzzer.a \    -o second_fuzzer

直接執行會得到類似下面的結果

haclh@ubuntu:~/vmdk_kernel/libfuzzer-workshop-master/lessons/04$ ./second_fuzzerINFO: Seed: 4141499174INFO: Loaded 1 modules (39 guards): [0x788f00, 0x788f9c), INFO: -max_len is not provided, using 64INFO: 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 我們發現函數可以處理的最大數據長度是 1024 , 所以給 fuzzer 加個參數設置一下最大數據長度。

./second_fuzzer -max_len=1024

可以看到檢測到了 棧溢出,觸發漏洞的指令位於

vulnerable_functions.h:61:3

跟到該文件內查看,發現是

std::copy(data, data + size, body.data());

觸發了漏洞,漏洞產生的原因在於, body 的 buf 的大小為 kMaxBodyLength , 而這裡可以往 body 的 buf 裡面寫入最多 kMaxPacketLen 字節。

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

就是動態生成了 verify_hash 的值,然後傳給 了 VulnerableFunction2。

我們也照著來就行

#include "vulnerable_functions.h"#include <functional>#include <string>extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {    VulnerableFunction3(data, size, 0x0001000); // 觸發 flag = true    VulnerableFunction3(data, size, 0x1000000); // 觸發 flag = false    return 0;}

編譯

clang++ -g -std=c++11 -fsanitize=address -fsanitize-coverage=trace-pc-guard \    third_fuzzer.cc ../../libFuzzer/Fuzzer/libFuzzer.a \    -o third_fuzzer

運行

總結

簡單理解 libfuzzer 。如果我們要 fuzz 一個程序,找到一個入口函數,然後利用

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {    ..    ..}

接口,我們可以拿到 libfuzzer 生成的 測試數據以及測試數據的長度,我們的任務就是把這些生成的測試數據 傳入到目標程序中 讓程序來處理 測試數據, 同時要儘可能的觸發更多的代碼邏輯

實戰兩個簡單的CVECVE-2014-0160 (openssl 心臟滴血漏洞 )

libfuzzer 是用於 fuzz 某個函數的,所以我們先編譯好目標代碼庫, 然後在根據要 fuzz 的功能編寫 fuzzer 函數。

本節資源文件位於

https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/05

首先用 clang 編譯 openssl.

./configmake cleanmake CC="clang -O2 -fno-omit-frame-pointer -g -fsanitize=address -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div" -j$(nproc)

主要是為了加上 AddressSanitizer ,用於檢測程序中出現的異常(uaf, 堆溢出,棧溢出等漏洞)

常用內存錯誤檢測工具

AddressSanitizer: 檢測 uaf, 緩衝區溢出,stack-use-after-return, container-overflow

MemorySanitizer: 檢測未初始化內存的訪問

UndefinedBehaviorSanitizer: 檢測一些其他的漏洞,整數溢出,類型混淆等

然後寫 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;}

感覺用 libfuzzer 的話,我們需要做的工作就是根據目標程序的邏輯,把 libfuzzer 生成的 測試數據 傳遞 給 目標程序去處理, 然後在編譯時採取合適的 Sanitizer 用於檢測運行時出現的內存錯誤

比如上面就是模擬了 SSL 握手的邏輯,然後把 libfuzzer 生成的 測試數據作為握手包傳遞給 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

運行然後就會出現 crash 信息了

haclh@ubuntu:~/vmdk_kernel/libfuzzer-workshop-master/lessons/05$ ./openssl_fuzzer ./corpus/INFO: Seed: 1472290074INFO: Loaded 1 modules (33464 guards): [0xc459b0, 0xc66490), Loading corpus dir: ./corpus/INFO: -max_len is not provided, using 64INFO: 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 0x7ffd5817fcd0READ 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 中 觸發了堆溢出 (heap-buffer-overflow )

CVE-2016-5180 (c-ares 堆溢出)

本節相關文件位於

https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/06

和前面一樣,首先編譯一下這個庫。

tar xzvf c-ares.tgzcd 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 的邏輯就非常簡單了

#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 傳過來的數據,轉成 char * , 然後扔給 ares_create_query 進行處理。

運行就有 crash 了。

總結

感覺libfuzzer 已經把 一個 fuzzer 的核心(樣本生成引擎和異常檢測系統) 給做好了, 我們需要做的是根據目標程序的邏輯,把 libfuzzer 生成的數據,交給目標程序處理。

libFuzzer進階

前面介紹了 libFuzzer 的一些簡單的使用方法,下面以 fuzz libxml2 為例,介紹一些 libFuzzer 的高級用法。

相關文件位於

https://github.com/Dor1s/libfuzzer-workshop/tree/master/lessons/08

Start fuzz

首先把 libxml2 用 clang 編譯

tar xzf libxml2.tgzcd 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"  ./configuremake -j$(nproc)

然後寫個 fuzzer, 這裡選擇 測試 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_fuzzer

ps: /usr/lib/x86_64-linux-gnu/liblzma.a 是 liblzma.a 的路徑,需要安裝 liblzma-dev:

apt-get install liblzma-dev

編譯好 fuzzer 後,我們運行它:

mkdir corpus1./xml_read_memory_fuzzer -max_total_time=300 -print_final_stats=1 corpus1

其中的一些參數做個解釋

-max_total_time : 設置最長的運行時間, 單位是 秒, 這裡是 300s , 也就是 5 分鐘

-print_final_stats:執行完 fuzz 後 列印統計信息

corpus1: fuzzer 程序可以有多個目錄作為參數,此時 fuzzer 會遞歸遍歷所有目錄,把目錄中的文件讀入最為樣本數據傳給測試函數,同時會把那些可以產生新的的代碼路徑的樣本保存到第一個目錄裡面。

運行完後會得到類似下面的結果

###### 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: 1464491stat::average_exec_per_sec:     4865stat::new_units_added:          1407stat::slowest_unit_time_sec:    0stat::peak_rss_mb:              407

開始由 #### 夾著的是 libfuzzer 在 fuzz 過程中挑選出來的 dictionary, 同時還給出了使用的次數,這些 dictionary 可以在以後 fuzz 同類型程序時 節省 fuzz 的時間。

然後以 stat: 開頭的是一些 fuzz 的統計信息, 主要看 stat::new_units_added 表示整個 fuzz 過程中觸發了多少個代碼單元。

可以看到直接 fuzz , 5分鐘 觸發了 1407 個代碼單元

使用 Dictionary 提升性能

Dictionary 貌似是 afl 中提出的 ,具體可以看下面這篇文章

https://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html

我們知道基本上所有的程序都是處理具有一定格式的數據,比如 xml 文檔, png圖片等等。 這些數據中會有一些特殊字符序列 (或者說關鍵字), 比如 在 xml 文檔中 就有 CDATA, <!ATTLIST 等,png圖片 就有 png 圖片頭

如果我們事先就把這些 字符序列 列舉出來, fuzz 直接使用這些關鍵字去 組合,就會就可以減少很多沒有意義的 嘗試,同時還有可能會走到更深的程序分支中去。

Dictionary 就是實現了這種思路。 libfuzzer 和 afl 使用的 dictionary 文件的語法是一樣的, 所以可以直接拿 afl 裡面的 dictionary 文件來給 libfuzzer 使用。

下面這個網址裡面就有一些 afl 的 dictionary 文件

https://github.com/rc0r/afl-fuzz/tree/master/dictionaries

以 libfuzzer 官網 的示例介紹一下語法

# 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 valueskw3="\xF7\xF8"# the name of the keyword followed by '=' may be omitted:"foo\x0Abar"

libfuzzer 使用 -dict 指定 dict 文件,下面使用 xml.dict 為 dictionary 文件,進行 fuzz。

./xml_read_memory_fuzzer -dict=./xml.dict -max_total_time=300     -print_final_stats=1 corpus2

最終這種方式可以探測到 2200+ 的代碼單元, 效率提升還是很明顯的。

###### End of recommended dictionary. ######Done 1379646 runs in 301 second(s)stat::number_of_executed_units: 1379646stat::average_exec_per_sec:     4583stat::new_units_added:          2215stat::slowest_unit_time_sec:    0stat::peak_rss_mb:              415

精簡樣本集

在上一節裡面我們獲得了很多的樣本,其中有很多其實是重複的,可以使用 libfuzzer 把樣本集進行精簡。

mkdir corpus1_min./xml_read_memory_fuzzer -merge=1 corpus1_min corpus1

corpus1_min: 精簡後的樣本集存放的位置

corpus1: 原始樣本集存放的位置

可以得到類似的結果

03:20 haclh@ubuntu:08 $ ls corpus1 | wc -l140303:20 haclh@ubuntu:08 $ ./xml_read_memory_fuzzer -merge=1 corpus1_min corpus1INFO: Seed: 3775041129#1024   pulse  cov: 1569 exec/s: 0 rss: 108MbMERGE-OUTER: succesfull in 1 attempt(s)MERGE-OUTER: the control file has 4150228 bytesMERGE-OUTER: consumed 2Mb (24Mb rss) to parse the control fileMERGE-OUTER: 928 new files with 5307 new features added

可以看到 1403 個樣本被精簡成了 928 個樣本

生成代碼覆蓋率報告

使用 -dump_coverage 參數。

03:28 haclh@ubuntu:08 $ ./xml_read_memory_fuzzer corpus1_min -runs=0 -dump_coverage=1INFO: Seed: 2354910000..../xml_read_memory_fuzzer.69494.sancov: 1603 PCs written

然後會生成一個 *.sancov 文件

03:26 haclh@ubuntu:08 $ ls *.sancovxml_read_memory_fuzzer.69494.sancov

然後把它轉換成 .symcov

sancov -symbolize xml_read_memory_fuzzer ./xml_read_memory_fuzzer.69494.sancov > xml_read_memory_fuzzer.symcov

然後使用 coverage-report-server 解析這個文件。

03:29 haclh@ubuntu:08 $ python3 coverage-report-server.py --symcov xml_read_memory_fuzzer.symcov     --srcpath libxml2Loading coverage...Serving at 127.0.0.1:8001

用瀏覽器訪問之

通過這個功能,我們可以非常直觀的看到每個源文件的覆蓋率。

Fuzz xmlRegexpCompile

前面已經 fuzz 了 xmlReadMemory,這裡 fuzz 另外一個函數 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;}

就是把 數據用 std::vector 做了個中轉,餵給 xmlRegexpCompile 函數。

編譯之

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

運行

mkdir corpus3./xml_compile_regexp_fuzzer -dict=./xml.dict corpus3

然後過一會就會出現 crash 了。

參考

https://github.com/Dor1s/libfuzzer-workshop/

↙↙↙   點擊 」閱讀原文「 與作者展開話題探討,直面交流。

相關焦點

  • 【表哥有話說 第72期】honggfuzz 學習筆記
    apt install binutils binutils-dev libunwind-devgit clone https://github.com/google/honggfuzz --depth=1cd honggfuzzmake -j$(
  • 漏洞挖掘|開源Fuzzer和Fuzzing學習資源整理
    libfuzzer fuzz qthttps://int21.de/slides/qtcon-fuzzing    afl + asan fuzz 教程https://mgba.io/2016/09/13/fuzzing-emulators/    fuzz 模擬器 、以及很多其他的東西https://security.tencent.com/
  • 從AFL開始FUZZ之旅
    前言我想介紹一些不一樣的東西-fuzz,也就是大家常說的模糊測試。Fuzz是近幾年來安全頂會的熱門,投稿難度也越來越大。一次成功的fuzz甚至能挖掘出幾十個CVE。我準備在該文章中先介紹fuzz相關的知識,然後以AFL為例演示一個fuzz例子;不足之處還請各位讀者斧正。
  • Peach原理簡介與實戰:以Fuzz Web API為例
    初學者,往往無從下手,本文從實戰出發,穿插講解Peach套件的一些語法和原理,讓你真正從0開始一段奇妙的模糊測試之旅!0x1 Peach簡介Peach是一個遵守MIT開源許可證的模糊測試框架,首個版本發布於2004年,使用python編寫,第二版於2007年發布,被收購後,最新的第三版使用C#重寫了整個框架,而且分為社區版和付費版。
  • 谷歌發布 OSS-Fuzz 開源模糊測試服務
    OSS-Fuzz結合了多種模糊測試技術/漏洞捕捉技術(即原來的libfuzzer)與清洗技術(即原來的AddressSanitizer),並且通過ClusterFuzz為大規模可分布式執行提供了測試環境。」這項服務填補了Springfield留下的空白:微軟的模糊測試服務針對的是願意付費的企業客戶。
  • 【技術分享】AFL源碼分析(III)——afl-fuzz分析(Part 1)
    這些函數到底是怎樣生效的呢,在本篇文章中,我將對AFL的主邏輯,也就是afl-fuzz進行分析。 依據官方github所述,afl-fuzz是AFL在執行fuzz時的主邏輯。對於直接從標準輸入(STDIN)直接讀取輸入的待測文件,可以使用如下命令進行測試:.
  • AFL分析與實戰
    AFL通過源碼插樁的方式在程序的每個基本塊前面插入 _afl_maybe_log 函數,當執行第一個基本塊時會啟動forkserver,afl-fuzz和forkserver之間通過管道通信,每當afl-fuzz生成一個測試用例
  • Google推出Fuzzer基準測試服務FuzzBench
    模糊測試是一種軟體測試技術,通過自動或半自動產生輸入另一個程序的亂數據,以探索程序可能出現的異常狀況,Google提到,他們內部使用了libFuzzer和AFL等各式Fuzzer,發現了數以萬計的bug。
  • SQL注入常規Fuzz全記錄
    前言本篇文章是在做ctf bugku的一道sql 盲注的題(題目地址:注入題目)中運用了fuzz的思路,完整記錄整個fuzz的過程
  • SQL 注入常規 Fuzz 全記錄
    的思路,完整記錄整個fuzz的過程,給師傅們當點心,方便大家加深對web sql注入 fuzz的理解。4、fuzz(1)從payload的形式可以猜測題目應該是過濾了注釋符(--+和#)(2)fuzz一遍特殊字符,看看過濾了什麼當存在過濾的字符時,響應包是這樣的
  • QuantLib 金融計算——基本組件之 Date 類
    需要注意的是,quantlib-python 中的 Date 類並不同於 python 自身包含的 datetime 類,也沒有繼承關係。載入 QuantLib:import QuantLib as qlprint(ql.
  • s3c2440頭文件之2440lib.h
    //===================================================================// File Name : 2440lib.h// Function : S3C2440// Date : February 26, 2002// Version : 0.0// History// 0.0
  • Logistic回歸實戰篇之預測病馬死亡率(三)
    random_state:隨機數種子,int類型,可選參數,默認為無,僅在正則化優化算法為sag,liblinear時有用。solver:優化算法選擇參數,只有五個可選參數,即newton-cg,lbfgs,liblinear,sag,saga。默認為liblinear。