TLDR
Using the environment variable LD_PRELOAD to hook and overwrite function calls to have fun in-game!


Watch on YouTube-

Introduction

We've been mostly trying to understand the game internals a little bit until now, maybe it's time we started with a small "hack". It was very important to document the process of approaching to understand the game because a big part of exploitation is studying and gathering information about the target before we break anything. The following tweet summaries it quite well.

I'm starting to think that hacking isn't about tech skill. It's being willing to stare at a screen for hours, try a million things, feel stupid, get mad, decide to eat a sandwich instead of throwing your laptop out a window, google more, figure it out, then WANT TO DO IT AGAIN
@hizeena

Where we at?

So far we've extracted class-related information using gdb via ptype, but there are a few caveats. If we copy a few classes into a header file libGameLogic.h, include this library in a new c++ file test.cpp and try to compile, we'll get a lot of errors which include syntactic errors to missing classes.

Errors from compilation

As you can see this reveals a lot of class references which are not declared. We can use this information to get back to gdb to extract more classes and fix the code. We repeat this until the file test.cpp compiles, tedious but works.

Additionally gdb also spits out some errors, we need to remove them.

Python Exceptions from gdb that needs to be removed

LD_PRELOAD

It's time to introduce an environment variable called LD_PRELOAD. This is a special one because you can provide paths to dynamic libraries for a dynamically typed executable to use. This means we can overwrite function calls with our own code by simply specifying the location to the shared object.

LD_PRELOAD is interpreted by dynamic linker/loader ld.so. Following is from the ld.so man page.

The programs ld.so and ld-linux.so* find and load the shared objects (shared libraries) needed by a program, prepare the program to run, and then run it.
Linux binaries require dynamic linking (linking at run time) unless the -static option was given to ld(1) during compilation.

This man page also talks about the LD_PRELOAD environment variable.

A list of additional, user-specified, ELF shared objects to be loaded before all others. This feature can be used to selectively override functions in other shared objects.

This is exactly what one might wish for when testing right?

Now the idea is to create our own library, load this before the shared object libGameLogic.so, and overwrite functions. To do this we just have to compile our test.cpp into a shared object.

$ g++ test.cpp -std=c++11 -shared -o test.so

If we list the dynamic libraries loaded by the program, you'll see that test.so is specified before other libraries which means we can overwrite functions.

$ LD_PRELOAD=test.so ldd ./PwnAdventure3-Linux-Shipping
    test.so => /home/live/pwn3/./test.so
    ...
    libGameLogic.so => /home/live/pwn3/./libGameLogic.so

In Action

Whenever the player in-game jumps there's a function call to Player::SetJumpState(bool), so let's try overwriting this.

/* Imports required to make the libGameLogic work properly */
#include <dlfcn.h>
#include <set>
#include <map>
#include <functional>
#include <string>
#include <cstring>
#include <vector>
#include <cfloat>
#include <memory>
#include <stdint.h>
#include "libGameLogic.h"

/* Overwriting `SetJumpState` with custom code */
void Player::SetJumpState(bool b) {
    printf("[*] SetJumpState(%d)\n", b);
}

If we define a function as shown above we also need to compile it with position independent code because it's a shared object and it can be loaded anywhere in the memory.

$ g++ test.cpp -std=c++11 -shared -o test.so -fPIC

Now we LD_PRELOAD our library, hop into the game and Jump!

Hook on jump in action

As you can see when we jump, we see logs in our console. Awesome right? yeah but there's one small problem. Since we are overwriting the function body, the original code will be replaced by the new one. In this case, we can see ourselves jump in-game, but the other players in the server can't see us jumping.

The "Jump" Problem

This can be a problem or not depending on what you want to do, but we'll keep it simple for now.

Handle to the Player

If you remember GameWorld.m_players object which has references to all the players in-game, I think it would be cool to interact with this object.

While investigating, I found a World::Tick function which exists for a lot of other objects. ClientWorld::Tick is executed multiple times a second and World::Tick is also called. Since this function doesn't seem to do much we can overwrite this safely.

void World::Tick(float f) {
    printf("[tick] %0.2f | \n", f);
}

But what can we do with this?

There's a function called dlsym which obtains the address of a symbol in a shared object or executable.

void *dlsym(void *handle, const char *symbol);

Function dlsym takes 2 arguments, a handle and the name of a symbol. A handle can be to an open dynamic library or we can also use RTLD_NEXT which finds the next occurrence of the desired symbol in the search order after the current object(man page). This is exactly what we need to solve the problem I described. We can wrap the original function with a custom one, kind of like a proxy.

We'll use dlsym to get a handle to the GameWorld object.

ClientWorld* w = *(ClientWorld**)dlsym(RTLD_NEXT, "GameWorld");

dlsym returns a void*, so we are typecasting it to ClientWorld** and then dereference it.

Now, let's try to access the player's name & mana values.

The GameWorld object looks something like shown below.

GameWorld
  * m_activePlayer, ...
    * m_object (reference to the player), ...
      - GetPlayerName()
      - (Player*) -> m_mana
void World::Tick(float f) {
    ClientWorld* world = *((ClientWorld**)(dlsym(RTLD_NEXT, "GameWorld")));
    IPlayer* iplayer = world->m_activePlay.m_object;
    printf("[LO] IPlayer->GetPlayerName: %s\n", iplayer->GetPlayerName());
    Player* player = ((Player*)(iplayer));
    printf("[LO] player->m_mana: %d\n", player->m_mana);
}

If we compile the shared library and run the game, we should start seeing some output.

We can clearly see our player's name, but the mana doesn't seem to be the right value as it shows zero all the time. Apparently, gdb was reporting some attributes such as m_playerName to be of type std::string, but in reality, it was const char*. The reason this matters here is the fact that std::string takes up more bytes than a char* and the structure no longer is byte-aligned because std::string probably pushes the other properties of the object down in the memory due to it's bigger size. Hence m_mana was fetching values from somewhere else in the object instead of getting it from the right place in memory.

Now it works, but this took me about 8-10 hours to debug! It was painful but learned a lot. The breakthrough came from combining 2 observations.

  1. Observing offsets from the start of the class to m_mana, there was a clear difference between the gdb's results and the compiled library.
  2. Noticing gdb's errors while printing the object.
...
m_timers = 0x0,
Python Exception <class 'gdb.error'> No type named std::basic_string<char> ...
m_blueprintName = ,
...

SpeedWalk Hack

If we take a closer look at the player class we can see an interesting property on the class called m_walkingSpeed, so we can set its value to be a very high number;

player->m_walkingSpeed = 99999;

If we jump back into the game and try to move around, we should start seeing ourselves run like the flash ⚡.

Flash ⚡ run!

There's also this m_health in Actor class, so can we make ourselves invincible?

player->m_health = 99999;

If we try to compare it now, we get an error 'int32_t Actor::m_health' is protected. The class members are defined inside protected, so we can just simply change this to public and compile it.

After compilation, if we head into the game, we see that our health is over 9000! well, it's more like 99999, but this should make use invincible right?

Well I guess it's not that simple, seems like the walking speed is blindly trusted by the server, but health is not. But since there are a ton of other variables to look at, we should be able to do a lot more!

Conclusion & Takeaways

  • LD_PRELOAD can be used to overwrite function calls if the executable is dynamically linked and uses shared objects.
  • dlsym obtains address of a symbol in a shared object or executable.

Resources