改善模糊效果

首次設定模糊目標時,模糊現象可能會非常快。模糊情形通常會產生最初發現的瑕疵高峰期,後面接著長尾。發現瑕疵的頻率下降的原因有很多,包括: * 測試的程式碼瑕疵數量較少。 * 程式碼中有某些區段尚未經過模糊處理工具測試。

為區分這些因素,您必須評估和改善模糊工具。

改善程式碼涵蓋率

改善模糊工具的第一步,就是瞭解目前的效能。程式碼涵蓋率的一大關鍵指標就是程式碼涵蓋率。您可以使用 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();
  ...
}

不過,「非常」謹慎地處理從一次疊代到下一個疊代的程式狀態。如果瑕疵不單只是目前的測試輸入內容,也包括先前所有先前所有輸入內容的子集,在不重播整個模糊執行作業的情況下,可能會難以重現問題。

預先分配的儲存空間

部分程式碼會在大量記憶體上運作,例如壓縮演算法。如果嘗試在每個疊代中分配及釋放大量記憶體的模糊工具,就會發現效能降低。另一種做法是預先分配大小最大的緩衝區。

例如:

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