Forensicsmedium
OakHash 2
by Pavel Knespl (capyplivl)
A slow key stretching hash: 65536 rounds of SHA-256 mixed with per-round XOR and bit rotation, salt oak-lab-v3. Password is SVIUSCG{<nature>_<pokemon>_<move>_<crc32hex>} where crc32hex is the CRC32 of nature_pokemon_move. The move must be one the Pokemon learns in gen 1, which keeps the keyspace small.
Solve
- Read
oakhash.pyto recover the round function:x = sha256(salt + p), then 65536 times XOR each of 32 bytes withcrc32(p)bytes, rotate left by 3, andx = sha256(block + p + i_le16). - The cost per guess is high, so iterate only over natures x Pokemon x the moves each Pokemon learns in gen 1, computing the embedded CRC32 for each candidate.
solve.pyreimplements the round function and walks that reduced keyspace (the name and learnset tables come fromgen1_data.py):import hashlib, zlib from gen1_data import NATURES, POKEMON, MOVES, LEARNSETS SALT = b"oak-lab-v3" def oakhash(pw, salt): raw = pw.encode() key = (zlib.crc32(raw) & 0xffffffff).to_bytes(4, "little") state = hashlib.sha256(salt + raw).digest() for r in range(65536): buf = bytearray(state) for k in range(32): buf[k] ^= key[(r + k) % 4] buf[k] = ((buf[k] << 3) | (buf[k] >> 5)) & 0xff state = hashlib.sha256(bytes(buf) + raw + r.to_bytes(2, "little")).digest() return state.hex() def candidate(nn, ppp, mmm): body = "%s_%s_%s" % (NATURES[nn], POKEMON[ppp], MOVES[mmm]) tag = zlib.crc32(body.encode()) & 0xffffffff return "SVIUSCG{%s_%08x}" % (body, tag) def main(): target = open("oak.hash").read().strip().split("$")[4] for nn in range(25): for ppp in range(1, 152): for mmm in LEARNSETS.get(ppp, ()): if not 1 <= mmm <= 165: continue guess = candidate(nn, ppp, mmm) if oakhash(guess, SALT) == target: print(guess) return main()python3 solve.py - Target
$oak$2$oak-lab-v3$59af26a7a32dd987cb1dd08d4c889c97d8145967a4a4134ae2ea89e703557d1fresolves to basequirky_eevee_tackle, CRC32780deef6.
Flag: SVIUSCG{quirky_eevee_tackle_780deef6}