C++淺析——虛表和虛表Hook

爲了探究虛表的今生前世,先來一段測試代碼

虛函數類:

class	CTest
{
public:
	int		m_nData;

	virtual	void	PrintData()
	{
		printf("Data = 0x%x\n", m_nData);
	}
};


class	CBase1
{
public:
	int		m_nData;

	virtual	void PrintData1() = 0;
};


class	CBase2
{
public:
	int		m_nData;

	virtual	void PrintData2() = 0;
};

class	CBaseTest : public CBase1, public CBase2
{
public:
	void	PrintData1()
	{
		printf("Data = 0x%x\n", CBase1::m_nData);
	}

	void	PrintData2()
	{
		printf("Data = 0x%x\n", CBase2::m_nData);
	}
};
測試代碼:

void	Test()
{
	CTest	oCTest;
	CTest*	pCTest = new CTest();

	pCTest->m_nData = 0x8888;
	pCTest->PrintData();

	oCTest.m_nData = 888;
	oCTest.PrintData();

	delete	pCTest;
}

void	BaseTest()
{
	CBaseTest	oCBaseTest;

	oCBaseTest.PrintData1();
}

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的方式)

測試代碼:

class	CVFHook
{
public:
	typedef	void (CVFHook::* MemberFunctionPtr)();

public:
	static	BOOL	Hook(void* pVirtrulFunctionVA);
	
private:
	void	PrintData();
};
BOOL	CVFHook::Hook(void* pVirtrulFunctionVA)
{
	HMODULE	hHookedModule = ::GetModuleHandle(NULL);

	if (NULL == hHookedModule)
	{
		_tprintf(_T("Get module handle fail\n"));
		return	FALSE;
	}

	//取重定位後的虛表地址
	MemberFunctionPtr* pMemberFunctionPtr = (MemberFunctionPtr*)((INT64)pVirtrulFunctionVA + (INT64)hHookedModule);
	
	//去除讀寫保護
	DWORD	dwOldProtect = 0;
	::VirtualProtect(pMemberFunctionPtr, sizeof(pMemberFunctionPtr), PAGE_READWRITE, &dwOldProtect);
	*pMemberFunctionPtr = &CVFHook::PrintData;

	return	TRUE;
}

void	CVFHook::PrintData()
{
	printf("PrintData is facked!!!\n");
}
然後再dll的加載位置傳入虛表的VA參數調用即可:

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		{
			CVFHook::Hook((void*)0x0007DF2C);
			break;
		}
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

注意:這裏我們僞造的虛函數 CVFHook::PrintData() 我直接用的是與要 Hook 的虛函數相同的成員函數形式,由於Release版的代碼可能會被編譯器優化掉函數調用形式,如果如此,需要我們手動編寫彙編形式的虛函數代碼。

啓動測試程序 Console.exe ,然後用 APIMonitor 工具將 VirtrualFunctionHook.dll 注入,然後按任意鍵觀察結果如下:


我們發現堆對象被成功Hook了,但局部對象沒有被Hook掉,這是爲什麼呢?難道上面的分析錯了?

我們來對前面的測試代碼進行反彙編後觀察:


發現對象調用PrintData時並沒有通過虛表調用,而是直接使用普通成員函數的調用方式,以節約開銷,只有使用對象指針或引用來調用虛函數時纔會使用虛表調用的方式,故虛表Hook的方式失效.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章