Creating A Multiplayer Game Hack

August 28, 2018
c++ binary hooking reverse-engineering

What?

This post covers creating a multihack for multiplayer games. The target is a Quake3 based game called Jedi Academy. I’ve found the techniques and process of doing this to be fairly undocumented and obscure, so this can serve as a starting point for researchers and game hackers.

I’ve released my multihack source code. These are the implemented features:

This is what the wallhack and player glow looks like:

Wallhax

Game Hacking Approaches

First of all, what are the necessary steps to influence another process in a way that it acts according to our intentions? Basically, you have to

  1. Inspect the target: What technologies are being used? Which game engine is in place? How do people write mods for the game in order to modify its logic? Is there some SDK available or is there any open source code?

  2. What parts of the game should be altered? Identify values and/or methods which should be modified/patched.

  3. Find a way to inject custom code into the process. In case of anti cheat mechanisms, search for techniques to hide the code injection process. This is necessary for most modern games but won’t be covered in this post.

  4. Write the code that will be injected into the game in a way that doesn’t break the game.

  5. Profit?

Hookless Hacks

Before talking about API hooks and what they are, let’s quickly check out a way to alter a remote process by directly patching functions in memory. This has already been covered in one of my previous posts. The basic approach is the identify function and memory addresses of interesting data and code and to modify it by spawning a thread in the target process or by overwriting them directly. A good example of this approach can be found here.

This approach might look easy and elegant. However, finding the correct offsets and memory addresses can be hard. Also, after game updates these values don’t match anymore. Because of this, it may be easier to use hooking to alter a remote process.

API Hooking

Hooking basically means intercepting function calls in order to execute code before or after the original function. In this blog post, I hooked a Windows API function in the context of the remote process to intercept calls to the game engine DLLs. This effectively forces the game to execute custom code. Please note that it may not work to be possible to just hook an arbitrary game method with pure API hooking. The best way to get started is to trace target methods to system calls and hooking the system calls instead.

There are easy-to-use API hooking libraries available, like mhook. The usage of this particular library will also be discussed in this post.

DLL Injection

To hook game engines methods, it’s necessary to install the hook from the context of the target application. This can be accomplished with a technique called DLL injection. Using this, the target process gets forced to load and execute a custom DLL in which contains our code. There are many approaches to do this – this post covers the LoadLibraryA method of injection DLLs.

Note: To simplify things, disable unicode and the usage of pre-compiled headers in VisualStudio or things will get ugly.

Building The DLL Loader

The DLL loader has to take care of these things:

  1. Execute the game binary
  2. Get a handle of the game process
  3. Locate the DLL to be injected
  4. Load the DLL in the context of the game

Of course the trickiest part of this is the last point, loading the DLL. This code segment does just this:

// Get the address of LoadLibraryA in kernel32.dll, used to
// pass it to the remote process in order to load our dll later on
LPVOID loadFunctionAddress = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");

// Allocate space in the target process for our DLL **PATH**
LPVOID allocatedMem = LPVOID(VirtualAllocEx(procHandle, nullptr, MAX_PATH, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));

// Write the **PATH** of our DLL in the allocated memory of the remote process
WriteProcessMemory(procHandle, allocatedMem, dllPath, MAX_PATH, nullptr);

// Load the DLL by causing the remote process to spawn a thread which calls LoadLibraryA
// which loads the DLL using the path we allocated previously.
HANDLE threadHandle = CreateRemoteThread(procHandle, nullptr, NULL, LPTHREAD_START_ROUTINE(loadFunctionAddress), allocatedMem, NULL, nullptr);

Please note that error handling has been removed for a better overview.

At this point the DLL has been injected into the target process and its DllMain method should have been called. Now, let’s build a simple DLL to test the injection process

DLL Structure

The starting point for this is a new DLL project created by VisualStudio. Don’t create a static library because this won’t work!

After creating the project, the file dllmain.cpp should have already been created. Let’s create a simple PoC that spawns a message box upon loading the DLL:

extern "C" __declspec(dllexport) void Initialize()
{
	MessageBox(0, "Locked and Loaded.", "DLL Injection Successful!", 0);
}

BOOL APIENTRY DllMain
(
	HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
		case DLL_PROCESS_ATTACH:
			// Do not need the thread based attach/detach messages in this DLL
			DisableThreadLibraryCalls(hModule);
			Initialize();
        break;
    }
    return TRUE;
}

Compile this DLL, pass it to the loader and check out the created message box. Let’s proceed with identifying the required hooks and game modifications.

Know Your Target

The target game is based on the Quake3 engine. Therefore, information on internal structures and processes are required in order to, for example, implement a wallhack. A Quake3 game is being executed in a “VM” that implements its own “system calls”.

The Q3 engine methods are present in a separate DLL called cgamex86.dll which will be loaded by the game process. To load DLLs, the Windows API call GetProcAddress is being used to get the address of a specific function in a DLL. This means that by hooking GetProcAddress it’s possible to intercept calls to the game engine:

// create the hook which serves as entry point for this
if (!Mhook_SetHook((PVOID *)&originalGetProcAddress, hookGetProcAddress))
{
    MessageBox(0, "Couldn't create hook", ":(", 0);
}

The original and unmodified call to GetProcAddress will be stored in originalGetProcAddress and our own function hookGetProcAddress will be executed instead of the real function.

While tracing the executed functions of cgamex86.dll, two calls have been identified as interesting in the game:

  1. vmMain: Main function of the Q3 VM, accepting system calls
  2. dllEntry: Used to execute additional Q3 system calls

