新聞中心
模糊測試(Fuzz testing )是一項對代碼質(zhì)量有著深遠(yuǎn)影響的簡單技術(shù)。在本文中,Elliotte Rusty Harold 故意將隨機的壞數(shù)據(jù)插入應(yīng)用程序,以觀察發(fā)生的結(jié)果。他也解釋了如何使用如校驗和、XML 數(shù)據(jù)存儲及代碼驗證等防護(hù)性編碼技術(shù),來加固您的程序以抵制隨機數(shù)據(jù)。他以一個練習(xí)進(jìn)行總結(jié),在練習(xí)中他以一個代碼破壞者的角度進(jìn)行思考 —— 這是一種用于防護(hù)代碼的至關(guān)重要的技術(shù)

多年來,我驚嘆于有如此大量能夠使 Microsoft Word 崩潰的壞文件。少數(shù)字節(jié)錯位,會使整個應(yīng)用程序毀于一旦。在舊式的、無內(nèi)存保護(hù)的操作系統(tǒng)中,整個計算機通常就這樣宕掉了。Word 為什么不能意識到它接收到了壞的數(shù)據(jù),并發(fā)出一條錯誤信息呢?為什么它會僅僅因為少數(shù)字節(jié)被損壞就破壞自己的棧、堆呢?當(dāng)然,Word 并不是惟一一個面對畸形文件時表現(xiàn)得如此糟糕的程序。
本文介紹了一種試圖避免這種災(zāi)難的技術(shù)。在模糊測試中,用隨機壞數(shù)據(jù)(也稱做 fuzz)攻擊一個程序,然后等著觀察哪里遭到了破壞。模糊測試的技巧在于,它是不符合邏輯的:自動模糊測試不去猜測哪個數(shù)據(jù)會導(dǎo)致破壞(就像人工測試員那樣),而是將盡可能多的雜亂數(shù)據(jù)投入程序中。由這個測試驗證過的失敗模式通常對程序員來說是個徹底的震憾,因為任何按邏輯思考的人都不會想到這種失敗。
模糊測試是一項簡單的技術(shù),但它卻能揭示出程序中的重要 bug。它能夠驗證出現(xiàn)實世界中的錯誤模式并在您的軟件發(fā)貨前對潛在的應(yīng)當(dāng)被堵塞的攻擊渠道進(jìn)行提示。
模糊測試如何運行
模糊測試的實現(xiàn)是一個非常簡單的過程:
- 準(zhǔn)備一份插入程序中的正確的文件。
- 用隨機數(shù)據(jù)替換該文件的某些部分。
- 用程序打開文件。
- 觀察破壞了什么。
可以用任意多種方式改變該隨機數(shù)據(jù)。例如,可以將整個文件打亂,而不是僅替換其中的一部分,也可以將該文件限制為 ASCII 文本或非零字節(jié)。不管用什么方式進(jìn)行分割,關(guān)鍵是將大量隨機數(shù)據(jù)放入應(yīng)用程序并觀察出故障的是什么。
測試基于 C 的應(yīng)用程序:當(dāng)字符串包含額外的零時,許多用 C 編寫的程序都會出問題 —— 這類問題太過頻繁以至于額外的零能夠徹底隱藏代碼中其他的問題。一旦驗證出程序存在零字節(jié)問題,就可以移除它們,從而讓其他的問題浮現(xiàn)出來。
可以手動進(jìn)行初始化測試,但要想達(dá)到最佳的效果則確實需要采用自動化模糊測試。在這種情況下,當(dāng)面臨破壞輸入時首先需要為應(yīng)用程序定義適當(dāng)?shù)腻e誤行為。(如果當(dāng)輸入數(shù)據(jù)被破壞時,您發(fā)現(xiàn)程序正常運行,且未定義發(fā)生的事件,那么這就是第一個 bug。)隨后將隨機數(shù)據(jù)傳遞到程序中直到找到了一個文件,該文件不會觸發(fā)適當(dāng)?shù)腻e誤對話框、消息、異常,等等。存儲并記錄該文件,這樣就能在稍后重現(xiàn)該問題。如此重復(fù)。
盡管模糊測試通常需要一些手動編碼,但還有一些工具能提供幫助。例如,清單 1 顯示了一個簡單的 Java? 類,該類隨機更改文件的特定長度。我常愿意在開始的幾個字節(jié)后面啟動模糊測試,因為程序似乎更可能注意到早期的錯誤而不是后面的錯誤。(您的目的是想找到程序未檢測到的錯誤,而不是尋找已經(jīng)檢測到的。)
清單 1. 用隨機數(shù)據(jù)替換文件部分的類
- import java.io.*;
- import java.security.SecureRandom;
- import java.util.Random;
- public class Fuzzer {
- private Random random = new SecureRandom();
- private int count = 1;
- public File fuzz(File in, int start, int length) throws IOException
- {
- byte[] data = new byte[(int) in.length()];
- DataInputStream din = new DataInputStream(new FileInputStream(in));
- din.readFully(data);
- fuzz(data, start, length);
- String name = "fuzz_" + count + "_" + in.getName();
- File fout = new File(name);
- FileOutputStream out = new FileOutputStream(fout);
- out.write(data);
- out.close();
- din.close();
- count++;
- return fout;
- }
- // Modifies byte array in place
- public void fuzz(byte[] in, int start, int length) {
- byte[] fuzz = new byte[length];
- random.nextBytes(fuzz);
- System.arraycopy(fuzz, 0, in, start, fuzz.length);
- }
- }
模糊測試文件很簡單。將其傳至應(yīng)用程序通常不那么困難。如 AppleScript 或 Perl 腳本語言通常是編寫模糊測試的最佳選擇。對于 GUI 程序,最困難的部分是辨認(rèn)出應(yīng)用程序是否檢測出正確的故障模式。有時,最簡單的方法是讓一個人坐在程序前將每一個測試通過或失敗的結(jié)果都標(biāo)記下來。一定要將所有生成的隨機測試用例單獨地命名并保存下來,這樣就能夠重現(xiàn)這個過程中檢測到的任何故障。
防護(hù)性編碼
可靠的編碼遵循了這樣的基本原則:絕不會讓程序中插入未經(jīng)過一致性及合理性驗證的外部數(shù)據(jù)。
如果從文件中讀入一個數(shù)字并期望其為正數(shù),那么,在使用其進(jìn)行進(jìn)一步處理前對其先驗證一下。如果期望字符串只包含 ASCII 字母,請確定它確實是這樣。如果認(rèn)為文件包含一個四字節(jié)的整數(shù)倍的數(shù)據(jù),請驗證一下。一定不要假設(shè)任何外部提供的數(shù)據(jù)中的字符都會如您所料。
最常見的錯誤是做出這樣的假設(shè):因為程序?qū)⒃摂?shù)據(jù)寫出,該程序就能不用驗證再一次將該數(shù)據(jù)讀回去。這是很危險的!因為該數(shù)據(jù)很可能已經(jīng)被另一個程序在磁盤上復(fù)寫過了。它也可能已經(jīng)被一個故障磁盤或壞的網(wǎng)絡(luò)傳輸所破壞了或已經(jīng)被另一個帶 bug 的程序更改過了。它甚至可能已經(jīng)被故意更改過以破壞程序的安全性。所以不要假設(shè)任何事,要進(jìn)行驗證。
當(dāng)然,錯誤處理及驗證十分令人生厭,也很不方便,并被全世界程序員們所輕視。計算機的誕生已進(jìn)入了六十個年頭,我們?nèi)耘f沒有檢查基本的東西,如成功打開一個文件及內(nèi)存分配是否成功。讓程序員們在閱讀一個文件時測試每一個字節(jié)和每一個不變量似乎是無望的 —— 但不這樣做就會使程序易被模糊攻擊。幸運的是,可以尋求幫助。恰當(dāng)使用現(xiàn)代工具和技術(shù)能夠顯著減輕加固應(yīng)用程序的痛苦,特別是如下三種技術(shù)更為突出:
- 校驗和
- 基于語法的格式,如 XML
- 驗證過的代碼如 Java
用校驗和進(jìn)行的模糊試驗
能夠保護(hù)程序抵御模糊攻擊的最簡單的方法是將一個檢驗和添加到數(shù)據(jù)中。例如,可以將文件中所有的字節(jié)都累加起來,然后取其除以 256 的余數(shù)。將得到的值存儲到文件尾部的一個額外字節(jié)中。然后,在輸入數(shù)據(jù)前,先驗證檢驗和是否匹配。這項簡單模式將未被發(fā)現(xiàn)的意外故障的風(fēng)險降低到約 1/256 。
健壯的校驗和算法如 MD5 和 SHA 并不僅僅取其除以 256 的余數(shù),它完成的要多得多。在 Java 語言中,java.security.DigestInputStream 和 java.security.DigestOutputStream 類為將一個校驗和附屬到數(shù)據(jù)中提供了便捷的方式。使用這些校驗和算法中的一種可以將程序遭受意外破壞的機率降低到少于十億分之一(盡管故意攻擊仍有可能)。
XML 存儲及驗證
將數(shù)據(jù)以 XML 形式存儲是一種避免數(shù)據(jù)損壞的好方法。XML 最初即著力于 Web 頁面、書籍、詩歌、文章及相似文檔,它幾乎在每個領(lǐng)域都獲取了巨大的成功,從金融數(shù)據(jù)到矢量圖形到序列化對象等等。
不切實際的限定
如果真想要破壞一個 XML 解析器,有幾種方法可以試試。例如,大多數(shù) XML 解析器服從于特定的最大尺寸。如果一個元素名長度超過 22 億字符(Java String 的最大尺寸),SAX 解析器將會失敗。盡管如此,在實踐中這些極限值如此之高,以至于在達(dá)到之前內(nèi)存就已經(jīng)耗盡。
使 XML 格式抵制模糊攻擊的關(guān)鍵特征是一個對輸入不做任何 假設(shè)的解析器。這就是真正想在一個健壯的文件格式中所獲得的。設(shè)計 XML 解析器是為了讓任何輸入(格式良好的或無格式的,有效的或無效的)都以定義好的形式處理。XML 解析器能夠處理任何 字節(jié)流。如果數(shù)據(jù)首先通過了 XML 解析器,則僅需要準(zhǔn)備好接受解析器所能提供的東西。例如,不需要檢查數(shù)據(jù)是否包含空字符,因為 XML 解析器絕不會傳送一個空值。如果 XML 解析器在其輸入中看到一個空字符,它就會發(fā)出異常并停止處理。當(dāng)然還需要處理這個異常,但編寫一個 catch 塊來處理檢測到的錯誤比起編寫代碼來檢測所有可能的錯誤來說要簡單得多。
為使程序更加安全,可以用 DTD 和/或模式來驗證文檔。這不僅檢查了 XML 是否格式良好,而且至少與所預(yù)期更加接近。驗證并不會告知關(guān)于文檔所需了解的一切,但它卻使編寫大量簡單檢查變得很簡單。用 XML,很明顯能夠?qū)⑺邮艿奈臋n嚴(yán)格地限定為能夠處理的格式。
盡管如此,還有多段代碼不能用 DTD 或模式進(jìn)行驗證。例如,不能測試發(fā)票上商品的價格是否和數(shù)據(jù)庫中庫存商品的價格一致。當(dāng)從客戶接收到一份包含價格的訂單文檔時,不論其是 XML 格式或是其他格式,在提交前通常都會檢查一下,以確??蛻舨⑽葱薷膬r格??梢杂枚ㄖ拼a實現(xiàn)這些最后的檢查。
基于語法的格式
使 XML 能夠?qū)δ:艟哂腥绱说牡钟芰Φ氖瞧涫褂冒涂扑?諾爾范式(Backus-Naur Form,BNF)語法仔細(xì)且標(biāo)準(zhǔn)地定義的格式。許多解析器都是使用如 JavaCC 或 Bison 等解析器-生成器工具直接從此語法中構(gòu)建的。這種工具的實質(zhì)是閱讀一個任意的輸入流并確定其是否符合此語法。
如果 XML 并不適合于您的文件格式,您仍可以從基于解析器的解決方案的健壯性中獲益。您必須為文件格式自行編寫語法,隨后開發(fā)自己的解析器來閱讀它。相比使用唾手可得的 XML 解析器,開發(fā)自己的解析器需要更多的工作。然而它是一個更為健壯的解決方案,而不是不根據(jù)語法正式地進(jìn)行驗證就將數(shù)據(jù)簡單地裝載到內(nèi)存中。
Java 代碼驗證
由模糊測試導(dǎo)致的許多故障都是內(nèi)存分配錯誤及緩沖器溢出的結(jié)果。用一種安全的垃圾收集語言(在如 Java 或 managed C# 等虛擬機上執(zhí)行的)來編寫應(yīng)用程序避免了許多潛在問題。即使用 C 或 C++ 來編寫代碼,還是需要使用一個可靠的垃圾收集庫。在 2006 年,臺式機程序員或服務(wù)器程序員不應(yīng)該還需要管理內(nèi)存。
Java 運行時對其自身的代碼起到了額外保護(hù)層的作用。在將一個 .class 文件裝載到虛擬機之前,該文件要由一個字節(jié)符驗證器或一個可選的 SecurityManager 進(jìn)行驗證。Java 并不假設(shè)創(chuàng)建 .class 文件的編譯器沒有 bug 且運轉(zhuǎn)正常。設(shè)計 Java 語言之初就是為了允許在一個安全沙箱中運行不信任的、潛在惡意的代碼。它甚至不信任其自身編譯過的代碼。畢竟,也許有人已經(jīng)用十六進(jìn)制編輯器手工修改了字節(jié)符,試圖觸發(fā)緩沖器溢出。我們大家都應(yīng)該對我們的程序也有對輸入這樣的偏執(zhí)。
以敵人的角度思考
之前介紹的每項技術(shù)都在阻止意外破壞方面造詣頗深。將它們綜合起來恰當(dāng)?shù)貙崿F(xiàn),會將未被發(fā)現(xiàn)的非蓄意破壞發(fā)生的可能性幾乎減少到零。(當(dāng)然,并不會減少到零,但其發(fā)生的可能性就如同一束偏離軌道的宇宙射線將您 CPU 運算 1+1 的結(jié)果變?yōu)?3 的可能性一樣微乎其微。)但不是所有的數(shù)據(jù)損壞都是非蓄意的。如果有人故意引入壞數(shù)據(jù)來破壞程序的安全性又該如何呢?以一個攻擊者的角度進(jìn)行思考是防護(hù)代碼的下一個步驟。
轉(zhuǎn)回到一個攻擊者的角度進(jìn)行思考,假設(shè)要攻擊的應(yīng)用程序是用 Java 編程語言編寫的、使用非本地代碼且將所有額外數(shù)據(jù)都以 XML(在接受前經(jīng)過徹底驗證)形式存儲,還能成功攻擊嗎?是的,能。但用隨機改變文件字節(jié)的低級方法顯然不行。需要一種更為復(fù)雜的方法來說明程序自身的錯誤檢測機制及路徑。
當(dāng)測試一個抵御模糊攻擊的應(yīng)用程序時,不可能做純黑盒測試,但通過一些明顯的修改,基本的想法還是可以應(yīng)用的。例如,考慮校驗和,如果文件格式包含一個校驗和,在將文件傳至應(yīng)用程序前僅僅修改此校驗和就可以使其同隨機數(shù)據(jù)相匹配。
對于 XML,試著模糊單獨的元素內(nèi)容和屬性值,而不是從文檔中挑選一部分隨機的字節(jié)進(jìn)行替換。一定要用合法的 XML 字符替換數(shù)據(jù),而不要用隨機字節(jié),因為即使一百字節(jié)的隨機數(shù)據(jù)也一定是畸形的。也可以改變元素名稱和屬性名稱,只要細(xì)心地確保得到的文檔格式仍是正確的就可以了。如果該 XML 文檔是由一個限制非常嚴(yán)格的模式進(jìn)行檢查的,還需要計算出該模式?jīng)]有 檢查什么,以決定在哪里進(jìn)行有效的模糊。
一個結(jié)合了對剩余數(shù)據(jù)進(jìn)行代碼級驗證的真正嚴(yán)格的模式也許不會留下可操縱的空間。這就是作為一個開發(fā)人員所需要追求的。應(yīng)用程序應(yīng)能夠處理所發(fā)送的任何有意義的字節(jié)流,而不會因無效而拒絕。
結(jié)束語
模糊測試能夠說明 bug 在程序中的出現(xiàn)。并不證明不存在這樣的 bug。而且,通過模糊測試會極大地提高您對應(yīng)用程序的健壯性及抵御意外輸入的安全性的自信心。如果您用 24 小時對程序進(jìn)行模糊測試而其依然無事,那么隨后同種類型的攻擊就不大可能再危及到它。(并不是不可能,提醒您,只是可能性很小。)如果模糊測試揭示出程序中的 bug,就應(yīng)該進(jìn)行修正,而不是當(dāng) bug 隨機出現(xiàn)時再對付它們。模糊測試通過明智地使用校驗和、XML、垃圾收集和/或基于語法的文件格式,更有效地從根本上加固了文件格式。
模糊測試是一項用于驗證程序中真實錯誤的重要工具,也是所有意識到安全性問題且著力于程序健壯性的程序員們的工具箱中所必備的工具。
【本文是專欄作者elknot的原創(chuàng)文章,轉(zhuǎn)載請通過獲取授權(quán)】
新聞標(biāo)題:關(guān)于模糊測試(FuzzTesting)
分享鏈接:http://www.dlmjj.cn/article/djjiecd.html


咨詢
建站咨詢
