1. 程式人生 > >20155306 白皎 0day漏洞——漏洞利用原理之棧溢出利用

20155306 白皎 0day漏洞——漏洞利用原理之棧溢出利用

put strong 3.2 base 十六進制 格式 correct 3.5 3.1

20155306 白皎 0day漏洞——漏洞利用原理之棧溢出利用

一、系統棧的工作原理

1.1內存的用途

根據不同的操作系統,一個進程可能被分配到不同的內存區域去執行。但是不管什麽樣的操作系統、什麽樣的計算機架構,進程使用的內存都可以按照功能大致分為以下4個部分:

  •  代碼區:這個區域存儲著被裝入執行的二進制機器代碼,處理器會到這個區域取指並執行。

  •  數據區:用於存儲全局變量等。

  •  堆區:進程可以在堆區動態地請求一定大小的內存,並在用完之後歸還給堆區。動態分配和回收是堆區的特點。

  •  棧區:用於動態地存儲函數之間的關系,以保證被調用函數在返回時恢復到母函數中繼續執行。

在Windows平臺下,高級語言寫出的程序經過編譯鏈接,最終會變成PE文件。當PE文件被裝載運行後,就成了所謂的進程。四個區有著各自的功能,在進程運行中缺一不可,大致過程如下:

PE文件代碼段中包含的二進制級別的機器代碼會被裝入內存的**代碼區**(.text),處理器將到內存的這個區域一條一條地取出指令和在**數據區**存放的全局變量等操作數,並送入運算邏輯單元進行運算;如果代碼中請求開辟動態內存,則會在內存的**堆區**分配一塊大小合適的區域返回給代碼區的代碼使用;當函數調用發生時,函數的調用關系等信息會動態地保存在內存的**棧區**,以供處理器在執行完被調用函數的代碼時,返回母函數。

1.2系統棧

棧指的是一種數據結構,是一種先進後出的數據表。內存中的戰區實際上指的就是系統棧

棧的最常見操作有兩種:

壓棧(PUSH)、彈棧(POP)。

用於標識棧的屬性也有兩個:

棧頂(TOP):push操作時,top增1;pop操作時,top減一。
棧底(BASE):與top正好相反,標識最下面的位置,一般不會變動的。

1.3寄存器與函數棧幀

每一個函數獨占自己的棧幀空間。當前正在運行的函數的棧幀總是在棧頂。Win32系統提供兩個特殊的寄存器用於標識位於系統棧頂端的棧幀。

  • ESP:棧指針寄存器(extended stack pointer),其內存放著一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。

  • EBP:基址指針寄存器(extended base pointer),其內存放著一個指針,該指針永遠指向系統棧最上面一個棧幀的底部,並非系統棧的底部。

技術分享圖片

除此之外,還有一個很重要的寄存器。

  • EIP:指令寄存器(extended instruction pointer),其內存放著一個指針,該指針永遠指向下一條等待執行的指令地址。 可以說如果控制了EIP寄存器的內容,就控制了進程——我們讓EIP指向哪裏,CPU就會去執行哪裏的指令。這裏不多說EIP的作用,我個人認為王爽老是的匯編裏面講EIP講的已經是挺好的了~這裏不想多寫關於EIP的事情。

1.4函數調用約定與相關指令

函數調用大概包括以下幾個步驟:

(1)參數入棧:將參數從右向左依次壓入系統棧中。

(2)返回地址入棧:將當前代碼區調用指令的下一條指令地址壓入棧中,供函數返回時繼續執行。

(3)代碼區跳轉:處理器從當前代碼區跳轉到被調用函數的入口處。

(4)棧幀調整:具體包括:   

            <1>保存當前棧幀狀態值,已備後面恢復本棧幀時使用(EBP入棧)。

      <2>將當前棧幀切換到新棧幀(將ESP值裝入EBP,更新棧幀底部)。

      <3>給新棧幀分配空間(把ESP減去所需空間的大小,擡高棧頂)。

      <4>對於_stdcall調用約定,函數調用時用到的指令序列大致如下:

      push 參數3      ;假設該函數有3個參數,將從右向做依次入棧

      push 參數2

      push 參數1

      call 函數地址   ;call指令將同時完成兩項工作:a)向棧中壓入當前指令地址的下一個指令地址,即保存返回地址。 b)跳轉到所調用函數的入口處。

      push  ebp        ;保存舊棧幀的底部

      mov  ebp,esp     ;設置新棧幀的底部 (棧幀切換)

      sub   esp,xxx     ;設置新棧幀的頂部 (擡高棧頂,為新棧幀開辟空間)

函數返回的步驟如下:
<1>保存返回值,通常將函數的返回值保存在寄存器EAX中。

<2>彈出當前幀,恢復上一個棧幀。具體包括:   

