源碼計算溢出
① 溢出的分析原因
當你用 C/C++ 書寫代碼時,應該處處留意如何處理來自用戶的數據。如果一個函數的數據來源不可靠,又用到內存緩沖區,那麼它就必須嚴格遵守下列規則:
必須知道內存緩沖區的總長度。
檢驗內存緩沖區。
提高警惕。
讓我們來具體看看這些「黃金規則」:
(1)必須知道內存緩沖區的總長度。
類似這樣的代碼就有可能導致 bug :
void Function(char *szName) {
char szBuff[MAX_NAME];
// 復制並使用 szName
strcpy(szBuff,szName);
}
它的問題出在函數並不知道 szName 的長度是多少,此時復制數據是不安全的。正確的做法是,在復制數據前首先獲取 szName 的長度:
void Function(char *szName, DWORD cbName) {
char szBuff[MAX_NAME];
// 復制並使用 szName
if (cbName < MAX_NAME)
strcpy(szBuff,szName);
}
這樣雖然有所改進,可它似乎又過於信任 cbName 了。攻擊者仍然有機會偽造 czName 和 szName 兩個參數以謊報數據長度和內存緩沖區長度。因此,你還得檢檢這兩個參數!
(2)檢驗內存緩沖區
如何知道由參數傳來的內存緩沖區長度是否真實呢?你會完全信任來自用戶的數據嗎?通常,答案是否定的。其實,有一種簡單的辦法可以檢驗內存緩沖區是否溢出。請看如下代碼片斷:
void Function(char *szName, DWORD cbName) {
char szBuff[MAX_NAME];
// 檢測內存
szBuff[cbName] = 0x42;
// 復制並使用 szName
if (cbName < MAX_NAME)
strcpy(szBuff,szName);
}
這段代碼試圖向欲檢測的內存緩沖區末尾寫入數據 0x42 。你也許會想:真是多此一舉,何不直接復制內存緩沖區呢?事實上,當內存緩沖區已經溢出時,一旦再向其中寫入常量值,就會導致程序代碼出錯並中止運行。這樣在開發早期就能及時發現代碼中的 bug 。試想,與其讓攻擊者得手,不如及時中止程序;你大概也不願看到攻擊者隨心所欲地向內存緩沖區復制數據吧。
(3)提高警惕
雖然檢驗內存緩沖區能夠有效地減小內存溢出問題的危害,卻不能從根本上避免內存溢出攻擊。只有從源代碼開始提高警惕,才能真正免除內存溢出攻擊的危脅。不錯,上一段代碼已經很對用戶數據相當警惕了。它能確保復制到內存緩沖區的數據總長度不會超過 szBuff 的長度。然而,某些函數在使用或復制不可靠的數據時也可能潛伏著內存溢出漏洞。為了檢查你的代碼是否存在內存溢出漏洞,你必須盡量追蹤傳入數據的流向,向代碼中的每一個假設提出質疑。一旦這么做了,你將會意識到其中某些假設是錯誤的;然後你還會驚訝地叫道:好多 bug 呀!
在調用 strcpy、strcat、gets 等經典函數時當然要保持警惕;可對於那些所謂的第 n 版 (n-versions) strcpy 或 strcat 函數 —— 比如 strncpy 或 strncat (其中 n = 1,2,3 ……) —— 也不可輕信。的確,這些改良版本的函數是安全一些、可靠一些,因為它們限制了進入內存緩沖區的數據長度;然而,它們也可能導致內存溢出問題!請看下列代碼,你能指出其中的錯誤嗎?
#define SIZE(b) (sizeof(b))
char buff[128];
strncpy(buff,szSomeData,SIZE(buff));
strncat(buff,szMoreData,SIZE(buff));
strncat(buff,szEvenMoreData,SIZE(buff));
給點提示:請注意這些字元串函數的最後一個參數。怎麼,棄權?我說啊,如果你是執意要放棄那些「不安全」的經典字元串函數,並且一律改用「相對安全」的第 n 版函數的話,恐怕你這下半輩子都要為了修復這些新函數帶來的新 bug 而疲於奔命了。呵呵,開個玩笑而已。為何這么說?首先,它們的最後一個參數並不代表內存緩沖區的總長度,它們只是其剩餘空間的長度;不難看出,每執行完一個 strn... 函數,內存緩沖區 buff 的長度就減少一些。其次,傳遞給函數的內存緩沖區長度難免存在「大小差一」(off-by-one)的誤差。你認為在計算 buff 的長度時包括了字元串末尾的空字元嗎?當我向聽眾提出這個問題時,得到的肯定答復和否定答復通常是 50 比 50 ,對半開。也就是說,大約一半人認為計算了末尾的空字元,而另一半人認為忽略了該字元。最後,那些第 n 版函數所返回的字元串不見得以空字元結束,所以在使用前務必要仔細閱讀它們的說明文檔。 「/GS」是Visual C++.NET 新引入的一個編譯選項。它指示編譯器在某些函數的堆棧幀(stack-frames) 中插入特定數據,以幫助消除針對堆棧的內存溢出問題隱患。切記,使用該選項並不能替你清除代碼中的內存溢出漏洞,也不可能消滅任何 bug 。它只是亡羊補牢,讓某些內存溢出問題隱患無法演變成真正的內存溢出問題;也就是說,它能防止攻擊者在發生內存溢出時向進程中插入和運行惡意代碼。無論如何,這也算是小小的安全保障吧。請注意,在新版的本地 Win32 C++ 中使用 Win32應用程序向導創建新項目時,默認設置已經打開了此選項。同樣,Windows .NET Server 環境也默認打開了此選項。關於 /GS 選項的更多信息,請參考 Brandon Bray 的《Compiler Security Checks In Depth》一書。
所謂定點數溢出是指定點數的運算結果的絕對值大於計算機能表示的最大數的絕對值。浮點數的溢出又可分為「上溢出」和「下溢出」兩種,「上溢出」與整數、定點數的含義相同,「下溢出」是指浮點數的運算結果的絕對值小於機器所能表示的最小數絕對值,此時將該運算結果處理成機器零。若發現溢出(上溢出),運算器將產生溢出標志或發出中斷請求,當溢出中斷未被屏蔽時,溢出中斷信號的出現可中止程序的執行而轉入溢出中斷處理程序。
例如:有兩個數0.1001111和0.1101011相加,其結果應為1.0111010。由於定點數計算機只能表示小於1的數,如果字長只有8位,那麼小數點前面的1,會因沒有觸發器存放而丟失。這樣,上述兩個數在計算機中相加,其結果變為0.0111010。若字長只有8位,則結果顯示為0.0000000,後面的1個0和6個1全部丟失,顯然這個結果有誤差。計算機的任何運算都不允許溢出,除非專門利用溢出做判斷,而不使用所得的結果。所以,當發生和不允許出現的溢出時,就要停機或轉入檢查程序,以找出產生溢出的原因,做出相應的處理。