目前最常見的安全問題是緩衝區溢位。這個特別的安全問題引發的病毒感染可能比其它原因引發的病毒感染數量的總和還要多。市場上幾乎每個應用程式和作業系統都存在駭客可能利用的緩衝區溢位漏洞。這個問題對於微軟Windows來說是如此嚴重,以至於微軟在產品的新版本(例如Windows XP Service Pack 2)中採用了一種完全不同的解決方案。本文的目的是協助你更清晰地認識緩衝區溢位,並提供了幾種用於減少(或者是消除)Visual C++應用程式緩衝區溢位問題的技術。
導航:
什麼是緩衝區溢位
緩衝區溢位證明了一個觀點:除非你看著使用者與你的應用程式互動操作,否則你根本就不知道使用者會嚮應用程式輸入什麼樣的資料。
驗證資料的範圍
程式設計語言提供的大多數資料範圍反映的都是下層硬體的實際情況,而不是現實世界的需要。例如,當你在代碼中把某個值定義為Int32的時候,就意味著使用者輸入的值應該在-2,147,483,648到2,147,483,647之間。
驗證資料的長度
有些資料類型不太容易進行快速檢查。例如,字串可以包含任意數量的字元,其數量最多隻受到.NET架構組件和機器的限制。當然,很少人真的需要這麼長的字串。通常開發人員要求字串有一個最小和最大的長度範圍。
排除非法的字元
駭客經常在輸入資訊中包含一些額外的非法字元,以瞭解會發生什麼情況。例如,駭客通常會通過添加特定的字元建立指令碼。在很多情況下,系統在沒有提供任何警告的情況下就會執行指令碼,賦予駭客訪問系統的權利。
提供進階的使用者協助
很多開發人員都不能把協助與良好的安全性聯絡到一起,但是良好的協助的確可以減少使用者犯錯誤來提高安全性。
什麼是緩衝區溢位
緩衝區溢位證明了一個觀點:除非你看著使用者與你的應用程式互動操作,否則你根本就不知道使用者會嚮應用程式輸入什麼樣的資料。這些攻擊依賴於一些奇怪的想法:駭客給應用程式提供的輸入資訊可能超過了緩衝區的長度,結果這些額外的(超出緩衝區長度的)資訊覆蓋了緩衝區控制之外的記憶體。在某些情況下,這些記憶體實際上儲存著可執行資訊(heap memory overrun,堆儲存泛濫),從而使應用程式不運行原始的可執行代碼,而是運行駭客的代碼;在另外一些情形中,駭客則覆蓋了應用程式的棧頁面(stack memory overrun,棧儲存泛濫)。
有些駭客甚至於分析你的代碼,尋找位置以供堆或棧儲存泛濫利用。但是在有些情況下,當駭客試圖向某個欄位輸入一些資訊,查看發生什麼情況的時候,這種利用可能被發現。例如,駭客可能試圖輸入一段簡單的代碼,看你的應用程式是否會執行它。不管該駭客是如何發現漏洞的,其結果都是相同的:你的應用程式失去了對駭客代碼的控制權--駭客現在可以享受那些曾經是你的應用程式才能享受的權力了。
很多開發人員認為駭客會通過某些秘密的通道來利用他們所建立的程式,但是很多利用方法是非常簡單的--讓作業系統顯示命令提示字元這樣的行為在某些情況下就足以擷取控制權了。如果系統的安全性稍微有一點鬆懈,駭客就可以擷取伺服器的控制權。至少,命令提示字元允許駭客探測系統的狀況,採用其它的某種方式來擷取更多的訪問權。駭客不需要在第一次嘗試的時候就獲得系統的控制權。他們所需要的是擷取累積起來的點點滴滴的控制權。
很明顯,如果要保證應用程式免受緩衝區泛濫的傷害,你就必須為應用程式提供某種保護措施。控制緩衝區泛濫的最好的方法是檢查程式收到的所有輸入資訊,即使這些資訊來自受信任的來源。本文考慮了每個程式應該執行的四個基本的檢查:檢查資料範圍、驗證資料長度、排除非法字元、為使用者提供足夠的協助以確保良好的輸入。
驗證資料的範圍
程式設計語言提供的大多數資料範圍反映的都是下層硬體的實際情況,而不是現實世界的需要。例如,當你在代碼中把某個值定義為Int32的時候,就意味著使用者輸入的值應該在-2,147,483,648到2,147,483,647之間。這個數字是依賴於硬體條件的,電腦使用31位儲存資料,1位儲存符號(2^31 = 2,147,483,648)。但是,你的應用程式可能沒有查明可接受的範圍。
當硬體需求與應用程式的現實需求不一致的時候,你就必須在應用程式中包含特定的代碼來檢查潛在的錯誤條件。你在代碼中可能希望接受1到40,000的數字,它超出了Int16的值範圍,但是在Int32的值範圍中。列表1顯示了這類檢查的例子。
列表1.檢查資料範圍錯誤
System::Void btnDataRange_Click(System::Object * sender, System::EventArgs * e) { Int32 TestData; // 保持輸入的值 try { // 永遠需要首先嘗試分析資料 TestData = Int32::Parse(txtInput1->Text); } catch (System::OverflowException *OE) { // 溢出錯誤處理 MessageBox::Show(S"Type a value between 1 and 40,000.", S"Input Error", MessageBoxButtons::OK, MessageBoxIcon::Error); return; } catch (System::FormatException *FE) { //溢出錯誤處理 MessageBox::Show(S"Type the number without extra charaters.", S"Input Error", MessageBoxButtons::OK, MessageBoxIcon::Error); return; } // 測試特定的資料範圍 if (TestData < 1 || TestData > 40000) //溢出錯誤處理 MessageBox::Show(S"Type a value between 1 and 40,000.", S"Input Error", MessageBoxButtons::OK, MessageBoxIcon::Error); } |
請注意,這段代碼首先使用Parse()方法把輸入資訊轉換成Int32類型。這種簡單的轉換可以為很多輸入方面的問題進行定位。在這個例子中,代碼使用System::OverflowException異常檢查值是否太大或太小,使用System::FormatException異常檢查值的格式是否正確。在代碼確保輸入資訊是一個合理的Int32值之後,接著檢查實際的輸入範圍。
值的資料類型是最容易檢查的,因為它們都有特定的範圍。值與對象不同,它沒有隱藏的元素,使開發人員感到驚訝的地方很少。
一般來說,用於驗證值資料類型的所有事務是在代碼中定義上下邊界,接著對值進行檢查。
當我們使用對象的時候,資料值驗證的問題就出現了。例如,你希望使用者把幾個字串中的一個作為輸入資訊,那麼使用列表框來減少使用者的輸入選擇是有協助的。當使用者面對只有數個選項的列表框的時候,他們是不可能輸入無效資訊(例如指令碼)的。
有時候你必須為問題設計獨特的方案。例如,你如何確保某個特定的方法接收數量固定的、範圍不連續的輸入資訊?在這種情況下枚舉(enumeration)可能會節約時間。列表2顯示了在代碼中如何把枚舉用於自動化的資料範圍變化。
類表2:使用枚舉檢查資料的範圍
請注意,DisplayString()的聲明需要一個SomeStrings枚舉類型的輸入資訊(參數)。調用者不可能使用其它的任何輸入類型,這意味著DisplayString()方法自動地受到了保護。例如,你不可能把某個指令碼作為輸入資訊,因為它不是正確的類型。
驗證資料的長度
有些資料類型不太容易進行快速檢查。例如,字串可以包含任意數量的字元,其數量最多隻受到.NET架構組件和機器的限制。當然,很少人真的需要這麼長的字串。通常開發人員要求字串有一個最小和最大的長度範圍。因此,你不需要驗證接收到的是否是字串,只需要驗證它的長度是否正確。否則,其他人可能發送任意長度的字串,而這樣就會導致緩衝區泛濫。列表3顯示了通過驗證每個參數的資料長度來防止發生問題的例子。
列表3:驗證資料的長度
System::Boolean ProcessData(String *Input, Int32 UpperLimit, Int32 LowerLimit) { StringBuilder *ErrorMsg; // 錯誤資訊 // 檢查輸入資訊錯誤 if (UpperLimit < LowerLimit) { // 建立錯誤訊息 ErrorMsg = new StringBuilder(); ErrorMsg->Append(S"The UpperLimit input must be greater than "); ErrorMsg->Append(S"the LowerLimit number."); // 定義新的錯誤 System::ArgumentException *AE; AE = new ArgumentException(ErrorMsg->ToString(),S"UpperLimit"); // 拋出錯誤 throw(AE); } // 檢查資料長度錯誤條件 if (Input->Length < LowerLimit || Input->Length > UpperLimit) { // 建立錯誤資訊 ErrorMsg = new StringBuilder(); ErrorMsg->Append(S"String is the wrong length. Use a string "); ErrorMsg->Append(S"between 4 and 8 characters long."); // 定義新的錯誤 System::Security::SecurityException *SE; SE = new SecurityException(ErrorMsg->ToString()); //拋出錯誤 throw(SE); } // 如果資料是正確的就返回true return true; } System::Void btnDataLength_Click(System::Object * sender, System::EventArgs * e) { try { // 處理輸入文本 if (ProcessData(txtInput2->Text, 8, 4)) // 顯示正確輸入的結果資訊 MessageBox::Show(txtInput2->Text, "Input String", MessageBoxButtons::OK, MessageBoxIcon::Information); } catch (System::Security::SecurityException *SE) { // 顯示錯誤輸入的錯誤資訊 MessageBox::Show(SE->Message, "Input Error", MessageBoxButtons::OK, MessageBoxIcon::Error); } catch (System::ArgumentException *AE) { // 顯示錯誤輸入的錯誤資訊 MessageBox::Show(AE->Message, "Argument Error", MessageBoxButtons::OK, MessageBoxIcon::Error); } } |
驗證過程發生在ProcessData()方法中,該方法把輸入的字串、最小的字串長度、最大的字串長度作為輸入資訊。請注意,這段代碼首先驗證輸入參數是否正確。UpperLimit參數必須比LowerLimit參數大。這部分代碼示範了良好的編程習慣--永遠不要相信你接收到的輸入資訊。請注意,這部分代碼產生System::ArgumentException異常而不是通用的異常。雖然特定的異常表現更好,但是大多數開發人員還是使用通用的異常。如果.NET架構組件不能為你的代碼需求提供特定的異常,你應該建立定製的異常。
代碼接著驗證字串。如果字串的字元數量太多或者太少,代碼就產生 System::Security::SecurityException異常。在這兒使用安全性異常是正確的,因為這類事件就會導致安全性異常。使用者可能決定輸入長字串以創造緩衝區溢位的條件。即使使用者只是犯了一個錯誤,你引發這個安全性異常意味著你至少可以驗證這個異常的起因,而不是簡單地跳過去。
這個例子的測試代碼在btnDataLength_Click()方法之中。這段代碼在try...catch代碼塊中執行以確保異常都會被捕捉到。真正的檢查只是一個簡單的if語句。這段代碼為每個異常都包含了catch語句。如果你希望確保應用程式注意到任何安全性異常並適當地作出處理,那麼捕捉異常就很重要了。
排除非法的字元
駭客經常在輸入資訊中包含一些額外的非法字元,以瞭解會發生什麼情況。例如,駭客通常會通過添加特定的字元建立指令碼。在很多情況下,系統在沒有提供任何警告的情況下就會執行指令碼,賦予駭客訪問系統的權利。對於這種利用方式來說,Web應用程式比傳統型應用程式受的影響更大,但是兩者你都必須受到保護。
幸運的是,.NET架構組件提供了強大的合格運算式(regular expression)支援。合格運算式定義了可接受的字串輸入,因此你可以輕易地檢測到非法的字元。列表4顯示了使用合格運算式的一個方法。
列表4:使用合格運算式
代碼開頭包含了Regex對象。在這種情況下,唯一可以接受的輸入是字母(甚至於不能包含空格)。合格運算式旁路了大量的輸入資訊。實際上,為ASP.NET應用程式提供的很多驗證支援中定義了很多的預設範本。其要點在於你可以建立一個字串,它定義了可接受的輸入資訊,包含了輸入樣式(例如電話號碼)。
Regex對象可以執行很多比較操作。在例子中它使用Matches()方法對比字串的長度和參照的數字。當這兩個數字匹配的時候,輸入資訊就是正確的。否則,輸入資訊就包含了非法的字元,CheckChars()方法會引發異常。
提供進階的使用者協助
很多開發人員都不能把協助與良好的安全性聯絡到一起,但是良好的協助的確可以減少使用者犯錯誤來提高安全性。例如,良好的協助檔案可以通過顯示應用程式希望接收的資訊,從而防止某類使用者輸入錯誤資訊。減少輸入錯誤可以使我們徹底地分析遺留的錯誤資訊,並最終減少不正確輸入帶來的安全風險。
協助可以來自於所有形式,包括有用的錯誤訊息。某些資料類型會提出一些特殊的挑戰,而你的應用程式必須處理這些問題以確保資料完整性和安全性。例如,日期就是經常會出現問題的一個資料輸入條目。首先,你必須考慮日期的格式。使用者可能輸入1 June 2003、06/01/2003、June 1, 2003、2003/06/01或其它可接受的變數。你應該約束自己的應用程式,只允許一種日期格式以便於檢查日期資訊的有效性。但是錯誤訊息和協助檔案可以告訴使用者必須使用哪種格式,這樣使用者使用錯誤格式輸入一個有效日期的時候就不會感到沮喪(因為有協助提醒格式)。
無論你怎樣做,仍然有一些使用者試圖濫用系統。他們可能使用錯誤的格式輸入日期,甚至於輸入根本不包含日期的資訊。但是,通過提供良好的協助,你就擁有了用於詢問使用者的基本要素了。你可以調用安全性措施來確保使用者知道這種行為是不可接受的。減少緩衝區溢位是一個主動的過程。你必須防止無效的輸入、為使用者提供良好的協助、並給決心忽視規則的使用者懲罰性的措施。