The Original Xbox was a total disaster from a security point of view. It has been fully cracked relatively quickly, so it became possible to modify your system to disable the security checks and run the unsigned code: Linux, homebrew, game backups - you name it.

For making that possible for the end user, 2 modding methods have been created:

  • Hardmod - requires soldering a modchip to the mainboard that contains the modified BIOS with security checks disabled and overrides the built-in flash memory. This is the most reliable and fool-proof method, but it requires modifying the hardware;
  • Softmod - requires triggering a chain of exploits in software. Usually, a commercially available videogame with exploitable vulnerabilities in the savefile loading code is used as an entry point. Less reliable since the console can be bricked by doing something wrong (however, hardmod would fix it in any case), but doesn’t require any soldering iron involvement.

Even today, the softmod is a preferred method for a lot of people, so we’re going to take a look at how some parts of the softmod work, and create a brand new savefile exploit.

Attack surface

The Xbox kernel is based on Windows 2000. However, a lot of the features have been stripped, including multitasking, so only a single executable is running at the time. Moreover, every executable is running with kernel privileges, to squeeze a few extra bits of performance. On top of that, The system is based on the Intel Pentium 3 CPU, which doesn’t have an NX-bit feature, so all the memory pages are executable. This makes the process quite straightforward - once we hijacked the program execution, we ultimately have control over the whole system.

Vanilla Xbox can ultimately run one of the following pieces of software:

  • Dashboard - starts if there’s no game disc present in the optical drive. Has its share of vulnerabilities, however, none of them is possible to trigger from the unmodified state;
  • Videogame - loaded from a DVD disc with lots of copy protection measures, contains an RSA signed executable (as well as every other official executable), can load and store savefiles from/to built-in HDD or a memory card.

Savefiles are of particular interest to us:

  • Often they store string values (like the player’s name) in form of null-terminated C strings. If the game doesn’t validate the length of the string, there’s a potential buffer overflow we can exploit;
  • Savefiles are the only adequate option to provide the initial shellcode that will be executed after hijacking a program control flow.

Microsoft clearly didn’t want anyone to tamper with the savefiles, so they obliged the game developers to incorporate a digital signature into every savefile and to check it on loading. They even provided a convenient API for doing that. However, after the system has been cracked, people discovered that the algorithm for generating signature looks like that:

derived_key = HMAC(game_key, xbox_key, SHA1)
signature = HMAC(derived_key, savefile_data, SHA1)

The aforementioned xbox_key is the key that is stored in the system software and is unique for every console, the game_key is the key that is stored in the game executable’s header and is unique to every game. It’s also possible to tie the savefile to the particular console by adding the per-console key to the mix. In Microsoft terminology it’s called a “non-roamable signature”, you can learn more about it here in my other post.

This gives us enough information to start exploring games for vulnerabilities. However, there are some more topics to cover first.

Existing savefile exploits

It shouldn’t be a surprise that this kind of exploit has been implemented multiple times. Let’s take a look at the existing ones:

  • 007: Agent Under Fire (March 2003) - The first one released. Created by habibi_xbox (David Jilli). Shellcode is encrypted and the decryption code is heavily obfuscated. The decryptor can be found here. Technical breakdown is available here;
  • MechAssault (June 2003) - Released soon after the 007 exploit. Shellcode is similar to the 007 and is stored as plaintext;
  • Splinter Cell (December 2003) - released at the 20th Chaos Communication Conference (20C3) by Andrew “bunnie” Huang and Michael Steil of the Xbox-Linux project. Shellcode differs from the previous exploits (more about that later). It is also encrypted with a simple XOR cipher, most likely to remove the zero bytes from the binary to copy it a part of the string. The decryptor can be found here;
  • Tony Hawk 4 (April 2017) - created by Grimdoomer. The shellcode looks similar to the decrypted one from the Splinter cell.
  • Metal Arms (January 2022) - created by dj0wns (Derek Jones). The first half of the shellcode seems to be borrowed from the 007 exploit. Uses some format string vulnerabilities as well to achieve code execution.

