標?題 :? 【技術專題】軟件漏洞分析入門_4_初級棧溢出C_修改程序流程
作?者
:? failwest
時?間
:? 2007 - 12 - 15 , 01 : 01
鏈?接 :? http : //bbs.pediy.com/showthread.php?t=56582

4 講??初級棧溢出C

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

沒有星星的夜里,我用知識吸引你

上節課沒有操練滴東西,不少蠢蠢欲動的同學肯定已經坐不住了。悟空,不要猴急,下面的兩堂課都是實踐課,用來在實踐中深入體會上節課中的知識,并且很有趣味性哦


??信息安全技術是一個對技術性要求極高的領域,除了扎實的計算機理論基礎外、更重要的是優秀的動手實踐能力。在我看來,不懂二進制就無從談起安全技術。


??緩沖區溢出的概念我若干年前已經了然于胸,不就是淹個返回地址把CPU指到緩沖區的shellcode去么。然而當我開始動手實踐的時候,才發現實際中的情況遠遠比原理復雜。

??國內近年來對網絡安全的重視程度正在逐漸增加,許多高校相繼成立了“信息安全學院”或者設立“網絡安全專業”。科班出身的學生往往具有扎實的理論基礎,他們通曉密碼學知識、知道PKI體系架構,但要談到如何真刀實槍的分析病毒樣本、如何拿掉PE上復雜的保護殼、如何在二進制文件中定位漏洞、如何對軟件實施有效的攻擊測試……能夠做到的人并不多。

??雖然每年有大量的網絡安全技術人才從高校涌入人力市場,真正能夠滿足用人單位需求的卻聊聊無幾。捧著書本去做應急響應和風險評估是濫竽充數的作法,社會需要的是能夠為客戶切實解決安全風險的技術精英,而不是滿腹教條的闊論者。


??我所知道的很多資深安全專家都并非科班出身,他們有的學醫、有的學文、有的根本沒有學歷和文憑,但他們卻技術精湛,充滿自信。

??這個行業屬于有興趣、夠執著的人,屬于為了夢想能夠不懈努力的意志堅定者。如果你是這樣的人,請跟著我把這個系列的所有實驗全部完成,之后你會發現眼中的軟件,程序,語言,計算機都與以前看到的有所不同——因為以前使用肉眼來看問題,我會教你用心和調試器以及手指來重新體驗它們。

首先簡單復習上節課的內容:

高級語言經過編譯后,最終函數調用通過為其開辟棧幀來實現

開辟棧幀的動作是編譯器加進去的,高級語言程序員不用在意

函數棧幀中首先是函數的局部變量,局部變量后面存放著函數返回地址

當前被調用的子函數返回時,會從它的棧幀底部取出返回地址,并跳轉到那個位置(母函數中)繼續執行母函數

我們這節課的思路是,讓溢出數組的數據躍過authenticated,一直淹沒到返回地址,把這個地址從main函數中分支判斷的地方直接改到密碼驗證通過的分支!

這樣當verify_password函數返回時,就會返回到錯誤的指令區去執行(密碼驗證通過的地方)


由于用鍵盤輸入字符的ASCII表示范圍有限,很多值如
0x11 0x12 等符號無法直接用鍵盤輸入,所以我們把用于實驗的代碼在第二講的基礎上稍加改動,將程序的輸入由鍵盤改為從文件中讀取字符串。

#include? < stdio . h >
#define? PASSWORD? "1234567"
int? verify_password? ( char? * password )
{
??
int? authenticated ;
??
char? buffer [ 8 ];
??
authenticated = strcmp ( password , PASSWORD );
??
strcpy ( buffer , password ); //over?flowed?here!??
??
return? authenticated ;
}
main ()
{
??
int? valid_flag = 0 ;
??
char? password [ 1024 ];
??
FILE? *? fp ;
??
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 );
}

