模糊測試: 初學者入門指南

Editor發表於2018-04-12


模糊測試簡介

模糊測試(Fuzzing),簡而言之,就是為了觸發新的或不可預見的程式碼執行路徑或bug而在程式中插入異常的、非預期的、甚至是隨機的輸入。因為模糊測試涉及到為目標提供大量的測試樣例,因此至少也會實現部分自動化。

模糊測試可以也應當用於測試每個需要接受某種形式輸入的介面。實際上,模糊測試最起碼就應該拿來用於測試每個從潛在惡意來源(比如網際網路或使用者提供的檔案)獲取輸入的介面。

模糊測試是對其他測試技術的補充。由模糊測試揭露出的問題往往是開發人員不太可能構建的輸入(例如,在處理一些邊界情況,資料正確性和錯誤處理例程時的輸入)觸發的。在常規自動化測試過程中,模糊測試擴大了程式碼覆蓋範圍,提高了程式碼覆蓋率測試程度。通過模糊測試使用的非預期輸入通常會觸發一些平時不會觸發的執行流。

很多地方都需要進行模糊測試。它是你係統開發生命週期(SDLC)的一部分,在這部分裡,你需要確保你完成了改善目標所要的系統性工作,或是你只是想解決一些bug也行。要如何費心於模糊測試取決於你的最終目標和相關資源,本文只是幫你如何從模糊測試中獲取更多的回報。

開始之前

也許到現在你已經躍躍欲試了。很多組織和個體經常急於根據部落格文章中的思路或會議上看到的酷炫演示來進行一次模糊測試,這雖不一定是壞事,但我們經常可以看到在模糊測試系統的背後有著大量的工作投入,這些模糊測試系統僅在作者分配去完成其他任務之前有在使用,稍加改動就會破壞相容性。

更糟糕的是這些模糊測試系統長年消耗硬體資源,卻經常沒能得出什麼結果來。就如同軟體開發專案的其他任何部分一樣,測試自動化和模糊測試與否,都需要一定的規劃,維護和提交。

你想拿fuzz幹什麼?

這是一個簡單的問題,但是答案卻並不一定如你所想那樣顯而易見。如果你已經有了一個目標,那很不錯。如果沒有,那你就得去找一個接受輸入的介面。介面可以是對外的,像是網路連線,可以是一些檔案。當然也完全可以是對內的,像是一個你程式碼正在使用的實用程式庫(utility)裡的函式呼叫約定。模糊測試就是為你所選擇的介面建立輸入,並觀察這些介面如何處理這些極端的輸入。你可以通過威脅建模(Threat modelling)和回執資料流圖來發現目標所擁有的潛在介面。

在每個介面背後可以有許多軟體層,選擇對哪個層進行模糊測試就顯得至關重要,因為輸入要到達那個層,就需要通過前面各層的所有檢查。

舉個例子,我們來看一個接收帶簽名二進位制資料的HTTP伺服器。我們有一個含JSON字串的二進位制資料,字串裡是我們應用程式要用到的值。在這個例子裡,我們就有4個潛在的層需要進行模糊測試:

伺服器接收的HTTP訊息

二進位制資料的簽名校驗

JSON字串解析

我們處理實際值的程式碼

暫且假定我們的HTTP,簽名和JSON庫都是魯棒的(我們並不想以這些庫為目標)。要對我們自己的程式碼進行模糊測試,我們就需要生成這些實際值, 然後將這些實際值包裝為JSON字串,對二進位制資料簽名,建立一個HTTP訊息並將其傳送給目標。除非我們已經有了可以複用的自動化測試程式碼,否則單獨構建這些測試樣例需要相當長的時間。在堆疊中進行模糊測試也會不斷帶來開銷,並且在更改某些層時也更容易被破壞。

在模糊測試中,測試樣例的吞吐量也相當關鍵。你應當考慮下目標是否有一些可禁用或繞過的功能,以減少開銷並擴大模糊測試覆蓋範圍。通常我們實現一個直接使用模糊值呼叫目的碼的小程式可以帶來不少好處。

在上面這個例子裡,寫一個直接將值傳遞給我們處理程式碼的程式,就可以繞過傳送網路訊息,好幾次雜湊計算、加密檢查、JSON轉字串以及解析這些步驟。

