Gabriel Jadderson

•

•

•

•

•

VMT hooking

VMT hooking

Virtual Method Tables (VMTs) or Virtual Function Tables (VFTs) have become ubiquitous in modern software development due to the prevalence of object-oriented programming practices. These tables are integral to implementing polymorphism in languages like C++. In this article, we'll explore how VMTs can be manipulated through a process called VMT hooking, discuss the vulnerabilities it exposes, and propose methods to protect programs from such exploits. There are some limitations to VMT hooking to keep in mind, such as thread safety, reentrancy, or the impact on program stability.

Initially, we'll need to know how the application is structured and if there are any VMT's at all. This can be done with a reverse engineering tools such as IDA Pro, Ghidra, or WinDbg.

Understanding VMT Structure and Storage

VMTs are typically stored as the first element of a class or struct. However, the compiler abstracts this placement, storing a pointer to the VMT within the struct. This makes locating and manipulating VMTs a non-trivial task. To manipulate VMTs, we need to understand their structure and function pointers stored within. VMTs store pointers to virtual functions, which are essentially addresses of these functions in memory.

Consider the following code snippet:

struct fruit 
{
    int Calories;
    virtual void PrintCalories() 
    {       
        printf("fruit->Calories: %d\n", Calories);
    }
    virtual void A() 
    {
        printf("A \n");
    }
    virtual void B() 
    {
        printf("B \n");
    }
};

struct apple : fruit
{
    void PrintCalories() override
    {
        printf("apple->Calories: %d\n", Calories);
    }
};

In Visual Studio, the VMT is denoted as __vfptr, which can be inspected during debugging sessions.

visual studio inspection 1

We can clearly see our virtual functions PrintCalories, A, B in there:

visual studio inspection 2

Since the VMT is stored as the first element, Getting the VMT can be done by casting it to a triple pointer and dereferencing it. The reason why we're storing it as a double pointer is that it's a pointer to a list of memory addresses. Storing it as a double pointer makes accessing much easier as we'll notice later on.

void** VirtualFunctionTable = *(void***)&MyFruit;

At this point, it would be useful to know how big the Virtual Function Table is. Here's an easy and fast way to retrieve it without calling into windows and querying page sizes:

unsigned long long VMTLength(void** VMT) 
{
    unsigned long long i = 0;
    for (; VMT[i] && VMT[i] < (void *)VMT; i++);
    return i;
}

And retrieve the count like so:

unsigned long long NumberOfVirtualFunctions = VMTLength(VirtualFunctionTable);

Getting to the Virtual functions

First, we enumerate the entries in the VMT and store the addresses of the virtual functions: Recall that the VMT stores pointers to the functions, essentially that means that FuncEntry1 is a pointer to a function.

//retrieving the functions:
void* FuncEntry1 = VirtualFunctionTable[0];
void* FuncEntry2 = VirtualFunctionTable[1];
void* FuncEntry3 = VirtualFunctionTable[2];

At this point, we could even call the functions directly. However, there's a caveat; we can't call FuncEntry1 (PrintCalories), the reason is that it implicitly requires the 'this' pointer to the base class because we're passing in Calories.

printf("fruit->Calories: %d\n", Calories);

Implicitly the actual translation is more like this.

printf("fruit->Calories: %d\n", this.Calories);

But our function does not take any arguments where does it get 'this' pointer from? The answer is that the compiler puts the pointer in a register or on the stack depending on the Calling Convention.
From the documentation: /Gd, the default setting, specifies the __cdecl calling convention for all functions except C++ member functions and functions that are marked __stdcall, __fastcall, or __vectorcall. The __cdecl convention places the 'this' pointer as the last element on the stack. In our case, the 'this' pointer is pointing to a different place in memory. We could pass the pointer on the stack via inline assembly instructions, but unfortunately, MSVC doesn't support inline assembly instructions on x64 applications. ...

A workaround is to use another calling convention namely the __thiscall convention. We will define the new convention along with a global variable that acts as two things; stores the address of the original virtual function, and so that we can easily invoke it.

typedef void(__thiscall* VirtualFunction001)(void* ThisPtr);
VirtualFunction001 GlobalVirtualFunction001 = 0;

We can now call FuncEntry1 like so:

