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