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
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–104if 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–8def 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 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
.
import reimport timefrom 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 31301Congrats! You have pulled the limited SSS-rated rarity flag-chan!STF22{W@IFU5_L@1FU5}
STF22{W@IFU5_L@1FU5}