改善模糊效果

當你第一次開始模糊新目標時,模糊測試工具可能會快速當機。通常模糊不清 初期最初的瑕疵數量 隨後突然出現長尾瑕疵出現的頻率 這些發現下滑的原因有很多,包括: * 所測試程式碼中的瑕疵數量較少。 * 程式碼的某些部分並未由模糊工具進行測試。

要辨別兩者的差異,您需要能評估及改善模糊效果。

擴大程式碼涵蓋率

改善模糊化的第一步,就是瞭解模型目前的成效。一個 就涵蓋範圍指引的模糊化技術而言,明顯的主要指標是程式碼涵蓋率。您可以收集這些資訊 使用 fx fuzz

例如:

fx fuzz analyze package/fuzzer

這會執行模糊測試 60 秒,並回報裝置上語料庫的程式碼涵蓋率。 如果您指定 --staging 選項,系統會將該目錄中的檔案新增至語料庫,

新增或改善種子語料庫

如果程式碼涵蓋率出現落差,您可以將個別輸入內容新增至種子語料庫中:

  1. 在模糊工具附近的來源樹狀結構中新增目錄。
  2. 在這個目錄中新增一或多個檔案,每個檔案都包含測試輸入的原始位元組 會導致模糊工具到達先前發現的程式碼。
  3. 為這個目錄新增 resource GN 目標,並新增至模糊工具的 deps
  4. 新增引數,並將該引數與模糊程式的 元件資訊清單來源。

例如:

cd $FUCHSIA_DIR
mkdir path-to-library/my-fuzzer-corpus
cp handcrafted-input path-to-library/corpus

並在 //path/to/library/BUILD.gn 中:


import("//build/fuzz.gni")
import("//build/dist/resource.gni")

resource("my-library-corpus") {
  sources = [
    ...
    "corpus/handcrafted-input",
    ...
  ]
  outputs = [ "data/my-library-corpus/{{source_file_part}}" ]
}

cpp_fuzzer("my_fuzzer"){
  sources = [ "my-fuzzer.cc" ]
  deps = [
    ":my-library",
    ":my-library-corpus"
  ]
}

在模糊工具的元件資訊清單來源中:

{
  ...
  program: {
    args: [
      ...
      "data/my-library-corpus",
      ...
    ]
  }
}

讓程式碼更好用

一般來說,libFuzzer 在尋找探索新條件式分支的輸入內容時,效果不錯 根據輸入內容的位元組做出決定舉例來說 CMP 等比較指示,藉此判斷在部分項目中符合檢查條件所需的值 部分輸入區塊

但假如模糊的情況發生「模糊不清」的現象,此方法就會失敗條件。以下是符合規定的功能:

C/C++

  • 使用外部來源資料的條件。例如:
zx_cprng_draw(&val, sizeof(val));
if (val == 0) { ... }
  • 用來檢查可建構但難以猜到的值。例如:
uint32_t actual = header.checksum;
header.checksum = 0;
uint32_t expected =  crc32(0, reinterpret_cast<const uint8_t*>(&header), sizeof(header));
if (actual == expected) { ... }
int result = ECDSA_verify(0, data, data_len, signature, signature_len, ec_key);
if (result == 0) { ... }

Rust

  • 使用外部來源資料的條件。例如:
let mut randbuf = [0; 8];
zx::cprng_draw(&mut randbuf)?;
let val = u64::from_le_bytes(randbuf);
if val == 0 { ... }
  • 用來檢查可建構但難以猜到的值。例如:
let mut c = Checksum::new();
c.add_bytes(&buf);
c.checksum()
if c == expected { ... }
let digest = H::hash(message);
if boringssl::ecdsa_verify(digest.as_ref(), self.bytes(), &key.inner.key) { ... }

查看

  • 使用外部來源資料的條件。例如: golang if rand.Intn(100) == 0 { ... }

  • 用來檢查可建構但難以猜到的值。例如:

iCksum := ipv4.CalculateChecksum()
if iCksum != want { ... }
ecdsaKey, ok := key.(*ecdsa.PublicKey)
h := e.hash.New()
h.Write(msg)
if ecdsa.Verify(ecdsaKey, h.Sum(nil), ecdsaSignature.R, ecdsaSignature.S) { ... }

身為程式碼維護人員,您可以使用條件式編譯,在 測試用的程式碼libFuzzer 稱之為使用易於模糊的建構模式。

C/C++

使用常見的建構巨集 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION

例如:

#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
  // Use hard-coded value when fuzzing.
  memset(&val, 0, sizeof(val));
#else
  zx_cprng_draw(&val, sizeof(val));
#endif

在此範例中,我們已將 val 的所有位元組設為零。視程式碼而定 如果 val 是其他確定值,甚至 直接取決於模糊的輸入內容

Rust

使用 fuzz cfg 屬性

例如:

#[cfg(not(fuzz))]
fn is_valid(&self, key: &EcPubKey<C>, message: &[u8]) -> bool {
    let digest = H::hash(message);
    boringssl::ecdsa_verify(digest.as_ref(), self.bytes(), &key.inner.key)
}

#[cfg(fuzz)]
fn is_valid(&self, key: &EcPubKey<C>, message: &[u8]) -> bool {
    // Skip validation when fuzzing.
    return true;
}

