Understanding C++ Virtual Tables and VTable Hooking
Exploring Virtual Tables (VTables)
To understand the lifecycle and structure of virtual tables in C++, let's start with a set of test classes involving virtual functions and multiple inheritance.
Virtual Function Classes
#include <cstdio>
class SampleClass {
public:
int internalValue;
virtual void displayValue() {
printf("Value = 0x%x\n", internalValue);
}
};
class AbstractBaseA {
public:
int dataA;
virtual void executeTaskA() = 0;
};
class AbstractBaseB {
public:
int dataB;
virtual void executeTaskB() = 0;
};
class MultiDerived : public AbstractBaseA, public AbstractBaseB {
public:
void executeTaskA() override {
printf("Task A Data = 0x%x\n", AbstractBaseA::dataA);
}
void executeTaskB() override {
printf("Task B Data = 0x%x\n", AbstractBaseB::dataB);
}
};
Test Execution
void runTest() {
SampleClass stackObj;
SampleClass* heapObj = new SampleClass();
heapObj->internalValue = 0x8888;
heapObj->displayValue();
stackObj.internalValue = 0x777;
stackObj.displayValue();
delete heapObj;
}
void runMultiInheritanceTest() {
MultiDerived derivedObj;
derivedObj.executeTaskA();
}
1. Where is the VTable Located?
Using a debugger like WinDbg, we can inspect the memory attributes of a vtable address. Typically, the vtable is found in a region mapped to the binary image (RegionUsageImage) with read-only permissions.
2. Do Instances of the Same Class Share a VTable?
Yes, every instance of the same class points to the same virtual table location. If you check the memory address of the first 4/8 bytes (the vtable pointer) of multiple objects, they will point to the same memory address in the global data section.
3. Is the VTable Initialized at Runtime?
No, the vtable is generally stored in the .rdata section of the PE (Portable Executable) file. This section is used for constant data and is marked as read-only.
By calculating the Virtual Address (VA) and checking the raw bytes in the PE file (accounting for little-endian storage), we can see that the function pointers with in the vtable are already populated by the compiler/linker. The loader simply performs relocations if the module base address changes.
4. VTable Layout in Multiple Inheritance
In multiple inheritance, the derived class object contains the data members of each parent class in the order they were inherited. Consequently, the object holds multiple vtable pointers—one for each base class that contains virtual functions.
5. What is VTable Hooking?
If you are familiar with IAT (Import Address Table) hooking, vtable hooking follows a similar logic. There are two primary methods:
- Static Binary Patching: Directly modifying the function pointers within the vtable in the PE file to point to a custom function.
- Memory Hooking: Modifying the vtable pointers in memory after the module has loaded. This requires changing page protections (e.g., using
VirtualProtect) to make the read-only.rdatasection writable.
Implementation of Memory-Based VTable Hooking
#include <windows.h>
#include <tchar.h>
class VTableHijacker {
public:
// Define a member function pointer type for the target signature
typedef void (VTableHijacker::*FuncPtr)();
static BOOL ApplyHook(void* vtableOffsetVA) {
HMODULE hModule = GetModuleHandle(NULL);
if (!hModule) return FALSE;
// Calculate the actual memory address after relocation
FuncPtr* targetEntry = reinterpret_cast<FuncPtr*>(
reinterpret_cast<UINT_PTR>(vtableOffsetVA) + reinterpret_cast<UINT_PTR>(hModule)
);
DWORD oldProtect;
// Remove write protection
if (VirtualProtect(targetEntry, sizeof(FuncPtr), PAGE_READWRITE, &oldProtect)) {
FuncPtr hookFunc = &VTableHijacker::detourDisplay;
*targetEntry = hookFunc;
VirtualProtect(targetEntry, sizeof(FuncPtr), oldProtect, &oldProtect);
return TRUE;
}
return FALSE;
}
private:
void detourDisplay() {
printf("The function has been successfully hijacked!\n");
}
};
This hook can be triggered during DLL injection via DllMain:
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
// Provide the RVA of the specific virtual function entry
VTableHijacker::ApplyHook((void*)0x0007DF2C);
}
return TRUE;
}
Analysis of Hooking Results
After injecting the hook into a target process, we observe an interesting phenomenon: objects allocated on the heap are successfully hooked, but local stack objects might still execute the original code.
Why does this happen?
When inspecting the disassembly, we find that compilers often optimize virtual function calls. If the compiler can determine the exact type of the object at compile time (as is often the case with local stack objects), it may bypass the vtable entirely and perform a direct function call to save overhead. This is known as static dispatch or devirtualization. The vtable is only reliably used when calling functions through a pointer or reference where the polymorphic type is determined at runtime.