A story about reverse engineering and way too smart FTP client
Some time ago, I decided to swap HDD in my Original Xbox. It already has been upgraded with a 40 Gb IDE drive, but I had a spare 160 Gb SATA drive laying around and wanted to have some more storage in the system. I bought a SATA to IDE converter, swapped the drives, installed the system software, and transferred all the content back. To my utmost disappointment, savefiles for some games were corrupted. One of them was Black, where I had a decent amount of played hours. I did some basic troubleshooting but wasn’t able to find the source of the problem. At that moment it was obvious to me that the problem lay in the new hard drive.
If you’re here to know how to patch a game’s binary, jump straight to the Patching the binary yourself.
OG Xbox Savefiles 101
To prevent any tampering, all the game developers were obliged to add a digital signature to the savefiles of their games.
Additionally, developers were able to make savefiles “non-roamable”, i.e. lock them to the particular console they were created on. For achieving that, Xbox has a so-called “XboxHDKey” - a per-console value stored in the EEPROM intended specifically for the content locking. The list of such games is well known and maintained by the community. However, Black wasn’t on the list. (While writing this post, I was able to find an extended list, and even find a resigner specifically for Black. Funny enough, it wouldn’t help me with the issue I had in the first place.)
If you want to dive deeper into the topic, those pages are for you:
So, at that moment I was convinced that some additional checks were implemented in the game. I thought that probably HDD’s model and serial number were also used for the signature generating. There was only one way to know for sure.
Reverse engineering and patching game’s executable
I decided to analyze the executable binary with Ghidra. To be able to do so, I additionally grabbed ghidra-xbe, an extension for the Ghidra to support loading Xbox Executable Format (XBE) files, and xbox-includes, a set of GPL-sourced Xbox headers. Loading and analyzing the game’s executable was straightforward, but importing headers had a quirk. For some reason, Ghidra wasn’t happy with the HANDLE
type, but renaming it to something different like HANDLE_MY
solved the problem.
Now we are fully prepared to begin digging. Searching for the word “signature” in the program text immediately yielded some promising results:
A lot of recovered (or maybe unstripped?) symbols, the most interesting of which is “XCalculateSignatureBegin”. Quick googling gave me the following function definitions from the XAPI:
HANDLE XCalculateSignatureBegin(DWORD dwFlags);
DWORD XCalculateSignatureUpdate(HANDLE hCalcSig, BYTE* pbData, ULONG cbData);
DWORD XCalculateSignatureEnd(HANDLE hCalcSig, BYTE* pbSignature);
By making an educated guess, we can assume that 1st function creates a handle object set up with some flags, 2nd one takes the binary data and its size to calculate the signature for, and the last one writes the signature into the provided address. The flags are also conveniently listed on the page mentioned above, but we’ll get to them later.
Now we can complete the function signature and take a look a look at its callers. There are 2 of them, and for the reader’s convenience, we’ll start with the 2nd one. This is how it looks after decompilation:
The obtained handler is checked not to be -1
, and after that 2 functions are called, whose parameters are suspiciously similar to XCalculateSignatureUpdate
and XCalculateSignatureEnd
. After completing their signatures the decompiled code of the function started to look like this:
Now let’s take a close look. The ~Update
function takes EAX
as the pointer to the source data and 0x17ec
as the data length. The ~End
function takes EAX
advanced by the same 0x17ec
bytes. Most of the games stored the signature at the end of the savefile, so we can conclude that this function is responsible for adding the signature to the savefile while saving the game. You can be creative and replace this function implementation to add some custom data instead of the signature. Does anyone want to place an ad to the savefile of the 15 years old game where nobody will ever see it? No? Okay, let’s move forward then.
Now we’ll take a look at the 1st of 2 callers. Its decompiled code looks like this:
Here, the ~Update
function gets called identically, but the ~End
function takes a pointer to the local 20 bytes long variable as its second parameter. Perfect! The rest of decompiled code looks like total gibberish, so let’s take a look at the assembly listing:
After calling the ~End
function, pointers to the signature from the savefile and calculated signature get loaded to the EDI and ECI respectively, signature length gets loaded to ECX, and they got compared by the following instruction:
001f2dfd f3 a6 CMPSB.REPE ES:EDI,ESI
If the comparison is successful, i.e. the difference between signatures is 0
, the following conditional jump gets performed:
001f2e01 74 05 JZ LAB_001f2e08
The rest of the code is some compiler optimized preparation of the return value, which is not interesting. What’s interesting is to skip the code that gets executed if the check was not successful. The most straightforward thing to do is to replace the conditional jump with an unconditional one. It can be achieved by changing the instruction from JZ
(opcode 74
) to JMP
(opcode EB
). After doing so, saving the patched binary, and transferring it to the directory with the game backup on the Xbox via FTP, I was ready for testing. Unfortunately, an unexpected discovery has happened.
A terrible mistake
With patched binary, I also transferred my old savefile to the console. And I noticed something. The savefile size on my computer was 6144 bytes, while on the console it was a few bytes larger, something like 6147 bytes. I took a few seconds to digest it and then realized that I made a terrible mistake. As I figured out, FileZilla, an FTP client that I used automatically corrects line breaks while transferring text files between Windows and Unix systems, and by default, it treats files without an extension as text files. So, while I was thinking about some sophisticated anti-tampering system in the game, my FTP client was just corrupting the files.
After properly configuring FileZilla, I copied the savefile to the console, verified its size, launched the game from the vanilla executable, and it loaded the savefile normally. Just to verify whether my patch is working, I changed a few bytes in the savefile signature, copied it to the console, launched the game from the patched binary, and it also loaded the savefile normally! Yay!
It could be the end of the story, but in the process, I found something else.
Signing flags and non-roamable saves
Remember the flag that is passed into the ~Begin
function? If we look back at the decompiled code, the 1
is passed in both function calls:
hCalcSig = XAPILIB::XCalculateSignatureBegin(1);
What does the 1
mean? Fortunately, the same Reddit page mentioned above gives us the answer:
DWORD XCALCSIG_FLAG_NON_ROAMABLE = 0x00000001;
So, this precisely tells us that the game incorporates non-roamable savefiles. Their signatures are tied to a particular console with mentioned above XboxHDKey
and can’t be transferred to another console. But since we disabled signature checking, they now can be!
So, to recap: this patch allows you to freely move the savefiles between consoles without any additional actions like resigning. And this is some great news!
Patching the binary yourself
For obvious reasons, I can’t distribute the patched binary. However, I can show you how to patch one. During my research, I was using default.xbe
file marked as region-free (game region value in the xbe certificate is 7
, which means all regions
) with md5 hash d21025a12520a74a2edd6c86fef9b57e
.
The patching is simple - open the file with a hex editor, find a byte 74
at the offset 0x1E2E01
, and replace it with EB
. That’s it.
If there’s something different at the aforementioned offset, you can also try looking for the following byte sequence 74240833 C0F3A65F 5E74051B C083D8FF
and replace its 10th byte as described above.
Transferring savefiles without modifying the game
If you don’t have an option to use the patched binary (for example, you prefer to run games from the original discs), it’s also possible to resign the savefile. You can find a standalone resigner for Windows, its source code, and additional config for XSavSig in the feudalnate’s GitHub repository.
Wrapping up
So, what do I think about all this in the end? Was all this needed from the perspective of just playing video games? Not at all. Did I learn a lot about reverse engineering in the process? Absolutely yes! And I really enjoyed the process, it was quite interesting to dive deep and figure out how things work. And in the process, I somewhat contributed to video games preservation, which is also amazing!
Now I can confidently say that the Original Xbox helped me learn some new skills and has a special place in my life!