Bypassing 12 years old Xbox 360 Game Security

Hi

Today I will show you how I’ve bypassed Forza 3 savegame encryption.

Long story short, an Xbox 360 emulator, Xenia, is currently in dev on PC, and I’ve tried my favorite game on it. But there is a problem, Forza 3 cipher its savegames, and as Xenia is in dev, Forza 3 crash when you try to load a savegame. As I did worked a lot on Forza when I was young, I tried to look around the issue and I successfully bypass the savegame encryption.

Back in old good days, with savegame modding with tools such as Xbox360Tool, published on X360Haven forum, RIP. I remember that Forza always used crypto (AES) for there games since Forza 2, so I thouth it could be related to that.

To start somewhere, I’ve ripped my game disk with my RGH console, and uploaded it to my PC. This is the issue when you try to load a savegame made from Xenia or my retail xbox kernel.

NOTE : My knowledge of xboxkrnl and PPC is null, and I don’t like the syntax of it, but I will try to fix the issue anyway.

Let’s reverse

Like I said, the only thing I know about Forza savegame is that they are ciphered, and Forza love AES. So I started with this in mind because searching in such big executable could took a long time.

So there are 2 possible choises, Forza implemented AES by head in the executable, or they used the crypto Xbox Kernel API (xboxkrnl). First thing first, let’s take a look at the savegame its self (crafted with Xenia).

The file that contain the main information about your gameplay is in ForzaProfile. When we look at it, we can see a header and an ciphered buffer, due to its entropy.

Now let’s take a look at the executable, let’s find out if there is some informations about savegame or AES. Nothing interesting for thoses two.

The vtables you can see on the screenshot is not linked to the code btw. Note that this vtable .?AVCForzaProfileManager@Forza2@@ has as main namespace Forza2. So this confirm that Forza use this system since Forza 2, and possibly didn’t change since (in fact, it’s the same for Forza 2,3,4 and Horizon 1 and 2)

I did a pattern scan, to see if there is some crypto, hashing data pattern, but this lead to nothing. So I looked around imports, and I found this :

Forza seems to use crypto xboxkrnl api, and I felt very lucky when I saw that there is only two call to this function.

Here are those function :

So I reversed those two, some informations were found because of findings found later

int sub_825F18E0(u0, u1, BYTE* HMAC_SHA1_KEY_XOR_XUID, BYTE* AES_KEY_XOR_XUID){
    // ...
    // obscure the decryption key runtime
    if ( XeKeysObscureKey(&HMAC_SHA1_KEY_XOR_XUID, &HASH_KEY_OBS) < 0 ){
        // ...
    }
    // ...
    DWORD IV[5] = ...
    DWORD iDataSizeCiphered = ...
    // cipher profile size
    iDataSizeCiphered ^= IV[0] ^ IV[1] ^ IV[2] ^ IV[3] ^ (IV[4] & 0xFFFFFFF0);
    // ...
    BYTE* HEADER_FILE_HASH;
    BYTE* MAPPED_FILE_HASH = ...
    // get HMAC SHA1 of crypted buffer
    // ciphered with KEY_OBFUSCATION_KEY unique to each console
    if ( XeKeysHmacShaUsingKey(&HASH_KEY_OBS, &MAPPED_FILE_HASH, 0x14) >= 0 ){
        // compare the ciphered buffer hash
        if ( !strcmp(&HEADER_FILE_HASH, &MAPPED_FILE_HASH) ){
            // decipher the crypted buffer
            if ( XeKeysAesCbcUsingKey(..., 0) >= 0 ){
                // ...
                // load the savegame
                // ...
                VALID_FILE = 1;
            }
        }
        // ...
    }
}

int sub_825F1CF0(){
    // ...
    DWORD iProfileSize = forzaProfile->EndOffset - forzaProfile->StartOffset;
    // cipher profile size
    iProfileSize ^= IV[0] ^ IV[1] ^ IV[2] ^ IV[3] ^ (IV[4] & 0xFFFFFFF0);
    // ...
    // cipher the profile buffer
    if ( XeKeysAesCbcUsingKey(..., 1) >= 0 ){
        // ...
        // craft its HMAC SHA1 hash
        if ( XeKeysHmacShaUsingKey() >= 0 ){
            // ...
        }
    }
}

XeKeysObscureKey and XeKeysAesCbcUsingKey based on this documentation. The function sub_825F18E0 seems to decrypt something using a key and an IV, and right before that, the function do a hash check of this crypted data (the hash is ciphered too). The function sub_825F1CF0 seems to do the same thing (without the strcmp), but in the reversed order.

So I will assume that sub_825F18E0 is used in a decryption routine for the savegame. First because of my reverse, the code seems to be checking hash of ciphered content and only after, deciphering it. And also because of this code :

