How We Wrote a Self-Hacking Game in C++
My co-founder Matt and I are working on Squally, a game in C++ to teach hacking. However, this creates a small problem: If we want to publish the game on Steam, then we can’t force people to download hacking tools first. Our solution? Put the tools inside the game, and let the game hack itself.
This had another unintended consequence. If the game hacks itself, then we need some low-level control over the game — and for that, we need a low-level language. C# and Unity were no longer an option, so we had to start looking at some C++ options. We eventually settled on Cocos2d-x for our engine due to its popularity for 2D games.
To emulate real hacking tools, there are only two things that we needed. First, a disassembler for converting the raw machine code into human-readable x86 assembly (or at least nerd-readable). The second thing we need is an assembler to do the opposite.
With these two tools, we effectively have a game that can read and write its own code. We just have to tell the game exactly where to look, which we will explore later.
Of course this means you can totally crash this game, but as any hacker knows, crashing things is just part of the job.
However, there are some cool ways to mitigate this. One option is to save the state of the program (registers/stack) before the hackable region of code, and restore it after it executes to prevent silly mistakes.
For those who like to live a bit more dangerously, there are libraries for ignoring segfaults and other errors, which is pretty frightening. This is no match for a simple
jmp 0x00000000 , but it’s a good start for easing some of the inevitable frustration of an aspiring assembly programmer.
For those interested in the nitty gritty details, the rest of this article will be about setting up some self-modifying code. It’s actually pretty easy.
Just a quick note, all code samples have been uploaded to: https://github.com/Squalr/SelfHackingApp
There’s a few catches as far as the implementation goes. We’ve got an assembler and disassembler. We find an instruction, and disassemble it. We change it to something else, and assemble it back into bytes. Now all we have to do is write those bytes to memory… and the program crashes.
Upon digging deeper, it turns out that the protections for the page of memory that contain the code is marked as
Execute only. It needs to be readable and writable. In Windows, this can be done using the
VirtualProtect function in
windows.h . In Unix, this is done via
Below, we create a function called
hackableRoutine(). Notice the line
i += 60 . This is the instruction we are going to self-modify. There’s a few macros surrounding it, which simply get the start and end addresses of the code we want to hack. These are used to set up our
HackableCode object, which is just a thin wrapper over FASM and Udis86.
At our program entry, we call the
hackableRoutine() function once to initialize the
hackableCode object. It returns 100, as you might expect from the code above.
Next, we update the
hackableCode object and change it’s code to anything we want! In this example, we settle for a simple
nop . As you might be able to guess, the second time we call
hackableRoutine() , a value of 40 is returned! The
i += 60 line has been effectively removed by self modifying code!
Here is my output, although this can change depending on the compiler:
mov eax, [ebp-0x2c]
add eax, 0x3c
mov [ebp-0x2c], eax
For those with a background in assembly, you may have noticed that 1 nop is far less bytes than the instruction
i += 60 . The remaining bytes are filled with
nop instructions automatically behind the scenes.
I’m not going to explain the implementation details around the
HackableCode class — as stated before, it’s really just a thin wrapper over Fasm and Udis86. For reference, I’ll put the code for these at the end of this article.
One thing to note is that there is a limitation to this approach. Setting up the
hackableCode object requires that the code be run at least once! This is not ideal for all situations. There should be a solution to this. If you find one, drop a comment below! If no C++ wizards beat me to it, I’ll post a follow-up when I figure it out.