acordier's personal website

Hacking and signing KOTOR Xbox saves with Python

2024/11/13

Tags: hacking, videogames, python

How KOTOR saves work

Don’t ask me why, I was in the need of converting some PC saves from the famous Star Wars: Knights of the Old Republic (KOTOR) videogame to the Xbox format. In theory, the save format is the same for both PC and original Xbox, and a given save KOTOR actually consists of 6 files:

Here is the issue: simply transferring these save files to the appropriate folders (inside E:\UDATA\4C410003\) on your Xbox via FTP (using something like LithiumX) will not work.

This is because, to avoid save files alteration, the Xbox version of the game (like many games at the time) implements a security layer that uses an authentication key to check for proper SHA-1 signature of the save files .

How Xbox saves work

Fortunately, KOTOR is one of these games for which the saves are roamable, which means you could and still can transfer them from one Xbox console to another. This means that the save signing is performed with a static game key, which is the same for any save of this game. The way this game authentication key is computed is actually also interesting, but is not the topic of today. For non-roamable games like Ninja Gaiden Black, the save files are first signed with the authentication key, and then re-signed with a key that is specific to the current Xbox, called the HDKey, and located in the EEPROM memory chip of the console (see XSaveSig software).

The authentication signature key for the original KOTOR (US) on Xbox has been computed for a while now and can thus be used for signing the saves. Now, let’s move on to how the actual files should be signed.

Expected .sig files for KOTOR saves

As a consequence, if we can get our hands on the KOTOR authentication key, it should be easy to sign these PC save file with the HMAC SHA-1 algorithm so that they can be transferred and read by the Xbox. Here are the signature files expected by the Xbox:

The information above was mostly retro-engineered (i.e. guessed) by tk102 when writing the Kotor Save Editor (KSE) software. This software actually can generate the signature files properly, except… for the main save file SAVEGAME.sav, rendering the signing of un-signed PC saves and re-signing of KSE-edited Xbox saves difficult.

The Xbox expects 3 signature files for the SAVEGAME.sav file:

The person behind KSE (tk102) found that the SAVEGAME.sav file could indeed be split into three:

The first few bytes of the header are dedicated for variables such as version, entry_count, build_year, build_day, and various offsets for different variables. I didn’t have time to get a good understanding of everything, but from the KSE source code, I understood that the length (size in bytes) of the secondary header can be computed like this: offset_to_resource_list + (8 * entry_count) - 160.

In other words, the actual body of the save starts when the pointer reaches the offset_to_resource_list + (8 * entry_count) position, since the first header is of size 160 bytes.

Separating the SAVEGAME.sav file like this, it is indeed possible to properly sign the secondary header, as well as the body, resulting in proper SAVE_HEADER_VAR.sig and SAVE_DATA.sig files.

The SAVE_HEADER.sig issue

However, like tk102 explained and tried, simply signing the first 160 bytes header does NOT output a working SAVE_HEADER.sig.

Retro-engineering from a working, untouched Xbox save with lots of trial-and-error (xor-ing with static bytes, xor-ing with the authentication key, flipping bits, signing with the console key, etc.), I finally understood. The experience from users online helped me understanding what was happening. People were essentially explaining that, after editing an Xbox save with KSE, you could simply copy the original SAVE_HEADER.sig (hence regenerating it was not needed), on the only condition that you only edited and did NOT add nor remove stuff in your save. Similarly, if you decided to edit the player name from the save with KSE, you should NOT add nor remove spaces.

This led me to think that the total size (length) of the SAVEGAME.sav file must be concatenated to the header first before signing in order to generate SAVE_HEADER.sig.

And… it works! The (almost) 20-year mystery is now solved.

Concatenating the 160-bytes header of the SAVEGAME.sav with the size (number of bytes) of the SAVEGAME.sav file (converted to 4 bytes format) before signing with the authentication leads to a perfectly signed, working SAVE_HEADER.sig file.

Here is a short except of the Python code that sums up the solution:

# Read 160 first bytes (header)
header = read_file_bytes("SAVEGAME.sav", start=0, end=160)
# Compute total filesize
file_size = os.path.getsize("SAVEGAME.sav")
# Convert filesize to bytes
file_size_bytes = file_size.to_bytes(4, 'little')
# Concatenate header with the filesize
header_with_size = header + file_size_bytes
# Hash (sign)
signed = hash_with_key(header_with_size, auth_key)

Conclusions and Python tool

Thinking about it, the solution for generating the SAVE_HEADER.sig was pretty basic, since using the file size is common practice to ensure file integrity. Adding it to the header before signing definitely makes sense. The moral of the story is: work smart, not hard (e.g. brute-forcing all possible xor-ing and bit-flipping methods).

If you don’t want to get bothered with all these details and simply want to get the job done (signing your saves to generate the .sig file), I wrote a simple Python script that takes a save folder as an input, computes the signature and writes the proper signature files inside. You can download it here.

I hope that Kotor Save Editor (KSE) can be updated at some point to incorporate this solution. In the meantime, happy hacking!

Note that all of this should work with KOTOR 2 too, since the save formats are relatively similar I believe (only the authentication key for the game differs, obviously).