(1)在堆棧平衡的基礎上,給ESP加上棧幀的大小,降低棧頂,回收當前棧幀的空間。

(2)將當前棧幀底部保存的前棧幀EBP值彈入EBP寄存器,恢復出上一個棧幀。

(3)將函數返回地址彈給EIP寄存器。

<3>跳轉:按照函數返回地址跳回母函數中繼續執行。

add esp,xxx     ;降低棧頂,回收當前的棧幀

pop ebp      ;將上一個棧幀底部位置恢復到ebp

retn    ;該指令有兩個功能:a)彈出當前棧頂元素,即彈出棧幀中的返回地址,至此,棧幀恢復到上一個棧幀工作完成。b)讓處理器跳轉到彈出的返回地址,恢復調用前代碼區
   

技術分享圖片

二、棧溢出利用之修改鄰接變量

-原理分析

本實驗目的:是研究如何通過非法的超長密碼去修改buffer的鄰接變量authenticated來繞過密碼驗證。

原理:一般情況下函數的局部變量在棧中一個挨著一個排列,如果這些局部變量中有數組之類的緩沖區,並且程序中存在數組越界的缺陷,那麽越界的數組元素就有可能破壞棧中相鄰變量的值,甚至破壞棧幀中所保存的EBP值、返回地址等重要數據。

實驗代碼:

#include <stdio.h>
#include <string.h>
#define PASSWORD "1234567"
int verify_password(char *password){
    int authenticated;
    char buffer[8];
    authenticated= strcmp(password,PASSWORD);
    strcpy(buffer,password);
    return flag;
}
void main(){
    int valid_flag;
    char password[1024];
    while(1){
        printf("Please input password: ");
        scanf("%s",password);
        valid_flag = verify_password(password);
        if(valid_flag){
            printf("Incorrect password!\n");
        }
        else{
            printf("Congratulations!\n");
            break;
        }
    }
}

通過代碼,我們可以想象出代碼執行到verify_password時候的棧幀狀態,如圖所示:

技術分享圖片

我們分析一下:局部變量authenticated正好位於緩沖區buffer的下方,為int型,占用4字節。因此,如果buffer越界,則buffer[8]——buffer[11]正好寫入相鄰的authenticated中。同時,通過源碼,我們可以發現當authenticated為0時,驗證成功;反之則不成功。所以,我們只要做到讓越界的ASCII碼修改authenticated的值為0,則繞過了密碼認證。

-實驗步驟

2.1 首先驗證程序運行結果,只有正確輸入“1234567”才可以通過驗證:

技術分享圖片

2.2假設我們輸入的密碼為7個“qqqqqqq”,按照字符串的關系大於1234567,strcmp返回1,因此authenticated值為1,通過ollydbg調試的實際內存如圖:【0x71是"q"的ASCII碼表示】

技術分享圖片

2.3下面我們試試輸入超過7個字符,輸入“qqqqqqqqrst”,如圖所示,正好從第9個字符開始,開始寫入authenticated中,因此authenticated的值為0x00747372:

技術分享圖片

2.4 我們知道,字符串數據最後都有座位結束標誌的NULL(0),當我們嘗試輸入8個“q”,正好第九個字符0被寫入authenticated中,我們看一下:

技術分享圖片

果然密碼驗證成功了:

技術分享圖片

最後,我們可以明白只要輸入一個大於1234567的8個字符的字符串,那麽隱藏的第九個截斷符就能將authenticated覆蓋為0,從而繞過驗證。

三、棧溢出利用之修改函數返回地址

-原理分析

上一個實驗介紹的改寫鄰接變量的方法似然很管用,但是並不太通用,本節介紹一個相對更通用的辦法,修改棧幀最下方的EBP和函數返回地址等棧幀狀態值。

下面,我們分析一下本實驗的原理:如果繼續增加輸入的字符,那麽超出buffer[8]邊界的字符將依次淹沒authenticated、前棧幀EBP、返回地址。也就是說,控制好字符串的長度就可以讓字符串中相應位置字符的ASCII碼覆蓋掉這些棧幀狀態值。

因此,本實驗的目的是:我們通過溢出來覆蓋返回地址從而控制程序的執行流程。

我們大致可以得出以下結論:
可以得出以下的結論:

  • 輸入11個‘q‘,第9-11個字符連同NULL結束符將authenticated沖刷為0x00717171。

  • 輸入15個‘q‘,第9-12個字符將authenticated沖刷為0x71717171;第13-15個字符連同NULL結束符將前棧幀EBP沖刷為0x00717171。

  • 輸入19個‘q‘,第9-12字符將authenticated沖刷為0x71717171;第13-16個字符連同NULL結束符將前棧幀EBP沖刷為0x71717171;第17-19個字符連同NULL結束符將返回地址沖刷為0x00717171。