在一些優化更好的模糊測試環境裡,諸如不必要的日誌記錄、CRC校驗、檔案I/O以及遠端資源呼叫等功能都會在一個更適合模糊測試的模式("fuzz-friendly mode")下禁用。

我們可以用一些ifdef,建立虛擬(Mock)函式或其他僅用於構建模糊測試的配置來實現一個對模糊測試友好的模式("fuzz-friendly mode")。當然,當你在進行一些會改變目標行為的模糊測試優化時,你必須能確保這些修改不會建立或隱含任何的bug。

不過,在剛開始時,不要太擔心想著要一個高效的每秒將數千個測試用例注入進優化的模糊測試環境中去的策略。開始模糊測試的一個非常有效的辦法就是將隨機(或位翻轉)的資料傳送到你找到的任何介面去。如果這能很快地找到問題,那麼你就算是找到了你第一個目標介面了!

你想找尋的是什麼?

很多時候當你進行模糊測試,目標可能會崩潰,這是很難避免的。然而,為了能充分利用你的勞動,你就還需要找到其他的錯誤情況。目標都有它自己的功能需求,需求裡定義了程式應該幹什麼,你可以從跟這個點找到它不應該做的事情。

除此之外,所有程式都可能存在邏輯缺陷,可能導致記憶體洩露或CPU及記憶體消耗過多等問題。根據底層技術,目標也可能容易發生記憶體腐敗,命令注入或其他應當注意的問題類別。

起初,所有可能的潛在問題型別及其影響都應該記錄下來。現有的檢測工具和技術可以適用於不同的問題型別,但有些檢測工具和技術使用起來相當複雜,或是執行開銷高昂。

影響評估有助於你判斷使用工具或某技術是否值得。例如,影像壓縮中顏色值的錯誤計算可能影響很小,但卻難以檢測。如果你只是想找到這些問題,那麼一些能使用模糊的和無效的身份繞過驗證的地方十分致命,也相當容易被檢測到。

在研究不同的工具和技術時,還要考慮其他的自動化測試方法。例如,在很多情況下,你會發現你的單元測試(unit test)一次又一次觸發了一些錯誤,但你可能因為沒有用到單元測試而無法發覺。

如何進行模糊測試?

模糊測試是一項一人一機器就能執行的技術。中等規模的模糊測試可以作為持續整合(CI)系統的一部分來執行,針對不同的專案每天執行幾次模糊測試。大規模的模糊測試可以通過使用數百上千臺機器在雲端並行自動地模糊測試。所有這些環境都有著最終系統必須滿足的不同需求。因為最初基本不會考慮到與另一些部件的可用性,所以通常情況下不會使用大型fuzzer。

和所有的測試相同,測試規模越大,自動化就越重要。使用單個例項來fuzz你的程式非常簡單。你可用不斷地將模糊輸入注入到目標程式中,直到觸發bug,然後修復bug,如此不斷重複即可。但當你同時處理成百上千個例項時,你就會知道為什麼重複篩選等功能相當重要了。在一個在CI中針對不同構建版本並行執行模糊測試的大型組織中,你也可能會忽視自動問題報告,最小化測試用例和補丁驗證這些需要注意的問題。

準備開始

在這裡,你應該對模糊測試所需的三個部件建立一個粗略的需求規格說明:測試用例生成方案,測試用例注入方法和裝置。現在你終於可以開始真正的工作了。

網際網路裡有許多開源或商業性質的模糊測試解答方案。有些僅僅實現了測試用例生成,有些則結合了測試用例生成和注入,還有一些則具備完整的含有裝置和自動化的堆疊。一般而言,商業產品可用性更強,並且通常可以為大部分測試用例提供完整的解答方案。

特別是對於希望快速開始對多個產品進行模糊測試的組織而言,商業解決方案是真正的選擇。而對於因為樂趣和利潤,想解決bug的個人,商業解決方案通常會超出預算。

無論你是決定使用已有的解決方案或是自己實現一個,都總會遇上一些問題。

Fuzzer靈活性

特別當你的最終目標是能對多個不同目標使用同一個工具解決時,務必要確保你要使用的解決方案足夠靈活,以涵蓋所有的樣例。如果整個系統必須進行重構,或者最壞可能需要構建另一個系統,那麼對目標模糊測試會浪費大量的時間。

不同的工具也會揭示出不同的問題,以長遠眼光來看,總會有新的工具再次揭露出新的問題。所以元件切換,特別是裝置切換,是一個十分有價值的功能。