Now, there’s an interesting bit. During his talk at the 22C3, Michael Steil mentioned Frogger Beyond among the rest of the exploited games. However, this is the only mention of this game, and there’s no exploit in public for it. And since I was already playing with the Xbox internals, I decided to figure out how can the Frogger be exploited.

Prerequisite: debug hardware

It would be quite hard to learn about the program internals and to craft an exploit without a debugger. Fortunately for us, it’s possible to convert a retail Xbox into a debug kit by adding the SuperIO board to get the serial port connectivity and by running the debug system software. After doing that, you can debug the Xbox with WinDbg kernel debugger. Here’s a good reference of the WinDbg commands.

Debug conversion is out of the scope of this article, but this is how my Xbox ended up hardware-wise:

1 1 1 1

Examining the game for a possible overflow

Time to get our hands dirty.
First of all, let’s boot the game and play a bit until the game allows us to save the progress. After saving the game, transferring the savefile from the console, and opening it with a hex editor, we can see the following:

1

The file starts with a magic byte 0x01, after that comes the player name if the form of a null-terminated string, and after that there are some sparse bits of data. The rest is just zeros, and the last 20 bytes are the signature.

Now the obvious thing for us to do is to fuzz the string. We extend the string with a long non-null pattern of bytes, sign the savefile, copy it to the console, and let the game try to load it:

1

Unsurprisingly, the game crashes with Access violation: writing to location 0x07070723, which indicates that we probably overwrote some pointer in the local stack frame which got used before returning from the function:

1

By playing with the long string contents I figured out that the pointer is overwritten with the bytes at the offset 0x159-0x15C in the savefile.
The debugger also shows us the call stack during the exception, the top entries of which are 0xd68ef and 0xdd51e (and the rest is a corrupted stack):

1

Another thing we can do is to search the RAM for the loaded savefiles. Fortunately for us, the game loads the first savefile to the memory address 0x00429918:

1 1

Conveniently, the address remains the same between the reboots, so we don’t need to copy our payload as part of the string, it’s already waiting for us in the memory.

Examining the executable internals

Now, let’s fire up Ghidra and take a look at what’s going on in the game executable by the addresses from the call stack.

First comes the function containing the offset 0xdd51e. In short, there’s a big function for handling a lot of different stuff related to loading the savefiles of the game. Let’s skip the Assembly and go straight to the decompilation of the interesting part. The instruction at the aforementioned offset is the next one after the CALL to the my_presentAlert function. Oh, by the way, all the symbols are stripped from the executable, what you see is what I was able to recover:

1

The interesting lines are 344-346. Here we can see that the string is formatted and copied to the local_170 buffer, and the function my_prepareAlert is being called with the pointer to the local_170 as one of its parameters.

After examining the executable with Ghidra and the memory of the running game with WinDbg, we can recreate the interesting part of the stack frame of this function:

1

As you can see, there is a local_170 buffer that we can overflow, some other local variables we’re not interested in, a this_ pointer to an instance of some game class, saved EBP value, and the return address.

Now let’s take a look at the function containing the offset 0xd68ef. It contains the logic related to presenting the on-screen alert. In our case, it gets called with the following parameters:

  • this: pointer to memory
  • selector: 2
  • message: pointer to memory
  • unknown1: 0
  • unknown2: 0

This is what the result of its decompilation looks like:

1

The instruction at the aforementioned offset is represented by the line 6. It aligns perfectly with the “Access violation” exception - we corrupted the pointer and tried to write something to some invalid address.
Since at this point we’re not planning to return back to the running game, we can just overwrite this_ pointer with some “safe to write to” address (e.g. a higher address in the stack, like 0xd014a9b0) and check if the game “survives” until returning from the dd51e function. Running the game step-by-step in the debugger shows that it happily reaches the end of the function and tries to return to the address that we overwrote. Perfect.

Now it’s time to take a step aside and think what code we want to execute after hijacking the execution flow, aka the shellcode.