GlobalVirtualFunction001 = (VirtualFunction001)FuncEntry1;
GlobalVirtualFunction001(&MyFruit); //we'll have to pass in MyFruit such that we have access to MyFruit.Calories.

If we instead execute FuncEntry2; it'll work fine without any explicit definition.

((void (*)(void))*FuncEntry2)(); //we know it returns void and takes no arguments.

This is the output of both the execution of FuncEntry1 and FuncEntry2.

VMT function calling

Hooking

VMT hooking involves intercepting calls to virtual functions and redirecting them to alternate functions, known as hooks. This can be useful for various purposes, including debugging or adding additional functionality.

Now lets try to hook into FuncEntry1 or PrintCalories.

First, we need to define the function that'll be the 'Hook'. We'll use the __fastcall convention since it is the easiest way and requires less code. For now, all our 'Hook' function does is call into printf.

//as specified by https://docs.microsoft.com/en-us/cpp/cpp/fastcall?view=vs-2019
//"The first two DWORD or smaller arguments that are found in the argument list
//from left to right are passed in ECX and EDX registers;
//all other arguments are passed on the stack from right to left."
void __fastcall OurHookFunction(void* ECXRegister, void* EDXRegister)
{
    //that means that the pointer to which "this" operator points to is now stored in the ECS register.
    //and we can retrieve it like so:
    fruit* FruitPtr = (fruit*)ECXRegister;
    //FruitPtr->PrintCalories(); //infinite recursion!
    FruitPtr->A();
    FruitPtr->B();

    printf("Broccoli is not a fruit but a malicious vegetable!\n");

    //Call the original 
    GlobalVirtualFunction001(ECXRegister);
}

Notice that we can call the other virtual functions from inside.

Next, we'll have to make sure to overwrite the protection rights to the memory region by calling VirtualProtect. We then save the address of the original Function such that we can restore it later if need be. Then we can simply place in OurHookFunction in the VMT. Memcpy can also be used for this, and if the address space is in another module, we want to use WriteProcessMemory instead. Then remember to restore the memory rights.

//begin hook
DWORD OldProtectionFlags = 0;
VirtualProtect(&VirtualFunctionTable[0], sizeof(void*), PAGE_EXECUTE_READWRITE, &OldProtectionFlags);

//save the original function
GlobalVirtualFunction001 = (VirtualFunction001)VirtualFunctionTable[0];

//overwrite with our function.
VirtualFunctionTable[0] = &OurHookFunction;
//memcpy(VirtualFunctionTable[0], &OurHookFunction, sizeof(void*));
//WriteProcessMemory if we're hooking outside our address space.
 
//Restore the original page protection
VirtualProtect(&VirtualFunctionTable[0], sizeof(void*), OldProtectionFlags, &OldProtectionFlags);

And we have now successfully hooked the VMT with our function.

Sucessfull hook.png

It is important to realize that there exists one VMT per struct that contains the defined virtual functions, in our case that's the fruit struct. The VMT is created when the compiler encounters the first instance of the struct being used. That means if we define MyFruit on the stack, the corresponding VMT will be stored in memory by the compiler, when we then define AllocatedAppleFruit or ``MyApple``` then both will share the same pointer to the VMT.

    fruit MyFruit;
    apple MyApple;
    fruit* AllocatedAppleFruit = new apple;

If we Hook the VMT for MyApple then AllocatedAppleFruit will use our hooked function as well, so will MyFruit unless the function is overloaded! If the function is overloaded a different Function pointer exists to that specific function in the overloaded struct.

Another method is to hook into a sub-function that is being called by our function of interest. E.g. we want to hook into FOO(), but FOO() calls BAR() we can try hooking BAR() instead.

Detection and Prevention

Detecting VMT tampering can be challenging but can involve actively tracking VMT addresses and function pointers. Additionally, monitoring execution within memory sections can provide indications of tampering.

Another detection method is by checking if the execution occurs in the .data section of the memory instead of the .text section. Since that will give a clear indication that memory has been copied there for execution.

Contrary to popular beliefs VirtualProtect does not work, there are many ways to bypass permissions in windows the most common way is to exploit a vulnerable certified driver by buffer overflows or other bugs. There's a big list of vulnerable certified drivers from reputable brands like Asus and Logitech. A driver will be able to write to protected memory bypassing the permissions of a regular application.