處理結果

如果你正在建立一個模糊測試系統,但你並不是實際發現問題解決問題的開發人員之一,那麼請聯絡那些正準備處理你係統得出結果的人員。他們想從模糊測試的的bug報告中得到什麼呀的資訊呢? 如果開發人員每天早上在收件箱裡一眼看到滿滿的像下面這樣的bug報告,那他們是真的很少意識到自己想要的資訊:

標題: 程式X出現了崩潰

描述: 附件裡的資料使得程式X崩潰了

附件:fuzz-test-case-1337 (22MB)

預設情況下, 錯誤報告至少應該包含重現問題的全部資訊。例如這樣但不限於這樣:配置資訊,使用的作業系統,目標的版本或構建版本號,CPU和記憶體資訊,以及適用的編譯器選項和除錯標誌。

模糊測試中使用的設定對於開發人員來說應該要很容易重現才行,並且你應該為每次對目標模糊測試所做的優化進行解釋。舉個例子,開發人員可能並不想解決只有在CRC校驗被關閉的情形下才能重現的問題,除非你能解釋清楚,當啟用CRC校驗時如何構造輸入也能重現bug。

自動化模糊測試還包括有:相似問題分類,測試用例最小化、迴歸範圍查詢、修復驗證、甚至可以提供像容器、虛擬機器、映像一樣配好的測試環境。

跟蹤進展

在長時間執行模糊測試之後,你可能沒有發現任何新錯誤,這表明可能是如下兩種情況之一:

你的模糊測試工作非常出色, 目標的魯棒性正在提高.

或者你的模糊測試卡住了, 一次又一次地重複相同的程式碼路徑.

正如模糊測試簡介那一節所述,模糊測試需要不斷的維護和提交,以保持長時間有效。你可以使用一些技術來幫助你確保你的模糊測試始終保持有效,並且還能對目標發生的一些變化進行測試。

如果你正在使用基於程式碼覆蓋率的fuzzer,你也許已經覆蓋到了該覆蓋的範圍。只要你的程式碼覆蓋率在模糊測試過程中持續上升,就無需擔心。但如果你的程式碼覆蓋率不再上升了,那你可能就遇到了一些需要更加深入分析的問題。

你不能僅根據現已覆蓋的程式碼行數獲知多少資訊。例如,目標可能有的程式碼行,沒有特別配置是無法執行的。或是可能存在無法到達的程式碼,使得無法完全覆蓋等等情況。使用工具來顯示執行測試樣例時,哪些部分的程式碼有執行,哪些沒有執行是相當有幫助的。將這次的結果與之前fuzzer跑出的結果,或其他自動化測試的結果進行比對,並檢查之前出現bug的位置,可以幫助確保你的模糊測試效果不會倒退,並且依舊涵蓋著所有相關的程式碼路徑。

如果還是少了一些未經過的程式碼路徑,那麼下一步就是分析如何讓你的測試用例生成器生成能觸發這些路徑的測試資料了。特別是對於那些基於模型的fuzzer,你經常會發現說fuzzer並沒有拿到模型實現所需要的資訊或欄位。變異測試的模糊器缺少程式碼覆蓋率的原因通常都是因為初始樣本檔案覆蓋率過低,或是對輸入的驗證過於嚴格所導致的。對於後一種情況,請考慮"fuzz-friendly mode"

沒有程式碼覆蓋反饋,事情就會變得有些棘手。如果你已經實現了"fuzz-friendly mode",那麼你有一個簡單的解決方案:那就是製造bug。在你fuzzer應該到達的位置新增適當的print函式,assert斷言或aborts函式,並根據實際這些位置的到達情況進行跟蹤。你也可以將類似的"bug"新增到之前存在bug的位置。

只要記住,你進行檢查的時候不應影響到你的fuzzer,使得你的fuzzer過於注重你剛剛新增"bug"的那部分程式碼,並且在最後的投入使用之前將這些檢查移除掉。你可以自動化測試可以使用舊的構建版本進行測試,那你還可以用那些有已知bug的版本來驗證你的fuzzer能否找到這些已知bug,測試那些已發現的舊bug同樣也是一個找到系統中有待提高之處的好辦法。



本文由看雪翻譯小組 Vancir 編譯,來源github@attekett ,轉載請註明來自看雪社群

更多資訊請關注看雪學院公眾號(ikanxue)檢視!

相關文章