官术网_书友最值得收藏!

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

主站蜘蛛池模板: 称多县| 长顺县| 美姑县| 呼和浩特市| 丰镇市| 黑河市| 八宿县| 衡山县| 昂仁县| 绥芬河市| 平乡县| 三亚市| 翼城县| 寿阳县| 应城市| 鹤山市| 琼中| 化德县| 来宾市| 铜鼓县| 田东县| 盐源县| 万荣县| 万年县| 岢岚县| 会泽县| 保靖县| 合山市| 白玉县| 安新县| 观塘区| 改则县| 盐源县| 玉门市| 荆州市| 嵩明县| 娄底市| 黄骅市| 遂昌县| 儋州市| 新和县|