緩沖區溢出通常表現為一個最為常見的漏洞而存在於今天的各種軟件之中,黑客可以用惡意的輸入,從而更改程序的執行流程,由此入侵相應的進程、電腦、或整個域。如果進程運行於一個高度受信的賬戶之下,如管理員或本地系統賬戶,那麼黑客帶來的破壞將是極其嚴重,並有潛在廣泛傳播的危險。近幾年來爆發的一些"知名"病毒,如紅色代碼、沖擊波、震蕩波等等,都源於C/C++代碼緩沖區溢出的結果。
從程序的角度來看,緩沖區溢出只是一個再簡單不過的編程錯誤--都是關於復制一個內存區域的內容到另一個內存區域,而目標內存區域容量太小無法容納。以下的代碼作了簡單的演示:
char* source = "A reasonably long string";
char dest[10];
::strcpy(dest, source);
在本例中,源字符串的長度為25個字符(包括了空結束符),它對目標內存塊來說,無疑太大了,而目標內存塊聲明在堆棧上;當此代碼執行時,將會破壞掉原有堆棧,程序會因為一個訪問違例而崩潰。如果此源內存塊由外部第三方提供,那麼就有可能存在一個漏洞,因為它允許傳入函數的內存塊以一種特定的方式修改堆棧。
當在C/C++中調用一個函數時,調用函數的返回地址被存放在堆棧中,因此在被調用函數執行完畢時,執行流程能重新返回到原處。如果調用了一個可能包含潛在緩沖區溢出的函數,返回地址可能會被修改,而且執行流程將會跳到緩沖區數據中指定的地方。通過改變函數的返回地址,攻擊者可獲取進程中任意位置的代碼以執行,一般而言,主要可以兩種方式被利用:
·如果帶有漏洞的程序是已知、且容易訪問到的,攻擊者可查找某函數的地址,這通常會在所有進程實例的一處固定地址處被找到;並修改堆棧,等著此函數被調用。
·要執行的指令可作為緩沖區的一部分傳遞到進程地址空間,攻擊者利用此來完成攻擊。
防范緩沖區溢出
防范緩沖區溢出最簡單的方式是限制復制的數據大小,使其不能大於目標緩沖區容量。雖然此方法看上去微不足道,但實際上,經驗證明,要在那些大型的C/C++代碼中,完全消除了緩沖區溢出的隱患,是件非常艱巨的任務。另外,使用如 .NET或Java這樣的受托管技術,也能極大地降低緩沖區溢出的危險,但把大型項目移植到此技術上,實施起來不太可能也不適當。
基於堆棧的緩沖區溢出可如此簡單地被利用的原因在於,編譯器生成的指令,會把函數的返回地址存儲在堆棧中,但要認識到,編譯器在這個問題中,只扮演了一個小小的角色。從Visual C++.NET(7.0)開始,Visual C++開發小組采取了一種方法,可從編譯器方面減少此類問題發生的機率,他們在堆棧中保存函數返回地址的數據之下,插入了一個帶有已知數值的cookie,由此,如果緩沖區溢出改變了函數的返回地址值,同樣也會覆蓋這個cookie,而在函數返回時,一般會對這個cookie進行檢測,如果檢測到cookie已被修改,就會拋出一個安全異常,而如果這個異常未被處理,此進程就會終止。以下的代碼演示了一個帶有安全異常處理方法的簡單程序:
void _cdecl sec_handler( int code, void *)
{
if ( code == _SECERR_BUFFER_OVERRUN )
{
printf("檢測到一個緩沖區溢出。\n");
exit(1);
}
}
int main()
{
_set_security_error_handler( sec_handler );
//主程序代碼在此省略。
}
Visual C++.NET 2003(7.1)通過移動易受攻擊的數據結構--(如異常處理方法的地址)--到堆棧中位於緩沖區之下的某個位置,增強了緩沖區溢出的保護力度。在編譯器的7.0版本中,可通過破壞緩沖區與cookie之間的敏感數據,繞過安全cookie所提供的保護;然而,在新版本的編譯器中,已把這些數據移到位於緩沖區下的一個區域,現在,想要通過修改這些數據而達到溢出,似乎是不太可能了。
圖1演示了在C++編譯器6、7.0、7.1中,堆棧概念上的布局,並演示了堆棧由高地址向低地址空間方向增長,這也是當程序執行時,堆棧增長的方向。堆棧向下增長,正是導致緩沖區溢出的主要原因,因為溢出會覆寫在比緩沖區更高的內存地址空間上,而此正是易受攻擊數據結構的棲身之地。
圖1:堆棧邏輯布局
除了把異常處理方法等信息移到堆棧中數據緩沖區之下,Visual C++.NET 2003的鏈接器也把結構化異常處理方法的地址放到可執行文件的頭部中。當異常發生時,操作系統可以檢查堆棧中的異常信息地址,是否符合記錄在文件頭信息中的異常處理方法,如果情況不符,異常處理方法將不會執行。比如說,Windows Server 2003就可檢查結構化異常信息,而此項技術也在Service Pack 2中移植到了Windows XP上。
而Visual C++ 2005(8.0)在此基礎上又更進了一步,通常當有函數調用發生時,如果其中的一個本地緩沖區超出限度了,攻擊者可能改寫堆棧中在此之上的任何東西,包括異常處理、安全cookie、幀指針、返回地址和函數參數。而這些值的大多數被不同的機制所保護(如安全異常處理),但對一個有函數指針作參數的函數來說,仍有機會被溢出。如果一個函數接受一個函數指針(或結構、類中包含有函數指針)作為參數,攻擊者就有可能改寫指針中的值,使代碼執行任何他想要的函數。鑒於此,Visual C++ 2005編譯器將分析所有可能存在此漏洞的函數參數,並復制一份函數參數--並不使用原有的函數參數,把它放在堆棧中本地變量之下。如果原有函數參數被溢出改寫了,只要副本中的值仍保持不變,整個函數就不會被攻破。
應用緩沖區保護
只需簡單地打開/GS編譯器開關,就可啟用緩沖區保護。在Visual Studio中,此開關可在"C/C++"選項頁的"代碼生成"選項中找到(如圖2所示)。默認情況下,在Debug配置下為關,而在Release配置下為開。
圖2:設置/GS開關
如果用最新版本的編譯器進行編譯,並生成結構化異常信息,那麼在默認情況下,安全結構化異常處理將是打開的,另外,也可以使用/SAFESEH:NO命令行選項來關閉安全結構化異常處理,在Visual Studio的工程設置中,是沒辦法關閉安全結構化異常處理的,但仍可在鏈接器中使用此命令行選項來完成。
/GS及更遠的安全前景
僅僅是打開一個編譯器開關,不會使一個程序徹底變得安全,但在安全漏洞以各種形式出現的今天,它將有助於使程序更加安全。基於堆棧的緩沖區溢出是安全漏洞中的一大類,但隨著黑客攻擊技術的不斷更新,相信它的謝幕,還有一段很長的路要走。
在Microsoft正式的術語中,/GS和SAGESEH均為軟件強制的數據執行保護(DEP),軟件強制的DEP也能以硬件的方式實現,如在實現了此功能的CPU中,如果數據出現在被標記為"不可執行"的內存頁中,將不會執行它。Windows XP SP2及Windows Server 2003現在已支持這些技術,目前市面上的大多數的32位CPU及全部的64位CPU,都支持No Execute(NX)這類安全增強技術。
任何一個好的安全系統,均有多層防范措施應對安全威脅。本文所涉及的編譯器開關,它能防范或減少普通編碼錯誤所帶來的安全隱患,而且它具有易於使用和低成本的特點,在這場沒有硝煙的戰爭中,不失為一個好的解決方案,絕對值得你的程序采用。