Welcome back to another instalment of the ‘alternative approaches’ series! This time we’re revisiting one of the fundamentals of internal game hacking on the Source Engine: the CreateInterface function, or more specifically, the underlying list of version strings and pointers that you should be using instead.
In previous tutorials I’ve demonstrated how to get game classes by calling the exported CreateInterface function. It’s a fine method but you have to provide a full version string when calling it. Fair enough, these don’t change that often, but it’s still one more thing that’s inevitably going to stop working after a game update.
A quick look at the Source SDK 2013 code shows CreateInterface calling CreateInterfaceInternal. These functions are rather straightforward, simply looping through the s_pInterfaceRegs linked list (see class definition) and comparing the m_pName member variable against the version string provided by the user. If the interface is found, return the result of the m_CreateFn function, if not, return NULL.
Well, seems simple enough so let’s get straight into it. I’ll start off with Counter-Strike: Source, a 32-bit game. Pick any game library, open it in IDA, switch to the ‘Exports’ tab and double-click on CreateInterface.
We’ll revisit this function in a moment. For now, jump to sub_5B3D70.
Looks like we’ve found the list already at 0xD0BE50! I’ll be using my Linux Basehook project for testing here. I’ve simply appended the following code to the bottom of the initialisation function, recompiled and loaded the library into the game.
Make sure to substitute the engine_address variable with the base address of engine.so in memory.
InterfaceReg* interface_list = *reinterpret_cast<InterfaceReg**>(engine_address + 0xD0BE50);
for (InterfaceReg* current = interface_list; current; current = current->m_pNext) {
printf("%s => 0x%X\n", current->m_pName, current->m_CreateFn());
}
Right, now we can start thinking about a more permanent solution. You could always use pattern scanning, but of course, there’s another way. Interested? Of course you are. Let’s take another look at the CreateInterface function.
The address to s_pInterfaceRegs can be found in CreateInterfaceInternal. Unfortunately, we can’t get there directly since it’s not an exported symbol. On the other hand, CreateInterface is exported and we’ve already seen it doesn’t do anything but jump directly to CreateInterfaceInternal.
That means we can use dlsym to get to CreateInterface, find where it’s jumping to, go there ourselves and extract the address to s_pInterfaceRegs. Seems simple enough, doesn’t it? Let’s get started.
Here’s the disassembled CreateInterface again for reference:
.text:005B3E10 55 push ebp .text:005B3E11 89 E5 mov ebp, esp .text:005B3E13 5D pop ebp .text:005B3E14 E9 57 FF FF FF jmp CreateInterfaceInternal
The jump to CreateInterfaceInternal is 4 bytes into this function.
It’s a relative jump so we’ll need to read the displacement value after the E9 opcode.
uintptr_t jump_instruction_addr = uintptr_t(createinterface_symbol) + 4;
int32_t jump_displacement = *reinterpret_cast<int32_t*>(jump_instruction_addr + 1);
In this case, the displacement is relative to the next instruction. Since the complete jump instruction is 5 bytes long we need to ‘move’ the instruction pointer to 0x5B3E19, then add the displacement value to get our final address.
uintptr_t createinterfaceinternal_addr = (jump_instruction_addr + 5) + jump_displacement;
Alright, now we’ve got the absolute address to CreateInterfaceInternal. Let’s look at it one more time.
.text:005B3D70 55 push ebp .text:005B3D71 89 E5 mov ebp, esp .text:005B3D73 57 push edi .text:005B3D74 56 push esi .text:005B3D75 53 push ebx .text:005B3D76 83 EC 1C sub esp, 1Ch .text:005B3D79 8B 1D 50 BE D0 00 mov ebx, ds:s_pInterfaceRegs
Nice and easy. Simply read the address from the move instruction, cast to appropriate type and you’re done!
uintptr_t interface_list = *reinterpret_cast<uintptr_t*>(createinterfaceinternal_addr + 11);
InterfaceReg* interface_list = *reinterpret_cast<InterfaceReg**>(interface_list);
Now then, let’s move on to a 64-bit game – specifically Counter-Strike: Global Offensive.
Oh, well isn’t that convenient? It certainly makes the implementation a lot shorter.
interfaceregs_symbol = dlsym(library_handle, "s_pInterfaceRegs");
InterfaceReg* interface_list = *reinterpret_cast<InterfaceReg**>(interfaceregs_symbol);
Some 32-bit games like Insurgency also export this symbol so it’s worth checking for. Here’s a link to a commit I recently pushed to my Linux Basehook project that includes this new implementation. The project is designed for Counter-Strike: Source but this new code has also been tested on Team Fortress 2, Garry’s Mod and Insurgency.