Shellcode

Generally, a shellcode should achieve 2 goals:

  • Circumvent the RSA signature check of the executable;
  • Run the unsigned executable that is provided as part of the savefile package.

To achieve that, shellcode does the following:

  • Disables memory write protection:
    • Achieved by clearing out the Write Protect bit in the CR0 register. As the IntelĀ® 64 and IA-32 Architectures Software Developer Manual, Vol. 3A describes it, “when set, inhibits supervisor-level procedures from writing into read-only pages; when clear, allows supervisor-level procedures to write into read-only pages”. Conveniently, we’re already running in ring 0, so all the memory magically becomes writable!
    • another way to do that is kinda obscure. As Thrimbor said (message screenshot), it looks like pathing the R/W bit of page table entries from the offset 0xc0200000;
  • Patches the RSA key. The original 007 exploit tried to limit the use of the exploit to Linux booting only. Instead of disabling the signature check, the author decided to change the last 4 bytes of the public key. The resultant key is divisible by 3, which makes it easily factorable, which in turn allows us to retrieve the private key and sign our own executables. This pair of keys is called “habibi” after the nickname of the author of the original exploit habibi_xbox (David Jilli). There are multiple approaches to patching the key:
    • Search for the key in the kernel address space. That was the original method of doing that - the exploit does a linear search for the last for 4 bytes of the key in memory, and patches them once they are found;
    • Get the address of the kernel structure which holds the public key from the kernel exports table. After getting the address, we just patch the bytes at the known offset from the beginning of the structure;
  • Launches the provided executable. It can also be done in several ways:
    • By calling the XAPI methods. First, the DVD drive should be unmapped from the “D:” drive and the partition containing the game saves should be mapped to D: instead. This is done since XAPI will refuse to launch an executable from the drive different from D:. It can be achieved by calling IoDeleteSymbolicLink and IoCreateSymbolicLink kernel functions respectively. After that, it’s possible to run the XLaunchNewImage XAPI function to run the executable. The main downside of this method is that we need to know the offset of the statically linked XAPI inside the executable, which makes the exploit non-portable. Used by 007 and MechAssault exploits;
    • By calling the kernel methods directly. This is what the XLaunchNewImage XAPI function does under the hood. An example code can be found in the OpenXDK repository. In contrast, this code is portable between the executables, and doesn’t even require the target executable to be statically linked against XAPI. Achieved by:
      • Getting the address of the memory page containing LaunchDataPage kernel structure;
      • Allocating the new memory page if the pointer is NULL by calling MmAllocateContiguousMemory;
      • Saving the pointer back in the kernel by calling MmPersistContiguousMemory;
      • Zeroing out the memory page and setting it up with the required parameters, including the path to the executable;
      • Calling HalReturnToFirmware with the parameter ReturnFirmwareQuickReboot.

In addition, exploits that disable memory write protection through the CR0 register are combining the write protection disabling and the key patching inside the critical section, achieved by temporarily disabling the CPU interrupts. Some of the exploits also invalidate TLB and flush the CPU caches as a precaution.

The table below shows which exploit uses which of the described approaches:

007 MechAssault Splinter Cell Tony Hawk 4 MetalArms
Disabling memory write protection Page table patching CR0 CR0 CR0 Page table patching
Public key patching Search for the key Known offset Known offset Known offset Search for the key
Launching the executable XAPI XAPI Kernel calls Kernel calls Kernel calls
Critical section No Yes Yes Yes No
Invalidates TLB No Yes (twice) Yes (twice) No No
Flushes CPU caches No Yes Yes No No

Choosing a shellcode

Well, there are plenty of options to choose from. We can construct our own shellcode using parts of the existing ones, or just adapt an existing shellcode to our purposes. However, it’s not really convenient to do that.