??
程序的基本邏輯和第二講中的代碼大體相同,只是現在將從同目錄下的password . txt文件中讀取字符串而不是用鍵盤輸入。我們可以用十六進制的編輯器把我們想寫入的但不能直接鍵入的ASCII字符寫進這個password . txt文件。
??
??用VC6
.0 將上述代碼編譯鏈接。我這里使用默認編譯選項,BUILD成debug版本。鑒于有些同學反映自己的用的是VS2003和VS2005,我好人做到底,把我build出來的PE一并在附件中雙手奉上——沒話說了吧!不許不學,不許學不會,不許說難,不許不做實驗!呵呵。

要PE的點這里:?stack_overflow_ret
. rar



在與PE文件同目錄下建立password
. txt并寫入測試用的密碼之后,就可以用OllyDbg加載調試了。

停~~~啥是OllyDbg,開玩笑,在這里問啥是Ollydbg分明是不給看雪老大的面子么!如果沒有這個調試器的話,去工具版找吧,帖子附件要掛出個OD的話會給被人鄙視的。

??在開始動手之前,我們先理理思路,看看要達到實驗目的我們都需要做哪些工作。

??要摸清楚棧中的狀況,如函數地址距離緩沖區的偏移量,到底第幾個字節能淹到返回地址等。這雖然可以通過分析代碼得到,但我還是推薦從動態調試中獲得這些信息。
??要得到程序中密碼驗證通過的指令地址,以便程序直接跳去這個分支執行
??要在password
. txt文件的相應偏移處填上這個地址

??這樣verify_password函數返回后就會直接跳轉到驗證通過的正確分支去執行了。


??首先用OllyDbg加載得到的可執行PE文件如圖:


1??



??
閱讀上圖中顯示的反匯編代碼,可以知道通過驗證的程序分支的指令地址為 0x00401122

簡單解釋一下這段匯編與C語言的對應關系,其實憑著OD給出的注釋,就算你沒學過匯編語言,讀懂也應該沒啥問題。

0x00401102 處的函數調用就是verify_password函數,之后在 0x0040110A 處將EAX中的函數返回值取出?,在? 0x0040110D 處與 0 比較,然后決定跳轉到提示驗證錯誤的分支或提示驗證通過的分支。提示驗證通過的分支從 0x00401122 處的參數壓棧開始。

啥?用OllyDbg加載后找不到verify_password函數的位置?這個嘛,我這里只說一次啊。

OllyDbg在默認情況下將程序中斷在PE裝載器開始處,而不是main函數的開始。如果您有興趣的話可以按F8單步跟蹤一下看看在main函數被運行之前,裝載器都做了哪些準備工作。一般情況下main函數位于GetCommandLineA函數調用后不遠處,并且有明顯的特征:在調用之前有
3 次連續的壓棧操作,因為系統要給main傳入默認的argc、argv等參數。找到main函數調用后,按F7單步跟入就可以看到真正的代碼了。

我相信你,你一定行的,找到了嗎?什么?還找不到?好吧,按ctr
+ g后面輸入截圖中的地址 0x00401102 ,這回看到了吧。建議你按F2下個斷點記住這個位置,別一會兒又在PE里邊迷路了。

這步完成后,您應該對這個PE的主要代碼有了一個把握了。這才牙長一點指令啊,真正的漏洞要對付的是軟件,那個難纏~~~好,不潑冷水了

如果我們把返回地址覆蓋成這個地址,那么在
0x00401102? 處的函數調用返回后,程序將跳轉到驗證通過的分支,而不是進入 0x00401107 處分支判斷代碼。這個過程如下圖所示:

??

2



??
通過動態調試,發現棧幀中的變量分布情況基本沒變。這樣我們就可以按照如下方法構造password . txt中的數據:

??仍然出于字節對齊、容易辨認的目的,我們將“
4321 ”作為一個輸入單元。

??buffer
[ 8 ] 共需要 2 個這樣的單元
??第
3 個輸入單元將authenticated覆蓋
??第
4 個輸入單元將前棧幀EBP值覆蓋
??第
5 個輸入單元將返回地址覆蓋