查看

使用 fuzz 套件。由於 Go 只對檔案執行條件式編譯 層級,這個套件包含兩個定義 const Enabled <bool> 的檔案。哪一個檔案? 因此,Enabled 的值取決於程式碼是否內建。 不一定是模糊的變體

例如:

import "fuzz"

func (b IPv4) CalculateChecksum() uint16 {
  if fuzz.Enabled {
    // Return hard-coded value when fuzzing.
    return uint16(0xffff)
  }
  return Checksum(b[:b.HeaderLength()], 0)
}

新增自訂變動器

在某些情況下,模糊工具提供的輸入內容可能會先經過轉換,再對其採取行動。 此做法可大幅減少模糊人士將輸入內容與產生行為建立關聯的能力。 例如,程式庫在處理之前先將輸入內容解壓縮,再進行處理。最常出現 這個程式庫能有效模糊化,就是對未壓縮的輸入內容執行異動,然後壓縮 再叫用程式庫

其中一個方法是新增自訂變動器。這些是使用者提供的 由 LLVM 模糊介面定義的選用函式:

extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
                                          size_t MaxSize, unsigned int Seed);

如有提供,libFuzzer 一開始會以 MaxSize 大小的緩衝區 Data 呼叫這個函式 已填入語料庫中有效輸入的 Size 個位元組。這個函式可以轉換這項資料 再呼叫另一個 LLVM 模糊介面函式:

size_t LLVMFuzzerMutate(uint8_t *Data, size_t Size, size_t MaxSize);

這個函式會執行實際的異動以建立新的輸入內容。

在需要壓縮輸入的程式庫範例中,自訂變動器可將 對語料庫呼叫 LLVMFuzzerMutate,建立新的輸入內容並壓縮結果。

Google 的公開模糊說明文件有這類情況的詳細範例。詳情請見 使用 libFuzzer 建立結構感知模糊

提供字典

字典是一組可在介面的有效輸入內容中找到的一組符記。如有提供, 模糊工具會使用字典來建構更可能為有效的輸入項目,進而達成上述目的。 提供更詳盡的資料

如果您知道程式碼預期的符記類型,可以將其新增至字典檔案, 互動接著,以 resource 的形式將檔案提供給模糊工具,方法與種子相同 然後將這些文字新增至模糊元件的元件資訊清單來源。

例如:

{
  ...
  program: {
    args: [
      ...
      "-dict=data/my-dictionary.txt",
      ...
    ]
  }
}

改善模糊效能

如果模糊不清能達到良好的涵蓋率,另一項關鍵指標就是疊代次數 可針對一組有限的運算資源 執行工作負載模糊工具和測試程式碼可以 作業複雜度不高,因此每秒不會進行單次疊代作業 或單一記憶體限制,模糊效果應保持在下列狀態。但如果一切都沒問題 相同的速度,隨著速度愈快愈好 且會耗用大量記憶體

啟動初始化

一般來說,測試中的程式碼需要投入大量昂貴的設定才能進行測試。表演中 因為每次疊代時初始化 都會拖慢模糊速度在這些情況下 最好在變數生命週期等同處理程序時,延遲初始化變數。

例如:

bool SetUp() {
   DoSomeExpensiveWork();
   return true;
}

extern "C" LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  static bool ready = SetUp();
  ...
}

但是,請非常謹慎處理從一次疊代到 下一步。如果問題不僅限於目前的測試輸入內容,也取決於先前輸入的部分資料 如果不重播整個模糊執行作業,可能會難以重現

預先分配儲存空間

有些程式碼會在大量記憶體上執行,例如壓縮演算法。令人欲罷不能的解謎器 在每次疊代時分配及釋出許多記憶體 (以 MB 為單位),效能會降低。一個 替代方法是使用預先分配大小上限的緩衝區。

例如:

static const size_t kMaxOutSize = 0x10000000; // 256 MB

extern "C" LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  static uint8_t out_buf[kMaxOutSize];
  size_t max = Decompressor::GetMaxUncompressedSize(size);
  if (sizeof(out_buf) < max) {
    return 0;
  }
  Decompressor::Decompress(data, size, out_buf, max);
  return 0;
}

如上所述,這在效能和消毒的精確度之間進行取捨。如果程式碼 而是分配一個長度為 max 的記憶體區域,AddressSanitizer 會 才能偵測 Decompress 是否溢位任何金額如果是預先分配的區域 完全無法啟動。幸好 AddressSanitizer 提供了一種方法, 手動中毒記憶。

例如:

#include <sanitizer/asan_interface.h>

static const size_t kMaxOutSize = 0x10000000; // 256 MB

extern "C" LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  static uint8_t out_buf[kMaxOutSize];
  size_t max = Decompressor::GetMaxUncompressedSize(size);
  if (sizeof(out_buf) < max) {
    return 0;
  }
  ASAN_POISON_MEMORY_REGION(&data[max], sizeof(out_buf) - max);
  Decompressor::Decompress(data, size, out_buf, max);
  ASAN_UNPOISON_MEMORY_REGION(&data[max], sizeof(out_buf) - max);
  return 0;
}