Hacking
Writeups
Crypto
jagacha

jagacha

Description

CTF: STACK the Flags 2022

As a wise man once said, there is no greater happiness in life than to hit a homerun with your SSS-tier gacha pulls on the first try. However, reality is not that easy. Therefore, we are providing a chance for you to pull the SSS-rarity flag if you can guess the number correctly. However, if you are still unsure of your chances of guessing it right, you're always welcome to do a random gacha pull until you change your mind.

Note: Submit the flag in all caps.


jagacha.py

config.py

Solution

Pwned by @skytect (opens in a new tab)

Looking through jagacha.py, you may realise that option 1 generates and shows you the next 64 random bits, while option 2 allows you to guess the next 64 random bits for the flag.

# jagacha.py, L75–104
if option == 1:
    num = rand.getrandbits(64)
    gacha = GACHA_KEYS[num % len(GACHA_KEYS)]
    writer.writelines(
        (
            f"Congrats! You have pulled a {gacha}!\n".encode(),
            GACHAS[gacha],
            b"Here are the stats of your character:\n",
            f"STR: {num>>48 & 0xffff}\n".encode(),
            f"DEX: {num>>32 & 0xffff}\n".encode(),
            f"INT: {num>>16 & 0xffff}\n".encode(),
            f"LUK: {num & 0xffff}\n".encode(),
            b'\n',
        )
    )
elif option == 2:
    num = rand.getrandbits(64)
    lucky_number = await read_number(
        reader, writer, b"Enter your lucky number: "
    )
    if lucky_number == num:
        writer.writelines((
            b"Congrats! You have pulled the limited SSS-rated rarity flag-chan!\n",
            FLAG,
        ))
        option = 3  # Quit
    else:
        writer.write(
            b"Oops! Looks like you are not as lucky as you thought! Try again!\n\n"
        )

Next let's see how the seed is generated. Unfortunately, config.py shows us that we can't really determine the seed as it's generated by the nanosecond.

# config.py, L6–8
def get_seed():
    SEED = 0xdeadc0de
    return SEED + time.time_ns()

However, we can still use existing knowledge to determine the state of the random generator. Python uses the Mersenne Twister (opens in a new tab) algorithm to generate random numbers, which shifts its state every 624 32-bit numbers. So, we can crack it with that many bits.

I wrote a solve script below using randcrack (opens in a new tab).

import re
import time
from pwn import *
from randcrack import RandCrack
 
 
conn = remote('157.245.52.169', '31301')
conn.recvuntil(b'> ')
 
def get_64_bits() -> int:
    conn.sendline(b'1')
    r = conn.recvuntil(b'> ').decode()
    m = re.findall(r'Here are the stats of your character:\nSTR: (\d+)\nDEX: (\d+)\nINT: (\d+)\nLUK: (\d+)\n', r)
    n = [int(s) for s in m[0]]
    return n[0] << 48 | n[1] << 32 | n[2] << 16 | n[3]
 
def submit_flag(n: int) -> str:
    conn.sendline(b'2')
    r = conn.recvuntil(b': ')
    conn.sendline(str(n).encode())
    r = conn.recvall().decode()
    return r
 
rc = RandCrack()
for i in range(624 // 2):
    b = get_64_bits()
    rc.submit(b & 0xffffffff)
    rc.submit((b >> 32) & 0xffffffff)
 
r = submit_flag(rc.predict_getrandbits(64))
print(r)

Running the script gives us the flag.

[x] Opening connection to 157.245.52.169 on port 31301
[x] Opening connection to 157.245.52.169 on port 31301: Trying 157.245.52.169
[+] Opening connection to 157.245.52.169 on port 31301: Done
[x] Receiving all data
[x] Receiving all data: 0B
[x] Receiving all data: 86B
[+] Receiving all data: Done (86B)
[*] Closed connection to 157.245.52.169 port 31301
Congrats! You have pulled the limited SSS-rated rarity flag-chan!
STF22{W@IFU5_L@1FU5}

STF22{W@IFU5_L@1FU5}