Resource decryption and generating registration codes
As mentioned in my post about unpacking LZRW compressed game
assets for Reckless Drivin’, levels
4 through 10 are encrypted and can only be played with valid registration
information. Jonas released free registration information once he discontinued
selling the game: the code B3FB09B1EB
registered to Free
. Thanks to this
free registration information, it is relatively simple to both decrypt the
remaining levels, and to create a program to generate new registration codes.
Decryption
The original source code includes a function called CryptData()
which is used
to decrypt a stream of bytes using a global decryption key. The decryption is
done through a simple XOR with the key four bytes at a time, with some
additional logic to handle streams that aren’t a multiple of four in length. As
the bytes are decrypted, a check value1 is also computed and is used to verify
valid decryption for level 4.
UInt32 CryptData(UInt32 *data, UInt32 len) {
UInt32 check = 0;
data += kUnCryptedHeader / 4;
len -= kUnCryptedHeader;
while (len >= 4) {
*data ^= gKey;
check += *data;
data++;
len -= 4;
}
if (len) {
*((UInt8*)data) ^= gKey >> 24;
check += (*((UInt8*)data)++) << 24;
if (len > 1) {
*((UInt8*)data) ^= (gKey >> 16) & 0xff;
check += (*((UInt8*)data)++) << 16;
if (len > 2) {
*((UInt8*)data) ^= (gKey >> 8) & 0xff;
check += (*((UInt8*)data)++) << 8;
}
}
}
return check;
}
The global decryption key gKey
is computed by a simple formula involving an
XOR relationship between the name and registration code: gKey = name ^ code
.
The actual process is a little more involved, but the name and code can be used
to generate the decryption key (and the decryption key, once known, can be
combined with any name to generate a registration code).
The exact steps to generate the decryption key are:
- Remove spaces and uppercase the name.
- Ensure the name is at least 4 characters long.
-
Bitcast the last 4 letters in the name to an int. The name
FREE
is represented in hex as0x46 0x52 0x45 0x45
which yields the int0x46524545
. -
Create a mask by repeating the last byte of the registration code 4 times.
The registration code
B3FB09B1EB
gives the mask0xEBEBEBEB
by repeating the last byte0xEB
four times. -
Compute the XOR of the first 4 bytes of the registration number and the mask.
In this case that would be
0xB3FB09B1 ^ 0xEBEBEBEB = 0x5810E25A
. -
XOR this new value with the number generated from the name to compute the
decryption key.
0x5810E25A ^ 0x46524545 = 0x1E42A71F
.
Valid registration data will always result in a key of 0x1E42A71F
which is
used to decrypt levels 4 through 10 before decompression.
Even though Jonas released the registration information for free, I thought it would be fun to generate new registration codes.
Generating registration codes
Because gKey = name ^ code
, and XOR is its own inverse, we can compute
registration codes easily with code = gKey ^ name
. The process is very similar
to the above. Given a name of at least 4 characters (stripped of whitespace):
- Uppercase the name.
- Bitcast the last 4 letters in the name to an int.
- XOR this number with the global key.
- Generate a mask of 4 repeating bytes (more on this later) and mask the number from the previous step.
- Append the repeated mask byte to the number to generate the registration code.
Because the mask2 is just a repeated byte, and is also appended to the
registration code, there isn’t any way to know how Jonas originally generated
these codes. I chose to sum the bytes in the name mod 0xFF
to generate the
mask byte. As a concrete example, let’s use my name: NATHAN
.
The last four characters of my name are the integer 0x5448414E
. This number
XORed with the key is 0x4A0AE651
. Summing (mod 0xFF
) the characters in my
name gives 0xBB
which creates the mask 0xBBBBBBBB
. These numbers XORed
together 0x4A0AE651 ^ 0xBBBBBBBB
results in 0xF1B15DEA
. The mask byte
appended to this number (as a string) is the registration code: F1B15DEABB
.
Using one of these codes I generated, we can see that the game (running in a virtual machine) shows my name in the lower right corner!
Thoughts
Because I am rewriting in Zig this is the second time I have implemented this decryption and registration logic. The first time the decryption code was written in C, and the registration code generator in Python. Implementing both in Zig was a good exercise. Here are some of my thoughts:
- Zig makes me much more aware of memory allocations by requiring passing an allocator to any functions that allocate memory. This means I can look at a piece of code and know how many allocations it makes without needing to traverse all function calls. If a function doesn’t take an allocator, neither it nor any child function calls will allocate.
-
When computing the decryption check value, the value ends up overflowing a
u32
multiple times. In C this behavior was assumed but went undetected. My Zig implementation uses the+%=
operator to explicitly allow overflow wrapping. Without this, debug and test builds would crash with runtime overflow protection. - Continuing this trend, Zig requires me to be much more explicit when casting integers.
-
I was able to take advantage of the
-|
operator to subtract while saturating at 0 to prevent underflow.
Sometimes Zig feels more verbose than C. Reckless Drivin’ involves a lot of bit casting, something that I once thought trivial in C. For example, Zig has forced me to acknowledge and specify the alignment of byte streams. Even though there is more up-front work than C, I really appreciate that the language is designed in a way to more precisely express to the compiler how I want to transform data. This has led me to write better code as I rewrite Reckless Drivin’ in Zig.
-
The computed value is the resource
Chck 128
which contains the exact check value expected from the decryption of level 4. Valid registration information is determined by comparing these two values.↩︎︎ -
Without a mask, the process to generate the decryption key would be to simply XOR the first 4 bytes of the registration code with the last four bytes of the name. This process is obfuscated slightly with a mask which alters the registration code so it no longer generates the key without unmasking it.↩︎︎