Fortunately, we can take a slightly different route. There is a dashboard exploit called Bert & Ernie that exploits a vulnerability in the font loading code. I wasn’t able to find the original exploit, but what I found instead is an offshoot of this exploit as part of the NKPatcher 11 tool in Rocky5’s softmod repository. It has all the pieces of the shellcode described above. More importantly, its Assembly source code is available. This basically makes it a DIY kit for our own exploit, especially since the code is portable due to the absence of the XAPI calls. In my exploit, I’m going to use this shellcode with some modifications.

Returning to shellcode

Now since we have all the required bits, it’s time for us to create an exploited savefile. We achieve that by doing the following:

  • Fill the gap from the end of the profile name to the offset 0x158 with any non-zero values. The key is to not have a zero byte (C string terminator) until we overwrote all the interesting things on the stack;
  • Put the overwrite value of this_ at the offset 0x159 of the savefile. As mentioned before, we’re overwriting it with a higher address on the stack, 0xd014a9b0;
  • Put the overwrite value of the saved EBP next, at the offset 0x15d. This should be overwritten with any non-zero values, let’s put 0xdeadf00d there for additional points of style;
  • Put the return address next, at the offset 0x161. We’ll figure it’s value a tiny bit later.
  • Put a zero byte next, at the offset 0x165 to finally terminate the string. In our case, we don’t need to copy any more bytes;
  • Put a reasonably large NOP slide somewhere in the rest of the file followed by the shellcode. Since we don’t directly control the address in memory where the whole savefile is loaded, it’s better to have a NOP slide in case the data moves a bit in memory.

Now let’s go back to the return address. The savefile is loaded to the address 0x00429918. By picking the address of the middle of the NOP slide in the savefile and adding it to the address above, I got a value of 0x00429bd2. This creates a problem for us. Since the x86 CPU is little-endian, the address would be written backwards in the file, like d2 9b 42 00 (as well as the rest of the values). As we remember, we can’t include zero bytes in the string, since this is a string terminator. However, this is the last byte that we need to copy, so that should be fine, it will be both the most significant byte of the address and the string terminator.

This is what the end result looks like:

1

(Note that all the addresses are written as little-endian values)

So, let’s sign our carefully crafted savefile, transfer it to the console and boot the game.

Unfortunately, the game just hangs after trying to load the savefile. The debugger shows that the game tries to execute something from the invalid address 0x3f429bd2 which crashes the system.

Wait, why did the most significant byte become 0x3f instead of 0x00? Let’s take a look at the format parameter of the my_sprintf. Its value is "Confirm Load of %s?", and as you can see, a question mark is added right after the string we’re controlling. The most significant byte of the return address effectively terminates our string, and the sprintf happily adds a question mark right after, which ASCII code is, as you may have guessed, 0x3f! This single byte is literally the only thing that blocks us from exploiting the executable like that. Let’s see what else can we do.

Plan B: bootstrap loader

The nice property of the stack is that it’s located in the part of the address space way above the address 0x00xxxxxx. We can write a small bootstrap loader that will construct the desired address in memory and jump there. The most important property of this bootstrap is not to contain zero bytes, so we can copy it to the stack as part of the string.

This is the code I ended up with:

mov eax, 0FEEDC0DEh
mov ebx, 0FEAF5B0Ch
xor eax, ebx
jmp eax

The address is stored as a product of XORing it with a constant (0xfeedc0de xor 0x00429bd2 = 0xfeaf5b0c). Since (a xor b) xor b = a the address is recovered by XORing the product with the same constant once again. After that, it’s just a matter of jumping to the recovered address. The compiled bootstrap looks like b8 de c0 ed fe bb 0c 5b af fe 31 d8 ff e0, so it perfectly matches the requirement of not having any zero bytes.

Returning to shellcode, take 2

Now we’re gonna do the following:

  • Include the bootstrap loader as part of the copied string;
  • Figure out its location in the stack with the debugger and put this address into the overwritten return address.

This is how it looks like:

1

After booting the game and trying to load the resultant savefile, I was greeted with a kernel exception UNEXPECTED_KERNEL_MODE_TRAP with the first parameter meaning “Double fault”:

1

