Old Vault [RE]

8 minute read

Reverse engineering PS2 game program to find correct password.

Challenge Description

I found this old cd rom in the basement, my father said its a vault he couldn’t open and contains a treasure of some kind.

zip pass: wgmy2022

vault.zip

Solution

Unzipping the file gives us two other files: vault.bin and vault.cue. Because I’m using a mac, I followed this to convert the two files into an ISO file to mount it and see what’s inside.

bchunk -w vault.bin vault.cue vault

This should produce vault.iso. After mounting it, there are two files PROGRAM.EXE and SYSTEM.CNF.

I ran a quick strings command on the exe and found the keyword “Playstation” from one of the strings and immediately guessed that this is a PSX program. I prepped Ghidra by installing this plugin. It required Ghidra 10.1.5, so I also installed that specific version.

Opening the EXE now with Ghidra, I navigated to start and started scanning through the functions it calls. They all did something, but doesn’t seem like much, except for the last call to a function at 0x80010020, which has a few while (true) loops (perhaps the game cycle?). I renamed this as main.

I also renamed some functions called here
I also renamed some functions called here

The setup function just has a lot of calls to SDK functions which were named clearly (with “init” or “setup”), hence named setup. Similary, draw_main had calls to DrawPrim and some user-defined strings, which I later found was drawn onto the screen.

take_input and proc_main are the two functions that we really need to look into. First take_input, which reads the input from PadRead(0), which upon googling, is available in the PS1 libraries. See here: https://www.retroreversing.com/ps1-libs.

More renamings
More renamings

For the value mapping of each key, I just googled the function name (PadRead) with its library (LIBETC.h) and found this file on github. Combining that with the value of _code, we get the following mapping of _code to pad key:

keymap = {
    0: "LUp",
    1: "LDown",
    2: "LLeft",
    3: "LRight",

    4: "RUp",
    5: "RRight",
    6: "RDown",
    7: "RLeft",
}

There are a bunch of code after what is shown in the screenshot, but after wasting 20 minutes on it, I figured it was only shifting the inputs after we input beyond the input size (of 8, we will see later).

Now we go to proc_main, which supposedly checks the input and gives us the flag if we input correctly.

You can ignore most of the renamed functions and look at only the last one cmp. It is quite easy to see that it does a per-byte comparison, and returns 0 if they are the same (similar to C’s strcmp). The third argument 0x14 indicates the length in bytes.

Looking into init, we see an initialization of values into the array variable.

A wise man once told me one should google the magic numbers before doing anything else. So I googled 0x67452301 and found that it is used in SHA1, and, in fact, the other numbers all matched. So we get to just assume that shuffle which is called after init is just SHA1. You can go and see shuffle, it would be a nightmare to understand it from just the decompilation. I ignored the third function and just assumed it is part of SHA1 (which it is, see here).

Looking back at proc_main, we can quickly figure that it is doing the below comparison:

sha1(sha1(input) ^ xbox) == local_24;

From the first call to SHA1, we see that the input size is 8 (bytes). Which corresponds to the values from take_input earlier. Each byte can only have 8 possible values, so there are 8^8 = 2^24 ≈ 1.6E7 combinations in total. We can bruteforce this.

I wrote a python script to find the correct key combination.

The output: RUp, RLeft, LUp, RDown, LRight, LUp, RDown, RLeft

As it was running, I installed PCSX2 on my Windows machine, which also required a PS2 bios to run. I don’t know why I installed a PS2 emulator instead of a PS1, but it worked out I guess. We need to configure the keymappings to allow for keyboard input.

I only configured the two pads
I only configured the two pads

Booting the ISO up, and pressing the keys accordingly.

Nice
Nice

Final Script

import hashlib
from tqdm import tqdm

keymap = {
    0: "LUp",
    1: "LDown",
    2: "LLeft",
    3: "LRight",

    4: "RUp",
    5: "RRight",
    6: "RDown",
    7: "RLeft",
}