According to the Q3 engine source code, vmMain contains code to CG_DrawActiveFrame – a function to draw a game frame. This may be handy later on.

Note for OpenJK Users: Because the maintainers of OpenJK decided to update the syscall process of the game, it’s required to modify CL_BindCGame to use legacy q3 VM system calls.

Similar to this, dllEntry accepts the CG_R_ADDREFENTITYTOSCENE system call that adds an entity (a player, a weapon, …) to the scene to be rendered. The parameter for this is a pointer to an entity. All entities are stored in a container called backEndData, which gets used by RE_RenderScene to display all entities. By modifying entities before RE_RenderScene gets called, entities can always be displayed - no matter if there’s a wall between the player and the entity.

Luckily, the Q3 engine implements a flag called RF_DEPTHHACK which does just this: It makes the engine ignore an entity’s depth, making it align with all entities in the same depth. By adding this flag, entities will be effectively displayed on top of walls!

Using Game Engine Data

At this point, it’s possible to execute code in the context of the target process and all required information on how to implement a wallhack are available. However, we still need access to the games internal entity values. These values are nested C-structs containing all kinds of information on players and objects in the game. By hooking the right system calls, the values of these structs can be obtained in the context of the thread in the injected DLL. Two steps are required for this:

While observing the required data and available syscalls, it became clear that not all game structs will be accessible by just hooking calls and saving their parameter values. Image a nested engine struct containing this data:

typedef struct {
    [...]
    snapshot_t* snap;
    [...]
} cg_t;

By hooking CG_GETSNAPSHOT it’s possible get the current game snapshot – a snapshot_t pointer value. We are not able to directly get a cg_t engine struct by hooking system calls but we know that the snapshot_t pointer is nested in a cg_t struct. By using some pointer arithmetic it’s still possible to get the parent struct, a cg_t value:

snapshot_t *snap = (snapshot_t *)arg[1];
// temp value
cg_t *tmp = 0;
// we have an `snapshot_t` pointer, so let's substract that values length and get the parent element
client_game = (cg_t *)((int)arg[1] - (int)&tmp->activeSnapshots);

This works for some cases but we will come across a point where this memory hackery indeed causes some problems.

Creating The Wallhack

By using dllMain to hook all executed system calls, we modify calls with the CG_R_ADDREFENTITYTOSCENE command in a way that the RF_DEPTHHACK flag gets added:

// get the passed parameter (an entity)
refEntity_t *ref = (refEntity_t *)arg[0];

// add the y flag to display the entity
// over walls, effectively disabling depth for players
// --> wallhack
ref->renderfx |= RF_DEPTHHACK | RDF_NOFOG;

if (playerGlowRequired())
{
    // add a glowing shader for a better view
    ref->customShader = client_gameState->media.disruptorShader;
}

As you can see, the player glow also gets added in this segment. playerGlowRequired checks whether an entity is a player by using internal engine structs, namely centity_t pointers which have been saved by hooking Q3 system calls.

As a neat side effect, adding the RF_DEPTHHACK flag to all entities and not just players also shows entities like doors, weapon and pickups through walls: Entities

Recognizing Teams

Normally, an entity’s team information is stored in the client_gameState->clientinfo[currentEntityClientNumber].teamName value. After the pointer arithmetic hackery, the fields of clientinfo structs have somehow been corrupted and point to invalid data. For example, teamName contains parts of the model name for some entities. But let’s just search in the memory region of the clientinfo struct!

// search for the team in the memory region of the `clientinfo` struct
// using `.teamName` isn't exact because of memory hackery while determining `client_gameState`
char red[] = "red";
char *base = client_gameState->clientinfo[currentEntityClientNumber].teamName - 100;
char *end = client_gameState->clientinfo[currentEntityClientNumber].teamName + 1000;

// check if he's red
BOOL found = FALSE;
while (base < end)
{
    if (_memicmp(red, base, sizeof(red)) == 0)
    {
        teamMap.insert(std::pair<int, int>(currentEntityClientNumber, TEAM_RED));
        return FALSE;
    }
    base += sizeof(red);
}
// then he must be blue <:

This works because CG_TeamName puts a string containg either RED or BLUE in an entities clientinfo. By searching for these values in memory, we can get around the corrupted pointers and can obtain team information quickly. This has also been optimized to cache search results to prevent searching for these value every time.

Pulling The Trigger

Implementing a trigger bot is pretty straight forward once the we are able to modify each frame. By hooking the CG_DRAW_ACTIVE_FRAME command, we can check if we’re aiming at an enemy by checking the value of crosshairClientNum:

// only trigger with a shootable weapon
if (ps->weapon > WP_SABER && ps->weapon != WP_THERMAL && ps->weapon != WP_TRIP_MINE && ps->weapon != WP_DET_PACK)
{
    // only shoot at enemies
    if (isEnemy(client_game->crosshairClientNum))
    {
        // send a left click
        // not using left click for fire? srsly why
        LeftClick();
        client_game->crosshairClientNum = 0;
    }
}

Bonus: CVar Unlocker

The blocking of cheats is based on a server setting called sv_cheats which is read only on clients. However, by setting this value in the injected code with syscall_hook(CG_CVAR_SET, "sv_cheats", "1"); circumvents this check :)

References

In-Process Fuzzing With Frida

October 24, 2019
frida exploiting fuzzing reverse-engineering

Dynamic Instrumentation: Frida And r2frida For Noobs

September 13, 2019
radare2 r2 frida r2frida ctf reverse-engineering

r2con 2019 CTF Writeups

September 2, 2019
r2 radare2 ctf reverse-engineering