模糊測試(fuzzing)
/ FUH-zing /
要知道一張椅子會不會搖,最快的方法不是去推理它——而是用各種方式坐上去,看它會不會倒。模糊測試就是軟體的那種經驗式做法:自動且高速地餵給程式一大堆畸形、出乎意料或隨機的輸入,盯著那些讓它崩潰、卡住、或觸發記憶體安全檢查的輸入。每一次崩潰都指向一個漏洞——往往是與安全相關的。
現代模糊測試器實際如何運作如下,因為純隨機很少能鑽得深。一個覆蓋率導向的模糊測試器(主流類型,如 AFL 或 libFuzzer)保有一組輸入語料庫,在目標上執行它們,同時量測每個輸入觸發了哪些程式碼路徑(透過輕量的編譯器插樁)。接著它「變異」輸入——翻位元、拼接、插入已知有趣的值——並保留任何達到「新」覆蓋率的變異輸入,培育出逐漸更深入程式的輸入,就像演化發現邊界情形。關鍵在於,模糊測試會搭配像 AddressSanitizer 這樣的消毒器,它讓程式在記憶體錯誤(堆積溢位、釋放後使用、越界讀取)發生的「確切時刻」就「大聲」崩潰,而非無聲地損毀。於是當一個模糊輸入觸發了例如一個一位元組的堆積溢位時,消毒器會報出精確的檔案、行號、以及那次被釋放的配置——把大海撈針般的記憶體漏洞變成一份清晰、可重現的報告。這就是「一眼看出、由模糊測試器找到的記憶體安全漏洞」:一份釘在惹禍輸入上的消毒器堆疊追蹤。
它之所以重要,是因為模糊測試已成為大規模尋找記憶體損毀漏洞最有生產力的單一方法——像 OSS-Fuzz 這樣的專案在關鍵開源程式碼上持續執行它,已找出數萬個漏洞。誠實的說法:模糊測試找出漏洞,但不「證明」漏洞不存在;覆蓋率是一種啟發法,需要結構化或有狀態輸入(一個有效的校驗和、一次交握)的深層邏輯,可能需要懂文法或懂結構的模糊測試器才能抵達;而一次乾淨的模糊測試代表「我們在這份預算內沒找到崩潰」,而非「這段程式碼是安全的」。最好把它理解為縱深防禦測試策略中一道強而自動化的層。
$ clang -fsanitize=address,fuzzer parse.c -o fuzz # ASan + libFuzzer $ ./fuzz corpus/ # 變異、執行、追蹤覆蓋率 ==1234==ERROR: AddressSanitizer: heap-buffer-overflow WRITE of size 1 at 0x60200000eff1 ... in parse_header parse.c:42
模糊測試器驅動輸入;消毒器把損毀的那一刻變成一份精確、可重現的崩潰報告。
模糊測試找出漏洞,卻從不證明漏洞不存在——一次乾淨的執行代表「在這份預算與覆蓋率內無崩潰」,而非「安全」;而要抵達深層、結構化的程式碼路徑,往往需要懂結構的模糊測試,外加一個消毒器把無聲的損毀變大聲。