虚函数实现机制
前言
虚函数本身就很玄学,像基类只做声明,派生类在定义,然后不同派生类之间还能依次找到不同的。
正文
注:x86环境,非x64
1 | class AIM{ |
当用sizeof查看aim类的大小时,发现是占用8个字节,但是按照逻辑,实际上应该是只有成员变量占用了实例化的内存。
而这多出来的4字节,就看要是在成员变量前面还是后面了。
std::cout << aim << " " << &aim->HP << std::endl;
发现这个四字节应该是存在于hp之前,说明大概率和后面的虚函数有关
通过反汇编之后
我们看到类调用一个虚函数会这么麻烦。
他先把aim对象地址上的值传给eax,然后又把eax传给edx。
至于aim又传给ecx先不管,看后面的,edx+4 传给eax之后,就直接call eax了,我们知道call的都是函数的地址。
说明通过edx+4之后偏移得到了die虚函数的地址。
而根本的一切还是从aim开头。
说明这个四字节很有可能就像地址表一样。
然后在调用之前的eat函数
看到基本步骤都差不多,差异就在后面不是edx+4,而是直接传edx。
也就是说这个带有虚函数的类前面多出来的四字节,就是一个指针,指向虚函数表
因为是指针,我们就可以取出来查看。
可以看到利用指针拆分掉这个类,变成两个数组成员,0的位置就是我们类的指针区域。
因为x86的指针大小就是4字节,所以找到vtable[0]这个基地址,再将其拆出两个虚函数的地址。
虽然这两个虚函数的地址可能看着很奇怪,毕竟不是放在同一个地方的。
可以用反汇编去印证一下。
当我们逐语句执行到call eax的时候,就能观察到eax现在经过偏移得到我们调用的die虚函数的地址。
所以对于多态类而言
1 | class AIM{ |
地址 | 变量名 | 含义 |
---|---|---|
+0x0 | vatble | aim类虚函数地址表 |
+0x4 | hp | 成员变量 |
他的内存分布上会有明显差异。
所以虚表的性质
- 同一个类的多个实例都指向同一个虚函数表
- 通过修改虚函数表的数据可以实现劫持
- 只有通过指针访问函数才会调用显示函数表
也就是基类的实例化对象再多,这个虚表都是一个地方,派生类肯定会和基类的虚表地址不同。
修改了这个虚表,就能通过这个数据偏移到其他地方
1 | void hack(){ |
随便创个函数,然后修改func[0]为这个函数地址,就有意思了。
不过vs的ide好像有保护,直接写入会报错
额个人是不会处理的,看大哥操作,调用windows先搞掉原有保护
1 |
|
增加这些之后,就可以修改虚地址表了。
可以看到两个指向虚函数的地址都被修改成了hack函数的地址。
至于第一个说性质在这里就能看出来,因为之前的是通过wolf构建初始化的,当我们修改wolf的虚表,是不会对基类造成影响的
至于第三个,像我们这种正常初始化的情况下,他没有指针,也没有通过所谓的虚表偏移去找到虚函数,而是当成一个简单的方法调用
就没用收到之前修改虚表指向hack的问题。
结语
指针。。地址,表还是很玄乎的东西。