模糊測試(fuzz testing / fuzzing)是把隨機、或半隨機的資料餵給網路協定,逼處理它的應用程式當機,藉此找出漏洞的技術。漏洞的本質是應用程式的行為偏離了原本的意圖;理論上一組好的測試可以防止這種偏離,但面對專有的網路應用程式,你通常拿不到它的測試,只能自己造。模糊測試不論協定多複雜都常常有斬獲,做法是產生大量測試案例(test case)——本質上是被改動過的協定結構——再送去給應用程式處理。這些測試案例可以隨機生成,也可以由分析者主導。

最簡單的模糊測試#

最陽春的模糊測試就是把隨機垃圾丟到網路端點,看會發生什麼事。在 Unix 系統上用 Netcat 一行指令就能做到:

cat /dev/urandom | nc hostname port

這行指令用 cat 從系統亂數產生器裝置讀出隨機資料,管線送進 netcat,由它開啟連線送往指定端點。

這種做法只可能在需求極少的簡單協定上觸發當機——純隨機資料幾乎不可能湊出複雜協定要求的有效檢查碼或魔術值。但它快到你沒理由不試,偶爾也真能給你有價值的結果。

別把這個 fuzzer 拿去對付正在運轉的工業控制系統,尤其是管理核反應爐那種。

突變式 Fuzzer#

多數情況你得對送出的資料更有選擇性。最簡單的技巧是拿既有的協定資料,稍加突變(mutation)再送出。這種突變式 fuzzer(mutation fuzzer)效果出奇地好。

最簡單的突變式 fuzzer 就是隨機翻轉位元:從資料中隨機挑一個位元組,再挑該位元組中的一個位元,用 XOR 把它翻轉後送出。

Listing 10-1:隨機位元翻轉突變 fuzzer
void SimpleFuzzer(const char* data, size_t length) {
  size_t position = RandomInt(length);
  size_t bit = RandomInt(8);
  char* copy = CopyData(data, length);
  copy[position] ^= (1 << bit);
  SendData(copy, length);
}

SimpleFuzzer() 在 0 到資料長度之間取一個位元組位置,再在 0 到 7 之間取一個位元,用 XOR 翻轉該位元後把突變資料送往目的地。

當 fuzzer 碰巧改到某個被應用程式錯誤使用的欄位,這個手法就會奏效。舉例來說,若它把一個設為 0x40 的長度欄位改成 0x80000040,而應用程式又把這個值乘以 4(例如要配置 32 位元陣列),就可能造成整數溢位(integer overflow);資料被改壞也可能讓解析程式讀到錯誤的記憶體位置。

  • 一次只翻一個位元比較好:能把突變效應侷限在應用程式相似的一小塊程式碼裡。若整個位元組都改,效果會發散——尤其當那個值是一組旗標時。
  • 記得重算檢查碼與關鍵欄位(例如總長度)。否則資料會在驗證步驟就被擋下,根本到不了真正處理該值的程式碼。

產生測試案例#

更複雜的模糊測試需要更聰明的改動:理解協定、鎖定特定資料型別。應用程式解析的資料越多,程式就越複雜,而邊界情況(例如長度值)往往檢查不足。如果你已經知道協定的結構,就可以從頭產生自己的測試案例。

自製測試案例讓你能精確掌控用到哪些協定欄位、各欄位多大。代價是開發較費工、需要仔細思考要生成哪些種類。它的優勢在於能測到那些在擷取流量、單純突變時永遠不會出現的協定值,因而觸及應用程式中較少被測試、也較可能藏有漏洞的程式碼路徑。

突變既有流量開發快、涵蓋常見路徑;從頭生成測試案例則能逼出罕見值、深入冷門程式碼。兩者互補,實務上常混用。