Virtual Function Hooking

We have seen in the previous post that sometimes we need to understand and use VF (Virtual Functions) in order to properly hook a function. So far we have seen this two times: when we hooked Present to control the rendering flow of DirectX (here); and when we hooked DrawIndexed to fingerprint models from DirectX (here).

For both cases, we gave for granted how this process works and we didn’t see in details how to implement this for other methods we may need to hook. Let’s quickly review what we saw in the previous posts.

What have we seen so far?

To obtain the real address of DrawIndexed, we did the following:

typedef void(__stdcall *ID3D11DrawIndexed)(ID3D11DeviceContext* pContext, UINT IndexCount, UINT StartIndexLocation, INT BaseVertexLocation); DWORD_PTR* pDeviceContextVTable = NULL; ID3D11DrawIndexed fnID3D11DrawIndexed; pDeviceContextVTable = (DWORD_PTR*)pContext; pDeviceContextVTable = (DWORD_PTR*)pDeviceContextVTable[0]; fnID3D11DrawIndexed = (ID3D11DrawIndexed)pDeviceContextVTable[12]; std::cout << "[+] pDeviceContextVTable Addr: " << std::hex << pDeviceContextVTable << std::endl; std::cout << "[+] fnID3D11DrawIndexed Addr: " << std::hex << fnID3D11DrawIndexed << std::endl;

Basically, what we need to know are two things: an instance of the class and the offset of the method inside the VF table. Getting an instance of the class may vary depending on the class we are trying to hook, sometimes we can create a fake and temporary object to traverse its VF table and find the real address of the function; or another option could be to obtain a reference to an already existent instance and traverse its VF table.

The big question is how we can know the correct offset for our so appreciated method. That’s what we are going to learn today.

When we hooked DrawIndexed we used pContext, which was a reference to an already existent instance of ID3D11DeviceContext. However, when we tried to do the same for Present, we had to create our own instance of IDXGISwapChain and then traverse its VF table. Which option you will choose will depend on what you are trying to do and how the game/engine has been implemented.

How VF works?

When we hook VF tables, we can achieve this through multiple ways, the most common ones are to overwrite the address on the VF table with a pointer to our function or to inject a JMP inside the original function. For the second case, we need to take into account that we will have to fix in our function whatever we are overwriting inside the original function. For example, if you overwrite a sub rsp,30 to place a new JMP, you have to make sure to fix RSP when you call back to the original function.

Modifying the real function address stored in the VF table sounds awesome, but what does this actually mean? What we do is basically the following:

Use an instance of the class to obtain its VF table

Traverse the table and find our target method using its offset

Store the original address from our target method

Modify this entry on the VF table with the address of our modified method

(Optional) restore the original address once we accomplish our goal

What will happen the next time that the game tries to call to the hooked method? The game will traverse the VF table of the object to obtain the real address of the method. Since this table has been modified by us, the game will end thinking that the real address of the method is our new injected address and will redirect the flow of the process to this address. Great! Now the game will execute our method. After that, we usually need to call the original method by using the address we stored before modifying the VF table.

Something that we need to be aware of, is that now always the VF table is used to identify the real address of a function. This won’t always happen, and if we can use this technique will depend on a few things.

Let’s say we have a class called “Foo” and we want to call its method “hack”. There are different ways of doing this, depending on how the class and their methods had been implemented:

fooInst.hack(); and fooInst->hack(); (*fooInst).hack()

In the first case, the application won’t traverse the VF table, unless is used as a reference to an instance, because it knows the exact address and type function. For the last two cases, the VF table will be traversed successfully because of something called type ambiguity.

This happens because the compiler checks for ambiguities at compile time, and it is unclear how to resolve the access to the function. There are multiple cases where the compiler finds a declaration ambiguous and there is plenty of information on the internet about this.

Finding the correct offset at runtime

Let’s imagine that we need to hook a method from ID3D11DeviceContext and we already have an instance of this object called pContext. We may have obtained this instance as a reference by locating an already existing object or by creating our own one. What we could do is print the address of our instance, so now we can attach to the process and locate it on memory:

By having this address we can attach any debugger we want and start analyzing our instance in run time. Below you can see the address where our instance is located:

The first pointer located at `436CFCB8` will be the address of the VF table we are looking for. Let’s follow that pointer:

It looks like there are a lot of pointers, perfect, this is what we were looking for, the VF table. Now the next step would be to identify the correct offset of our target function. If we do Right Click -> Follow QWORD in Disassembler and we have the debugging symbols for d3d11.dll we will be able to see the name of the function that starts at that address, as you can see at the top of the previous image:

`.text_hf:00007FFE06FC7CD0 d3d11.dll:$187CD0 #1868D0 <CContext::TID3D11DeviceContext_DrawIndexed_Amortized<1>>`

By counting the index of the offset we have just found we can obtain its offset. In this case will be 12.

Finding the correct offset statically

For DLLs with symbols, it’s pretty simple also to check it statically using IDA for example. By opening dx11.dll and looking through ´.rdata´ for adjacent function offsets, we can easily find the VF table of TID3D11DeviceContext:

You can see the offsets marked with red numbers on each reference. The number 12 will correspond to our target function.

Conclusion

As you can see, finding the correct offset it is quite simple and there are multiple ways to achieve the same. These are two examples that you can use to quickly find the offsets you need when hooking a function. Of course, if you are trying to hook a custom class without symbols, more reversing will be required in order to identify the class vtable inside the .exe/.dll.