標(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)世界真奇妙,呵呵。再見