??為了把第
5 個輸入單元的ASCII碼值 0x34333231 修改成驗證通過分支的指令地址 0x00401122 ,我們采取如下方式借助 16 進制編輯工具UltraEdit來完成( 0x40 0x11 等ASCII碼對應的符號很難用鍵盤輸入)。
??
??步驟
1 :創建一個名為password . txt的文件,并用記事本打開,在其中寫入 5 個“ 4321 ”后保存到與實驗程序同名的目錄下:
?

3



步驟 2 :保存后用UltraEdit_32重新打開,如圖:
?


4


啥?問啥是UltraEdit?去工具版找吧,多的不得了,這里是看雪!


步驟
3 :將UltraEdit_32切換到 16 進制編輯模式,如圖:
?

5

步驟寫到這個份上了,您不會還跟不上吧。


??步驟
4 :將最后四個字節修改成新的返回地址,注意這里是按照“內存數據”排列的,由于“大頂機”的緣故,為了讓最終的“數值數據”為 0x00401122 ,我們需要逆序輸入這四個字節。如圖:
?

6



步驟 5 :這時我們可以切換回文本模式,最后這四個字節對應的字符顯示為亂碼:
?

7



最終的password . txt我也給你附上。

要txt的點這里:??password
. txt

將password
. txt保存后,用OllyDbg加載程序并調試,可以看到最終的棧狀態如下表所示:

局部變量名??內存地址??偏移
3 處的值??偏移 2 處的值??偏移 1 處的值??偏移 0 處的值
buffer
[ 0 ~ 3 ]?? 0x0012FB14??0x31? ( 1 )?? 0x32? ( 2 )?? 0x33? ( 3 )?? 0x34? ( 4 )
buffer [ 4 ~ 7 ]?? 0x0012FB18??0x31? ( 1 )?? 0x32? ( 2 )?? 0x33? ( 3 )?? 0x34? ( 4 )
authenticated
(被覆蓋前)??
0x0012FB1C??0x00??0x00??0x00??0x01
authenticated
(被覆蓋后)??
0x0012FB1C??0x31? ( 1 )?? 0x32? ( 2 )?? 0x33? ( 3 )?? 0x34? ( 4 )
前棧幀EBP
(被覆蓋前)??
0x0012FB20??0x00??0x12??0xFF??0x80
前棧幀EBP
(被覆蓋后)??
0x0012FB20??0x31? ( 1 )?? 0x32? ( 2 )?? 0x33? ( 3 )?? 0x34? ( 4 )
返回地址
(被覆蓋前)??
0x0012FB24??0x00??0x40??0x11??0x07
返回地址
(被覆蓋后)??
0x0012FB24??0x00??0x40??0x11??0x22


程序執行狀態如圖:



??由于棧內EBP等被覆蓋為無效值,使得程序在退出時堆棧無法平衡,導致崩潰。雖然如此,我們已經成功的淹沒了返回地址,并讓處理器如我們設想的那樣,在函數返回時直接跳轉到了提示驗證通過的分支。


同學們,你們成功了么?

最后再總結一下這個實驗的內容:
通過Ollydbg調試PE文件確定密碼驗證成功的分支的指令所處的內存地址為
0x00401122
通過調試確定buffer數組距離棧幀中函數返回地址的偏移量
在password
. txt相應的偏移處準確的寫入 0x00401122 ,當password . txt被讀入后會同樣準確的把verify_password函數的返回地址從分支判斷處修改到 0x00401122 (密碼正確分支)
函數返回時,笨笨的返回到密碼正確的地方
程序繼續執行,但由于棧被破壞,不再平衡,故出錯

試想一下,如果我們在buffer
[] 中填入一些可執行的機器碼,然后用溢出的數據把返回地址指向buffer [] ,那么函數返回后這些代碼是不是就會執行了?

答案是肯定的,下一講我將用類似的敘述方式,同樣手把手的和您一起完成這段機器代碼的編寫,并把它們準確的布置在password
. txt中,這樣原本用來讀取密碼文件的程序讀了這樣一個精心構造的“黑文件”之后,就會做出一些“出格”的事情了。

明天見,順便說一下,我一般會在凌晨
1 點左右發文,想坐沙發的同學注意了呦。