RCE Endeavors 😅

May 25, 2023

Function Hooking: Virtual Table Hooks (5/7)

Filed under: Programming,Reverse Engineering — admin @ 11:44 PM

Table of Contents:

A special case of function hooking is virtual table hooking. This technique involves overwriting an address in the virtual table of an instance of a C++ class. Recall that when you have a class hierarchy, deriving classes can overwrite virtual functions of the base class with a different implementation. For example, take the two simple classes shown below:

class BaseClass {
public:
    virtual ~BaseClass() = default;

    virtual void Hello() const {
        std::cout << "Hello" << std::endl;
    }

    virtual void Name() const {
        std::cout << "Base" << std::endl;
    }

    virtual void Order() const {
        std::cout << "0" << std::endl;
    }
};

class DerivedClass : public BaseClass {
public:
    void Name() const override {
        std::cout << "Derived" << std::endl;
    }

    void Order() const override {
        std::cout << "1" << std::endl;
    }
};

Base class and derived class implementations. The derived class overwrites two virtual functions of the base class.

DerivedClass inherits from BaseClass and overwrites two virtual functions: Name and Order. If you instantiate DerivedClass through a BaseClass pointer, it will know to call the appropriate Name method.

int main(int argc, char* argv[]) {
    
    BaseClass* base{ new BaseClass{} };
    BaseClass* derived{ new DerivedClass{} };

    // Prints out "Base"
    base->Name();

    // Prints out "Derived"
    derived->Name();

    std::cout << std::format("Base type equals derived type? {}",
        (typeid(base) == typeid(derived))) << std::endl;


    return 0;
}

The base and derived instances both have the same type, but call different Name functions.

This dynamic dispatch functionality, where a class is able to know the appropriate function to call, is implemented by the compiler with virtual tables, which you can think of as an array of function pointers whose entries point to the appropriate method implementations for the class. The easiest way to see this in action is to look at the assembly listings for when the base->Name() and derived->Name() lines are executed to see what the difference is. The assembly instructions for each call are shown in the tables below:

Instruction BytesInstruction
48 8B 45 08mov rax, qword ptr [rbp+0x8]
48 8B 00mov rax, qword ptr [rax]
48 8B 4D 08mov rcx, qword ptr [rbp+0x8]
FF 50 10call qword ptr [rax+0x10]
The assembly instructions for the base->Name() function call.
Instruction BytesInstruction
48 8B 45 28mov rax, qword ptr [rbp+0x28]
48 8B 00mov rax, qword ptr [rax]
48 8B 4D 28mov rcx, qword ptr [rbp+0x28]
FF 50 10call qword ptr [rax+0x10]
The assembly instructions for the derived->Name() function call.

The instructions for both calls look pretty similar. In fact, the only difference is that the base instance loads destination register values from [RBP+0x8] while the derived instance loads from [RBP+0x28]. This is nothing surprising; these are two different declared variables so they will have different locations in the stack frame. The interesting part is what is happening with the RAX register. This register is initially loaded with the memory address of the class instance, then it is set again with the address of the first member of the class. The address at [RAX+0x10] is then invoked to perform the call to the Name function.

Walking the virtual table

You may notice that this function takes in an argument; the argument is passed in the RCX register per the x64 calling convention. The RCX register is loaded with [RBP+0x28], which is the address of the class instance. What this actually represents is the this pointer being passed to the member function. This is a convention that you will see with every invocation of a C++ member function.

This means that the first member of these two classes has a different value, and that calling it at +0x10 will take you to a different location in the program’s execution. Looking at the class definitions, there were no members defined for either class. This hidden member is the classes virtual table, which, as mentioned earlier, is an array of function pointers that hold the address of the appropriate function to call for the class. In this array, there will be an entry for every virtual function that the class type declares. The code below shows additional changes being added to iterate over the virtual address table.

int main(int argc, char* argv[]) {
    
    BaseClass* base{ new BaseClass{} };
    BaseClass* derived{ new DerivedClass{} };

    // Prints out "Base"
    base->Name();

    // Prints out "Derived"
    derived->Name();

    std::cout << std::format("Base type equals derived type? {}",
        (typeid(base) == typeid(derived))) << std::endl;

    auto** vtableDerivedBaseAddress{ reinterpret_cast<void**>(
        *reinterpret_cast<void**>(derived))};

    for (int i{}; i < 4; i++) {
        void* vtableEntry{ vtableDerivedBaseAddress[i] };
        std::cout << std::format("{}: 0x{:X}",  i,
            reinterpret_cast<size_t>(vtableEntry)) << std::endl;
    }

    return 0;
}

The modified code that traverses the virtual table of the derived instance.

We use this knowledge when declaring vtableDerivedBaseAddress above. This variable holds the virtual table of the derived instance. To look at the table, you can iterate over the pointers at the appropriate indices. Since BaseClass defines four virtual functions, there will be four entries in the virtual table; you can loop over these and print out the virtual function address. When running this program, you should see output similar to below, though the exact addresses will be different.

