Under the microscope: Medal of Honor: Rising Sun (PlayStation 2, GameCube, Xbox)
When meet-in-the-middle isn't enough
In this edition, we’re examining Medal of Honor: Rising Sun, the 2003 game for PlayStation 2, GameCube, and Xbox from EA Los Angeles.
The different versions of this game recognize dozens of different special passwords. Each one enables a special cheat effect. Despite the game having been out for decades, no comprehensive list has been published.
I’m here to remedy that – below are details on how we can derive the passwords that the game accepts from the obfuscated data that’s on each version’s disc.
Generating collisions
What happens when you enter something into the password screen on Medal of Honor: Rising Sun?
The first thing that reads your input is a function that computes a variant of the djb2 hash. When expressed as Python, its logic looks like this:
def get_hash(data):
val = 0xFFFFFFFF
for d in data:
val = ((val * 0x21) + d) & 0xFFFFFFFF
return valThe game is hashing your password input because it doesn’t store a real list of valid passwords. If it did, you could search the game data and get all of the passwords easily. Instead, it compares the hash of your password input to a list of hashes of valid passwords. Here’s Ghidra’s decompilation of the Xbox version’s code:
int check_password_001235d0(void) {
uint32_t djb2_hash;
djb2_hash = get_hash_0008d960();
if (djb2_hash == 0x674492f0) {
apply_cheat_000a0900();
return 1;
}
if (djb2_hash == 0xf8568d52) {
apply_cheat_0009f120();
return 1;
}
/* A lot more checks... */Can we derive the passwords from these hashes? Yes. In fact, we did something very similar for Medal of Honor: Underground on PlayStation a while back.
That game used the CRC-32 algorithm to obscure its passwords. Because of that algorithm’s properties, we were able to do a meet-in-the-middle attack. Can we do the same thing for this game?
If we ignore the AND operation for a moment, the djb2 hash update step above looks like this:
new_val = (old_val * 0x21) + char_codeIf we pretend like we’re in high school algebra, we can solve for old_val:
old_val = (new_val - char_code) / 0x21Division isn’t quite the right operation here because of the AND operation. Because we’re dealing with integer values that wrap around after 32-bits, we need the multiplicative inverse of 0x21 (mod 2^32).
Since that inverse is 0x3e0f83e1, we can get old_val like this:
old_val = ((new_val - char_code) * 0x3e0f83e1) & 0xffffffffThis operation allows us to undo the addition of a character to a hash. That means that we can use a meet-in-the-middle attack like we did for Medal of Honor: Underground. The steps are:
Generate all of the strings for half of the needed length.
Loop through all of the strings and compute their hashes.
Loop through all of the strings again and undo the addition of each character.
If, after all of the undos, we have one of the hashes from the first loop, we’ve got a match.
Following this process yields passwords for all of the hashes. Here are some examples:
| Hash | System | Password |
|----------|--------|-----------|
| 50d56df8 | PS2 | AURIGA |
| cbd17f55 | GC | ZFBDIQV |
| 674492f0 | XB | TUSKFISH |
| bb2534b0 | PS2 | IKFCPNV |
| 896b7366 | GC | BALLOON |AURIGA is the PlayStation 2 equivalent of the TUSKFISH code that’s listed on cheat sites for Xbox. It unlocks all missions, multiplayer skins, and special features.
This string must be what the developers intended for people to enter. C. Auriga is a type of butterfly fish, so the code fits in with the others – most of them have something to do with the names of fish.
ZFBDIQV is a GameCube equivalent of this code. It works, but it’s almost certainly not what the developers intended – it’s just a string whose hash happens to match the correct one’s. AHAMAVQW and BAULCAXV work just as well, but I don’t think they’re right either:
>>> hex(get_hash('ZFBDIQV'.encode()))
'0xcbd17f55'
>>> hex(get_hash('AHAMAVQW'.encode()))
'0xcbd17f55'
>>> hex(get_hash('BAULCAXV'.encode()))
'0xcbd17f55'ZFBDIQV is the smallest collision, and the only one with length 7. There are 54 with length 8 – a small enough number to check over without special tools:
YGRAAFJR MGZGANND AHAMAVQW RVLQBNKV FVTWBVOH NAMFBZUC BAULCAXV PYFKCJCL GPGVDAVG
QRZJDVJK TISOEEYT OUTDERBA RLMIFAQJ YXEYFEWE ICNTFZCE DOOIGELS BRICHADI EIBHHQSR
VWMLIIMQ JWURIQQC TZGFJEEG HZOLJMHZ DEPAJYRG ZGZPKEBY NHAVKMFK ITBKKYOY OAUULYMJ
JMVJMDVX YDGIMMAN WGACNHZD RSAYNUBR IJCCOLUM ZXNGPDOL EOWXQDDZ TFHWQLPP RIBQRHHF
FIJWRPKY MUCFRTQT DLDQSLCO SBVPSTOE XQHZTTLW EEXPTXJN VTBTUPDM CHRJUTBD RYWHWGTZ
ZDOXWKZU UPPMWXCB XGIRXGRK VJCLYCJA JJKRYKMT QVDAYOSO EVLGYWWA HMELZGEJ CYFAZSNXOnly a few of these are plausibly words: OUTDERBA, DOOIGELS, BRICHADI. Some searching suggests that the real answer is probably BRICHADI – it seems to be a misspelling of brichardi, a type of cichlid fish:

BALLOON is a new code for GameCube that lets you skip your current mission. A similar process to the one above reveals the PlayStation 2 equivalent to be BANGGHAI – this is a nonstandard spelling of Banggai, the name of another type of fish. The Xbox variant is at least spelled correctly – it’s LEOPARD, for the leopardfish.
Here is the full set of codes for all three systems – presented together for the first time:
You can get the script I used to generate the hash collisions from Github.
The script also generates a bunch of nonsense passwords that unlock skins for multiplayer mode. As far as I can tell, these aren’t intended to have used fish names, as their hashes differ only in the last byte.
Outro
Thanks for reading Rings of Saturn. If you liked this article, check out these from the archive – each involves cracking some sort of cheat code obfuscation scheme:
I’ll be back with more retro game reverse engineering analysis soon. Subscribe to get the next article as soon as it’s published.





