標(biāo)?題 :? 【技術(shù)專題】軟件漏洞分析入門_3_初級棧溢出B_系統(tǒng)棧原理
作?者
:? failwest
時?間
:? 2007 - 12 - 14 , 00 : 13
鏈?接 :? http : //bbs.pediy.com/showthread.php?t=56518

3 講??初級棧溢出B

To?be?the?apostrophe?which?changed?“Impossible”?into?“I’m?possible”
——?failwest

小荷才露尖尖角

掃盲班第三講開課啦!
上節(jié)課我們用越過數(shù)組邊界的一個字節(jié)把鄰接的標(biāo)志變量修改成
0 ,從而突破了密碼驗證程序。您實驗成功了嗎?沒有的話回去做完實驗在來聽今天的課!

有幾個同學(xué)反映編譯器的問題,我還是建議用VC6
.0 ,因為它build出來的PE最適合初學(xué)者領(lǐng)會概念。而且這門課動手很重要,基本上我的實驗指導(dǎo)都是按VC6 .0 來寫的,用別的build出來要是有點出入,實驗不成功的話會損失學(xué)習(xí)積極性滴——實驗獲得的成就感是學(xué)習(xí)最好的動力。

另外在回帖中已經(jīng)看到不少同學(xué)問了一些不錯的問題:
如果變量之間沒有相鄰怎么辦?
如果有一個編譯器楞要把authenticated變量放在buffer
[ 8 ] 數(shù)組前邊咋辦?

今天的課程將部分回答這些問題。

今天基本沒有程序和調(diào)試(下一講將重新回歸實踐),主要是一些理論知識的補充。聽課的對象是只用C語言編過水仙花數(shù)的同學(xué)。如果你不是這樣的同學(xué),可以飄過本講,否則你會說我羅嗦滴像唐僧~~~~我的目標(biāo)就是一定要讓你弄明白,不管多羅嗦,多俗氣,多傻瓜的方法,呵呵

找工作滴同學(xué)也可以看看這部分,很可能會對面試有幫助呦。根據(jù)我個人無數(shù)次的面試經(jīng)驗,會有很多考官饒有興趣的問你學(xué)校課本上從來不講的東東,比如堆和棧的區(qū)別,什么樣的變量在棧里,函數(shù)調(diào)用是怎么實現(xiàn)的,參數(shù)入棧順序,函數(shù)調(diào)用時參數(shù)的值傳遞、地址傳遞的原理之類。學(xué)完本節(jié)內(nèi)容,您將對高級語言的執(zhí)行原理有一個比較深入的認(rèn)識。

此外,這節(jié)課會對后面將反復(fù)用到的一些寄存器,指令進行掃盲。不要怕,就幾個,保管你能弄懂。

最后,上次提意見說圖少的同學(xué)注意了,這節(jié)課的配套圖示那叫一個多啊。

所以還是那句話,不許不學(xué),不許學(xué)不會,不許說難,呵呵

我們開始吧!

??根據(jù)不同的操作系統(tǒng),一個進程可能被分配到不同的內(nèi)存區(qū)域去執(zhí)行。但是不管什么樣的操作系統(tǒng)、什么樣的計算機架構(gòu),進程使用的內(nèi)存都可以按照功能大致分成以下四個部分:

代碼區(qū):這個區(qū)域存儲著被裝入執(zhí)行的二進制機器代碼,處理器會到這個區(qū)域來取指并執(zhí)行。
數(shù)據(jù)區(qū):用于存儲全局變量等。
堆區(qū):進程可以在堆區(qū)動態(tài)的請求一定大小的內(nèi)存,并在用完之后歸還給堆區(qū)。動態(tài)分配和回收是堆區(qū)的特點
棧區(qū):用于動態(tài)的存儲函數(shù)之間的調(diào)用關(guān)系,以保證被調(diào)用函數(shù)在返回時恢復(fù)到母函數(shù)中繼續(xù)執(zhí)行

?????注意:這種簡單的內(nèi)存劃分方式是為了讓您能夠更容易地理解程序的運行機制。《深入理解計算機系統(tǒng)》一書中有更詳細(xì)的關(guān)于內(nèi)存使用的論述,如果您對這部分知識有興趣,可以參考之?

??在windows平臺下,高級語言寫出的程序經(jīng)過編譯鏈接,最終會變成各位同學(xué)最熟悉不過的PE文件。當(dāng)PE文件被裝載運行后,就成了所謂的進程。


1

??????????????????????
??
如果把計算機看成一個有條不紊的工廠的話,那么可以簡單的看成是這樣組織起來的:

CPU是完成工作的工人;
數(shù)據(jù)區(qū),堆區(qū),棧區(qū)等則是用來存放原料,半成品,成品等各種東西的場所;
存在代碼區(qū)的指令則告訴CPU要做什么,怎么做,到哪里去領(lǐng)原材料,用什么工具來做,做完以后把成品放到哪個貨艙去;
值得一提的是,棧除了扮演存放原料,半成品的倉庫之外,它還是車間調(diào)度主任的辦公室。
??
??程序中所使用的緩沖區(qū)可以是堆區(qū)、棧區(qū)、甚至存放靜態(tài)變量的數(shù)據(jù)區(qū)。緩沖區(qū)溢出的利用方法和緩沖區(qū)到底屬于上面哪個內(nèi)存區(qū)域密不可分,本講座主要介紹在系統(tǒng)棧中發(fā)生溢出的情形。堆中的溢出稍微復(fù)雜點,我會考慮在中級班中給予介紹

??以下內(nèi)容針對正常情況下的大學(xué)本科二年級計算機水平或者計算機二級水平的讀者,明白棧的飄過即可。

??從計算機科學(xué)的角度來看,棧指的是一種數(shù)據(jù)結(jié)構(gòu),是一種先進后出的數(shù)據(jù)表。棧的最常見操作有兩種:壓棧
( PUSH ) ,彈棧 ( POP ) ;用于標(biāo)識棧的屬性也有兩個:棧頂 ( TOP ) ,棧底(BASE)

??可以把棧想象成一摞撲克牌:

??PUSH:為棧增加一個元素的操作叫做PUSH,相當(dāng)于給這摞撲克牌的最上面再放上一張;
??POP:從棧中取出一個元素的操作叫做POP,相當(dāng)于從這摞撲克牌取出最上面的一張;

??TOP:標(biāo)識棧頂位置,并且是動態(tài)變化的。每做一次PUSH操作,它都會自增
1 ;相反每做一次POP操作,它會自減 1 。棧頂元素相當(dāng)于撲克牌最上面一張,只有這張牌的花色是當(dāng)前可以看到的。
??BASE:標(biāo)識棧底位置,它記錄著撲克牌最下面一張的位置。BASE用于防止棧空后繼續(xù)彈棧,(牌發(fā)完時就不能再去揭牌了)。很明顯,一般情況下BASE是不會變動的。

??內(nèi)存的棧區(qū)實際上指的就是系統(tǒng)棧。系統(tǒng)棧由系統(tǒng)自動維護,它用于實現(xiàn)高級語言中函數(shù)的調(diào)用。對于類似C語言這樣的高級語言,系統(tǒng)棧的PUSH,POP等堆棧平衡細(xì)節(jié)是透明的。一般說來,只有在使用匯編語言開發(fā)程序的時候,才需要和它直接打交道。

??注意:系統(tǒng)棧在其他文獻中可能曾被叫做運行棧,調(diào)用棧等。如果不加特別說明,我們這里說的棧都是指系統(tǒng)棧這個概念,請您注意與求解“八皇后”問題時在自己在程序中實現(xiàn)的數(shù)據(jù)結(jié)構(gòu)區(qū)分開來。


??我們下面就來探究一下高級語言中函數(shù)的調(diào)用和遞歸等性質(zhì)是怎樣通過系統(tǒng)棧巧妙實現(xiàn)的。請看如下代碼:

int?? func_B ( int? arg_B1 ,? int? arg_B2 )
{
??
int? var_B1 ,? var_B2 ;
??
var_B1 = arg_B1 + arg_B2 ;
??
var_B2 = arg_B1 - arg_B2 ;
??
return? var_B1 * var_B2 ;
}

int?? func_A ( int? arg_A1 ,? int? arg_A2 )
{
??
int? var_A ;
??
var_A? =? func_B ( arg_A1 , arg_A2 )?+? arg_A1? ;
??
return? var_A ;
}

int? main ( int? argc ,? char? ** argv ,? char? ** envp )
{
??
int? var_main ;
??
var_main = func_A ( 4 , 3 );
??
return? var_main ;
}

??
這段代碼經(jīng)過編譯器編譯后,各個函數(shù)對應(yīng)的機器指令在代碼區(qū)中可能是這樣分布的:



2


??
根據(jù)操作系統(tǒng)的不同、編譯器和編譯選項的不同,同一文件不同函數(shù)的代碼在內(nèi)存代碼區(qū)中的分布可能相鄰也可能相離甚遠(yuǎn);可能先后有序也可能無序;但他們都在同一個PE文件的代碼所映射的一個“區(qū)”里。這里可以簡單的把它們在內(nèi)存代碼區(qū)中的分布位置理解成是散亂無關(guān)的。

