當你第一次開始模糊新目標時,模糊測試工具可能會快速當機。通常模糊不清 初期最初的瑕疵數量 隨後突然出現長尾瑕疵出現的頻率 這些發現下滑的原因有很多,包括: * 所測試程式碼中的瑕疵數量較少。 * 程式碼的某些部分並未由模糊工具進行測試。
要辨別兩者的差異,您需要能評估及改善模糊效果。
擴大程式碼涵蓋率
改善模糊化的第一步,就是瞭解模型目前的成效。一個
就涵蓋範圍指引的模糊化技術而言,明顯的主要指標是程式碼涵蓋率。您可以收集這些資訊
使用 fx fuzz
。
例如:
fx fuzz analyze package/fuzzer
這會執行模糊測試 60 秒,並回報裝置上語料庫的程式碼涵蓋率。
如果您指定 --staging
選項,系統會將該目錄中的檔案新增至語料庫,
新增或改善種子語料庫
如果程式碼涵蓋率出現落差,您可以將個別輸入內容新增至種子語料庫中:
- 在模糊工具附近的來源樹狀結構中新增目錄。
- 在這個目錄中新增一或多個檔案,每個檔案都包含測試輸入的原始位元組 會導致模糊工具到達先前發現的程式碼。
- 為這個目錄新增
resource
GN 目標,並新增至模糊工具的deps
。 - 新增引數,並將該引數與模糊程式的 元件資訊清單來源。
例如:
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;
}