Preventing VMT hooking can be achieved by avoiding the use of virtual functions altogether, as they inherently introduce vulnerabilities. Alternatively, employing techniques such as encryption or obfuscation can make VMT manipulation more difficult for attackers.

There are other ways of achieving polymorphism by simply placing a struct inside a struct.

Source Code

The full source code. The Complete project can be found on Github here.

#include <stdio.h>
#include <windows.h>

struct fruit 
{
    int Calories;
    
    virtual void PrintCalories() 
    {       
        printf("fruit->Calories: %d\n", Calories);
    }
    
    virtual void A() 
    {
        printf("A \n");
    }
    
    virtual void B() 
    {
        printf("B \n");
    }
};

struct apple : fruit
{
    void PrintCalories() override
    {
        printf("apple->Calories: %d\n", Calories);
    }
};

unsigned long long VMTLength(void** VMT) {
    unsigned long long i = 0;
    for (; VMT[i] && VMT[i] < (void *)VMT; i++);
    return i;
}

typedef void(__thiscall* VirtualFunction001)(void* ThisPtr);
VirtualFunction001 GlobalVirtualFunction001 = 0;

//as specified by https://docs.microsoft.com/en-us/cpp/cpp/fastcall?view=vs-2019
//"The first two DWORD or smaller arguments that are found in the argument list
//from left to right are passed in ECX and EDX registers;
//all other arguments are passed on the stack from right to left."
void __fastcall OurHookFunction(void* ECXRegister, void* EDXRegister)
{
    //that means that the pointer to which "this" operator points to is now stored in the ECS register.
    //and we can retrieve it like so:
    fruit* FruitPtr = (fruit*)ECXRegister;
    //FruitPtr->PrintCalories(); //infinite recursion!
    FruitPtr->A();
    FruitPtr->B();

    printf("Broccoli is not a fruit but a malicious vegetable!\n");

    //Call the original 
    GlobalVirtualFunction001(ECXRegister);
}

void __fastcall VMTHookFnc(void* pEcx, void* pEdx)
{
    fruit* pThisPtr = (fruit*)pEcx;

    printf("In VMTHookFnc\n");
}

void Test(fruit* fruit)
{
    fruit->PrintCalories();
    fruit->A();
    fruit->B();
}


int main() 
{
    fruit MyFruit;
    MyFruit.Calories = 0;

    apple MyApple;
    MyApple.Calories = 52;

    fruit* AllocatedAppleFruit = new apple;
    AllocatedAppleFruit->Calories = 42;
    
    Test(&MyFruit);

    printf("\n\n");

  
    void** VirtualFunctionTable = *(void***)&MyFruit;
    unsigned long long NumberOfVirtualFunctions = VMTLength(VirtualFunctionTable);

    //retrieving the functions:
    void* FuncEntry1 = VirtualFunctionTable[0];
    void* FuncEntry2 = VirtualFunctionTable[1];
    void* FuncEntry3 = VirtualFunctionTable[2];

    //Call FuncEntry1 directly
    GlobalVirtualFunction001 = (VirtualFunction001)FuncEntry1;
    GlobalVirtualFunction001(&MyFruit); //we'll have to pass in MyFruit such that we have access to MyFruit.Calories.

    ((void (*)(void))FuncEntry2)(); //we know it returns void and takes no arguments.

    
    //begin hook
    DWORD OldProtectionFlags = 0;
    VirtualProtect(&VirtualFunctionTable[0], sizeof(void*), PAGE_EXECUTE_READWRITE, &OldProtectionFlags);

    //save the original function
    GlobalVirtualFunction001 = (VirtualFunction001)VirtualFunctionTable[0];

    //overwrite with our function.
    VirtualFunctionTable[0] = &OurHookFunction;
    //memcpy(VirtualFunctionTable[0], &OurHookFunction, sizeof(void*));
    //WriteProcessMemory if we're hooking outside our address space.
 
    //Restore the original page protection
    VirtualProtect(&VirtualFunctionTable[0], sizeof(void*), OldProtectionFlags, &OldProtectionFlags);
    
    Test(&MyFruit);
    Test(&MyApple);
    Test(AllocatedAppleFruit);
    
    return 0;
}