idkfa? Those are two strings were etched into my brain at a very young age where fond memories of playing shareware Doom live. For SenseCon ’19, Lauren and Reino joined me as we dove into some reversing of chocolate-doom with the aim of recreating similar cheats. The results? Well, a video of it is shown below. We managed to get cheats working that would:
- Increment your ammo instead of decrement it.
- Increment everyone’s health for the amount it would have gone down for. Yes, you read right, everyone.
- Toggle cheats just like how they behaved in classic doom.
The source code for our cheats live here if you want to play along, or maybe even contribute new ones :)
The original Doom game was released in 1993, built for OSs a little different to what we have today. The chocolate-doom project exists and aims to be as historically correct as possible while still working on modern operating systems. Perfect nostalgia. We downloaded chocolate-doom for Windows from here, extracted and sourced a shareware WAD to use.
We also set some rules for our project. The chocolate-doom source code is available, however, we did not want to reference it at all. Once extracted,
chocolate-doom.exe was a stripped PE32 executable. This meant that reverse engineering efforts would be a little harder, but that was part of the challenge and learnings we wanted. Using tools such as IDA Freeware, CheatEngine and WindDBG was considered fair game though. However, any patches or binary modifications had to be implemented using Frida, and not by manually patching
Finding where to start – Windows
Sometimes, getting started is the hardest part. We decided to get a bit of a kick start by using CheatEngine to find interesting code paths based on the games UI.
First up was finding out what code was responsible for the ammo count. CheatEngine is a memory scanner and debugger that is particularly suited for this task. You can attach to a running process with CheatEngine, and scan the process’ memory to find all instances of a particular value. If our ammo count is currently 49, we can search for all instances of the value 49 in memory. There may however be quite a number of instances of this value within the process’ memory – a scan will often return several. Additionally, not all instances will be related to the ammo count.
To pinpoint exactly the right location, we can can change the value a bit, and then rescan using CheatEngine for any instances of the previously found value that was altered by the same amount. We could do this by shooting the handgun a few times and taking note of by how much the ammo count was changed. We can then use the “Next Scan” function and the “Decreased value by” scan type option to search in CheatEngine for a value that has also changed by the same amount. This decreased the amount of possible locations for the ammo count to only three.
At this point all the possible instances could be either the original, or a copy of the original ammo count. We can watch these memory locations to determine which instructions write to them in an attempt to identify the code that is responsible for decreasing the ammo. To do this in CheatEngine, you can simply click right a watched memory location, and select “Find out what writes to this address”.
When watching the first two locations we identified, we saw instructions rapidly writing to the target pointers even while the game was paused. These instructions also didn’t subtract from the ammo count which meant that these instructions were probably not what we were looking for.
The third match we had saw writes only when the handgun was fired. The instruction was a subtraction by one and therefore likely the instruction to decrease the ammo count we are interested in. CheatEngine allows you to disassemble code around the identified instruction. Using this, we had the location of the instruction responsible for decreasing the ammo count when the handgun was shot at
Finding where to start – Linux
While not the primary target for our project, we also had a go at patching the chocolate-doom ELF binary. This was all done on an Ubuntu machine using similar tools as mentioned above, however, cheat engine was not available for Linux. Instead, to start finding interesting code paths in the ELF binary, we used a tool called scanmem which is similar to CheatEngine, but only gives the addresses of a value in memory and not which instruction alters it. After starting up scanmem and entering the changing ammo value a couple of times, it also isolated 3 possible addresses for ammo. These would change at each invocation of chocolate-doom because of the use of ASLR.
To find out what instructions write to the identified pointers, we used
gdb and set a watchpoint on each address found by scanmem.
We then continued the game and shot once with the handgun to trigger a watchpoint. You can see the old and new ammo value below.
Next was to check where the instruction pointer was and to view the instructions surrounding it to see if we could spot the exact instruction that subtracts one from the ammo value each time a shot is fired.
As you can see, there is a
sub instruction just before where the instruction pointer is currently sitting. To search for the exact instruction in IDA, we viewed the hex value of the instruction in gdb.
Because the below values are little endian, we searched for the opcodes
83 ac 83 a8 in IDA and had the offset location of the instruction responsible for decreasing the ammo count when the handgun was shot, i.e.
Watching functions with Frida
With some target offsets at hand, we could start to watch them in real time with Frida. Frida has an Interceptor API, that allows you to “intercept” function calls and execute code as a function prolog or epilog. One also has access to the arguments and the return value using the
Interceptor, making it possible to log and if needed, change these values.
Given that we know which instructions were writing to memory regions that contained the ammo value for our handgun, we used IDA to analyse the function around the instruction to find the entry point for it. This way we would know which memory address to use with the Interceptor.
As you can see in the screenshot above, the function starts at
0x4309f0. With a base of
0x400000 that means that the function’s offset is just
0x309f0 from that (this will be important later). With a known offset, our first Frida script started. This script would simply find the
chocolate-doom module’s base address, calculate the offset to the function we are interested in and attach the Interceptor. The Interceptor
onEnter callback would then simply log that the function triggered.
We can see this script in action when attached to chocolate-doom below.
Knowing that function triggers as we fire the handgun helps confirm that we are on the right track.
Our first cheat
We figured we would start by writing a simple patch that just replaces the instruction to decrement the ammo count by 1 with
NOP instructions. Frida has a Code writer module that can help with this. The original decrement instruction was
sub dword ptr [ebx+edx*4+0A4h], 1 which is represented as 8 opcodes when viewed in hex.
The code writer could be used to patch
0x430a2c with exactly 8
NOP instructions using the
putNopPadding(8) method on a code writer instance.
Applying this patch meant that we no longer ran out of ammo with the handgun.
Improving the first ammo cheat
To test how effective our
NOP-based cheat was, we used one of the original cheats (“idkfa”) to get all of the available guns and ammo and see if it worked for those as well. Turns out, it didn’t, and some investigation revealed that each gun had its own ammo decrementing function. All functions eventually called an opcode that would
0x1 from an ammo total (except for the machine gun that would
0x2). An improvement was necessary.
We didn’t want to hardcode all of the instructions we found and looked for other options. When searching in IDA for the opcodes
0x83 0xac (part of the ammo
SUB opcodes for
sub dword ptr [ebx+edx*4+0A4h], 1), we noticed that the only matches were those that formed part of functions that decremented ammo. Frida has a memory scanner that we could use to match the locations of these functions that we were interested in dynamically (as
0x83 0xac), available as
Memory.scanSync(). We then used the same
Memory.patchCode() function to simply override the opcodes to
ADD instead of
SUB as a simple two-byte patch :)
This patch was a little more generic and did not require any hardcoded offsets to work. Depending on what you are working with, a better search may be to use some of the wildcarding features of
Memory.scanSync() so that you can have much more specific matches.
With our patch applied, all weapons now incremented their ammo count as you fired.
Writing a health cheat
After fiddling with ammo related routines, we changed our focus to health. We used the same CheatEngine technique as before to figure out where our health was being stored and who was writing to those locations. Finding the health locations however turned out to be a bit more tricky, as around four to five different locations would appear between different searches. Some of these locations had rapid firing instructions executing writes on them, as before with the ammo count, and were thus ignored. Around three locations had instructions which triggered when the player’s health decreased.
The instructions were however not sub instructions, but rather mov instructions. Looking at the disassembled code, we could however spot the
sub instruction a few lines higher up.
What is important to note is that instruction was a subtraction between two registers, i.e.
sub eax, esi. This is a fairly common instruction, and meant that we can’t just scan for all instances of it in memory and replace it with an
add instruction like we did with the ammo increment patch. Instead, we manually went to the location of each of the
sub instructions, and changed it to an
add instruction. When viewed as opcodes,
sub eax, esi is
0x29 0xF0, while
add eax, esi is
0x01 0xF0. So, a patch was simply a case of swapping out the
0x29 for a
sub instructions for the three different functions were at
0x3DEEC however often caused the game to crash, so it was removed later on. Patching
0x2C385, and especially
0x2c39 made the player’s health increase when attacked. It however also has the side effect of making all monster’s health increase as well when they are attacked – this might be because both the player and monsters in the game use the same logic for health deduction. ¯\_(?)_/¯
With this patch applied, the incrementing health cheat could be seen in the following video.
Making our patches, cheats – static analysis
Up to now we have been patching chocolate-doom for our cheats as soon as Frida was injected and our scripts were run. We really wanted to make our cheats behave like the originals did by simply typing them in the game, “iddqd” and “idkfa” style.
To implement this the chocolate-doom binary was analysed to find the logic that handles the current cheats. We knew that if you typed “idkfa”, the game would pop up a message saying “VERY HAPPY AMMO ADDED”.
A text search in IDA for this message revealed the location where it was used.
We focussed on the function this reference was in and realised that it was an incredibly long function. In fact, it appeared as though all of the cheats were processed by this single function, with a bunch of branches for each cheat. Code paths could be seen in the IDA graph view and gave us a reasonable idea of its complexity.
All of the different cheats branches also made a call to a function that lived at
0x040FE90. This function took two arguments, one being a character array and another an integer and appeared to be comparing a character to a string.
Making our patches, cheats – dynamic analysis
We decided to have a look at the invocation of this cheat comparison function (henceforth called
cheat_compare) at runtime, dumping the arguments it receives. Just like we have previously used the Frida
Interceptor to attach to a function, we simply calculate the offset for
cheat_compare and log the arguments it receives. This will also give us an opportunity to try and discover how to trigger this function in game.
From IDA we knew the first argument was a character array, so we just dumped the raw string using the
readCString() Frida method for that. For the second argument we weren’t entirely sure what that would have been, and left it raw for now.
With this script hooked, the results were rather… surprising…
Every keypress the game received appeared to enter the identified compare function we called
cheat_compare. Even arrow keys! In the video demo above, we slowly entered the cheat “iddqd”, where you can see many of the possible cheats Doom has being compared to the hex value of the ASCII character we entered. Once the cheat matched, we moved Doom guy left a few times using an arrow key, which is where values such as
0xffffffac entered the routine for a bunch of possible cheats too. Without understanding the full cheat routine, we were sure these would never match something legitimate, so we suspected we may have found an optimisation opportunity here :)
Making our patches, cheats – implementation
The two arguments
cheat_compare was receiving was enough for us to start building our own implementation. In fact, receiving the keycodes entered was all we needed. We could have gone and tried to patch the original routine to match some new strings and trigger our patches, but instead we chose an easier way out. We can read the keycodes that
cheat_compare received, perform some tests for our cheats and then let the original function continue as normal.
cheat_compare method, admittedly, was a little confusing. We decided to use the keycode we got as an argument, but had to work around the fact that we would receive the same keycode a number of times as the method was repeatedly called for different cheats with the same keycode. As a result, we decided on simply recording unique keycodes . This introduced only one limitation; our cheats couldn’t have repeating characters. The result was a method that would check a character and append it if it was unique, returning the full recorded buffer if a new character was added.
Next, we attached to
cheat_compare and fired this new
getCheatFromBufWithChar() to get the buffer of characters that were entered thus far. If a buffer ended with one of our strings, we fired the relevant patch to activate the cheat! To optimise the routine a little, we exited early if the entered keycode was not in the ASCII printable range.
The result of this script meant that any unique ascii characters that were read would be compared to toggle the status of a cheat. This also meant that we had to write smaller routines that would undo the patches we wrote, but those were relatively easy as we already knew the offsets and original opcodes.
The final script to play with these cheats is available here.
While choosing Doom may have been an easy target, we learnt a lot and got to play games while at it! We hope this inspires you to dig a little deeper into Frida and experiment more with it.