The return address however has been overwritten correctly, so the problem lies in a different place. (To be presice, I got this exception previous time as well. However, it wasn’t really important there.)

Xbox address space

The retail Xbox has 64 MB of RAM, mapped from 0x00000000 to 0x03ffffff. Stack addresses are clearly out of this range, which means that those are virtual addresses. This leads us to the conclusion that the Xbox kernel doesn’t support executing the code from the virtual addresses, so we have to stick to the physical addresses.

Since we can’t return to the address containing zero bytes, the first accessible address is 0x01010101, located at 16-something MB from the beginning. Excluding the rest of the addresses containing zero byte, we have the access to around ~40 MB of RAM.

The possible next steps would be:

  • Looking for the gadgets in the accessible areas of RAM and constructing the ROP chain (however, there’s no guarantee that the values used for gadgets are gonna be the same on every system and on every boot);
  • Finding another vulnerability we can leverage.

Let’s try the second option and see where it will lead us.

Looking for another vulnerability

After taking a break and looking at the my_presentAlert function with fresh eyes, it hit me - we control the value of this, and the function does some writes to the addresses relative to it. Let’s recap the passed parameters and the decompiled function itself:

  • this: pointer that we control
  • selector: 2
  • message: pointer to the string that we control
  • unknown1: 0
  • unknown2: 0

1

At first, I was examining if I can leverage single-byte writes to the relative offsets 0x1c-0x1f and 0x27, but they were barely useable in the given situation. And then I noticed the line 29. This call to _strcpy does precisely what you think - it copies the string we control to the address we control! This is a huge one! Let’s see how we can leverage it.

Returning to shellcode, take 3

Our course of action would be:

  • To find a place in RAM where we can write our string containing the bootstrap;
  • The rest at this point is obvious.

For the kicks and giggles, I tried overwriting the value of this with the first available physical address, 0x01010101. This is what I saw with the debugger:

1

The arbitrary write worked! The selected area is nothing but our bootstrap loader! Perfect!

Now we can do the following:

  • Overwrite this_ with 0x01010101;
  • Figure out the address of the shellcode in the string copied to the physical address, and overwrite the return address with this value. In my case, it was 0x01010281 (can be figured out with a debugger or calculated manually by adding the offset in the _strcpy call (0x51), length of the format string before the %s in the my_sprintf call (0x10), and the offset to the bootstrap from the beginning of the string in the savefile (in my case, 0x11f) to the value of this_).

Finally, it worked! Here’s a short demo of the exploit in action:

This is how the final savefile looks like:

1

The missing part only contains the rest of the NOP slide. Sources of the exploit with the extensive comments can be found here, as well as the ready to use exploit.

Please note that I tested the exploit on the modified Xbox. If you try to use it on a vanilla Xbox and something doesn’t work, let me know so we can fix it for everyone. If it works, let me know as well :)

What can be done differently

Instead of copying the bootstrap loader to the chosen place in RAM, we can copy the whole shellcode. However, it shouldn’t contain any zero bytes, so it should be prepared for that at first.

The very first thing in our shellcode is a call with no offset, which is compiled to e8 00 00 00 00. It’s possible to replace this code with the one that doesn’t contain zero bytes, and that would be a good solution. However, there are many other “holes” in the compiled shellcode, and working them around one-by-one would require a lot of additional labor. What we can do instead is find a byte that never occurs in the rest of the compiled shellcode, XOR this part of the shellcode with this byte (which will effectively change all bytes in the binary to a non-zero ones), and write a small loader what will XOR the whole shellcode back before executing. This is precisely what the Splinter Cell exploit does:

1

After that, the exploit is just copied as part of the string to the chosen place in RAM and executed. Theoretically, this approach should be more reliable since we control where to copy the shellcode. So if the exploit doesn’t work on some of the consoles, there’s a clear idea on how to fix it.

Credits

Big thanks to:

  • Derf, dj0wns and Thrimbor for their help with the project;
  • XboxDev community in general for being an amazing place.

Further reading