標?題 :? 【技術專題】軟件漏洞分析入門_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 點左右發文,想坐沙發的同學注意了呦。