// ...
sub_825F18E0();
// ...
sub_822680A0(TMP_0, "GAME:\\Media\\ProfileSchema\\ForzaProfile.sch");

A .sch schema file is used after the file load

This schema define how to parse the ForzaProfile file, so this code convised me that it’s the decryption routine when the game is loading a savegame.

And the last reason, the most obvious, is that the last argument of XeKeysAesCbcUsingKey is encrypt. Like each low level implementation of AESCBC, this mean decrypt if 0, and encrypt if 1.

dword_result_t XeKeysAesCbcUsingKey(..., dword_t encrypt);
// encrypt
sub_825F1CF0() => if ( XeKeysAesCbcUsingKey(..., 1) >= 0 )
// decrypt
sub_825F18E0() => if ( XeKeysAesCbcUsingKey(..., 0) >= 0 ) 
sub_825F18E0() => SAVE_DECRYPTION
sub_825F1CF0() => SAVE_ENCRYPTION

So after a little bit of exploration, here is the code executed right before the SAVE_DECRYPTION routine.

BYTE AES_KEY_XOR_XUID[16];

int i = 0;
int n = 16;
do
{
    // XUID is double
    AES_KEY_XOR_XUID[i] = AES_DECRYPTION_KEY[i & 15] ^ XUID[(1 + i) & 7];
    ++i; --n;
}
while ( n );

BYTE HMAC_SHA1_KEY_XOR_XUID[16];

int i = 0;
int n = 16;
do
{
    // XUID is double
    HMAC_SHA1_KEY_XOR_XUID[i] = HMAC_SHA1_DECRYPTION_KEY[i & 15] ^ XUID[i & 7];
    ++i; --n;
}
while ( n );

VALID_FILE = SAVE_DECRYPTION(..., HMAC_SHA1_KEY_XOR_XUID, AES_KEY_XOR_XUID);

As you can see, the hash key and the AES key are xorred with XUID (Xbox User ID). This mean that the savegame will be decrypted only by its user. And can’t be shared without using the same Profile. Because if someone try to decipher the Profile, the key will be different due to XUID, and the Profile will still be ciphered.

First I didn’t recognize what was xorred with the HMAC_SHA1_DECRYPTION_KEY and AES_DECRYPTION_KEY, then I look around what people said about Forza savegames in the past. I figured out that this was the XUID. So thanx to all thoses peoples who did a great job in xbox scene.

As you can guess, this routine is also used before the call of SAVE_ENCRYPTION routine.

This are the keys buffer :

// byte_820094A0
unsigned char HMAC_SHA1_DECRYPTION_KEY[16] = {
	0x8F, 0xF6, 0x54, 0xFC, 0x40, 0x64, 0x6A, 0x75, 0x68, 0x8D, 0xEE, 0xE8,
	0x9E, 0x24, 0x58, 0x58
};

// byte_82009490
unsigned char AES_DECRYPTION_KEY[16] = {
	0x27, 0x50, 0x37, 0x9D, 0x34, 0x8C, 0x47, 0xAE, 0x09, 0x5A, 0xA3, 0xC4,
	0x71, 0xB1, 0x2E, 0xD4
};

So, from now, we can do some datamining of the savegame, and define this struct (thx sh042067 and hetelek for there reverse)

// NOTE : All digits are in big endian
struct ForzaProfile_t {
    u8  szHeader[4];
    u32 iFileSize;
    u8  szHash[20];
    u8  szIV[20];
    u8  szCryptedDataBuffer[0x4FD8];
};

ForzaProfile_t profile @ 0x0;

Now, we can try to decrypt a real Forza 3 savegame with a real xbox kernel to see if the reverse is accurate.

Here, the save from a real kernel on the left, and Xenia one on the right :

It’s clear that the header of the real savegame contain a proper hash and IV, compared to the Xenia one.

Honorable mention to the ciphering, we can’t distinguish the header from ciphered data.

First try

To make sure my reverse is right, I’ve tried to decipher the real savegame to see if I’m correct.

TODO

Using my XUID E0000183AE******, I’ve xorred the AES key and deciphered the savegame content, and :

It worked :)

Now we know that my algorithm is correct, we can try to figure out what’s wrong with Xenia.

Let’s decipher the savegame made under Xenia, using this B13EBABEBABEBABE as XUID, the one used by Xenia (code)

Failed, so the issue is about parameters (hash and IV).

If we take a closer look to the data, specialy the IV, we can see that it contain the crypted buffer size 0x15B1

But remember, it should be ciphered by this line :

iProfileSize ^= IV[0] ^ IV[1] ^ IV[2] ^ IV[3] ^ (IV[4] & 0xFFFFFFF0);