??當(dāng)CPU在執(zhí)行調(diào)用func_A函數(shù)的時候,會從代碼區(qū)中main函數(shù)對應(yīng)的機器指令的區(qū)域跳轉(zhuǎn)到func_A函數(shù)對應(yīng)的機器指令區(qū)域,在那里取指并執(zhí)行;當(dāng)func_A函數(shù)執(zhí)行完閉,需要返回的時候,又會跳回到main函數(shù)對應(yīng)的指令區(qū)域,緊接著調(diào)用func_A后面的指令繼續(xù)執(zhí)行main函數(shù)的代碼。在這個過程中,CPU的取指軌跡如下圖所示:
??
?


3


??
那么CPU是怎么知道要去func_A的代碼區(qū)取指,在執(zhí)行完func_A后又是怎么知道跳回到main函數(shù)(而不是func_B的代碼區(qū))的呢?這些跳轉(zhuǎn)地址我們在C語言中并沒有直接說明,CPU是從哪里獲得這些函數(shù)的調(diào)用及返回的信息的呢?

??原來,這些代碼區(qū)中精確的跳轉(zhuǎn)都是在與系統(tǒng)棧巧妙地配合過程中完成的。當(dāng)函數(shù)被調(diào)用時,系統(tǒng)棧會為這個函數(shù)開辟一個新的棧幀,并把它壓入棧中。這個棧幀中的內(nèi)存空間被它所屬的函數(shù)獨占,正常情況下是不會和別的函數(shù)共享的。當(dāng)函數(shù)返回時,系統(tǒng)棧會彈出該函數(shù)所對應(yīng)的棧幀。


4

??
??
如圖所示,在函數(shù)調(diào)用的過程中,伴隨的系統(tǒng)棧中的操作如下:

??在main函數(shù)調(diào)用func_A的時候,首先在自己的棧幀中壓入函數(shù)返回地址,然后為func_A創(chuàng)建新棧幀并壓入系統(tǒng)棧
??在func_A調(diào)用func_B的時候,同樣先在自己的棧幀中壓入函數(shù)返回地址,然后為func_B創(chuàng)建新棧幀并壓入系統(tǒng)棧
??在func_B返回時,func_B的棧幀被彈出系統(tǒng)棧,func_A棧幀中的返回地址被“露”在棧頂,此時處理器按照這個返回地址重新跳到func_A代碼區(qū)中執(zhí)行
??在func_A返回時,func_A的棧幀被彈出系統(tǒng)棧,main函數(shù)棧幀中的返回地址被“露”在棧頂,此時處理器按照這個返回地址跳到main函數(shù)代碼區(qū)中執(zhí)行

??注意:在實際運行中,main函數(shù)并不是第一個被調(diào)用的函數(shù),程序被裝入內(nèi)存前還有一些其他操作,上圖只是棧在函數(shù)調(diào)用過程中所起作用的示意圖


??每一個函數(shù)獨占自己的棧幀空間。當(dāng)前正在運行的函數(shù)的棧幀總是在棧頂。WIN32系統(tǒng)提供兩個特殊的寄存器用于標(biāo)識位于系統(tǒng)棧棧頂?shù)臈?br />
??ESP:棧指針寄存器
( extended?stack?pointer ) ,其內(nèi)存放著一個指針,該指針永遠(yuǎn)指向系統(tǒng)棧最上面一個棧幀的棧頂
??EBP:基址指針寄存器
( extended?base?pointer ) ,其內(nèi)存放著一個指針,該指針永遠(yuǎn)指向系統(tǒng)棧最上面一個棧幀的底部


??寄存器對棧幀的標(biāo)識作用如下圖所示:



5



??
函數(shù)棧幀:ESP和EBP之間的內(nèi)存空間為當(dāng)前棧幀,EBP標(biāo)識了當(dāng)前棧幀的底部,ESP標(biāo)識了當(dāng)前棧幀的頂部。
??
??在函數(shù)棧幀中一般包含以下幾類重要信息:

??局部變量:為函數(shù)局部變量開辟內(nèi)存空間。
??棧幀狀態(tài)值:保存前棧幀的頂部和底部(實際上只保存前棧幀的底部,前棧幀的頂部可以通過堆棧平衡計算得到),用于在本幀被彈出后,恢復(fù)出上一個棧幀。
??函數(shù)返回地址:保存當(dāng)前函數(shù)調(diào)用前的“斷點”信息,也就是函數(shù)調(diào)用前的指令位置,以便函數(shù)返回時能夠恢復(fù)到函數(shù)被調(diào)用前的代碼區(qū)中繼續(xù)執(zhí)行指令。


