为了探究虚表的今生前世,先来一段测试代码
虚函数类:
1 class CTest 2 { 3 public: 4 int m_nData; 5 6 virtual void PrintData() 7 { 8 printf("Data = 0x%x\n", m_nData); 9 } 10 }; 11 12 13 class CBase1 14 { 15 public: 16 int m_nData; 17 18 virtual void PrintData1() = 0; 19 }; 20 21 22 class CBase2 23 { 24 public: 25 int m_nData; 26 27 virtual void PrintData2() = 0; 28 }; 29 30 class CBaseTest : public CBase1, public CBase2 31 { 32 public: 33 void PrintData1() 34 { 35 printf("Data = 0x%x\n", CBase1::m_nData); 36 } 37 38 void PrintData2() 39 { 40 printf("Data = 0x%x\n", CBase2::m_nData); 41 } 42 };
测试代码:
1 void Test() 2 { 3 CTest oCTest; 4 CTest* pCTest = new CTest(); 5 6 pCTest->m_nData = 0x8888; 7 pCTest->PrintData(); 8 9 oCTest.m_nData = 888; 10 oCTest.PrintData(); 11 12 delete pCTest; 13 } 14 15 void BaseTest() 16 { 17 CBaseTest oCBaseTest; 18 19 oCBaseTest.PrintData1(); 20 }
1、虚表位于何处?
WinDbg显示虚表的地址的属性:Usage RegionUsageImage(代表此地址区域被映射到二进制文件的镜像),为只读属性。
2、同一个类对象的虚表位置相同吗?
同一个类对象的虚表位置相同。
加载模块的内存位置:0x00380000
虚表的VA = 0x003FDF2C - 0x00380000 = 0x0007DF2C
3、虚表需要在加载后进行初始化吗?
否,虚表的位置在PE文件的 .rdata 节中,.rdata 是存放程序常量的地方,属性为只读
从2的截图中可以看出,虚表第一个函数(CTest::PrintData)的地址 = 0x003A9EFB
其VA = 0x003A9EFB- 0x00380000 = 0x00029EFB
由于该PE文件的基址 = 0x00040000(默认情况下)
故其未重定位前的虚拟地址 = 0x00040000 + 0x00029EFB = 0x00429EFB
由于x86平台文件都是小尾储存,倒过来写就是 FB 9E 42 00,即如图左上红圈所示
说明PE文件在编译好后已经将虚表和虚表中的虚函数地址填写完毕并写入.rdata区
4、多父类继承的虚表如何存放?
多重继承的子类对象实际上是将每个父类的完整数据按顺序依次排布,所以拥有每个父类的虚表,父类每个虚表的位置同样在每个父类的起始位置。
oCBaseTest对象内存分布
5、何为虚表Hook?
通过以上对虚表的来龙去脉的分析,有IAT Hook基础的同学可以很容易的想到如何进行虚表Hook了。
5.1 改PE文件
直接改掉PE文件虚表位置的函数指针,指向自己伪造的虚表地址(当然必须保证自己的虚表函数指针有效,且函数调用形式和参数个数一致)
5.2 内存中Hook
当PE文件加载后,直接在内存中修改虚表函数指针(注意要去写保护,当然调试工具是没问题的)。
5.3 Hook测试(内存Hook的方式)
测试代码:
1 class CVFHook 2 { 3 public: 4 typedef void (CVFHook::* MemberFunctionPtr)(); 5 6 public: 7 static BOOL Hook(void* pVirtrulFunctionVA); 8 9 private: 10 void PrintData(); 11 }; 12 BOOL CVFHook::Hook(void* pVirtrulFunctionVA) 13 { 14 HMODULE hHookedModule = ::GetModuleHandle(NULL); 15 16 if (NULL == hHookedModule) 17 { 18 _tprintf(_T("Get module handle fail\n")); 19 return FALSE; 20 } 21 22 //取重定位后的虚表地址 23 MemberFunctionPtr* pMemberFunctionPtr = (MemberFunctionPtr*)((INT64)pVirtrulFunctionVA + (INT64)hHookedModule); 24 25 //去除读写保护 26 DWORD dwOldProtect = 0; 27 ::VirtualProtect(pMemberFunctionPtr, sizeof(pMemberFunctionPtr), PAGE_READWRITE, &dwOldProtect); 28 *pMemberFunctionPtr = &CVFHook::PrintData; 29 30 return TRUE; 31 } 32 33 void CVFHook::PrintData() 34 { 35 printf("PrintData is facked!!!\n"); 36 }
然后再dll的加载位置传入虚表的VA参数调用即可:
1 BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 2 { 3 switch (ul_reason_for_call) 4 { 5 case DLL_PROCESS_ATTACH: 6 { 7 CVFHook::Hook((void*)0x0007DF2C); 8 break; 9 } 10 case DLL_THREAD_ATTACH: 11 case DLL_THREAD_DETACH: 12 case DLL_PROCESS_DETACH: 13 break; 14 } 15 return TRUE; 16 }
注意:这里我们伪造的虚函数 CVFHook::PrintData() 我直接用的是与要 Hook 的虚函数相同的成员函数形式,由于Release版的代码可能会被编译器优化掉函数调用形式,如果如此,需要我们手动编写汇编形式的虚函数代码。
启动测试程序 Console.exe ,然后用 APIMonitor 工具将 VirtrualFunctionHook.dll 注入,然后按任意键观察结果如下:
我们发现堆对象被成功Hook了,但局部对象没有被Hook掉,这是为什么呢?难道上面的分析错了?
我们来对前面的测试代码进行反汇编后观察:
发现对象调用PrintData时并没有通过虚表调用,而是直接使用普通成员函数的调用方式,以节约开销,只有使用对象指针或引用来调用虚函数时才会使用虚表调用的方式,故虚表Hook的方式失效.