這裏用19個字符作為輸入,看看淹沒返回地址會對程序產生什麽影響。出於雙字對齊的目的,我們輸入的字符串按照"4321"為一個單元進行組織,最後輸入的字符串為"4321432143214321432"進行測試,用OD分析如下圖所示:

技術分享圖片

實際的內存狀況和我們分析的結論一致,此時的棧狀態見下表的內容:
技術分享圖片

由於鍵盤輸入ASCII碼範圍有限,所以將代碼稍作改動改為從文件讀取字符串。源碼如下:

#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);
}

-實驗步驟

3.1 用OD加載可執行文件,通過閱讀反匯編代碼,可以知道通過驗證的程序分支的指令地址為:0x00401028
技術分享圖片

3.2正常的執行是調用verify_password函數,然後進行比較來決定跳轉到錯誤或正確的分支。如果我們直接把返回地址覆蓋為驗證通過的地址,而不進入需要比較判斷的分支,豈不是可以繞過密碼驗證了。首先創建一個password.txt的文件,寫入5個“4321”後保存到與實驗程序同名的目錄下,如圖:

【buffer[8]需要2個“4321”,authenticated需要一個,EBP需要一個,因此要覆蓋返回地址,需要5個“4321”】

技術分享圖片

3.3保存後,用Ultra_32打開,切換到十六進制編輯模式:

技術分享圖片

3.4將最後4個字節改為新的返回地址【由於“大端機”的緣故,為了使最終數據為0x00401128,我們需要逆序輸入】
技術分享圖片

3.5切換為文本格式,這時也就驗證了為什麽我們不再用鍵盤輸入字符串。

技術分享圖片

3.6將psaaword.txt保存後,用OD重新加載程序並調試,首先可以看到成功繞過密碼驗證:

技術分享圖片

3.7我們再回頭看一下最終的棧狀態:authenticated和EBP被覆蓋後均為0x31323334,返回地址被覆蓋後為0x00401128(正好為驗證成功的地址)

技術分享圖片

技術分享圖片

四、棧溢出利用之代碼植入

-原理分析

本實驗目的:在buffer中植入我們想讓他做的代碼,然後通過返回地址讓程序跳轉到系統棧中執行。這樣我們就可以讓進程去幹本來幹不了的事情啦!

為了在buffer中植入代碼,我們擴充了buffer的容量,來承載我們即將要植入的代碼!簡單的對代碼進行的修改,源碼如下:

#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);  
    return authenticated;  
}  
  
int 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);  
}  

同樣的,我們簡單分析一下棧的布局:如果buffer中有44個字符,那麽第45個字符null正好覆蓋掉authenticated低字節中的1,從而可以突破密碼的限制。

-實驗步驟

4.1我們仍然以“4321”為一個單元,在password.txt中寫入44個字符,如圖:

技術分享圖片

4.2果然通過了驗證。

技術分享圖片

4.3通過OD可以看到,authenticated低字節被覆蓋。同時,我們可以知道buffer的起始地址為0x0012FB7C。因此password.txt中的第53-56個字符的ASCII碼值將寫入棧幀的返回地址中,成為函數返回後執行的指令。

技術分享圖片

4.4接下來,我們給password.txt植入機器代碼。

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


(1)裝載動態鏈接庫user32.dll。MessageBoxA是動態鏈接庫user32.dll的導出函數。
(2)在匯編語言中調用這個函數需要獲得這個函數的入口地址。【 MessageBoxA的入口參數可以通過user32.dll在系統中加載的基址和MessageBoxA在庫中的偏移相加得到。(具體可以使用vc自帶工具“Dependency  Walker“獲得這些信息) 】
(3)在調用前需要向棧中按從右向左的順序壓入MessageBoxA。
 
  • 通過下圖,我們可以得知user32.dll的基地址為0x77D10000,MessageBoxA的偏移地址為0x000407EA,基地址加上偏移地址得到入口地址為0x77D507EA

技術分享圖片

  • 開始編寫函數調用的匯編代碼,這裏我們可以先把字符串“failwest”壓入棧區,寫出的匯編代碼和指令對應的機器代碼如圖:

技術分享圖片

  • 將上述匯編代碼一十六進制形式抄入password.txt,,但是要註意!第53~56字節為自己的buffer的起始地址。

技術分享圖片

技術分享圖片

4.5程序運行情況如下:
技術分享圖片

4.6我們可以對壓入的字符串進行修改,哈哈,改成自己的學號。
技術分享圖片
技術分享圖片

4.7在單擊彈框“ok”之後,程序會報錯崩潰,因為MessageA調用的代碼執行完成後,我們沒有寫安全退出的代碼。
技術分享圖片

20155306 白皎 0day漏洞——漏洞利用原理之棧溢出利用