??注意:函數(shù)棧幀的大小并不固定,一般與其對應(yīng)函數(shù)的局部變量多少有關(guān)。在以后幾講的調(diào)試實驗中您會發(fā)現(xiàn),函數(shù)運行過程中,其棧幀大小也是在不停變化的。


??除了與棧相關(guān)的寄存器外,您還需要記住另一個至關(guān)重要的寄存器:

??EIP:指令寄存器
( extended?instruction?pointer ) ,?其內(nèi)存放著一個指針,該指針永遠(yuǎn)指向下一條待執(zhí)行的指令地址




6


??
可以說如果控制了EIP寄存器的內(nèi)容,就控制了進程——我們讓EIP指向哪里,CPU就會去執(zhí)行哪里的指令。下面的講座我們就會逐步介紹如何控制EIP,劫持進程的原理及實驗。


函數(shù)調(diào)用約定與相關(guān)指令
??

函數(shù)調(diào)用約定描述了函數(shù)傳遞參數(shù)方式和棧協(xié)同工作的技術(shù)細(xì)節(jié)。不同的操作系統(tǒng)、不同的語言、不同的編譯器在實現(xiàn)函數(shù)調(diào)用時的原理雖然基本類同,但具體的調(diào)用約定還是有差別的。這包括參數(shù)傳遞方式,參數(shù)入棧順序是從右向左還是從左向右,函數(shù)返回時恢復(fù)堆棧平衡的操作在子函數(shù)中進行還是在母函數(shù)中進行。下面列出了幾種調(diào)用方式之間的差異。

??????????????????????????????C??????????SysCall??StdCall??BASIC??FORTRAN??PASCAL
參數(shù)入棧順序??????????????????右
-> 左??右 -> 左??右 -> 左??左 -> 右??左 -> 右??左 ->
恢復(fù)棧平衡操作的位置??母函數(shù)??子函數(shù)??子函數(shù)??子函數(shù)??子函數(shù)??子函數(shù)


??具體的,對于Visual?C++來說可支持以下三種函數(shù)調(diào)用約定
調(diào)用約定的聲明??參數(shù)入棧順序??恢復(fù)棧平衡的位置
__cdecl??右
-> 左??母函數(shù)
__fastcall??右
-> 左??子函數(shù)
__stdcall??右
-> 左??子函數(shù)

??要明確使用某一種調(diào)用約定的話只需要在函數(shù)前加上調(diào)用約定的聲明就行,否則默認(rèn)情況下VC會使用__stdcall的調(diào)用方式。本篇中所討論的技術(shù),在不加額外說明的情況下,都是指這種默認(rèn)的__stdcall調(diào)用方式。

??除了上邊的參數(shù)入棧方向和恢復(fù)棧平衡操作位置的不同之外,參數(shù)傳遞有時也會有所不同。例如每一個C
++ 類成員函數(shù)都有一個 this 指針,在?windows平臺中這個指針一般是用ECX寄存器來傳遞的,但如果用GCC編譯器編譯的話,這個指針會做為最后一個參數(shù)壓入棧中。

??同一段代碼用不同的編譯選項、不同的編譯器編譯鏈接后,得到的可執(zhí)行文件會有很多不同。

??函數(shù)調(diào)用大致包括以下幾個步驟:

??參數(shù)入棧:將參數(shù)從右向左依次壓入系統(tǒng)棧中
??返回地址入棧:將當(dāng)前代碼區(qū)調(diào)用指令的下一條指令地址壓入棧中,供函數(shù)返回時繼續(xù)執(zhí)行
??代碼區(qū)跳轉(zhuǎn):處理器從當(dāng)前代碼區(qū)跳轉(zhuǎn)到被調(diào)用函數(shù)的入口處
??棧幀調(diào)整:具體包括
??保存當(dāng)前棧幀狀態(tài)值,已備后面恢復(fù)本棧幀時使用(EBP入棧)
??將當(dāng)前棧幀切換到新棧幀。(將ESP值裝入EBP,更新棧幀底部)
??給新棧幀分配空間。(把ESP減去所需空間的大小,抬高棧頂)
??
??對于__stdcall調(diào)用約定,函數(shù)調(diào)用時用到的指令序列大致如下:

??
; 調(diào)用前
push?參數(shù)
3???? ;? 假設(shè)該函數(shù)有 3 個參數(shù),將從右向左依次入棧
push?參數(shù)
2????
push?參數(shù) 1????
call?函數(shù)地址?? ;? call指令將同時完成兩項工作:a)向棧中壓入當(dāng)前指令在內(nèi)存中的位置,?????????? ;? 即保存返回地址;b)跳轉(zhuǎn)到所調(diào)用函數(shù)的入口地址

