TLDR
Using the environment variable LD_PRELOAD
to hook and overwrite function calls to have fun in-game!
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.
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.
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!
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.
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.
- 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. - 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 ⚡.
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.