目前最常見的安全問題是緩沖區溢出。這個特別的安全問題引發的病毒感染可能比其它原因引發的病毒感染數量的總和還要多。市場上幾乎每個應用程序和操作系統都存在黑客可能利用的緩沖區溢出漏洞。這個問題對于微軟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或其它可接受的變量。你應該約束自己的應用程序,只允許一種日期格式以便于檢查日期信息的有效性。但是錯誤消息和幫助文件可以告訴用戶必須使用哪種格式,這樣用戶使用錯誤格式輸入一個有效日期的時候就不會感到沮喪(因為有幫助提醒格式)。
無論你怎樣做,仍然有一些用戶試圖濫用系統。他們可能使用錯誤的格式輸入日期,甚至于輸入根本不包含日期的信息。但是,通過提供良好的幫助,你就擁有了用于詢問用戶的基本要素了。你可以調用安全性措施來確保用戶知道這種行為是不可接受的。減少緩沖區溢出是一個主動的過程。你必須防止無效的輸入、為用戶提供良好的幫助、并給決心忽視規則的用戶懲罰性的措施。
|