??
; 函數(shù)入口處
push?ebp??????
;? 保存舊棧幀的底部
mov?ebp,esp????
;? 設(shè)置新棧幀的底部(棧幀切換)
sub?esp,xxx????
;? 設(shè)置新棧幀的頂部(抬高棧頂,為新棧幀開辟空間)

上面這段用于函數(shù)調(diào)用的指令在棧中引起的變化如下圖所示:


??



注意:關(guān)于棧幀的劃分不同參考書中有不同的約定。有的參考文獻中把返回地址和前棧幀EBP值做為一個棧幀的頂部元素,而有的則將其做為棧幀的底部進行劃分。在后面的調(diào)試中,您會發(fā)現(xiàn)OllyDbg在棧區(qū)標(biāo)示出的棧幀是按照前棧幀EBP值進行分界的,也就是說前棧幀EBP值即屬于上一個棧幀,也屬于下一個棧幀,這樣劃分棧幀后返回地址就成為了棧幀頂部的數(shù)據(jù)。我們這里將堅持按照EBP與ESP之間的位置做為一個棧幀的原則進行劃分。這樣劃分出的棧幀如上面最后一幅圖所示,棧幀的底部存放著前棧幀EBP,棧幀的頂部存放著返回地址。劃分棧幀只是為了更清晰的了解系統(tǒng)棧的運作過程,并不會影響它實際的工作。

??類似的,函數(shù)返回的步驟如下:

??保存返回值:通常將函數(shù)的返回值保存在寄存器EAX中
??彈出當(dāng)前棧幀,恢復(fù)上一個棧幀:
??具體包括
??在堆棧平衡的基礎(chǔ)上,給ESP加上棧幀的大小,降低棧頂,回收當(dāng)前棧幀的空間
??將當(dāng)前棧幀底部保存的前棧幀EBP值彈入EBP寄存器,恢復(fù)出上一個棧幀
??將函數(shù)返回地址彈給EIP寄存器
??跳轉(zhuǎn):按照函數(shù)返回地址跳回母函數(shù)中繼續(xù)執(zhí)行

??還是以C語言和WIN32平臺為例,函數(shù)返回時的相關(guān)的指令序列如下:??

??
add?xxx
,? esp?? ; 降低棧頂,回收當(dāng)前的棧幀
pop?ebp????
; 將上一個棧幀底部位置恢復(fù)到ebp,
retn??????
; 這條指令有兩個功能:a ) 彈出當(dāng)前棧頂元素,即彈出棧幀中的返回地址。至此???????? ; 棧幀恢復(fù)工作完成。b ) 讓處理器跳轉(zhuǎn)到彈出的返回地址,恢復(fù)調(diào)用前的代碼區(qū)


??按照這樣的函數(shù)調(diào)用約定組織起來的系統(tǒng)棧結(jié)構(gòu)如下:






喂!醒醒!說你吶!還睡!呵呵

不要怪我羅嗦,要徹底的掌握,真正的掌握,完全的掌握緩沖區(qū)溢出攻擊,這些知識是必須的!講到這里,如果你思維夠敏捷的話,應(yīng)該已經(jīng)可以看出我不是無中生有的花這么多篇幅來浪費版面的。

回憶上一講的那個例子,buffer后面是authenticated變量,authenticated變量后面是誰呢?就是我廢了好多口水講到的當(dāng)前的正在執(zhí)行的函數(shù)對應(yīng)的棧幀變量EBP與EIP(函數(shù)返回地址)的值!

verify_password函數(shù)返回之后,程序就會按照這個返回地址(EIP)所指示的內(nèi)存地址去取指令并執(zhí)行。

如果我們在多給幾個輸入的字符,讓輸入的數(shù)據(jù)躍過authenticated變量,一直淹沒到返回地址的位置,把它淹沒成我們想要執(zhí)行的指令的內(nèi)存地址,那么verify_password?函數(shù)返回后,就會乖乖滴去執(zhí)行我們想讓它執(zhí)行的東東了(例如直接返回到密碼正確的處理流程)。


哎呀,拖堂了,我平生最恨拖堂滴老師,今天就到這里吧。

下節(jié)課我會帶著大家一步一步的完成這節(jié)課的分析,讓躍過數(shù)組的字符串繼續(xù)躍過authenticated變量,直到把函數(shù)返回地址修改成我們想要的值,從而改變程序流程。

每天堅持用
20 分鐘讀帖一篇,兩周后會驚奇的發(fā)現(xiàn)世界真奇妙,呵呵。再見