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