在奧蘭多參加 Microsoft® Tech•Ed 2007 會議時,我有幸在“The Learning Center”的一個開發人員展位工作過。這次經歷讓我感觸最深的是圍繞最新的應用程序生命周期管 理 (ALM) 工具所展開的討論。同時還有大量關於熱門方法的討論,例如敏捷編程和測試驅動的開發 (TDD)。因此,Microsoft 的最新 ALM 套件 — Visual Studio® Team System (VSTS) 產品倍 受關注。
VSTS 為測試人員提供了一些強大的功能和可擴展性機會;本文的重點也在這裡。盡管如此,我應該要 說明的是,這不是一篇關於 TDD 的文章!實際上,我將介紹如何擴展 VSTS 的軟件測試功能。我將使用 一個稱為“模糊化”的安全性測試子集作為示例(稍後將詳細說明)。
什麼是 Visual Studio Team System?
VSTS 的服務器端組件稱為 Team Foundation Server (TFS)。客戶端組件 可以是 Visual Studio 的任何團隊版本。有關 VSTS 和 TFS 的詳細信息,請查看 msdn2.microsoft.com/teamsystem。
從技術角度講,Visual Studio Team Edition (VSTE) 產品 包含一組對 Visual Studio 2005 IDE 的擴展。不同的版本分別提供與特定職能(架構師、開發人員、測 試人員、數據庫專業人員)相關的擴展。目的是使特定於角色的功能與一致的用戶界面緊密結合。
Visual Studio 的最大優勢之一在於它的每個新版本都使之變得更具可擴展性。VSTE 就是這樣一 個例子;然而,有關產品可擴展性的更好的試金石是,不屬於該產品開發公司的人員實現它有多方便。
為此,我在此對 Visual Studio 2005 Team Edition for Software Testers 可擴展性的討論將 探討對現有測試接口提供程序 (TIP) 示例的修改,該示例已納入最新的 Visual Studio SDK。我還將詳 細介紹測試、部署和調試插件的相關信息。我的最終目標是獲得一個可以實現一種測試策略的 TIP,這種 策略最近廣受歡迎,特別是在安全測試人員當中:模糊測試。
什麼是模糊測試?
解釋模糊 測試(模糊化)的最好方法就是使用示例。最近以來,模糊化的最常見用處是驗證兩種類型的數據分析程 序:文件和網絡。
文件分析程序的一個例子是 Microsoft Word。對 Word 文件分析程序進行模糊 測試需要什麼?想象一下創建一百萬個長度和所包含數據均為隨機的 Word 文檔。假設這些文檔不是由 Word 自己創建,而實際上是通過將隨機的二進制數據源輸送到磁盤上的測試文件中創建的。在 Word 中 打開其中任意一個文件,看看會發生什麼。
這樣做可能會出現三種結果。第一種,對於大多數測 試文件,Word 會指出文件格式無法識別,或文件已破壞而無法打開等錯誤。第二種,一百萬個文件中的 少數文件可能實際上包含了某些可識別控件和可打印字符的組合。那些文件將會正常打開,沒有任何問題 。第三種,也是最有趣的情況是,可能有幾個文件包含了文件分析程序沒有預見到的數據。在這種情況下 ,程序可能會出故障。
對於安全性測試人員,第三類測試文檔才是真正有意義的情況。如果文件 分析程序出現故障,那意味著它存在 bug。如果有 bug,它可能是可利用的。例如,bug 可能會引起堆棧 損壞或堆損壞,從而造成故障。一般情況下,此類 bug 在聯網環境下屬於嚴重問題,因為各種各樣的文 件類型都可作為電子郵件附件分發。經驗表明,大多數用戶都會打開附件,而不管其性質。
在網 絡分析程序領域中也存在相似的問題。考慮一下典型的 PC、服務器或嵌入式網絡設備所能理解的協議數 量之多:DHCP(動態主機配置協議)、DNS、HTTP、SMB(服務器消息塊)和 SMTP 等等。每個協議實現都 包括一個針對來源於網絡的數據包的分析程序,而每個分析程序都可能需要一些復雜的邏輯。此類代碼也 往往會有許多邊界情況,從而使之難以驗證。
測試網絡協議代碼的一個方法是通過模糊化。但是 ,在這種情況下,我建議使用一種“向其提供隨機數據即可”方法(在上面的 Word 分析程序 上下文中說明)的變體。
假設我需要測試一個 DHCP 客戶端。再進一步假設 DHCP 客戶端代碼的某一特定部分特別糟糕,但是 該段代碼在某種程度上得到了分析程序另一部分(被認為較為可靠)的網絡保護。在這種情況下,用純隨 機數據模糊化 DHCP 客戶端不可能有效地使用時間,因為我預見代碼的可靠部分會篩選出大多數不正確的 數據。我改為采用一種更具針對性的方法。
實際上,根據我的經驗,在采用這種方式時模糊化最 有效。繼續該 DHCP 示例,我將模糊測試(模糊處理程序)配置成使用已知有效數據、故意錯誤數據和隨 機數據的組合,而不是用大量純隨機數據來測試該客戶端。使用已知有效數據的目的是跳過我不感興趣的 分析程序組件。使用故意錯誤數據的目的是利用已知或我懷疑將成為代碼中缺陷的情況。最後,使用隨機 數據的目的就是看看會發生什麼。
在模糊化的過程中,我的方法會隨著我對可疑行為的進一步了 解而得到完善。例如,我的測試 DHCP 包的某些部分最初包含了隨機數據,我隨後可能會在該處使用已知 的有效數據來進一步深入了解該分析程序。如果發現了錯誤,我會發送故意錯誤數據以試圖利用它。這種 啟發式方法是兩個領域的最佳方案:人類直覺和計算機自動化。
模糊化確實是功能強大的工具, 但是它在測試覆蓋率方面仍處於相對未知的領域。因此我發出以下行動呼吁(當然要在讀完本文其余部分 之後實現):選擇您熟悉的技術,或至少您感興趣的技術,為之編寫一個模糊處理程序。您多半可能會發 現 bug。
這裡有一個例子:我參加了今年在舊金山舉行的 RSA 會議上的一次智能模糊化演示。它 使我有一種沖動,我沖回酒店房間,並為曾一直仔細研究的一項技術編寫了一個模糊處理程序。使用自定 義的模糊處理程序,我應用了前面提到的迭代方法(即以隨機數據開始,然後對其微調),然後在幾小時 內就找到了有問題的地方。作為用戶,我能夠在作為 LocalSystem 運行的代碼中造成訪問沖突。
編寫測試接口提供程序
本文隨附的示例代碼是一個簡單模糊處理程序的實現。其目的有兩個方面 :顯示運行中的模糊測試以及演示模糊測試與 VSTE 自動化框架集成的示例。
該示例代碼包含以 下四個項目:
ConsoleApp — 被測試的應用程序。該代碼簡短而明了。此應用程的響應方式 是輸出一行文本或引發一個未處理的異常。
MyTest — 自定義的測試類型提供程序。該代碼 通常與示例不會有什麼不同,只是我將 MyTestAdapter 重命名為 FuzzTestAdapter,並添加了所有的代 碼以進行真正的測試。
MyTestUI — 自定義測試用戶界面的 C++ 代碼。它與原始示例一樣 。
TestProject — 用於調試的啟動項目。它很容易重新創建:只要創建一個新的測試項目 ,然後添加自定義測試類型即可。
我在本文稍後部分中將更詳細地討論這些項目。
必須有 Visual Studio 2005 SDK 才能構建該示例。該 SDK 可從 microsoft.com/downloads/details.aspx? familyid=51A5C65B-C020-4E08-8AC0-3EB9C06996F4 下載。SDK 的當前版本是 4.0,日期為 2/28/2007。 它包括我的示例所基於的原始示例;但是我在生成、安裝及運行原始示例時有些問題。雖然將現有的 “MyTest”示例轉變成本文討論的 FuzzTest 示例所需的改動很小,但是到達調試運行插件這 步所需的工作量卻不小。來自產品團隊的幫助和大量的反復嘗試已解決了這些問題,我稍後會詳細介紹這 些步驟。
再次說明,將原來的 MyTest 示例轉變成本文隨附的 FuzzTest 示例所進行的改動是非 常容易掌握的。實際上,使用 –T 開關運行 windiff.exe 來比較原來的和修改後的示例目錄樹, 即會顯示一個即有趣又具說明性的簡短更改列表。
第一個必要的更改是獲取基本 MyTest 示例中 缺少的兩個文件,至少是在先前引用的 Visual Studio SDK 版本中。這兩個文件是 mytest.ico 和 mytestui.rc。兩個文件都已由 VSTS 產品團隊提供給我,並包含在我的 FuzzTest 示例代碼中。它們都 會包括在解決方案 mytestui 子目錄下。
第二個代碼更改是針對 mytestui 項目文件。具體來說 ,生成後命令使用的是相對路徑,當我試圖在不同於其默認位置(也就是在 SDK 安裝樹中)的目錄中生 成示例項目時,該命令注定會失敗。此命令會啟動命令表配置編譯器 (ctc.exe),這是包含在 Visual Studio SDK 中的一個工具。有關 CTC 文件的詳細信息,請參閱 msdn2.microsoft.com/bb165048。
為了解決這個問題,我修改了項目以便從 ctc.exe 的默認絕對路徑啟動它,不可否認,這個方法有它 自身的缺點(例如,如果 SDK 不是安裝在 C: 驅動器),但是您至少會知道要查找什麼!
另請注 意,我之所以提到上述更改,是因為它們不僅與 FuzzTest 示例相關,而且還可在需要其他實驗時用來獲 取已生成的原始示例。還有一些重要的部署和調試問題需要注意,不過我將在本文稍後部分中再進行介紹 。
在將基本 MyTest 示例轉變成新測試插件的過程中,下一步是用 mytest\fuzztestadapter.cs 中新的 FuzzTestAdapter 實現來替代原來的 MyTestAdapter 類(位於 mytest\myhostadapter.cs 中,我已將其 刪除)。將這兩個文件復制到臨時目錄中,並在 mytestadapter.cs 和 fuzztestadapter.cs 上運行 windiff.exe,即可完全顯示創建新 TIP 所需的所有核心代碼更改。實際上,大多數有意思的更改都在 Run 函數中(請參見圖 1)。
Figure 1 IBaseAdapter::Run 方法的實現
[SuppressMessage("Microsoft.Design", "CA1031")] public void Run(ITestElement testElement, ITestContext testContext) { MyTestAssertHelper.ParameterNotNull(testElement, "testElement"); MyTest test = testElement as MyTest; MyTestAssertHelper.ParameterNotNull(test, "testElement"); MyTestResult result = new MyTestResult( new ComputerInfo(Environment.MachineName), m_runId, test); Stopwatch timer = null; try { // Start the timer for this test. timer = new Stopwatch(); timer.Start(); // This is a FUZZ Test: Loop until the test breaks, then report back // loop through unicode chars and pass into method bool ErrorFound = false; char ch; for (int i = 0; i < char.MaxValue; i++) { ch = Convert.ToChar(i); string testParam = ch.ToString(); string CommandLine = test.CommandLine.Replace( "<FuzzTestString>", "\"" + testParam + "\""); int spaceIndex = CommandLine.ToUpper().IndexOf(".EXE ") + 4; Process p = new Process(); p.StartInfo.FileName = CommandLine.Substring(0,spaceIndex); p.StartInfo.Arguments = CommandLine.Substring(spaceIndex+1); p.StartInfo.UseShellExecute = false; p.StartInfo.CreateNoWindow = true; p.StartInfo.RedirectStandardInput = true; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardError = true; p.Start(); StreamWriter sIn = p.StandardInput; StreamReader sOut = p.StandardOutput; StreamReader sErr = p.StandardError; sIn.AutoFlush = true; sIn.Write(Environment.NewLine); string OutputString = sOut.ReadToEnd(); string ErrorString = sErr.ReadToEnd(); p.WaitForExit(); if (ErrorString != "") { // Record the process exit code. ErrorFound = true; result.ProcessExitCode2 = -1;//failed result.Outcome = TestOutcome.Failed; result.ErrorMessage = "Test failed with input: " + CommandLine + ", Error Message: " + ErrorString; timer.Stop(); break; } } if (!ErrorFound) { // all tests succeeded: result.ProcessExitCode2 = 0; // success result.Outcome = TestOutcome.Passed; timer.Stop(); } } catch (Exception ex) { result.Outcome = TestOutcome.Failed; result.ErrorMessage = ex.ToString(); } finally { if (timer != null) { timer.Stop(); result.Duration = timer.Elapsed; } } testContext.ResultSink.AddResult(result); }
分析圖 1 中 Run 函數的代碼便可了解新 FuzzTestAdapter 的工作方式。簡言之,如果要在 VSTE 中 執行類型為 FuzzTestAdapter 的測試,第一步(在最初的一些對象初始化之後)是啟動一個計時器。測 試運行持續時間不一定是與模糊測試(與性能測試相對)相關的最有意思的統計信息,但是它作為 TestResultMessage 類的內置功能受到支持,所以我保留了它。
Run 中的下一步是為目標測試實例提取命令行配置選項,該選項指示如何運行它。請注意,依照目標 測試二進制文件的完整路徑,該插件預期將找到占位符字符串“<FuzzTestString>”。該字符串是 通過 MyTest 類的 CommandLine 成員作為整體進行檢索的。該字符串的用途是向用戶公開一種為不同的 測試重用 TIP 的便捷方法。這在我的 Fuzzer TIP 上下文中還會變得更加方便,因為命令行會在每次測 試循環時發生更改。
繼續介紹 Run 方法,下一步是實例化 System.Diagnostics.Process 對象,以便在本地計算機上啟動 測試程序。如果作為測試輸入(或其他任何原因)的結果,測試過程引發了異常或返回了錯誤,則會標記 測試失敗。請注意,我為 Run 方法的實現增設了 [SuppressMessage] 屬性。該屬性在此環境中是必需的 ,因為測試過程的執行由一個處理程序封裝,該處理程序將捕獲所有的異常。通常認為這是糟糕的做法, 而且在默認情況下 VSTS 靜態代碼分析程序會標記此做法,但在此種情況下需要采用這種做法,目的是確 保所有的不成功的測試完成情形都會作為失敗被捕獲和跟蹤。
為了總結對 Run 的討論,也為了說明由我的 TIP 實現的特定模糊測試類型,請觀察圖 1,前面兩段 中介紹的大多數邏輯都已封裝在一個循環中,該循環遍歷托管字符類型可以采用的每個可能值。最終結果 是使用每個可能的字符值調用測試二進制文件(本示例中為 ConsoleApp.exe),然後繼續,直到該循環 完成或測試程序無法正確處理該范圍中的某一個值。當然,出於演示目的,我已確保測試會失敗;否則它 就不是一個有意義的示例!
ConsoleApp 測試演示了如何使用模糊化公開錯誤(即使是在簡單的編程接口上)。在這種情況下,接 口的編寫是基於對要處理的字符值范圍的隱含而大膽的假設。根據 ConsoleApp 代碼改寫的以下代碼段體 現了此假設:
Int32 arraySize = Math.Max(Math.Max('Z', 'A'), Math.Max('z', 'a')) + 1;
charCountArray = new Int32[arraySize];
char c = inputString[0];
charCountArray[c] += 1;
在此代碼中,inputString 變量是從 FuzzTestAdapter 收到的第二個命令行參數。ASCII 碼大寫和 小寫字母范圍之外的任何字符值都會導致“數組索引超出范圍”錯誤。當模糊處理程序循環訪問其字符值 時,很快就會遇到造成代碼失敗的值。發生這種情況時,ConsoleApp 進程便會終止,並出現未處理的異 常。該結果會由模糊處理程序通過其 Process p 對象檢索到,並返回給 VSTE。
原始示例代碼中的 MyTestAdapter 替換成 FuzzTestAdapter 類後,我的實現中剩下的工作便是根據對後者的引用來重命名 對前者的引用。
至此我已解釋了將原始 TIP 示例轉變成 FuzzTestAdapter 所需的改動,接下來讓我 們了解一下生成、部署和調試。
生成
要生成示例 FuzzTestSample.sln 解決方案,可在 VSTE 中打開它,然後從頂層菜單中選擇“生成 ”|“生成解決方案”。如果您想從 SDK 包含的基本示例解決方案開始,則首先需要獲得缺少的兩個文件 ,它們包含在本文隨附的代碼下載中。
部署
原始 TIP 示例和更新後的模糊處理程序版本都包含了部署批處理腳本。腳本是為測試安裝 TIP 二進制文件的推薦方法。腳本假設它在生成二進制文件的同一台計算機上運行,而且在 Visual Studio 命令窗口內。
在運行腳本之前(但當然是在生成解決方案之後),我已發現在部署階段關閉 Visual Studio 是最安全的。不可否認這不太方便,但如果不執行這一步的話,部署期間的某些文件復制 操作就會失敗。
若要運行 Visual Studio 命令窗口,選擇“開始”|“所有程序”|“Microsoft Visual Studio 2005”|“Visual Studio 工具”|“Visual Studio 2005 命令提示”。然後運行 deploy.bat 安裝腳本。我對腳本略微做了改動;您可能也會發現需要對原始腳本進行一些調整(這取決 於在何處運行它)。看一下在用於部署二進制文件的 copy 和 xcopy 命令中使用的相對路徑。同時請注 意,腳本執行的最後一個命令是對 regpkg.exe 的調用,它基於 SDK 中 Visual Studio 集成工具目錄的 相對路徑。根據已使用 SDK 默認安裝目錄這一假設,我將此改成了使用 %ProgramFiles% Windows 環境 變量的絕對路徑。
最後,不要忘記這最後一步,因為它不是批處理文件的一部分!從 Visual Studio 命令窗口中,運行 devenv /setup 命令。此命令可以確保插件的資源元數據可以與其他已安裝 Visual Studio 包的元數據合並,並完成新插件的安裝。
在准備本文時,我向 Visual Studio 產品組詢問了 究竟如何使用 TIP。每個 Test Type 必須定義一個 TIP,它將被 TMI(測試管理接口)實例化。TMI 將 使用這個 TIP 從存儲加載測試、創建新測試、保存測試和解釋結果。
調試
部署完成後,下一步就是創建和運行新類型的測試。以下是我用來完成此過程的步驟:
1. 在 VSTE 中重新打開 FuzzTestSample 解決方案。
2.確保 TestProject 已設置為默認啟動項目。為此 ,請右鍵單擊“解決方案資源管理器”窗口中的“TestProject”,然後選擇“設為啟動項目”。完成後 ,“解決方案資源管理器”中的 TestProject 名稱應以粗體顯示,如圖 2 中屏幕快照的右側所示。
3.再次右鍵單擊“解決方案資源管理器”中的“TestProject”,然後選擇“添加”|“新測試”。
4.在出現的“新測試”窗口中,應該可以看到標題為 My Test 的測試類型。那是示例 TIP 中的新測 試類型,它的出現意味著注冊步驟已成功。選擇“My Test”圖標,然後單擊“確定”。
5.在新測試的 “命令行”字段中,將默認路徑替換為 ConsoleApp.exe 測試程序的完整路徑,加上預期的命令行值(在 前面有關 Run 例程的部分中介紹過)。例如,您可以輸入 “c:\projects\FuzzTestSample\ConsoleApp\bin\Debug\ConsoleApp.exe ProcessString <FuzzTestString>”。請參見圖 2 中左邊中間的窗格。
6.按 F5 鍵運行測試。
圖 2 帶有示例測試結果的 VSTE
此方法具有在調試器中運行 TIP 的優點。例如,在按 F5 之前,我可以在 MyTest\MyTest\FuzzTestAdapter.cs 中的 Run 函數上設置一個斷點,然後觀察它使用不同的字符值啟動 ConsoleApp.exe 的情況。當然,在生產階段,我不會希望插件在調試器中運行,因為那樣會大大減緩測 試執行的速度。
這個特定的測試需要一些時間才能完成。完成之後,測試失敗窗格(顯示在圖 2 中屏幕快照的底部) 就會出現。從顯示在測試失敗窗格中的錯誤文本可以看出,就像預期的那樣,顯然是非字母字符引發了 IndexOutOfRange 異常。
值得提及的是我學到的一個方法,即,在成功的最初部署之後對自定義測試類型進行代碼更改。根據 實驗結果,要以能夠再次調試插件的方式重新部署它,這一系列步驟似乎是最低要求。
1.進行必要的代碼更改並重新生成解決方案(增量式也可以)。
2.關閉 Visual Studio。
3.將更新後的文件 Microsoft.VisualStudio.QualityTools.Samples.MyTestTIP.dll 從解決方案輸出 目錄復制到其部署的位置。默認情況下,後者是 C:\Program Files\Microsoft Visual Studio 8 \Common7\IDE\PrivateAssemblies。
4.在 Visual Studio 中重新打開該解決方案。
5.添加新測試(例如 MyTest2.mytest)。
模糊處理程序已經成功實現...
有效的自動化被視為大多數軟件質量保證團隊的終極目標。事實已經證明,要自動發現許多類別的缺 陷(包括安全性 bug)相當困難。啟發式方法(例如本文開始時介紹的自適應模糊化方法)與自動化框架 (例如 Visual Studio Team Edition)的結合,提供了兩個領域的優勢。也就是說,人類直覺加上計算 機對無聊重復性工作的承受力,等於完美的測試覆蓋率!
在此,我想特別感謝 Microsoft 的 Euan Garden 和 Jeff Wang,以及 Personify Design 的 Juan Perez 對本文的貢獻。
本文配套源碼:http://www.bianceng.net/dotnet/201212/768.htm