書名: 0day安全作者名: 王清主編本章字數: 2470字更新時間: 2019-01-01 12:26:13
2.3 修改函數返回地址
2.3.1 返回地址與程序流程
上節實驗介紹的改寫鄰接變量的方法是很有用的,但這種漏洞利用對代碼環境的要求相對比較苛刻。更通用、更強大的攻擊通過緩沖區溢出改寫的目標往往不是某一個變量,而是瞄準棧幀最下方的EBP和函數返回地址等棧幀狀態值。
回顧上節實驗中輸入7個‘q’程序正常運行時的棧狀態,如表2-3-1所示。
表2-3-1 棧幀數據
如果繼續增加輸入的字符,那么超出buffer[8]邊界的字符將依次淹沒authenticated、前棧幀EBP、返回地址。也就是說,控制好字符串的長度就可以讓字符串中相應位置字符的ASCII碼覆蓋掉這些棧幀狀態值。
按照上面對棧幀的分析,不難得出下面的結論。
(1)輸入11個‘q’,第9~11個字符連同NULL結束符將authenticated沖刷為0x00717171。
(2)輸入15個‘q’,第9~12個字符將authenticated沖刷為0x71717171;第13~15個字符連同NULL結束符將前棧幀EBP沖刷為0x00717171。
(3)輸入19個‘q’,第9~12個字符將authenticated沖刷為0x71717171;第13~16個字將前棧幀EBP沖刷為0x71717171;第17~19個字符連同NULL結束符將返回地址沖刷為0x00717171。
這里用19個字符作為輸入,看看淹沒返回地址會對程序產生什么影響。出于雙字對齊的目的,我們輸入的字符串按照“4321”為一個單元進行組織,最后輸入的字符串為“4321432143214321432”,運行情況如圖2.3.1所示。

圖2.3.1 棧溢出導致程序崩潰
用OllyDbg加載程序,在字符串復制函數調用結束后觀察棧狀態,如圖2.3.2所示。實際的內存狀況和我們分析的結論一致,此時的棧狀態如表2-3-2所示。
表2-3-2 棧幀數據

圖2.3.2 溢出前棧中的布局
前面已經說過,返回地址用于在當前函數返回時重定向程序的代碼。在函數返回的“retn”指令執行時,棧頂元素恰好是這個返回地址。“retn”指令會把這個返回地址彈入EIP寄存器,之后跳轉到這個地址去執行。
在這個例子中,返回地址本來是0x004010EB,對應的是main函數代碼區的指令,如圖2.3.3所示。

圖2.3.3 正常情況下函數返回后的指令
現在我們已經把這個地址用字符的ASCII碼覆蓋成了0x00323334,函數返回時的狀態如圖2.3.4所示。
我們可以從調試器中的顯示看出計算機中發生的事件。
(1)函數返回時將返回地址裝入EIP寄存器。
(2)處理器按照EIP寄存器的地址0x00323334取指。
(3)內存0x00323334處并沒有合法的指令,處理器不知道該如何處理,報錯。

圖2.3.4 溢出后程序返回到無效地址0x00323334
由于0x00323334是一個無效的指令地址,所以處理器在取指的時候發生了錯誤使程序崩潰。但如果這里我們給出一個有效的指令地址,就可以讓處理器跳轉到任意指令區去執行(比如直接跳轉到程序驗證通過的部分),也就是說,我們可以通過淹沒返回地址而控制程序的執行流程。以上就是通過淹沒棧幀狀態值控制程序流程的原理,也是本節實驗要做的事。
2.3.2 控制程序的執行流程
用鍵盤輸入字符的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文件。
實驗環境如表2-3-3所示。
表2-3-3 實驗環境
如果完全采用實驗指導所推薦的實驗環境,將精確地重現指導中所有的細節,否則需要根據具體情況重新調試。
用VC6.0將上述代碼編譯鏈接(使用默認編譯選項,Build成debug版本),在與PE文件同目錄下建立password.txt并寫入測試用的密碼之后,就可以用OllyDbg加載調試了。
開始動手之前,我們先理清思路,看看要達到實驗目的我們都需要做哪些工作。
(1)要摸清楚棧中的狀況,如函數地址距離緩沖區的偏移量等。這雖然可以通過分析代碼得到,但我還是推薦從動態調試中獲得這些信息。
(2)要得到程序中密碼驗證通過的指令地址,以便程序直接跳去這個分支執行。
(3)要在password.txt文件的相應偏移處填上這個地址。
這樣verify_password函數返回后就會直接跳轉到驗證通過的正確分支去執行了。
首先用OllyDbg加載得到可執行PE文件,如圖2.3.5所示。

圖2.3.5 提示驗證通過的代碼位置
閱讀圖2.3.5中顯示的反匯編代碼,可以知道通過驗證的程序分支的指令地址為0x00401122。
0x00401102處的函數調用就是verify_password函數,之后在0x0040110A處將EAX中的函數返回值取出,在0x0040110D處與0比較,然后決定跳轉到提示驗證錯誤的分支或提示驗證通過的分支。
提示驗證通過的分支從0x00401122處的參數壓棧開始。如果我們把返回地址覆蓋成這個地址,那么在0x00401102處的函數調用返回后,程序將跳轉到驗證通過的分支,而不是進入0x00401107處分支判斷代碼。這個過程如圖2.3.6所示。
通過動態調試,發現棧幀中的變量分布情況基本沒變。這樣我們就可以按照如下方法構造password.txt中的數據。

圖2.3.6 棧溢出攻擊示意圖
仍然出于字節對齊、容易辨認的目的,我們將“4321”作為一個輸入單元。
buffer[8]共需要兩個這樣的單元。
第3個輸入單元將authenticated覆蓋;第4個輸入單元將前棧幀EBP值覆蓋;第5個輸入單元將返回地址覆蓋。
為了把第5個輸入單元的ASCII碼值0x34333231修改成驗證通過分支的指令地址0x00401122,我們將借助十六進制編輯工具UltraEdit來完成(0x40、0x11等ASCII碼對應的符號很難用鍵盤輸入)。
步驟1:創建一個名為password.txt的文件,并用記事本打開,在其中寫入5個“4321”后保存到與實驗程序同名的目錄下,如圖2.3.7所示。

圖2.3.7 制作觸發棧溢出的輸入文件
步驟2:保存后用UltraEdit_32重新打開,如圖2.3.8所示。

圖2.3.8 制作觸發棧溢出的輸入文件
步驟3:將UltraEdit_32切換到十六進制編輯模式,如圖2.3.9所示。

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

圖2.3.10 制作觸發棧溢出的輸入文件
步驟5:這時我們可以切換回文本模式,最后這4個字節對應的字符顯示為亂碼,如圖2.3.11所示。

圖2.3.11 制作觸發棧溢出的輸入文件
將password.txt保存后,用OllyDbg加載程序并調試,可以看到最終的棧狀態如表2-3-4所示。
表2-3-4 棧幀數據
程序執行狀態如圖2.3.12所示。

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