Target

Our target for this lesson will be Urban Terror 4.3.4.

Overview

Most games make use of external graphics libraries for rendering. The two most popular graphics libraries are DirectX and OpenGL. Both of these libraries are loaded by games dynamically. Once they are loaded, games then invoke functions in these libraries. For example, with OpenGL, games can make use of the glDrawElements function to draw a series of elements from data stored in an array. Since these libraries are external, the game’s developers do not need to implement the rendering logic themselves.

Identify

Our goal in this lesson is to create a wallhack by hooking the game’s graphics library and modifying its logic to display entities through walls.

Understand

DirectX and OpenGL each have different functions for rendering that require different approaches to hook. Our first goal is to identify the library that the game is using. As each library has several functions to handle rendering and shading, we will then need to find a function that is used by the game for rendering. With the function identified, we can then hook it and disable depth testing through the use of a code cave. This will cause all entities to be rendered regardless of where they are in the 3D world.

Locating Drawing Library

Since graphics libraries are loaded dynamically, they must expose their functions to the main executable. Most debuggers allow you to view all the libraries loaded into an executable when attached. In x64dbg, this information is collected under the Symbols tab.

Urban Terror Symbols

As we can see in the highlighted elements, opengl32.dll is being loaded into the game’s process. By selecting the OpenGL module, we can see that it exports many drawing-related functions. From this information, we can conclude that this game is using OpenGL to render its graphics.

Locating the Drawing Function

OpenGL has several rendering approaches, and different games will use different approaches. For example, older games may use glBegin, glVertex, and glEnd; some games may use glDrawArrays; and others may use glDrawElements. Some even use a combination of these approaches to render different aspects, such as glDrawElements for player models and glBegin for screen effects like blood.

Typically, modern games will not use glBegin, glVertex, and glEnd, as these functions are considered deprecated. For that reason, we won’t be focusing on those functions right now. Instead, we will first investigate glDrawElements, as this is a commonly used function. Due to how OpenGL works, we will expect this function to be called constantly if it is used by the game.

By scrolling down to the glDrawElements export in the Symbols tab, we can see that OpenGL exports it to the process, though this is not a guarantee that it is being used:

Urban Terror Symbols

By double-clicking on the export entry, x64dbg will display the function:

Urban Terror glDrawElements

Next, we can start a game and set a breakpoint on glDrawElements. You will notice that it will immediately pop, and pop continuously every time the game is resumed. This is a good indication that this function is responsible for rendering entities. To verify that this is the case, we can replace the first instruction with the ret statement we see at the end of the function. The effect of this will be to immediately return to the calling code without executing any of the glDrawElements logic:

Urban Terror glDrawElements

If you then resume execution and attempt to play the game, you will notice that no new entities are being rendered to the screen:

Urban Terror glDrawElements

This gives us strong proof that Urban Terror is using glDrawElements. to display entities.

Hooking glDrawElements

Examining the glDrawElements function, we can see that it has very few instructions. Given the complexity of rendering entities to a screen, the majority of the code must be contained in the two calls near the end of the function:

Urban Terror glDrawElements

Therefore, if we hook an instruction before these calls, we should be able to accomplish our goal of disabling depth testing. A good candidate instruction is the mov at 0x61B9C526.

Since OpenGL is loaded dynamically, our hooking approach will have to be slightly different. First, we will need to ensure that OpenGL is actually loaded. As we are injecting our DLL into the application when it is first started, this will not be the case. After we ensure that OpenGL is loaded, we need to figure out where it is loaded. Once we determine the base address of OpenGL, we can then determine where glDrawElements is located inside the OpenGL module.

We will use a combination of techniques that we explored in previous lessons. To address the issue that OpenGL will not be loaded when our DLL is injected, we will create a thread to handle the hooking logic. This will allow us to create an infinite loop that waits until OpenGL is loaded, similar to the thread we saw in the DLL Memory Hack lesson:

if (fdwReason == DLL_PROCESS_ATTACH) {
  CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)injected_thread, NULL, 0, NULL);
}

