深入探索c/c++函数(3)---虚成员函数调用的基本过程
2011-08-18 21:00
453 查看
学过c++一段时间都会知道,c++是依靠虚函数实现多态的,如下代码:
view
plain
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Print()
{
cout<<"^-^"<<endl;
}
};
class Derive:public Base
{
public:
virtual void Print()
{
cout<<"T-T"<<endl;
}
};
int main()
{
Base *p=new Derive();
p->Print();
}
呵呵,输出T-T~~~~~~
对于理解虚函数的实现原理,历来是一个c++新手到中手的必经之路之一,关于其实现原理,个人推荐《深入探索c++对象模型》这本书,
原理讲的很透彻的。现在分析一下一些主流的编译器的具体实现方式,并从汇编的角度来分析编译器的虚函数的实现原理(最近找c/c++工作,估计虚函数被问到可能性很高~~)。
首先c++标准仅仅规定了虚函数的行为,并没有规定这种行为的具体实现,但目前主流的编译器(vc,g++)在实现上达成了一定默契,都是通过在对象前4个字节安插一个虚表指针,
这个虚表指针指向对应类的虚表,在调用虚函数时,通过虚表指针查找虚表最终获得要调用的函数的,这也就是动态绑定的底层实现方式。
以下是vc10默认编译选项debug下上面程序的反汇编:
view
plain
257: int main()
258: {
01031500 push ebp
01031501 mov ebp,esp
01031503 sub esp,0DCh
01031509 push ebx
0103150A push esi
0103150B push edi
0103150C lea edi,[ebp-0DCh]
01031512 mov ecx,37h
01031517 mov eax,0CCCCCCCCh
0103151C rep stos dword ptr es:[edi];开启堆栈帧(/RTCs)后,就会有类似的汇编,将未
;初始化的局部变量值初始化为cc,一个int 3指令
;如果输出未初始化的一个int 变量,值就是-858993460
;因为数据以补码保存,-858993460补码就是0xcccccccc
;输出未初始化的字符则输出 烫,这就是我们debug时,
;内存里老多 烫烫烫烫烫烫的原因。
259: Base *p=new Derive();
0103151E push 4 ;operator new函数参数入栈,即要为Derive对象分配四个字节的空间。
01031520 call operator new (1031208h) ;调用operator new函数
01031525 add esp,4 ;__cdecl调用约定,函数调用者,调整栈帧,
01031528 mov dword ptr [ebp-0D4h],eax;将operator new 函数返回结果存入dword ptr[ebp-0D4h]
;这段空间,operator new结果返回一个指针,指向分配的内
;存的地址,vc中整形或者能隐式转化成整形的返回值放入eax
0103152E cmp dword ptr [ebp-0D4h],0 ;测试返回值是否为0
01031535 je main+4Ah (103154Ah) ;为0则调转
01031537 mov ecx,dword ptr [ebp-0D4h];将operator new 分配的内存地址放入ecx,vc中成员函数
;调用时this指针存入ecx的。
0103153D call Derive::Derive (1031127h) ;调用构造函数,在构造函数里完成虚表指针的初始化,
;由于没有显时定义默认构造函数,所以编译器负责生成一个
01031542 mov dword ptr [ebp-0DCh],eax ;编译器生成的默认构造函数中,将构造好虚表指针的对象的地
;址放入了eax,所以这句相当于取对象的地址。编译器生成的
;默认构造函数代码稍后介绍
01031548 jmp main+54h (1031554h) ;跳过下一条指令的执行。
0103154A mov dword ptr [ebp-0DCh],0;如果走这条指令说明是je main+4Ah (103154Ah)
;跳转过来的,说明内存分配失败,这条指令的作用就是将p值设为0,
;也就是this值设为0,以期望this+偏移访问数据时触发一个异常。
01031554 mov eax,dword ptr [ebp-0DCh] ;如果内存分配没有问题的话,那么dword ptr [ebp-0DCh]
;保存的是对象的地址值。
0103155A mov dword ptr [p],eax ;把对象的地址值赋给dword ptr [p]这段空间,下面的代码就是
;就是通过虚表指针查找虚表的关键代码了,要说关键点了
260: p->Print();
0103155D mov eax,dword ptr [p] ;将对象的地址值存入eax,现在eax=p(p指向对象的起始地址)
01031560 mov edx,dword ptr [eax] ;通过eax寻址,对应的操作是从eax对应的地址值开始往高地址涵盖
;双字,即4个字节,将这4个字节里的数据按照整形方式读出赋给edx
;相当于edx=*(int*)p,前面说了对象的前4个字节是为虚表指针所分
;配的空间,这句指令相当于获取虚表指针的值。
01031562 mov esi,esp
01031564 mov ecx,dword ptr [p] ;this指针存入ecx
01031567 mov eax,dword ptr [edx];查找虚表的操作,跟上面的分析一样,从edx对应地址值开始,往高
;地址涵盖4个字节的内存,读出这段内存里的数据,可以知道edx的值
;即是虚表指针的值,虚表指针指向一个虚表,虚表的地址假设为
;0x00100000,那么0x00100000~0x00100003是存储第一个虚函数的
;地址,0x00100004~0x00100007是存储第二个虚函数的地址.....
;这条指令即是获取第一个虚函数的地址,eax=*(int*)*(int*)p
;现在eax值是一个合法函数指针的值了
01031569 call eax ;进行函数调用
0103156B cmp esi,esp
0103156D call @ILT+435(__RTC_CheckEsp) (10311B8h)
261: }
01031572 xor eax,eax
01031574 pop edi
01031575 pop esi
01031576 pop ebx
01031577 add esp,0DCh
0103157D cmp ebp,esp
0103157F call @ILT+435(__RTC_CheckEsp) (10311B8h)
01031584 mov esp,ebp
01031586 pop ebp
01031587 ret
通过上述分析,类似*(int*)*(int*)p这样的表达式来获取虚表中函数方法大家应该明白了吧,这个地方确实是考察指针应用的基本功的。
void(*f)()=(void(*)())*(int*)*(int*)p;
f();
最终调用的是Derive::Print();很显然*(int*)(*(int*)p+4)是虚表中第二个函数地址地址值,如果有的话~~~~~
下面来看下,编译器生成的构造函数里到底做了些什么,
Derive::Derive:
01031127 jmp Derive::Derive (10315B0h)
找到内存10315B0h处的汇编指令:
view
plain
Derive::Derive:
010315B0 push ebp
010315B1 mov ebp,esp
010315B3 sub esp,0CCh
010315B9 push ebx
010315BA push esi
010315BB push edi
010315BC push ecx
010315BD lea edi,[ebp-0CCh]
010315C3 mov ecx,33h
010315C8 mov eax,0CCCCCCCCh
010315CD rep stos dword ptr es:[edi]
010315CF pop ecx
010315D0 mov dword ptr [ebp-8],ecx;将ecx中保存的this指针值存入dword ptr [ebp-8]
010315D3 mov ecx,dword ptr [this] ;this指针存入ecx,调用成员函数用,单继承下
;dword ptr [this]和dword ptr [ebp-8]值是一样
010315D6 call Base::Base (1031131h);调用基类构造函数
010315DB mov eax,dword ptr [this] ;dword ptr [this]指向对象已通过Base::Base
;进行了初始化,此时虚表指针指向了父类的虚表
010315DE mov dword ptr [eax],offset Derive::`vftable' (1037834h)
;将dword ptr [this]指向对象的虚表指针修改成
; Derive::`vftable' ,dword ptr [this]相当于
;一个对象指针,假设为p,这句指令相当于*(int*)p=
;Derive::`vftable' .
010315E4 mov eax,dword ptr [this] ;将初始化好的对象地址存入eax,相当于设置返回值
010315E7 pop edi
010315E8 pop esi
010315E9 pop ebx
010315EA add esp,0CCh
010315F0 cmp ebp,esp
010315F2 call @ILT+435(__RTC_CheckEsp) (10311B8h)
010315F7 mov esp,ebp
010315F9 pop ebp
010315FA ret
看一下Base::Base (1031131h);汇编代码
view
plain
Base::Base:
01031690 push ebp
01031691 mov ebp,esp
01031693 sub esp,0CCh
01031699 push ebx
0103169A push esi
0103169B push edi
0103169C push ecx
0103169D lea edi,[ebp-0CCh]
010316A3 mov ecx,33h
010316A8 mov eax,0CCCCCCCCh
010316AD rep stos dword ptr es:[edi]
010316AF pop ecx
010316B0 mov dword ptr [ebp-8],ecx
010316B3 mov eax,dword ptr [this]
010316B6 mov dword ptr [eax],offset Base::`vftable' (1037844h)
010316BC mov eax,dword ptr [this] ;和Derive类似,也有一个设置虚表指针的操作
010316BF pop edi
010316C0 pop esi
010316C1 pop ebx
010316C2 mov esp,ebp
010316C4 pop ebp
010316C5 ret
分析到这里,相信大家对虚函数调用有个基本的认识了,编译器在实现虚函数时,主要有以下步骤:
1 编译时,根据类的声明,生成一个虚函数表
2 创建对象时,编译器会在类的构造函数内安插一部分代码,用来初始化对象的虚表指针,一般(vc g++)在进入构造函数
开始部分便安插代码。
3 当以指针或引用来调用虚函数时便激活动态绑定,实质是一个通过虚表指针查找函数的过程
所以类似这样代码Derive(){memset(this,0,sizeof(Derive));}将是灾难性的~~~
由于虚函数的实现要借助构造函数,所以构造函数不能是虚拟函数~~~
最后介绍两个关于c++虚函数的hack的简单程序,以加深编对译器实现虚函数机制的了解~~~~
view
plain
#include <iostream>
#include <vector>
using namespace std;
class Base
{
public:
virtual void PrintA()
{
cout<<"^-^"<<endl;
}
virtual void PrintB()
{
cout<<"T-T"<<endl;
}
};
class Derive:public Base
{
public:
virtual void PrintA()
{
cout<<":)"<<endl;
}
virtual void PrintB()
{
cout<<":("<<endl;
}
};
void Hack1()
{
cout<<"Hack1"<<endl;
}
void Hack2()
{
cout<<"Hack2"<<endl;
}
int main()
{
Base *p=new Derive();
int *pVtable[2]={(int*)Hack1,(int*)Hack2};//构造一个虚表
*(int*)p=(int)pVtable;//设置虚表指针
p->PrintA();
p->PrintB();
system("pause");
}
很显然通过修改虚表指针来劫持程序,下面来通过修改虚表来劫持程序~~~~~~~~~~~~~~
view
plain
#include <iostream>
#include <Windows.h>
using namespace std;
class Base
{
public:
virtual void PrintA()
{
cout<<"^-^"<<endl;
}
virtual void PrintB()
{
cout<<"T-T"<<endl;
}
};
class Derive:public Base
{
public:
virtual void PrintA()
{
cout<<":)"<<endl;
}
virtual void PrintB()
{
cout<<":("<<endl;
}
};
void Hack1()
{
cout<<"Hack1"<<endl;
}
int main()
{
Base *p=new Derive();
int PrintAAdress=*(int*)*(int*)p;//获取PrintA在虚表中的地址值
int PrintBAdress=*(int*)(*(int*)p+4);//获取PrintB在虚表中的地址值
//vc debug下函数指针值和函数名对应的地址开始,存放是一个jmp指令
//对应机器吗是0xe9
if (*(unsigned char*)PrintAAdress==0xe9)
{
DWORD d;
int PrintBOffset=*(int*)(PrintBAdress+1);//获取jmp指令后立即数的值
int Hack1Offset=*(int*)((int)Hack1+1);
//jmp 后立即数是相对于本条jmp指令的偏移,这里想把虚表的PrintA地址修
//改成PrintB,所以重新计算偏移
int diff=PrintBOffset-(PrintAAdress-PrintBAdress);
WriteProcessMemory(GetCurrentProcess(),(int*)(PrintAAdress+1), &diff, 4, &d);
diff=Hack1Offset-(PrintBAdress-(int)Hack1);
WriteProcessMemory(GetCurrentProcess(),(int*)(PrintBAdress+1), &diff, 4, &d);
//release下函数指针和函数名的值就是函数对应汇编指令的起始地址
}else{
DWORD dwIdOld;
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,1,GetCurrentProcessId());
//把对应的内存页修改成可读写的,debug下权限比较大,所以可以直接读写
VirtualProtectEx(hProcess,(int*)*(int*)p,4,PAGE_READWRITE,&dwIdOld);
WriteProcessMemory(hProcess,(int*)*(int*)p, &PrintBAdress, 4, 0);
VirtualProtectEx(hProcess,(int*)*(int*)p,4,dwIdOld,&dwIdOld);
int Hack1Adress= (int)Hack1;
VirtualProtectEx(hProcess,(int*)(*(int*)p+4),4,PAGE_READWRITE,&dwIdOld);
WriteProcessMemory(hProcess,(int*)(*(int*)p+4), &Hack1Adress, 4, 0);
VirtualProtectEx(hProcess,(int*)(*(int*)p+4),4,dwIdOld,&dwIdOld);
}
//现在成功改写了虚表,所有Derive对象动态绑定,都会转到PrintB和Hack1上
p->PrintA();
p->PrintB();
}
上面程序就是成功修改了编译器创建的虚表,可以真正算得上一个hack了~~~,上面程序vc9/10+win7 debug/release默认编译选项通过~~~~
如果您看懂上述两个程序,相信您对虚表的编译器实现的认识更加深刻了~~~~
原理就是这个样子了,在多继承情况下,可能麻烦一些,因为对象可能产生多个虚表指针,另外虚析构函数在虚表中布局,各个编译器差异也比较大,
也就是为什么com在实现时要有一个类似release的接口~~~~
声明:转自 demon__hunter
view
plain
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Print()
{
cout<<"^-^"<<endl;
}
};
class Derive:public Base
{
public:
virtual void Print()
{
cout<<"T-T"<<endl;
}
};
int main()
{
Base *p=new Derive();
p->Print();
}
呵呵,输出T-T~~~~~~
对于理解虚函数的实现原理,历来是一个c++新手到中手的必经之路之一,关于其实现原理,个人推荐《深入探索c++对象模型》这本书,
原理讲的很透彻的。现在分析一下一些主流的编译器的具体实现方式,并从汇编的角度来分析编译器的虚函数的实现原理(最近找c/c++工作,估计虚函数被问到可能性很高~~)。
首先c++标准仅仅规定了虚函数的行为,并没有规定这种行为的具体实现,但目前主流的编译器(vc,g++)在实现上达成了一定默契,都是通过在对象前4个字节安插一个虚表指针,
这个虚表指针指向对应类的虚表,在调用虚函数时,通过虚表指针查找虚表最终获得要调用的函数的,这也就是动态绑定的底层实现方式。
以下是vc10默认编译选项debug下上面程序的反汇编:
view
plain
257: int main()
258: {
01031500 push ebp
01031501 mov ebp,esp
01031503 sub esp,0DCh
01031509 push ebx
0103150A push esi
0103150B push edi
0103150C lea edi,[ebp-0DCh]
01031512 mov ecx,37h
01031517 mov eax,0CCCCCCCCh
0103151C rep stos dword ptr es:[edi];开启堆栈帧(/RTCs)后,就会有类似的汇编,将未
;初始化的局部变量值初始化为cc,一个int 3指令
;如果输出未初始化的一个int 变量,值就是-858993460
;因为数据以补码保存,-858993460补码就是0xcccccccc
;输出未初始化的字符则输出 烫,这就是我们debug时,
;内存里老多 烫烫烫烫烫烫的原因。
259: Base *p=new Derive();
0103151E push 4 ;operator new函数参数入栈,即要为Derive对象分配四个字节的空间。
01031520 call operator new (1031208h) ;调用operator new函数
01031525 add esp,4 ;__cdecl调用约定,函数调用者,调整栈帧,
01031528 mov dword ptr [ebp-0D4h],eax;将operator new 函数返回结果存入dword ptr[ebp-0D4h]
;这段空间,operator new结果返回一个指针,指向分配的内
;存的地址,vc中整形或者能隐式转化成整形的返回值放入eax
0103152E cmp dword ptr [ebp-0D4h],0 ;测试返回值是否为0
01031535 je main+4Ah (103154Ah) ;为0则调转
01031537 mov ecx,dword ptr [ebp-0D4h];将operator new 分配的内存地址放入ecx,vc中成员函数
;调用时this指针存入ecx的。
0103153D call Derive::Derive (1031127h) ;调用构造函数,在构造函数里完成虚表指针的初始化,
;由于没有显时定义默认构造函数,所以编译器负责生成一个
01031542 mov dword ptr [ebp-0DCh],eax ;编译器生成的默认构造函数中,将构造好虚表指针的对象的地
;址放入了eax,所以这句相当于取对象的地址。编译器生成的
;默认构造函数代码稍后介绍
01031548 jmp main+54h (1031554h) ;跳过下一条指令的执行。
0103154A mov dword ptr [ebp-0DCh],0;如果走这条指令说明是je main+4Ah (103154Ah)
;跳转过来的,说明内存分配失败,这条指令的作用就是将p值设为0,
;也就是this值设为0,以期望this+偏移访问数据时触发一个异常。
01031554 mov eax,dword ptr [ebp-0DCh] ;如果内存分配没有问题的话,那么dword ptr [ebp-0DCh]
;保存的是对象的地址值。
0103155A mov dword ptr [p],eax ;把对象的地址值赋给dword ptr [p]这段空间,下面的代码就是
;就是通过虚表指针查找虚表的关键代码了,要说关键点了
260: p->Print();
0103155D mov eax,dword ptr [p] ;将对象的地址值存入eax,现在eax=p(p指向对象的起始地址)
01031560 mov edx,dword ptr [eax] ;通过eax寻址,对应的操作是从eax对应的地址值开始往高地址涵盖
;双字,即4个字节,将这4个字节里的数据按照整形方式读出赋给edx
;相当于edx=*(int*)p,前面说了对象的前4个字节是为虚表指针所分
;配的空间,这句指令相当于获取虚表指针的值。
01031562 mov esi,esp
01031564 mov ecx,dword ptr [p] ;this指针存入ecx
01031567 mov eax,dword ptr [edx];查找虚表的操作,跟上面的分析一样,从edx对应地址值开始,往高
;地址涵盖4个字节的内存,读出这段内存里的数据,可以知道edx的值
;即是虚表指针的值,虚表指针指向一个虚表,虚表的地址假设为
;0x00100000,那么0x00100000~0x00100003是存储第一个虚函数的
;地址,0x00100004~0x00100007是存储第二个虚函数的地址.....
;这条指令即是获取第一个虚函数的地址,eax=*(int*)*(int*)p
;现在eax值是一个合法函数指针的值了
01031569 call eax ;进行函数调用
0103156B cmp esi,esp
0103156D call @ILT+435(__RTC_CheckEsp) (10311B8h)
261: }
01031572 xor eax,eax
01031574 pop edi
01031575 pop esi
01031576 pop ebx
01031577 add esp,0DCh
0103157D cmp ebp,esp
0103157F call @ILT+435(__RTC_CheckEsp) (10311B8h)
01031584 mov esp,ebp
01031586 pop ebp
01031587 ret
通过上述分析,类似*(int*)*(int*)p这样的表达式来获取虚表中函数方法大家应该明白了吧,这个地方确实是考察指针应用的基本功的。
void(*f)()=(void(*)())*(int*)*(int*)p;
f();
最终调用的是Derive::Print();很显然*(int*)(*(int*)p+4)是虚表中第二个函数地址地址值,如果有的话~~~~~
下面来看下,编译器生成的构造函数里到底做了些什么,
Derive::Derive:
01031127 jmp Derive::Derive (10315B0h)
找到内存10315B0h处的汇编指令:
view
plain
Derive::Derive:
010315B0 push ebp
010315B1 mov ebp,esp
010315B3 sub esp,0CCh
010315B9 push ebx
010315BA push esi
010315BB push edi
010315BC push ecx
010315BD lea edi,[ebp-0CCh]
010315C3 mov ecx,33h
010315C8 mov eax,0CCCCCCCCh
010315CD rep stos dword ptr es:[edi]
010315CF pop ecx
010315D0 mov dword ptr [ebp-8],ecx;将ecx中保存的this指针值存入dword ptr [ebp-8]
010315D3 mov ecx,dword ptr [this] ;this指针存入ecx,调用成员函数用,单继承下
;dword ptr [this]和dword ptr [ebp-8]值是一样
010315D6 call Base::Base (1031131h);调用基类构造函数
010315DB mov eax,dword ptr [this] ;dword ptr [this]指向对象已通过Base::Base
;进行了初始化,此时虚表指针指向了父类的虚表
010315DE mov dword ptr [eax],offset Derive::`vftable' (1037834h)
;将dword ptr [this]指向对象的虚表指针修改成
; Derive::`vftable' ,dword ptr [this]相当于
;一个对象指针,假设为p,这句指令相当于*(int*)p=
;Derive::`vftable' .
010315E4 mov eax,dword ptr [this] ;将初始化好的对象地址存入eax,相当于设置返回值
010315E7 pop edi
010315E8 pop esi
010315E9 pop ebx
010315EA add esp,0CCh
010315F0 cmp ebp,esp
010315F2 call @ILT+435(__RTC_CheckEsp) (10311B8h)
010315F7 mov esp,ebp
010315F9 pop ebp
010315FA ret
看一下Base::Base (1031131h);汇编代码
view
plain
Base::Base:
01031690 push ebp
01031691 mov ebp,esp
01031693 sub esp,0CCh
01031699 push ebx
0103169A push esi
0103169B push edi
0103169C push ecx
0103169D lea edi,[ebp-0CCh]
010316A3 mov ecx,33h
010316A8 mov eax,0CCCCCCCCh
010316AD rep stos dword ptr es:[edi]
010316AF pop ecx
010316B0 mov dword ptr [ebp-8],ecx
010316B3 mov eax,dword ptr [this]
010316B6 mov dword ptr [eax],offset Base::`vftable' (1037844h)
010316BC mov eax,dword ptr [this] ;和Derive类似,也有一个设置虚表指针的操作
010316BF pop edi
010316C0 pop esi
010316C1 pop ebx
010316C2 mov esp,ebp
010316C4 pop ebp
010316C5 ret
分析到这里,相信大家对虚函数调用有个基本的认识了,编译器在实现虚函数时,主要有以下步骤:
1 编译时,根据类的声明,生成一个虚函数表
2 创建对象时,编译器会在类的构造函数内安插一部分代码,用来初始化对象的虚表指针,一般(vc g++)在进入构造函数
开始部分便安插代码。
3 当以指针或引用来调用虚函数时便激活动态绑定,实质是一个通过虚表指针查找函数的过程
所以类似这样代码Derive(){memset(this,0,sizeof(Derive));}将是灾难性的~~~
由于虚函数的实现要借助构造函数,所以构造函数不能是虚拟函数~~~
最后介绍两个关于c++虚函数的hack的简单程序,以加深编对译器实现虚函数机制的了解~~~~
view
plain
#include <iostream>
#include <vector>
using namespace std;
class Base
{
public:
virtual void PrintA()
{
cout<<"^-^"<<endl;
}
virtual void PrintB()
{
cout<<"T-T"<<endl;
}
};
class Derive:public Base
{
public:
virtual void PrintA()
{
cout<<":)"<<endl;
}
virtual void PrintB()
{
cout<<":("<<endl;
}
};
void Hack1()
{
cout<<"Hack1"<<endl;
}
void Hack2()
{
cout<<"Hack2"<<endl;
}
int main()
{
Base *p=new Derive();
int *pVtable[2]={(int*)Hack1,(int*)Hack2};//构造一个虚表
*(int*)p=(int)pVtable;//设置虚表指针
p->PrintA();
p->PrintB();
system("pause");
}
很显然通过修改虚表指针来劫持程序,下面来通过修改虚表来劫持程序~~~~~~~~~~~~~~
view
plain
#include <iostream>
#include <Windows.h>
using namespace std;
class Base
{
public:
virtual void PrintA()
{
cout<<"^-^"<<endl;
}
virtual void PrintB()
{
cout<<"T-T"<<endl;
}
};
class Derive:public Base
{
public:
virtual void PrintA()
{
cout<<":)"<<endl;
}
virtual void PrintB()
{
cout<<":("<<endl;
}
};
void Hack1()
{
cout<<"Hack1"<<endl;
}
int main()
{
Base *p=new Derive();
int PrintAAdress=*(int*)*(int*)p;//获取PrintA在虚表中的地址值
int PrintBAdress=*(int*)(*(int*)p+4);//获取PrintB在虚表中的地址值
//vc debug下函数指针值和函数名对应的地址开始,存放是一个jmp指令
//对应机器吗是0xe9
if (*(unsigned char*)PrintAAdress==0xe9)
{
DWORD d;
int PrintBOffset=*(int*)(PrintBAdress+1);//获取jmp指令后立即数的值
int Hack1Offset=*(int*)((int)Hack1+1);
//jmp 后立即数是相对于本条jmp指令的偏移,这里想把虚表的PrintA地址修
//改成PrintB,所以重新计算偏移
int diff=PrintBOffset-(PrintAAdress-PrintBAdress);
WriteProcessMemory(GetCurrentProcess(),(int*)(PrintAAdress+1), &diff, 4, &d);
diff=Hack1Offset-(PrintBAdress-(int)Hack1);
WriteProcessMemory(GetCurrentProcess(),(int*)(PrintBAdress+1), &diff, 4, &d);
//release下函数指针和函数名的值就是函数对应汇编指令的起始地址
}else{
DWORD dwIdOld;
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,1,GetCurrentProcessId());
//把对应的内存页修改成可读写的,debug下权限比较大,所以可以直接读写
VirtualProtectEx(hProcess,(int*)*(int*)p,4,PAGE_READWRITE,&dwIdOld);
WriteProcessMemory(hProcess,(int*)*(int*)p, &PrintBAdress, 4, 0);
VirtualProtectEx(hProcess,(int*)*(int*)p,4,dwIdOld,&dwIdOld);
int Hack1Adress= (int)Hack1;
VirtualProtectEx(hProcess,(int*)(*(int*)p+4),4,PAGE_READWRITE,&dwIdOld);
WriteProcessMemory(hProcess,(int*)(*(int*)p+4), &Hack1Adress, 4, 0);
VirtualProtectEx(hProcess,(int*)(*(int*)p+4),4,dwIdOld,&dwIdOld);
}
//现在成功改写了虚表,所有Derive对象动态绑定,都会转到PrintB和Hack1上
p->PrintA();
p->PrintB();
}
上面程序就是成功修改了编译器创建的虚表,可以真正算得上一个hack了~~~,上面程序vc9/10+win7 debug/release默认编译选项通过~~~~
如果您看懂上述两个程序,相信您对虚表的编译器实现的认识更加深刻了~~~~
原理就是这个样子了,在多继承情况下,可能麻烦一些,因为对象可能产生多个虚表指针,另外虚析构函数在虚表中布局,各个编译器差异也比较大,
也就是为什么com在实现时要有一个类似release的接口~~~~
声明:转自 demon__hunter
相关文章推荐
- 深入探索c/c++函数(3)---虚成员函数调用的基本过程
- 深入探索c/c++函数(3)---虚成员函数调用的基本过程
- 深入探索c/c++函数(2)---普通成员函数调用的基本过程
- 深入探索c/c++函数(1)---全局函数调用的基本过程
- 深入探索c/c++函数(2)---普通成员函数调用的基本过程
- 深入探索c/c++函数(2)---普通成员函数调用的基本过程
- 深入探索c/c++函数(1)---全局函数调用的基本过程
- 深入理解递归函数的调用过程
- 【深入探索c++对象模型】c++中构造函数调用虚函数的讨论
- 深入C++中构造函数、拷贝构造函数、赋值操作符、析构函数的调用过程总结
- CLR探索系列:深入追踪托管exe加载执行过程
- 深入理解递归算法的调用过程
- 深入理解计算机系统(3.7)------过程(函数的调用原理)
- 深入理解递归函数的调用过程
- 通过分析system_call中断处理过程来深入理解系统调用
- 深入剖析wince6.0下kernelIOctl()的调用过程分析
- Hbase的WAL在RegionServer基本调用过程
- 【转】深入理解递归函数的调用过程
- (深入理解计算机系统)过程调用
- 深入理解C语言的函数调用过程