Base
Derived
Base type equals derived type? true
0: 0x7FF6D3203A60
1: 0x7FF6D3203D40
2: 0x7FF6D3203E40
3: 0x7FF6D3203EE0

The console output when the program above is run.

Putting these four addresses into Visual Studio’s Disassembly window lets you verify that these pointers are indeed pointing to the appropriate virtual functions. The mappings between indices, addresses, and function names are shown below:

IndexAddressFunction Name
00x7FF6D3203A60DerivedClass::~DerivedClass
10x7FF6D3203D40BaseClass::Hello
20x7FF6D3203E40DerivedClass::Name
30x7FF6D3203EE0DerivedClass::Order
The full virtual table for the derived instance.

Installing the hook

Having located the virtual table, you can now hook a function by overwriting a pointer stored in this table. Note that this hook is specific to the class instance; only the virtual table for the instance is being overwritten; this method does not install a global hook that affects all callers of the method. Thus, virtual table hooking is a good technique when you have the special need of hooking a C++ class member function, but also need the granularity of the hook applying to specific instance(s) of the class. Overwriting the virtual table entry is simply a matter of setting the appropriate memory permissions and writing in the new pointer. The code below shows how to overwrite a virtual function at a specified index.

void OverwriteVTablePointer(void** const vtableBaseAddress,
    const size_t index, const void* const hookAddress) {

    auto oldProtections{ ChangeMemoryPermissions(&vtableBaseAddress[index],
        sizeof(void*), PAGE_EXECUTE_READWRITE) };
    std::memcpy(&vtableBaseAddress[index], &hookAddress, sizeof(void*));
    ChangeMemoryPermissions(&vtableBaseAddress[index],
        sizeof(void*), oldProtections);
}

The OverwriteVTablePointer writes in a new pointer at a specified index in the provided virtual table.

As an example, the Name function in the derived instance will be hooked. A function pointer to the original Name function is also declared since the hooked function will invoke it.

using NamePtr = void(__stdcall*)(void* const thisPointer);
static NamePtr OriginalName{};

void HookName(void* const thisPointer) {
    std::cout << "Hooked Name!" << std::endl;
    OriginalName(thisPointer);
}

The HookName function will be invoked instead of the derived instance’s Name function.

Now, with the hook function definition in place, the virtual table address can be overwritten. This is done by adding the following code to the end of the main function.

std::cout << "Performing function hook on derived instance"
    << std::endl;

OriginalName = reinterpret_cast<NamePtr>(vtableDerivedBaseAddress[2]);

std::cout << "Calling Name" << std::endl;
derived->Name();

OverwriteVTablePointer(vtableDerivedBaseAddress, 2, HookName);

std::cout << "Calling Name after hook was installed" << std::endl;
derived->Name();

The new code that hooks the Name function of the derived instance.

Verifying the hook

After running this code, the following new output should be present in the console:

Calling Name
Derived
Calling Name after hook was installed
Hooked Name!
Derived

New console output after adding in the code from above.

This shows that the hook was installed successfully. After replacing the function pointer in the virtual table, the custom HookName function was called, at which point it printed out “Hooked Name!” and invoked the original Name function.

Running the demo

The VirtualTableHook project provides the full implementation that was presented in this section. To inspect the virtual table of the Derived instance, set a breakpoint after the vtableDerivedBaseAddress variable is instantiated. Hover your cursor over vtableDerivedBaseAddress and copy the address that pops up in the context menu, as shown below:

The address that vtableDerivedBaseAddress points to.

Paste the address into Visual Studio’s Memory window, accessed though Debug -> Windows -> Memory from Visual Studio’s menu bar. Make sure to set the memory bytes to be treated as 8-byte integers, as shown in Figure 9.20. This is because you will be looking at 64-bit addresses to functions in the virtual table.

Options for how to view memory content.

After changing how the memory is displayed, there will be four sequential addresses that start at the base address of the virtual table. These are the four virtual table entries of the Derived instance. If you wish to verify this, you can take these addresses and put them in Visual Studio’s Disassembly window. You will find that they resolve to the various virtual functions in the Base or Derived classes.

The memory content at the base of the virtual table when interpreted as 8-byte integers.

While still in a broken state, set a new breakpoint after the call to the OverwriteVTablePointer function that is called from main. Continue execution and let your new breakpoint get hit. In the Memory window, you will notice that the third address in the virtual table was overwritten and is now highlighted in red.

The third entry in the virtual table after being overwritten.

This shows the second index in the virtual table being overwritten. Inspecting this address in the Disassembly window will show that this is the address of the HookName function. This shows that the virtual table hook was successful and that future calls to the Name function will instead result in calls to the HookName function for the instance stored in the derived instance.

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

 

Powered by WordPress