In our thread, we will create an infinite loop that will call GetModuleHandle. This API returns the module handle, or base address, for a loaded module. If the module is not loaded, it will return NULL:

HMODULE openGLHandle = NULL;

void injected_thread() {
  while (true) {
    if (openGLHandle == NULL) {
      openGLHandle = GetModuleHandle(L"opengl32.dll");
    }
    ... 
    Sleep(1);

When we have the base address of OpenGL, we can then find where glDrawElements is located. To do this, we will make use of the GetProcAddress API. When given a module and the name of a function, this API returns the address of the function:

unsigned char* hook_location;
...
if (openGLHandle != NULL) {
  hook_location = (unsigned char*)GetProcAddress(openGLHandle, "glDrawElements");

This API will return the location of the first instruction in the function, in this case nop. Since we want to hook the mov instruction, we can subtract its distance from the first instruction and then add that difference to the result of GetProcAddress. The distance between these two instructions will always be the same, as they are part of the library’s code and not loaded dynamically.

Urban Terror glDrawElements

This offset can be added directly to the hook_location variable to get our location:

hook_location += 0x16;

Finally, we can hook the code as we have done in previous lessons:

VirtualProtect((void*)hook_location, 5, PAGE_EXECUTE_READWRITE, &old_protect);
*hook_location = 0xE9;
*(DWORD*)(hook_location + 1) = (DWORD)&codecave - ((DWORD)hook_location + 5);
*(hook_location + 5) = 0x90;

Function Pointers

With glDrawElements hooked, we can start working on the code cave. Our goal is to disable depth testing when an element is being drawn. To do this, we can use an OpenGL function called glDepthFunc. glDepthFunc allows you to set the function used for depth comparisons when OpenGL attempts to render the screen. This can be several values, but the ones we are interested in are GL_LEQUAL (draw if the element is in front of another element) and GL_ALWAYS (always draw).

For our wallhack, we will set the depth function to GL_ALWAYS right before any element is drawn. This will have the effect of making all elements always appear, regardless of where they actually are in the 3D space.

To start, we will need to locate glDepthFunc. We can use GetProcAddress in a similar manner to glDrawElements. However, instead of finding an address to hook, our goal with this call to GetProcAddress is to store the function’s address in a way that we can then invoke in our code cave. The easiest way to do this is through a function pointer.

Just like pointers we have used in previous lessons, function pointers point to an address. However, unlike the pointers we have been using to modify data and code, we can also declare a pointer to point to a function. We can then call this function, or address, like we would call any other C++ function.

To declare a function pointer, we need to know the original function’s definition. The definition of a function includes its return type and its parameters. We can get this information from the Khronos Group site:

void glDepthFunc(GLenum func);

Looking at the gl.h header file header file, we can find out what GLenum is:

typedef unsigned int GLenum;

So far, we can define our glDepthFunc function like so:

void glDepthFunc(unsigned int) = NULL;

Next, we will modify this declaration to have it act as a pointer to this function:

void (*glDepthFunc)(unsigned int) = NULL;

We can now assign this to the result of GetProcAddress:

glDepthFunc = GetProcAddress(openGLHandle, "glDepthFunc");

However, if we try to build this, we will get the following error:

error C2440: '=': cannot convert from 'FARPROC' to 'void (__cdecl *)(unsigned int)'
message : This conversion requires a reinterpret_cast, a C-style cast or function-style cast

Like we have seen in previous lessons, we need to cast the result of GetProcAddress properly for the compiler to understand how to translate the result. We can use the error message to quickly figure out how we need to cast the result:

glDepthFunc = (void(__cdecl *)(unsigned int))(openGLHandle, "glDepthFunc");

glDrawElements Code Cave

Our code cave will be similar to code caves we have written previously. We will start with our skeleton and restore the original code:

DWORD ret_address = 0;

__declspec(naked) void codecave() {
  __asm {
    pushad
  }
  ...
  __asm {
    popad
    mov esi, dword ptr ds : [esi + 0xA18]
    jmp ret_address
  }

Unlike previous lessons, we do not have a static location to jump to. Instead, we will need to calculate our return location similarly to how to we calculated the hook location. In our thread, after we assign the hook location, we can also dynamically assign the return location:

ret_address = (DWORD)(hook_location + 0x6);

With our skeleton in place, we can now add in our call to glDepthFunc. First, we need to find the value for GL_ALWAYS. We can find this in the gl.h header file:

#define GL_ALWAYS 0x0207

Next, we can invoke glDepthFunc to disable depth testing. Since it is a function pointer, we need to dereference the pointer to invoke the function:

(*glDepthFunc)(0x207);

Our code looks like:

#include <Windows.h>

HMODULE openGLHandle = NULL;

void (*glDepthFunc)(unsigned int) = NULL;

unsigned char* hook_location;

DWORD ret_address = 0;
DWORD old_protect;

__declspec(naked) void codecave() {
  __asm {
    pushad
  }

  (*glDepthFunc)(0x207);

  __asm {
    popad
    mov esi, dword ptr ds:[esi+0xA18]
    jmp ret_address
  }
}

void injected_thread() {
  while (true) {
    if (openGLHandle == NULL) {
      openGLHandle = GetModuleHandle(L"opengl32.dll");
    }

    if (openGLHandle != NULL) {
      glDepthFunc = (void(__cdecl *)(unsigned int))GetProcAddress(openGLHandle, "glDepthFunc");

      hook_location = (unsigned char*)GetProcAddress(openGLHandle, "glDrawElements");
      hook_location += 0x16;

      VirtualProtect((void*)hook_location, 5, PAGE_EXECUTE_READWRITE, &old_protect);
      *hook_location = 0xE9;
      *(DWORD*)(hook_location + 1) = (DWORD)&codecave - ((DWORD)hook_location + 5);
      *(hook_location + 5) = 0x90;

      ret_address = (DWORD)(hook_location + 0x6);
    }

    Sleep(1);
  }
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
  if (fdwReason == DLL_PROCESS_ATTACH) {
    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)injected_thread, NULL, 0, NULL);
  }

  return true;
}

We can now build this code and inject it into Urban Terror to see if it works.

Calling Conventions

When our DLL is injected, you will notice that the game will crash instantly when starting with the following error:

Urban Terror glDrawElements Hook Error

If we remove the call to glDepthFunc in our code cave, the game no longer crashes. It looks like our function pointer is not correct in some way. If we look at gl.h, we see that glDepthFunc is defined as:

GLAPI void APIENTRY glDepthFunc (GLenum func);

In Microsoft’s documentation on data types, we see that APIENTRY is a reference for WINAPI. If we look at the entry for WINAPI, we see that it is a reference for __stdcall. Let’s try adding this prefix to our function pointer:

void (__stdcall *glDepthFunc)(unsigned int) = NULL;

Building this code results in a familiar error:

error C2440: '=': cannot convert from 'void (__cdecl *)(unsigned int)' to 'void (__stdcall *)(unsigned int)'

This can be fixed by changing the cast as we did before:

glDepthFunc = (void(__stdcall*)(unsigned int))GetProcAddress(openGLHandle, "glDepthFunc");

Calling conventions control how parameters are handled by functions when called. There are many different types, but for our purposes, we just need to know that Visual Studio uses __cdecl by default, whereas OpenGL defaults to __stdcall.

With this change, build the code and inject it again. You will notice that Urban Terror no longer crashes.

Checking Counts

If you join a game, you will notice that you are now able to see entities through walls. The only problem is that you can see too many things:

Urban Terror Wallhack

In our current hack, we are disabling depth testing for every element drawn on the screen, including walls and stairs. Ideally, we only want to draw players through walls. To accomplish this, we will have to filter out elements that we do not care about.

glDrawElements has the following definition:

void glDrawElements(    GLenum mode,
              GLsizei count,
              GLenum type,
              const void * indices);

The count parameter specifies the amount of elements, or vertices, to be rendered. More detailed objects will have a higher amount of vertices. For example, a player model will have more detail (nose, hands, fingers, etc.) than a floor. By ensuring that the count parameter is a certain value, we can filter out elements that we do not want to display through walls.

We know that this parameter will be on the stack when our hook is jumped to. To retrieve its exact location, we can inject our DLL and set a breakpoint on glDrawElements. As we step through the code, we can identify where it is on the stack at the time our code cave gets called.

One feature of x64dbg is the ability to view the current parameters on the stack in a similar manner to how they would be passed in C. This feature is under the panel showing the values of the registers:

Stack Parameters

x64dbg is not able to fill this in automatically, so you will need to set the calling conventions and the number of parameters. If we trigger our breakpoint on glDrawElements multiple times, we can see that [esp+8] is the only value that appears to change. We can assume that it holds the value for the count parameter at the start of the function:

Stack Parameters

If we look at the stack panel at the bottom right, we can see how this information is represented on the stack. By default, the top of the stack (esp) will always appear at the top of the window:

Stack Parameters

Continue stepping through the function and then step into the jump to our code cave. After the pushad instruction in our code cave, examine the stack again:

Stack Parameters

At this point, we can see that the count parameter is at esp+0x10. We can reference this value in our code cave to retrieve the current count value of the element being rendered. In the first asm block, after the pushad instruction, we can take the value of esp+0x10 and store it in a local variable:

DWORD count = 0;
...
__asm {
  pushad
  mov eax, dword ptr ds : [esp + 0x10]
  mov count, eax
  popad
  pushad
}

We now have a local variable count that will hold the value of count passed to glDrawElements. We can then compare this value to a baseline and only disable depth testing if we exceed that baseline. If we don’t exceed it, we will re-enable depth testing. The value for GL_LEQUAL (0x203) can be found in the same way that we found the value for GL_ALWAYS. For now, we will use 500 as a baseline value:

if (count > 500) {
  (*glDepthFunc)(0x207);
}
else {
  (*glDepthFunc)(0x203);
}

If we build and inject this, we can see that our view is much cleaner now, and only certain elements appear through walls:

Urban Terror Wallhack with Count Filtering

Clipping Planes

Now we are filtering many elements, but we’ve encountered the problem that no player models are appearing. Instead, we can only see their weapons and blood effects through walls:

Urban Terror Wallhack with Count Filtering

If we enable third-person view, our player model is also invisible. The only place our player model will appear is if we turn on no-clip and fly out-of-bounds. This is most likely due to our player model being drawn first, when the scene is being rendered, and then other elements of the level drawn on top of it. When we disable depth testing, these entities are all drawn on top of the player.

draw_player();
draw_guns();
draw_doors();
draw_level_walls();

To force players to be drawn above these elements, we can use the glDepthRange function. This function sets the near and far clipping planes for the scene. Clipping planes are planes that extend across the game scene and clip (or remove) any entities behind them. By setting these values to be equal to 0, the planes will intersect, causing all elements to be drawn on the same plane and “fight” for rendering space. This will result in some flickering, but the player models will appear through walls.

We can create a function pointer for this function identically to the approach we used for glDepthFunc. The only alterations we need to make are in the parameters:

void (__stdcall* glDepthRange)(double, double) = NULL;
... 
glDepthRange = (void(__stdcall*)(double, double))GetProcAddress(openGLHandle, "glDepthRange");

We can then call this function in the same location that we change the depth function. The default values for these planes are (0,1), which we will reset if the count is too low.

if (count > 500) {
  (*glDepthRange)(0.0, 0.0);
  (*glDepthFunc)(0x207);
}
else {
  (*glDepthRange)(0.0, 1.0);
  (*glDepthFunc)(0x203);
}

With this, player models will now appear through walls, indicating that our wallhack is successful:

Urban Terror Wallhack with Count Filtering

The full code for this lesson is available on github.

 

results matching ""

    No results matching ""