標?題
:?
【技術專題】軟件漏洞分析入門_3_初級棧溢出B_系統棧原理
作?者
:?
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
小荷才露尖尖角
掃盲班第三講開課啦!
上節課我們用越過數組邊界的一個字節把鄰接的標志變量修改成
0
,從而突破了密碼驗證程序。您實驗成功了嗎?沒有的話回去做完實驗在來聽今天的課!
有幾個同學反映編譯器的問題,我還是建議用VC6
.0
,因為它build出來的PE最適合初學者領會概念。而且這門課動手很重要,基本上我的實驗指導都是按VC6
.0
來寫的,用別的build出來要是有點出入,實驗不成功的話會損失學習積極性滴——實驗獲得的成就感是學習最好的動力。
另外在回帖中已經看到不少同學問了一些不錯的問題:
如果變量之間沒有相鄰怎么辦?
如果有一個編譯器楞要把authenticated變量放在buffer
[
8
]
數組前邊咋辦?
今天的課程將部分回答這些問題。
今天基本沒有程序和調試(下一講將重新回歸實踐),主要是一些理論知識的補充。聽課的對象是只用C語言編過水仙花數的同學。如果你不是這樣的同學,可以飄過本講,否則你會說我羅嗦滴像唐僧~~~~我的目標就是一定要讓你弄明白,不管多羅嗦,多俗氣,多傻瓜的方法,呵呵
找工作滴同學也可以看看這部分,很可能會對面試有幫助呦。根據我個人無數次的面試經驗,會有很多考官饒有興趣的問你學校課本上從來不講的東東,比如堆和棧的區別,什么樣的變量在棧里,函數調用是怎么實現的,參數入棧順序,函數調用時參數的值傳遞、地址傳遞的原理之類。學完本節內容,您將對高級語言的執行原理有一個比較深入的認識。
此外,這節課會對后面將反復用到的一些寄存器,指令進行掃盲。不要怕,就幾個,保管你能弄懂。
最后,上次提意見說圖少的同學注意了,這節課的配套圖示那叫一個多啊。
所以還是那句話,不許不學,不許學不會,不許說難,呵呵
我們開始吧!
??根據不同的操作系統,一個進程可能被分配到不同的內存區域去執行。但是不管什么樣的操作系統、什么樣的計算機架構,進程使用的內存都可以按照功能大致分成以下四個部分:
代碼區:這個區域存儲著被裝入執行的二進制機器代碼,處理器會到這個區域來取指并執行。
數據區:用于存儲全局變量等。
堆區:進程可以在堆區動態的請求一定大小的內存,并在用完之后歸還給堆區。動態分配和回收是堆區的特點
棧區:用于動態的存儲函數之間的調用關系,以保證被調用函數在返回時恢復到母函數中繼續執行
?????注意:這種簡單的內存劃分方式是為了讓您能夠更容易地理解程序的運行機制。《深入理解計算機系統》一書中有更詳細的關于內存使用的論述,如果您對這部分知識有興趣,可以參考之?
??在windows平臺下,高級語言寫出的程序經過編譯鏈接,最終會變成各位同學最熟悉不過的PE文件。當PE文件被裝載運行后,就成了所謂的進程。
圖
1
??????????????????????
??
如果把計算機看成一個有條不紊的工廠的話,那么可以簡單的看成是這樣組織起來的:
CPU是完成工作的工人;
數據區,堆區,棧區等則是用來存放原料,半成品,成品等各種東西的場所;
存在代碼區的指令則告訴CPU要做什么,怎么做,到哪里去領原材料,用什么工具來做,做完以后把成品放到哪個貨艙去;
值得一提的是,棧除了扮演存放原料,半成品的倉庫之外,它還是車間調度主任的辦公室。
??
??程序中所使用的緩沖區可以是堆區、棧區、甚至存放靜態變量的數據區。緩沖區溢出的利用方法和緩沖區到底屬于上面哪個內存區域密不可分,本講座主要介紹在系統棧中發生溢出的情形。堆中的溢出稍微復雜點,我會考慮在中級班中給予介紹
??以下內容針對正常情況下的大學本科二年級計算機水平或者計算機二級水平的讀者,明白棧的飄過即可。
??從計算機科學的角度來看,棧指的是一種數據結構,是一種先進后出的數據表。棧的最常見操作有兩種:壓棧
(
PUSH
)
,彈棧
(
POP
)
;用于標識棧的屬性也有兩個:棧頂
(
TOP
)
,棧底(BASE)
??可以把棧想象成一摞撲克牌:
??PUSH:為棧增加一個元素的操作叫做PUSH,相當于給這摞撲克牌的最上面再放上一張;
??POP:從棧中取出一個元素的操作叫做POP,相當于從這摞撲克牌取出最上面的一張;
??TOP:標識棧頂位置,并且是動態變化的。每做一次PUSH操作,它都會自增
1
;相反每做一次POP操作,它會自減
1
。棧頂元素相當于撲克牌最上面一張,只有這張牌的花色是當前可以看到的。
??BASE:標識棧底位置,它記錄著撲克牌最下面一張的位置。BASE用于防止棧空后繼續彈棧,(牌發完時就不能再去揭牌了)。很明顯,一般情況下BASE是不會變動的。
??內存的棧區實際上指的就是系統棧。系統棧由系統自動維護,它用于實現高級語言中函數的調用。對于類似C語言這樣的高級語言,系統棧的PUSH,POP等堆棧平衡細節是透明的。一般說來,只有在使用匯編語言開發程序的時候,才需要和它直接打交道。
??注意:系統棧在其他文獻中可能曾被叫做運行棧,調用棧等。如果不加特別說明,我們這里說的棧都是指系統棧這個概念,請您注意與求解“八皇后”問題時在自己在程序中實現的數據結構區分開來。
??我們下面就來探究一下高級語言中函數的調用和遞歸等性質是怎樣通過系統棧巧妙實現的。請看如下代碼:
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
;
}
??
這段代碼經過編譯器編譯后,各個函數對應的機器指令在代碼區中可能是這樣分布的:
圖
2
??
根據操作系統的不同、編譯器和編譯選項的不同,同一文件不同函數的代碼在內存代碼區中的分布可能相鄰也可能相離甚遠;可能先后有序也可能無序;但他們都在同一個PE文件的代碼所映射的一個“區”里。這里可以簡單的把它們在內存代碼區中的分布位置理解成是散亂無關的。
??當CPU在執行調用func_A函數的時候,會從代碼區中main函數對應的機器指令的區域跳轉到func_A函數對應的機器指令區域,在那里取指并執行;當func_A函數執行完閉,需要返回的時候,又會跳回到main函數對應的指令區域,緊接著調用func_A后面的指令繼續執行main函數的代碼。在這個過程中,CPU的取指軌跡如下圖所示:
??
?
圖
3
??
那么CPU是怎么知道要去func_A的代碼區取指,在執行完func_A后又是怎么知道跳回到main函數(而不是func_B的代碼區)的呢?這些跳轉地址我們在C語言中并沒有直接說明,CPU是從哪里獲得這些函數的調用及返回的信息的呢?
??原來,這些代碼區中精確的跳轉都是在與系統棧巧妙地配合過程中完成的。當函數被調用時,系統棧會為這個函數開辟一個新的棧幀,并把它壓入棧中。這個棧幀中的內存空間被它所屬的函數獨占,正常情況下是不會和別的函數共享的。當函數返回時,系統棧會彈出該函數所對應的棧幀。
圖
4
??
??
如圖所示,在函數調用的過程中,伴隨的系統棧中的操作如下:
??在main函數調用func_A的時候,首先在自己的棧幀中壓入函數返回地址,然后為func_A創建新棧幀并壓入系統棧
??在func_A調用func_B的時候,同樣先在自己的棧幀中壓入函數返回地址,然后為func_B創建新棧幀并壓入系統棧
??在func_B返回時,func_B的棧幀被彈出系統棧,func_A棧幀中的返回地址被“露”在棧頂,此時處理器按照這個返回地址重新跳到func_A代碼區中執行
??在func_A返回時,func_A的棧幀被彈出系統棧,main函數棧幀中的返回地址被“露”在棧頂,此時處理器按照這個返回地址跳到main函數代碼區中執行
??注意:在實際運行中,main函數并不是第一個被調用的函數,程序被裝入內存前還有一些其他操作,上圖只是棧在函數調用過程中所起作用的示意圖
??每一個函數獨占自己的棧幀空間。當前正在運行的函數的棧幀總是在棧頂。WIN32系統提供兩個特殊的寄存器用于標識位于系統棧棧頂的棧幀:
??ESP:棧指針寄存器
(
extended?stack?pointer
)
,其內存放著一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂
??EBP:基址指針寄存器
(
extended?base?pointer
)
,其內存放著一個指針,該指針永遠指向系統棧最上面一個棧幀的底部
??寄存器對棧幀的標識作用如下圖所示:
圖
5
??
函數棧幀:ESP和EBP之間的內存空間為當前棧幀,EBP標識了當前棧幀的底部,ESP標識了當前棧幀的頂部。
??
??在函數棧幀中一般包含以下幾類重要信息:
??局部變量:為函數局部變量開辟內存空間。
??棧幀狀態值:保存前棧幀的頂部和底部(實際上只保存前棧幀的底部,前棧幀的頂部可以通過堆棧平衡計算得到),用于在本幀被彈出后,恢復出上一個棧幀。
??函數返回地址:保存當前函數調用前的“斷點”信息,也就是函數調用前的指令位置,以便函數返回時能夠恢復到函數被調用前的代碼區中繼續執行指令。
??注意:函數棧幀的大小并不固定,一般與其對應函數的局部變量多少有關。在以后幾講的調試實驗中您會發現,函數運行過程中,其棧幀大小也是在不停變化的。
??除了與棧相關的寄存器外,您還需要記住另一個至關重要的寄存器:
??EIP:指令寄存器
(
extended?instruction?pointer
)
,?其內存放著一個指針,該指針永遠指向下一條待執行的指令地址
圖
6
??
可以說如果控制了EIP寄存器的內容,就控制了進程——我們讓EIP指向哪里,CPU就會去執行哪里的指令。下面的講座我們就會逐步介紹如何控制EIP,劫持進程的原理及實驗。
函數調用約定與相關指令
??
函數調用約定描述了函數傳遞參數方式和棧協同工作的技術細節。不同的操作系統、不同的語言、不同的編譯器在實現函數調用時的原理雖然基本類同,但具體的調用約定還是有差別的。這包括參數傳遞方式,參數入棧順序是從右向左還是從左向右,函數返回時恢復堆棧平衡的操作在子函數中進行還是在母函數中進行。下面列出了幾種調用方式之間的差異。
??????????????????????????????C??????????SysCall??StdCall??BASIC??FORTRAN??PASCAL
參數入棧順序??????????????????右
->
左??右
->
左??右
->
左??左
->
右??左
->
右??左
->
右
恢復棧平衡操作的位置??母函數??子函數??子函數??子函數??子函數??子函數
??具體的,對于Visual?C++來說可支持以下三種函數調用約定
調用約定的聲明??參數入棧順序??恢復棧平衡的位置
__cdecl??右
->
左??母函數
__fastcall??右
->
左??子函數
__stdcall??右
->
左??子函數
??要明確使用某一種調用約定的話只需要在函數前加上調用約定的聲明就行,否則默認情況下VC會使用__stdcall的調用方式。本篇中所討論的技術,在不加額外說明的情況下,都是指這種默認的__stdcall調用方式。
??除了上邊的參數入棧方向和恢復棧平衡操作位置的不同之外,參數傳遞有時也會有所不同。例如每一個C
++
類成員函數都有一個
this
指針,在?windows平臺中這個指針一般是用ECX寄存器來傳遞的,但如果用GCC編譯器編譯的話,這個指針會做為最后一個參數壓入棧中。
??同一段代碼用不同的編譯選項、不同的編譯器編譯鏈接后,得到的可執行文件會有很多不同。
??函數調用大致包括以下幾個步驟:
??參數入棧:將參數從右向左依次壓入系統棧中
??返回地址入棧:將當前代碼區調用指令的下一條指令地址壓入棧中,供函數返回時繼續執行
??代碼區跳轉:處理器從當前代碼區跳轉到被調用函數的入口處
??棧幀調整:具體包括
??保存當前棧幀狀態值,已備后面恢復本棧幀時使用(EBP入棧)
??將當前棧幀切換到新棧幀。(將ESP值裝入EBP,更新棧幀底部)
??給新棧幀分配空間。(把ESP減去所需空間的大小,抬高棧頂)
??
??對于__stdcall調用約定,函數調用時用到的指令序列大致如下:
??
;
調用前
push?參數
3????
;?
假設該函數有
3
個參數,將從右向左依次入棧
push?參數
2????
push?參數
1????
call?函數地址??
;?
call指令將同時完成兩項工作:a)向棧中壓入當前指令在內存中的位置,??????????
;?
即保存返回地址;b)跳轉到所調用函數的入口地址
??
;
函數入口處
push?ebp??????
;?
保存舊棧幀的底部
mov?ebp,esp????
;?
設置新棧幀的底部(棧幀切換)
sub?esp,xxx????
;?
設置新棧幀的頂部(抬高棧頂,為新棧幀開辟空間)
上面這段用于函數調用的指令在棧中引起的變化如下圖所示:
??
注意:關于棧幀的劃分不同參考書中有不同的約定。有的參考文獻中把返回地址和前棧幀EBP值做為一個棧幀的頂部元素,而有的則將其做為棧幀的底部進行劃分。在后面的調試中,您會發現OllyDbg在棧區標示出的棧幀是按照前棧幀EBP值進行分界的,也就是說前棧幀EBP值即屬于上一個棧幀,也屬于下一個棧幀,這樣劃分棧幀后返回地址就成為了棧幀頂部的數據。我們這里將堅持按照EBP與ESP之間的位置做為一個棧幀的原則進行劃分。這樣劃分出的棧幀如上面最后一幅圖所示,棧幀的底部存放著前棧幀EBP,棧幀的頂部存放著返回地址。劃分棧幀只是為了更清晰的了解系統棧的運作過程,并不會影響它實際的工作。
??類似的,函數返回的步驟如下:
??保存返回值:通常將函數的返回值保存在寄存器EAX中
??彈出當前棧幀,恢復上一個棧幀:
??具體包括
??在堆棧平衡的基礎上,給ESP加上棧幀的大小,降低棧頂,回收當前棧幀的空間
??將當前棧幀底部保存的前棧幀EBP值彈入EBP寄存器,恢復出上一個棧幀
??將函數返回地址彈給EIP寄存器
??跳轉:按照函數返回地址跳回母函數中繼續執行
??還是以C語言和WIN32平臺為例,函數返回時的相關的指令序列如下:??
??
add?xxx
,?
esp??
;
降低棧頂,回收當前的棧幀
pop?ebp????
;
將上一個棧幀底部位置恢復到ebp,
retn??????
;
這條指令有兩個功能:a
)
彈出當前棧頂元素,即彈出棧幀中的返回地址。至此????????
;
棧幀恢復工作完成。b
)
讓處理器跳轉到彈出的返回地址,恢復調用前的代碼區
??按照這樣的函數調用約定組織起來的系統棧結構如下:
喂!醒醒!說你吶!還睡!呵呵
不要怪我羅嗦,要徹底的掌握,真正的掌握,完全的掌握緩沖區溢出攻擊,這些知識是必須的!講到這里,如果你思維夠敏捷的話,應該已經可以看出我不是無中生有的花這么多篇幅來浪費版面的。
回憶上一講的那個例子,buffer后面是authenticated變量,authenticated變量后面是誰呢?就是我廢了好多口水講到的當前的正在執行的函數對應的棧幀變量EBP與EIP(函數返回地址)的值!
verify_password函數返回之后,程序就會按照這個返回地址(EIP)所指示的內存地址去取指令并執行。
如果我們在多給幾個輸入的字符,讓輸入的數據躍過authenticated變量,一直淹沒到返回地址的位置,把它淹沒成我們想要執行的指令的內存地址,那么verify_password?函數返回后,就會乖乖滴去執行我們想讓它執行的東東了(例如直接返回到密碼正確的處理流程)。
哎呀,拖堂了,我平生最恨拖堂滴老師,今天就到這里吧。
下節課我會帶著大家一步一步的完成這節課的分析,讓躍過數組的字符串繼續躍過authenticated變量,直到把函數返回地址修改成我們想要的值,從而改變程序流程。
每天堅持用
20
分鐘讀帖一篇,兩周后會驚奇的發現世界真奇妙,呵呵。再見