Under the microscope: Tony Hawk's Pro Skater 2 (PlayStation, Dreamcast)
Mounting a dictionary attack against the Tony Hawk cheat code hashing system
In this edition:
-
By analyzing Tony Hawk’s Pro Skater 2 in Ghidra, I found that it has some cheat codes that have gone unreported for 24 years.
-
Finding that the codes exist was the easy part. To figure out how to put them into the game, I wrote a Python script that attacks the game’s hashing function.
The article describes what the codes do, how the Python script works, and how the game applies the cheat effects. This one was fun to figure out!
Intro
Tony Hawk’s Pro Skater 2 came out for Dreamcast in late 2000. I first played it on one of the Official Dreamcast Magazine demo discs and certainly got a lot out of that preview. My neighbor later got the PlayStation version, and we played it together for the next few years. Decades later the soundtrack is still buzzing in my head.
The Internet has a a bunch of cheat codes for THPS2 — most places list more than a dozen. IGN and CheatCC have decent lists for for PlayStation, but they don’t fully overlap. By combining data from multiple sites, I found 33 distinct codes.
Experience shows that games with lots of known codes often have some unknown ones, too, so I decided to investigate. I thought it would be a breeze to figure things out after I immediately located the game’s cheat code handling functions (that’s often is a challenge by itself)1. But the search turned into one of the more ambitious reverse engineering projects I’ve done — read on for details.
The code checker
The function loaded to address 80080fb8
(on the NTSC-U version for PlayStation) handles cheat code input. It runs when the game is paused and you’re holding L1
(L
on Dreamcast).
The Ghidra decompilation of that function is kind of a mess, but here’s what it’s doing in pseudo-Python:
if HOLDING_SQUARE:
x, y = 0x03185332, 0x80FE4187
elif HOLDING_X:
x, y = 0xB87610DB, 0xDE098401
elif HOLDING_CIRCLE:
x, y = 0xDEADBEEF, 0xFE3010F3
elif HOLDING_TRIANGLE:
x, y = 0x31415926, 0x7720DE42
elif HOLDING_LEFT:
x, y = 0x93FE1682, 0x92551072
elif HOLDING_DOWN:
x, y = 0x776643D1, 0x0901D3E8
elif HOLDING_RIGHT:
x, y = 0xAB432901, 0x88D3A109
elif HOLDING_UP:
x, y = 0x01234567, 0x34859F3A
HASH_1 = (((HASH_1 ^ x) << 1 ^ (HASH_1 ^ x) >> 0x1F) * 0x209) & 0xFFFFFFFF
HASH_2 = ((((HASH_2 ^ y ^ HASH_1 >> 8) << 1) & 0xFFFFFFFF) ^ (HASH_2 ^ y) >> 0x1F) & 0xFFFFFFFF
That is: Each button you press is associated with a pair of values. Those are used to update a pair of addresses (800d81d8
and 800d81dc
) using a custom hashing algorithm.
The hash outputs are then compared to various values. For example for the “Double moon physics” cheat:
if (HASH_1 = 0x943ed118) and (HASH_2 = 0xDDBA0D98):
DAT_800d27ec ^= 1
return 1
If there’s a match, a cheat effect is applied (that’s the XOR
) and the text on the pause screen is made to shake (that happens if this function returns 1
). Some cheats only look at the first hash value.
There is substantially identical code in the Dreamcast version. The Nintendo 64 version uses a slightly simpler hash2.
There are 35 hash comparisons, which implies that there are two undiscovered cheat codes:
Hash
(70153315, ANY)
sets a flag at800d61e0
.Hash (
D7DDA9E0
,0x8F37160D
) sets a flag at800d779c
Now the task is to find them…
Attacking the hash
The simplest way to find the button sequences associated with the hashes would be to:
Generate every possible button sequence
Compute the hash values associated with each sequence
Compare them to the unknown cheat hash values
This brute force approach would work for short codes, but not for long ones. To generate all of the length 10 sequences would require computing about a billion hashes (8^10). That would work on my laptop, but length 11 codes (8 billion hashes) would be take a while, and 12 (68 billion hashes) would be a stretch.
The search space can be narrowed, however. Notice that the cheat codes for PlayStation aren’t random — most of them spell out a word like RUST
or STUD
. We can use this to our advantage and mount a dictionary attack. That is, start with a list of words made up of U, D, L, R, S, T, C, and X; generate permutations of them; hash the permutations; and check the results.
I used the /usr/share/dict/words
file as a starting place:
subset_words = set()
letters = set('UDLRSTCX')
with open('/usr/share/dict/words', 'rt') as f:
for word in f:
word = word.strip().upper()
if set(word) <= letters:
subset_words.add(word)
The Python itertools
module can generate the needed permutations:
from itertools import permutations
MAX_WORDS = 4
MAX_REPEATS = 3
def get_button_stream():
for perm_length in range(1, MAX_WORDS + 1):
for perm in permutations(subset_words * MAX_REPEATS, perm_length):
yield ''.join(perm)
When combined with the hashing code above, this script generated several of the values associated with cheat codes! But neither of the two unknown ones were revealed.
The word list doesn’t have some of the strings that the known codes like: XXX
and XTC
don’t appear, for example. I put those into the word list, and…
Hash 1 | Hash 2 | Buttons
---------|----------|--------
70153315 | 00000000 | XXXTUTUXTC
Hey, it’s one of the new ones!
The “no shadows” code
This new code turns off player shadows. On PlayStation, pause the game and hold L1
while entering X, X, X, Triangle, Up, Triangle, Up, X, Triangle, Circle
.
I’m not sure why you would want to do this, but maybe it’s a leftover from debugging?
It works by setting the address at 800d61e0
from a value of 0
to 1
. The function at 800756ec
checks whether the value is nonzero and bails out early if it is — so it’s presumably shadow-related code.
Alas, the effect doesn’t work on Dreamcast. The corresponding button sequence is A, A, A, Y, Up, Y, Up, A, Y, B
(while holding L
) is recognized — it makes the screen shake and sets a flag — but your shadow persists.
The “10x score” code
There’s still one code that my dictionary attack didn’t reveal. I tried a bunch of things to try to figure it out, but still couldn’t come up with it. I enlisted help from the SegaXtreme Discord, and several people there jumped into assist.
After lots of failed attempts, I noticed that the prototype versions of THPS2 (e.g.) have shorter versions of the codes. For example, “Skater gains" weight” code is XXXXL
in the prototype and XXXXL XXXXL XXXXL
in the final game.
With that in mind, I put all of the prototype code words into my dictionary. When I tried the attack again, the answer came out!
Hash 1 | Hash 2 | Buttons
---------|----------|--------
D7DDA9E0 | 8F37160D | TXTUDUTXTUDUTXTUDU
So the code is:
PlayStation: (Holding L1) Enter “Triangle, X, Triangle, Up, Down, Up” three times
Dreamcast: (Holding L) Enter “Y, A, Y, Up, Down, Up” three times
This sequence is a repeated version of the prototype’s TXT UDU “Score times 10” code.
Here’s a video showing how things look without the cheat and then with the cheat:
On PlayStation the cheat sets the flag at 800d779c
. On Dreamcast its flag is at 8c1b5ce0
.
Outro
There were lots of contributors to this article — thanks to Vindico for identifying the effect of the “No shadow” cheat. And thanks to purist, Malenko, celeriyacc, erik, Knight0fDragon, Hassmaschine, and supahfly for contributing ideas, code, and number crunching power.
My Python code is here. I’m sure there’s a way to attack the hash function based on its lack of cryptographic strength. If you have an idea for that, do let me know.
For previous adventures in code finding, see my archive. Send me suggestions for other games to examine!
As usual I did this by comparing diffing memory snapshots and then loading one of the snapshots into Ghidra. You can easily do before-and-after memory comparisons with Cheat Engine.
The Nintendo 64 version recognizes 11 different cheat codes, all of which are already documented.