xbox = [ 0x52, 0x07, 0xca, 0x72, 0xc8, 0xc3, 0xe6, 0x37, 0x96, 0x68, 0x7c, 0x4f, 0x03, 0x6e, 0xef, 0x2b, 0xab, 0x84, 0xfc, 0xc1, 0x13, 0x50, 0x97, 0x6a, 0x82, 0xc5, 0xb5, 0x64, 0xc0, 0x3f, 0x29, 0x47, 0x58, 0x6e, 0xeb, 0x2a, 0xa9, 0xcd, 0x99, 0x21, 0x13, 0x57, 0x03, 0x06, 0x71, 0x26, 0x94, 0x0b, 0xfc, 0x23, 0x25, 0xbb, 0x7a, 0x16, 0xf5, 0x58, 0x2b, 0x97, 0x4d, 0x8d, 0x57, 0xa3, 0xe7, 0x72, 0x0d, 0xea, 0xe8, 0x37, 0x38, 0x37, 0x41, 0xcf, 0xf6, 0x4a, 0xd9, 0xf4, 0xef, 0x03, 0xe9, 0x19, 0x4b, 0x6f, 0x3b, 0xae, 0xc9, 0xde, 0x0c, 0x23, 0xb4, 0x0b, 0x2d, 0x23, 0xb2, 0xfe, 0x7d, 0x40, 0xd3, 0x4f, 0x55, 0xe5, 0x0f, 0x7b, 0x1f, 0x9a, 0x45, 0x02, 0xe0, 0xcf, 0x70, 0x9f, 0xc9, 0xb7, 0xae, 0xb2, 0x21, 0x9c, 0x17, 0xbb, 0x71, 0xf1, 0x83, 0x87, 0x33, 0x36, 0x81, 0x76, 0x84, 0x3b, 0x8c, 0xd3, 0x35, 0x8b, 0xea, 0xa6, 0xc5, 0x68, 0x9b, 0x67, 0x5d, 0x5d, 0xc7, 0x13, 0x97, 0xa2, 0x9d, 0x3a, 0xd8, 0x67, 0xc8, 0xe7, 0x51, 0x9f, 0x66, 0x9a, 0x29, 0x04, 0x5f, 0xd3, 0xf9, 0xe9, 0xfb, 0xdf, 0x2b, 0xde, 0x59, 0x2e, 0xfc, 0x53, 0x44, 0xbb, 0x3d, 0xf3, 0x22, 0x0e, 0x4d, 0x90, 0x43, 0x1f, 0x25, 0xb5, 0xbf, 0xab, 0x0f, 0xca, 0xd5, 0x52, 0xd0, 0xff, 0x00, 0x4f, 0xd9, 0x87, 0x5e, 0xc2, 0x31, 0xac, 0x87, 0x0b, 0x41, 0xc1, 0x33, 0xf7, 0x23, 0x66, 0x11, 0xc6, 0x74, 0x6b, 0x1c, 0x83, 0x05, 0x5b, 0x9a, 0xf6, 0xd5, 0xb8, 0x0b, 0x37, 0x2d, 0x2d, 0x77, 0x43, 0x87, 0xd2, 0x2d, 0x8a, 0xc8, 0x97, 0x58, 0x97, 0x21, 0x6f, 0x16, 0x2a, 0x39, 0x54, 0xcf, 0xa3, 0xc9, 0xb9, 0x6b, 0x0f, 0xdb, 0x0e, 0xe9, 0x7e, 0xec, 0x83, 0xd4, 0x6b, 0x0d, 0xc3, 0xd2, 0x5e, 0x5d, 0xa0, 0x33, 0xef, 0x35, 0x85, 0x2f, 0x1b, 0x3f, 0xfa, 0x65, 0xa2, 0xc0, 0x2f, 0x90, 0xff, 0xa9, 0x57, 0xce, 0x12, 0x01, 0xfc, 0x77, 0xdb, 0x51, 0x91, 0xa3, 0x27, 0xd3, 0x96, 0xa1, 0x16, 0x64, 0x9b, 0xac, 0x33, 0x15, 0x2b, 0x0a, 0x06, 0x25, 0xc8, 0xfb, 0x87, 0x3d, 0xfd, 0xe7, 0xb3, 0xb7, 0x02, 0xbd, 0xda, 0xb8, 0xc7, 0xe8, 0x47, 0x31, 0x3f, 0x86, 0x7a, 0x09, 0x64, 0xbf, 0x73, 0xd9, 0x89, 0x9b, 0x7f, 0xcb, 0x3e, 0x79, 0xce, 0xdc, 0xb3, 0x64, 0x1b, 0x1d, 0x93, 0x42, 0xee, 0xad, 0xf0, 0xa3, 0xbf, 0x05, 0x55, 0x5f, 0x4b, 0x2f, 0x2a, 0xf5, 0xf2, 0xb0, 0x5f, 0x20, 0xaf, 0xb9, 0x27, 0x7e, 0xa2, 0x11, 0x0c, 0xe7, 0xab, 0x21, 0x61, 0xd3, 0x97, 0xc3, 0xc6, 0x31, 0x66, 0x54, 0xcb, 0x3c, 0xe3, 0xe5, 0xfb, 0xba, 0x56, 0x35, 0x18, 0x6b, 0x57, 0x0d, 0xcd, 0x17, 0xe3, 0xa7, 0x32, 0xcd, 0x2a, 0xa8, 0xf7, 0x78, 0xf7, 0x01, 0x0f, 0x36, 0x8a, 0x19, 0xb4, 0x2f, 0xc3, 0xa9, 0x59, 0x0b, 0xaf, 0xfb, 0x6e, 0x89, 0x1e, 0xcc, 0xe3, 0xf4, 0xcb, 0xed, 0x63, 0xf2, 0x3e, 0xbd, 0x00, 0x13, 0x0f, 0x15, 0x25, 0xcf, 0xbb, 0xdf, 0x5a, 0x05, 0x42, 0xa0, 0x8f, 0xb0, 0x5f, 0x89, 0xf7, 0xee, 0xf2, 0x61, 0x5c, 0x57, 0x7b, 0x31, 0x31, 0x43, 0xc7, 0xf3, 0xf6, 0x41, 0xb6, 0x44, 0xfb, 0xcc, 0x93, 0xf5, 0xcb, 0x2a, 0xe6, 0x05, 0x28, 0xdb, 0x27, 0x1d, 0x9d, 0x87, 0x53, 0x57, 0x62, 0x5d, 0x7a, 0x98, 0x27, 0x08, 0xa7, 0x11, 0xdf, 0xa6, 0xda, 0x69, 0xc4, 0x9f, 0x93, 0xb9, 0x29, 0xbb, 0x1f, 0xeb, 0x9e, 0x19, 0x6e, 0xbc, 0x13, 0x84, 0x7b, 0xfd, 0x33, 0x62, 0x4e, 0x8d, 0x50, 0x83, 0xdf, 0xe5, 0xf5, 0x7f, 0xeb, 0xcf, 0x8a, 0x95, 0x92, 0x90, 0xbf, 0x40, 0x0f, 0x99, 0xc7 ]
assert(len(xbox) == 0x200)

t = 0x8047435a4236b560d4f114390dabde1ef7aa5be7.to_bytes(0x14, 'little')
assert(t[0] == 0xe7)

for inp in tqdm(range(8**8)):
    vals = [(inp // (8**i)) % 8 for i in range(8)]
    h = hashlib.sha1(bytes(vals)).digest()
    inter = [xbox[i] ^ h[i % len(h)] for i in range(0x200)]
    h = hashlib.sha1(bytes(inter)).digest()

    if h[:len(t)] == t:
        print(", ".join(list(map(lambda x : keymap[x], vals))))
        break

The solution script takes around 12 minutes on my laptop.

Flag: wgmy{6b5dde1c322f4e1600a101f23098363d}