Our Blog

Hacking doom for fun, health and ammo

Reading time ~20 min

Remember iddqd and 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 setup

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 chocolate-doom.exe.

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.

Searching for the value 49 returns several memory locations

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.

Only three possible locations found for the ammo count.

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”.

Watching updates to a memory location.

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.

A rapid firing instruction on the watched memory location.

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 0x0430A2c.

Instruction to reduce the handgun’s ammo by 1

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.

Scanmem showing the addresses storing the current ammo value.

To find out what instructions write to the identified pointers, we used gdb and set a watchpoint on each address found by scanmem.

Watch points set for the ammo decrease.

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.

Watchpoint triggered after shooting with the handgun.

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.

rip and the surrounding instructions

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.

Viewing the sub instruction in hex

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. 0x49F28.

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.

Entrypoint into the function that affected the handgun’s ammo

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.

Interceptor attached to the hangun fire function’s entry point

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.

Hex opcodes for the instruction decrementing ammo of the handgun

The code writer could be used to patch 0x430a2c with exactly 8 NOP instructions using the putNopPadding(8) method on a code writer instance.

Frida code writer used to replace the ammo decrementing instructions with 8 NOPs.

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 SUB 0x1 from an ammo total (except for the machine gun that would SUB 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 :)

Ammo patcher to increment instead of decrement ammo for each matched opcode search

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 locations for the player’s health.

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.

One of the set of instructions that triggered when a player’s health decreased. Note the sub instruction above the mov instruction, that did the actual subtraction.

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 0x01. The sub instructions for the three different functions were at 0x3DEEC, 0x2C385, and 0x2c39.

Patching 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. ¯\_(?)_/¯

Health patch to ADD instead of SUB

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”.

Message after using the idkfa cheat

A text search in IDA for this message revealed the location where it was used.

Very Happy Ammo Added search in IDA

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.

IDA graph view of the cheat function

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.

Comparison function called for each cheat in the previous routine.

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.

Argument dumping using the Frida Interceptor for cheat_compare

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.

Herein lies an important concept I suspect many don’t immediately realise when using Frida. While Frida is a fantastic runtime instrumentation library, it can also be used to easily execute some JavaScript logic from within any function. In other words, we can introduce and execute code that was *not* part of the original binary, from within that binary. We don’t have to patch some code to jump to opcodes we have wrote, no, we can just execute pure JavaScript.

The 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.

Method used to record keycodes received in cheat_compare()

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.

cheat_compare entrypoint used to match new, custom cheats

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.

Conclusion

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.