引言:棧溢出攻擊是通過構(gòu)造特殊的代碼來達(dá)到溢出攻擊的一種攻擊方式,可以造成系統(tǒng)異常甚至獲取計(jì)算機(jī)權(quán)限等危害。本文通過對棧的結(jié)構(gòu)分析,探討了棧溢出的形成原理及防范辦法,是提高軟件的安全性、網(wǎng)絡(luò)的安全性的一個重要部分。
程序在開發(fā)過程中,出現(xiàn)溢出錯誤是正?,F(xiàn)象,也是一種較嚴(yán)重的程序錯誤,因?yàn)橐绯鲥e誤不僅僅會造成程序的異常、丟失數(shù)據(jù)等問題,嚴(yán)重時還會造成操作系統(tǒng)的異常甚至崩潰。
程序在運(yùn)行過程中,進(jìn)程會被加載到內(nèi)存的不同區(qū)域中執(zhí)行,而按功能劃分,進(jìn)程所使用的內(nèi)存空間可以分成四類:1.數(shù)據(jù)區(qū),用來存儲全局變量、常量等;2.棧區(qū),用來存儲函數(shù)間的調(diào)用關(guān)系,從而保證函數(shù)調(diào)用結(jié)束后,返回到調(diào)用點(diǎn)繼續(xù)向下執(zhí)行;3.堆區(qū),是系統(tǒng)動態(tài)分配和回收的一段特殊內(nèi)存空間,進(jìn)程可以動態(tài)地申請,作為緩沖區(qū)來使用,使用完成后,按照不同的堆算法回收;4.代碼區(qū),用于存儲程序執(zhí)行過程中的機(jī)器指令,CPU會按照程序執(zhí)行流程逐條取出后依次執(zhí)行。
上述四類內(nèi)存空間中,棧區(qū)是由操作系統(tǒng)自動維護(hù)的,這是保證函數(shù)調(diào)用的基礎(chǔ),也是簡化程序設(shè)計(jì)的難度和降低程序的復(fù)雜度。一般來說,棧的絕大多數(shù)操作,如PUSH、POP等,對于C語言等高級設(shè)計(jì)語言來說都是透明的,操作系統(tǒng)都有豐富、完善的函數(shù)、接口等供程序員直接調(diào)用。同一個文件的不同函數(shù)的代碼在內(nèi)存代碼區(qū)中的分布的先后順序也不固定,一般是根據(jù)一定的內(nèi)存分配算法來隨機(jī)分配的。
當(dāng)CPU在執(zhí)行到調(diào)用function_A函數(shù)時,會從代碼區(qū)中的main方法所在的區(qū)域跳轉(zhuǎn)到function_A函數(shù)對應(yīng)的代碼區(qū),并從那里取得指令繼續(xù)執(zhí)行。當(dāng)function_A函數(shù)執(zhí)行完后需要返回時,又會跳轉(zhuǎn)到main方法對應(yīng)的指令區(qū)域并繼續(xù)向下執(zhí)行。上述代碼區(qū)中的跳轉(zhuǎn)都是通過棧來實(shí)現(xiàn)的,當(dāng)函數(shù)調(diào)用發(fā)生時,棧區(qū)會為每個函數(shù)開辟一個新的棧,并把函數(shù)的相關(guān)的各寄存器信息等壓入棧中,同時該棧在內(nèi)存中會以獨(dú)占方式存在,正常情況下,其它函數(shù)不會訪問到它的。當(dāng)函數(shù)調(diào)用完成后,棧中的數(shù)據(jù)也會被依次POP,恢復(fù)到調(diào)用函數(shù)前的各寄存器數(shù)據(jù),代碼繼續(xù)向下執(zhí)行。
正常情況下,代碼區(qū)中的跳轉(zhuǎn)都是通過棧區(qū)來完成的,當(dāng)函數(shù)調(diào)用發(fā)生時,棧區(qū)會為函數(shù)開辟一個新的棧區(qū)單元,并將相關(guān)數(shù)據(jù)PUSH后,這一內(nèi)存區(qū)域理論上是獨(dú)占屬性,不會再被分配或占用,只有當(dāng)函數(shù)返回時,棧里數(shù)據(jù)全部POP后才會調(diào)用相應(yīng)的回收機(jī)制回收內(nèi)存后再分配。
以C語言為例,其函數(shù)調(diào)用時,Main函數(shù)調(diào)用function_A函數(shù)后,會在系統(tǒng)分配的棧區(qū)中PUSH相關(guān)的數(shù)據(jù),而當(dāng)Function_A調(diào)用Function_B時,同樣會先把自己的棧區(qū)單元壓入函數(shù)返回地址,然后為Function_B創(chuàng)建新的棧區(qū)單元并壓入棧區(qū)。在Function_B返回時,F(xiàn)unction_B的棧區(qū)單元被彈出棧區(qū),而Function_A棧區(qū)單元中的返回地址則會位于棧頂,而此時程序會繼續(xù)跳轉(zhuǎn)到Function_A代碼區(qū)中執(zhí)行。在Runction_A返回時,其棧區(qū)單元彈出棧區(qū),main方法棧區(qū)單元中的返回地址位于棧頂,此時CPU則會按這個地址跳轉(zhuǎn),回到main方法中繼續(xù)向下執(zhí)行。也就是說,每個函數(shù)獨(dú)占自己的棧區(qū)單元空間,當(dāng)前運(yùn)行的函數(shù)的棧區(qū)單元總是位于棧頂,而棧頂?shù)膯卧刂?,通常也是由CPU的ESP和EBP兩個寄存器來標(biāo)識,其中ESP為指針寄存器,而EBP則為基址指針寄存器,共同來指向棧區(qū)的頂部單元。
棧區(qū)中,一般會包含幾類較重要的信息,包括局部變量、狀態(tài)值、返回地址,函數(shù)調(diào)用的相關(guān)指令,一般如下:
上面代碼部分,包括了函數(shù)調(diào)用的幾個基本步驟:
1.參數(shù)入棧。
2.返回地址入棧:將當(dāng)前代碼區(qū)調(diào)用的指令下一條地址入棧,供函數(shù)返回時繼續(xù)執(zhí)行。
3.代碼區(qū)跳轉(zhuǎn):CPU從當(dāng)前代碼區(qū)跳轉(zhuǎn)到被調(diào)用函數(shù)入口地址處。
4.棧區(qū)單元調(diào)整,包括了保存當(dāng)前棧區(qū)單元狀態(tài)值,EBP入棧后,當(dāng)前棧區(qū)單元切換到新棧區(qū)單元,將ESP值裝入EBP,更新棧區(qū)單元底部,給新棧區(qū)單元分配空間,將ESP減去所需要的空間大小。
棧溢出的基本思路是人為構(gòu)造代碼數(shù)據(jù),覆蓋函數(shù)的返回地址,從而改變程序的執(zhí)行流程,其難點(diǎn)在于如何準(zhǔn)確定位,并構(gòu)造一段數(shù)據(jù)代碼,恰好覆蓋返回地址,而又不造成程序的執(zhí)行錯誤。
除了上述構(gòu)造特殊的入口地址達(dá)到棧溢出的效果以外,局部靜態(tài)變量過大也比較常見。對棧溢出原理清楚以后,解決辦法主要有兩種:
1.增加棧內(nèi)在的數(shù)目,增加辦法相對較簡單,不同的編譯器都有類似的設(shè)置,但這種辦法也容易造成一些不可預(yù)計(jì)的問題,例如影響穩(wěn)定性、數(shù)據(jù)庫ADO連接異常等。
2.使用堆內(nèi)存,這也是得到多數(shù)程序員認(rèn)可的可行性較高的辦法,實(shí)現(xiàn)手段也有多種,如可以把數(shù)組的定義改為指針,然后申請動態(tài)內(nèi)存,也可以把局部變量設(shè)置成全局變量或靜態(tài)變量,當(dāng)然定義一個大數(shù)組,有時能更好的解決棧溢出問題。