VMT hooking

VMT hooking

Today Virtual Method Tables(VMTs)/Virtual Function Tables(VFTs) can be found everywhere. The reason for that is the religious practices of object-oriented programming.

I'll show how easily they can be hacked into and exploited and how to protect programs from VMT hooking.

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 IDA Pro, Ghidra, or WinDbg.

How does it look like and what is stored there?

The VMT is always stored as the first element of a class/struct, the compiler actively knows this and hides the VMT when necessary. The compiler puts the VMT in a different place than the struct, what's put in the struct, however, is a pointer to the VMT.

This is the code we'll hook into:

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 Virtual Method Table is denoted as __vfptr, and if we hover over the struct in debug mode to view its contents, and we can inspect the __vfptr:

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

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

One way of detecting VMT tampering is to actively track the address of the VMT and the addresses of the functions within at all times. This can be done by XORing the addresses of the vtables we want to protect and storing them and spawn a thread in the background that periodically checks for changes.

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.

The best way to protect against VMT hooking is to not use virtual functions at all. That might sound crazy to some, but in reality, if we're using a virtual function our entire architecture is flawed anyway and VMT hooking is the least of our problems.

There are however very rare instances where virtual functions could be useful and more in the ballpark of 1 in 50.000 lines of code estimate. There are other ways of achieving polymorphism which is why people use virtual functions for, and that can be achieved by 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;
}