So there is definitly something wrong above the AES encryption.

The possible issue of Xenia

Now that we know aproximativly how Forza loads savegame, could we find the issue ?

During my documentation, I found this about XeKeysObscureKey : https://github.com/xenia-project/xenia/pull/1747

It seems that the issue is that Xenia don’t implement XeKeysHmacShaUsingKey.

As the XeKeysHmacShaUsingKey and XeKeysAesCbcUsingKey functions uses obscuration keys in addition of all other thing mentioned above. The call to XeKeysObscureKey followed by XeKeysHmacShaUsingKey should produce kind of keychain kernel land or something like that. The keyvault contain could be used to crypto as XeKeysAesCbcUsingKey does, so without the call of XeKeysHmacShaUsingKey, everything should produce a bad file.

I didn’t reversed the entire code, so I can’t say it’s the only thing that produce the bug. But I’m sure that the ciphering is the main reason why Forza can’t load the savegame.

I’ve also found this issue : https://github.com/xenia-project/xenia/pull/1440

Apparently, keys used to obscure data while calling XeKeysObscureKey and XeKeysAesCbcUsingKey are null

This should not affect the “crypto” behavior as the key is null, but it’s still something that produce a different behavior of a real xboxkrnl regarding savegame.

Let’s patch

How to fix it, first choise, we remove the crypto from savegame loading and saving routine. Second choise, we implement XeKeysHmacShaUsingKey in xenia, but there is maybe another bug somewhere else.

So, as it’s trickier, I’ve tried the first choise !

My first aproach was to patch the call to save decryption and encryption routine, but I realised that those “load” the savegame right after. So the patch should be in those functions.

My second approach was this one :

int SAVE_DECRYPTION(u0, u1, BYTE* HMAC_SHA1_KEY_XOR_XUID, BYTE* AES_KEY_XOR_XUID){
    // ...
    if ( XeKeysHmacShaUsingKey(&HASH_KEY_OBS, &MAPPED_FILE_HASH, 0x14) >= 0 ){

        // don't care about its result
        strcmp(&HEADER_FILE_HASH, &MAPPED_FILE_HASH);

        // remove the ciphering
        // XeKeysAesCbcUsingKey(..., 0)

        // load the savegame

        VALID_FILE = 1;
    }
    // ...
}

int SAVE_ENCRYPTION(){
    // ...

    // remove the ciphering
    // XeKeysAesCbcUsingKey(..., 1)

    // useless since we don't check it anymore
    if ( XeKeysHmacShaUsingKey() >= 0 ){
        // ...
    }
    // ...
}

And then, while I was looking for a way to patch my xex (Xbox Executable), I’ve found this :

https://github.com/mojobojo/PublicXboxStuff/blob/master/PPC/ForzaHax.S

A guy named mojobojo already did that for Forza 4, the exact same way as my plan. So… I will do the same thing since there is not 100 methods to do it.

First we need to patch the strcmp comparison to zero in BREAK_FAIL block. We could just nop the cmpwi (compare word immediate), and nop the bne (branch not equl), so the comparison is not done, and the code continue directly in AES decryption routine.

Then, we can nop the AES decryption, the bl XeKeysAesCbcUsingKey (branch), the cmpwi of the XeKeysAesCbcUsingKey result and the blt loc_825F1EB0 (branch if less than)

And the same in AES encryption routine…

Using xextool I’ve decompressed and decrypted the main executable, and using xepatcher, I’ve the following patch to it

# nop file encryption
.long 0x825F1E08 
.long ( 1f - 0f ) / 4
0:
    nop # bl        XeKeysAesCbcUsingKey
    nop # cmpwi     r3, 0
    nop # blt       loc_825F1EB0
1:

# nop hash strcmp
.long 0x825F1B00
.long ( 1f - 0f ) / 4
0:
    nop # cmpwi     r9, 0  
    nop # bne       loc_825F1B94
1:

# nop file decryption
.long 0x825F1B44
.long ( 1f - 0f ) / 4
0:
    nop # bl        XeKeysAesCbcUsingKey
    nop # cmpwi     r3, 0
    nop # blt       loc_825F1B94
1:

.long 0xFFFFFFFF

And voila :)

Conclusion

Done, now I can save, load and play my game :0 And change my money ingame as well :p

Ciphering savegame data with XUID is cleaver, Forza was well made regarding its gameplay, and I’m happy to see that its security was good too.

It was very fun and I’ve learned a lot of things (Thanks to BinaryNinja pseudo code btw)

~r0da

References and Greatz

  • Xenia Project
  • XDK 2008
  • cs.sin.ru crackers
  • X360Haven
  • sh042067 and hetelek work
  • mojobojo 2011 work