Headshot: Game Hacking on macOS
This post will cover the method I approached to do some rudimentary reverse engineering and wrote a console based game trainer for an open source FPS game called Assault Cube. Continuing with the spirit of game hacking from my last post which covered my approach for hacking an iOS game using various tools and techniques, I’ll discuss the requisites and tools which aided me in this endeavour. All the source code used in this post is present in this GitHub repo including the final trainer implementations: headshot
My inital aim was to develop a subset of the common and popular features which are usually associated with a game trainer such an infinite health and ammuntion, and some of the more exotic features including an aimbot (automatic aiming at enemies) and ESP (extra sensory perception - just a fancy term for being able to see enemies through walls).
While there are a lot of tutorials for developing game trainers for Windows based games online, the resources for doing the same on macOS are fairly scarce and are mostly present in undocumented code repositiories on GitHub. Since I was unable to find a step-by-step approach for doing this on macOS, I decided to take up this task for fun and this eventually turned into a conference talk as well which I gave at BSides Delhi 2018. The slides for the presentation can be viewed here.
Before I get started with the technicalities, I would like to mention the resources which were extremely helpful for me to approach this problem:
-
Guided Hacking - this is a forum which discusses the development of cheats for a plethora of games and has an active forum and extremly helpful community. I learned about all the model-view matrix calculations which I talk about later in this post, from this website.
-
Frida - this is a dynamic binary instrumentation tool which supports various OSes and architectures. It is an extremely powerful and easy to use. I had also mentioned this tool in my last post.
-
mach_inject - this is a tool for dylib injection on macOS. This proved to be an pragmatic resource detailing the usage of the
mach_vm_*
API which is essential for interacting with another process on macOS. -
OpenGL rendering pipeline - this gives a detailed overview of the rendering pipeline used by OpenGL. This was an invaluable resource while developing the ESP hack,
Now lets begin with the actual details.
This article is roughly divided into the following sections:
- Finding the needle in the haystack
- Finding Player offsets
- Native memory read-write
- Aimbot
- Finding enemies in our FOV
- Seeing through walls!
- Wrapping up
Finding the needle in the haystack
So first we’ll start with the easy stuff, how do we get unlimited ammo and armour and health? We need to figure out how the game stores these values and how we can control them. So we can assume that there are some properties which hold for all player entities in the game. Every player has a health value which decreases when you’re inflicted with damage when another player shoots at you. There are also variable which hold the count of your bullets in your current magazine along with a variable which holds the number of magazines your player has. These could possibly be stored as members of a Player
class.
Alright, so our goal is to control these variables (health
, armour
, ammo
), but first we need to find them. So there are lots of tools out there which are famous in the Windows game hacking scene. One of them is Cheat Engine which lucky for us, is also supported on macOS. This tool simplifies the game hacking process so that we don’t have to spend too much time with the building blocks such as finding memory offsets, pointers, structures, instructions which read and write to our data, etc. But I wanted a more bare bones approach with less abstraction of the process. So I decided to take the homebrew route to finding these entities in the game memory map.
I decided to utilise Frida for prototyping a quick solution for helping me search process memory and find offsets and pointers to important memory locations such as the address of the structure which stores our health. For this, I utilised the Frida Memory
and Process
APIs. To search and write to process memory, we need the basic primitives of reading and writing. This can easily be achieved with the following simple functions.
We can extend this to searching memory for data of our choosing.
To effectively search for arbitrary patterns in memory, we need to search for our needle in a preformatted way according to how the memory is stored for a certain architecture specification. In x86, data is stored in little endian format and also, a pointer is of size 4 bytes on x86. The Assault Cube release comes on in x86 (32 bit) and doesn’t offer a 64 bit version for macOS. To use the helper functions we’ve defined above, we thus need to format our queried pattern accordingly.
We can then call the corresponding JS helper function we defined through Python bindings like so:
To find a memory address, such as the current ammo, we can follow a process of minimisation. We will start with some amount of ammo and search for that value with the help of our defined functions. We can then change the ammo value again by shooting some bullets and then search again for this new value but only in the subset of memory locations we found in the last search.
And similar to the write mem API, we can effortlessly do the same for modifying the ammo amount!
This sets the number of bullets in our magazine to 999 as you can see in the picture below.
Finding Player offsets
Okay so now that we know that we can modify game memory to increase our ammo (we can do the same for health and total ammo as well), we need to find a reliable way to get this address. Since this feature has to be included as part of our trainer, we don’t want to manually search for this address every time we start a new game. We have to find a constant base address and figure out the offsets of the target memory locations we need relative to the constant base address. Even if we don’t look at the code/disassembly of the game, it is safe to assume that there has to be some location in the game memory that is used to store the player base address, probably in the __DATA
section of the binary.
Frida supports an API called MemoryAccessMonitor
which works similar to debugger watchpoints, but unfortunately, this is currently only supported on Windows. So to proceed, we’ll use lldb
to debug the game and find the player base address.
Assuming that there’s a health variable in my player object, the corresponding assembly instructions to access the health would be adding an offset to the object base address. If we can identify and break at this instruction, we can identify the player base address. Considering something like:
We can set a watchpoint
in lldb to break at any instruction which modifies are health. This would happen when my player incurs damage, either from getting shot or from a grenade. So we’ll find our health address first using the approach mentioned above and then set a watchpoint to break at any instruction which writes to our health address.
Here esi
stores our player structure and health
is at an offset of 0xf8 from the base of the player struct. We can now search for a global variable which holds the pointer to our player struct. We can limit our scans to the rw-
segments.
So we now know that 0x12b8a8
holds the address of our player struct and this’ll remain constant throughout. Now it is trivial to find the offsets of the other variables of interest to us and start building our game trainer.
Native memory read-write
Before we go ahead with some of the other hacks, I want to write about how to do this using mach_vm_*
API. We’ve used Frida to read and write to memory, but Frida also abstracts away the OS specific details. For example, on Windows we would use the ReadProcessMemory
and WriteProcessMemory
APIs to interact with the memory of a process. Similarly, we can use mach_vm_read
and mach_vm_write
on macOS to do the same. You could also just attach a debugger and modify memory with that, but that is difficult to automate and slows things down quite a bit. On linux, you might use the ptrace
API to achieve this.
Before we can read and write process memory, we need to obtain the corresponding task
for the pid
of the game. You can get this using the task_for_pid
function.
This is what our Trainer
class roughly looks like:
To get the task
of the game process, we simply do:
I’ve defined some wrapper functions over vm_read
and vm_write
which allow you to read and write arbitrary data types from a process’ memory.
Read:
Write:
These functions basically allow you to modify the health and ammo of the game similar to what we had done earlier using Frida.
Aimbot
Now lets tackle some of the more fun hacks (aimbot and ESP). We’ll start with making an aimbot.
An aimbot will automatically aim at enemy players, so all we have to do is move around and press the left mouse button to kill an enemy. To achieve this, we need to calculate the yaw and pitch angles from our player to the enemy player so we can programmatically adjust our aim.
All the information we need to calculate this stuff is already present in the player structs. (The enemy player structs can also be found in game memory). In-fact, pointers to the enemy player structs are stored exactly adjacent to our player struct pointer.
The player structure stores (x, y, z)
cooridinates of the player. We can apply some basic trigonometry principles to calculate the angles at which we need to aim.
Here’s an image from https://en.wikipedia.org/wiki/Aircraft_principal_axes describing what yaw and pitch is.
So, if we have the coordinates of two players, we need to calculate alpha
and beta
where alpha
and beta
are described throught the following pitcures.
This depicts the side view of 2 players from which we’ll calculate the pitch.
This depicts the top view of 2 players from which we’ll calculate the yaw.
(Please excuse my ugly diagrams :p)
So now we have
pitch = arctan ((z2 - z1) / dist))
yaw = arctan ((y2 - y1) / (x2 - x1))
dist = euclidean distance = sqrt((x2 - x1)^2 + (y2 - y1)^2)
Before we go ahead and implement this, here’s a cool video in which we can see the pitch change when we move our aim up and down.
This is done using the dump_region
function defined below. Basically, we’ve fed it the address of the pitch in the player struct and we’re visualising it using the curses
library to see it change when we move the crosshair.
Here’s code to calculate the angles we want to aim correctly.
We can use the return values from here and write them to the yaw
and pitch
members of the player struct using the wrapper functions defined above.
Here’s a video showing the aimbot in action.
If you looked closely at this video, you’ll notice that our aimbot sucks. Yes we can lock onto enemy players and shoot them with ease, but we’re still locking onto players which aren’t in our field of vision. This is really annoying because it makes it really difficult to navigate the map if you’re locked onto something which is moving around.
Finding enemies in our FOV
We need to improve this. FPS games usually implement a function which traces a line from our gun to the enemy player to check whether the line intersects with any other object on its way to the player. Basically for checking whether our bullet will hit the enemy or will it hit some object in between, like a wall.
We need to find this function and call it at will. By spending some time analysing the code and playing with the game with a debugger, you can find the traceline function in memory. Since this function is a part of the __TEXT
section and the binary is a non-PIE binary, the address of the function will remain the same each time we start the game.
We can leverage the mach_vm_*
API here again to aid us in calling this function as per our need. The functions of interest to us are:
kern_return_t thread_create_running(task_t parent_task, thread_state_flavor_t flavor, thread_state_t new_state, mach_msg_type_number_t new_stateCnt, thread_act_t *child_act)
kern_return_t mach_vm_allocate(vm_map_t target, mach_vm_address_t *address, mach_vm_size_t size, int flags)
kern_return_t mach_vm_protect(vm_map_t target_task, mach_vm_address_t address, mach_vm_size_t size, boolean_t set_maximum, vm_prot_t new_protection)
thread_create_running
allows us to create and start a new thread with a state we specify. This state describes the values of the processor registers which allows us to execute arbitrary code in the context of the game process. We can set the program counter register eip
on x86 to specify what we want to execute.
We’ll create 2 sections in the memory:
- A section with
r-x
permission in which we’ll store our shellcode. This will consist of code which will call thetraceline
function - A section with
rw-
permission which will be used as a fake stack
Since this is a 32-bit process, function arguments are passed on the stack.
The traceline function takes the player positions as arguments and returns a struct which contains 3 float
s which contain (x, y, z)
coordinates and a bool
which stores whether the line collides with any object.
We’ll allocate the stack and text section for our shellcode and space for our result.
The traceline function takes 9 parameters, something like the following:
void traceline(float x1,
float y1,
float z1,
float x2,
float y2,
float z2,
uintptr_t player_address,
uint32_t something,
traceresult_t *t)
We’ll copy over the parameters onto the remote stack.
We’ll finally setup the register context and start the remote thread in the game process. We’ll wait for a few miliseconds for the function call to complete, and we’ll retrieve the return value from the remote process.
One important function we skipped is the prepare_shellcode
function which is used above. This is the function which actually returns shellcode which will run in the remote process. Here we actually call the traceline
function which takes arguments from the fake stack we had setup above.
This video shows what the traceline
function looks like by inspecting it in the remote process using lldb
.
So if we combine our aimbot with this function, it will give us a reliable aimbot which aims only at enemies which we can actually shoot at.
Another way to achieve calling traceline
is by using Frida. Frida provides a handy API called X86Writer
which allows us to write arbitrary code to a process. We can also use the NativeFunction
API to call traceline
in the game process.
Here’s an excerpt from the trainer which uses the Frida to call traceline
.
This is what the final aimbot looks like.
Seeing through walls!
Since this post is getting pretty long now, I’ll skim through the implementation of the ESP.
This is the crux of what ESP allows you to do :p
Jokes apart, Assault Cube uses OpenGL for rendering. Now similar to how we had called the traceline
function in the context of the game process, we can also call OpenGL functions using the methods described above. If we don’t want to use OpenGL, we can use Apple’s Cocoa API for drawing to the screen as well (though that didn’t work out well for me because I’m sure I didn’t do it the right way. Take a look at the source code if you want to have a look at that, I won’t be describing that here).
This is what a generic rendering pipeline looks like
These details are already present in the game process. The hard part is finding them in memory. We want to find the Model View Projection matrix in memory. If you’ve found that, then you only need to do some matrix multiplication to get screen coordinates, and call some OpenGL functions to display anything to the screen.
More on finding the MVP matrix is described in this amazing Guided Hacking thread.
Here’s a video of what the MVP matrix looks like when you move your crosshairs around.
Since we know the (x, y, z)
coordinates of all the players in the game, we want to draw bounding boxes around them on the screen. This will make it easier for us to spot them, even if they’re behind a wall. We’ll create NativeFunction
s for the OpenGL functions we would like to use with the help of Frida.
We can read the actual matrix from game memory with the following function
To calculate the coordinates we want to pass to the OpenGL API, we need to multiply the player coordinates with the matrix we read using the _read_view_matrix
function above. We take care of this in the object_to_screen
function.
We can finally display the bounding boxes by calling the OpenGL functions. I seem to be doing something wrong though because in the final implementation, I see that the bounding boxes are flickering and they are slightly slow. If you find out what’s wrong please file an issue or submit a pull request to the github project here.
The function which does this is
The final implementation of the ESP is show here.
Wrapping up
Phew, that was a pretty long post which covered some topics to help you get started with game hacking on macOS. This is certainly not a comprehensive article about game hacking and I possibly may have done things in a very dumb way. Please correct me if I’m wrong. There might be errors in the code above and if you can improve it, please file an issue or submit a PR to the git project at https://github.com/jaiverma/headshot.
I urge you to try this trainer out and improve upon it. The aimbot is extremely jerky and a lot of enhancements can be make. Try patching the recoil function so that the aimbot is even more accurate. I also haven’t added functionality to distinguish between players on my team vs players on the enemy team. This trainer currently doesn’t stand a chance to go undetected by any of the anti-cheat solutions. Try to improve this so that it can bypass anti-cheat!
Alright that’s enough from me. I hope you enjoyed the post!