TFCCTF 2022 - Crypto

TFCCTF is a CTF event hosted by The Few Chosen, a Hungarian-based CTF team. I participated as an individual team and solved all crypto challenges. The highest-point for crypto challenge was Admin Panel but Harder and Fixed, and here’s the write-up.

Admin Panel but Harder and Fixed

Challenge

This challenge service ran on 01.linux.challenges.ctf.thefewchosen.com 60711.

import os
import random

from Crypto.Cipher import AES

KEY = os.urandom(16)
PASSWORD = os.urandom(16)
FLAG = os.getenv('FLAG')

menu = """========================
1. Access Flag
2. Change Password
========================"""


def xor(bytes_first, bytes_second):
    d = b''
    for i in range(len(bytes_second)):
        d += bytes([bytes_first[i] ^ bytes_second[i]])
    return d


def decrypt(ciphertext):
    iv = ciphertext[:16]
    ct = ciphertext[16:]
    cipher = AES.new(KEY, AES.MODE_ECB)
    pt = b''
    state = iv
    for i in range(len(ct)):
        b = cipher.encrypt(state)[0]
        c = b ^ ct[i]
        pt += bytes([c])
        state = state[1:] + bytes([ct[i]])
    return pt


if __name__ == "__main__":
    while True:
        print(menu)
        option = int(input("> "))
        if option == 1:
            password = bytes.fromhex(input("Password > "))
            if password == PASSWORD:
                print(FLAG)
                exit(0)
            else:
                print("Wrong password!")
                continue
        elif option == 2:
            token = input("Token > ").strip()
            if len(token) != 64:
                print("Wrong length!")
                continue
            hex_token = bytes.fromhex(token)
            r_bytes = random.randbytes(32)
            print(f"XORing with: {r_bytes.hex()}")
            xorred = xor(r_bytes, hex_token)
            PASSWORD = decrypt(xorred)

Solution

The program offers 2 options:

  1. Guess the password
  2. Reset the password using a token

In option 2, the decrypt function processes the XOR of random.randbytes and the input. These random bytes come from Python’s built-in module, which uses the Mersenne Twister pseudorandom number generator.

$ python3
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import random
>>> help(random)

...

    General notes on the underlying Mersenne Twister core generator:
    
    * The period is 2**19937-1.
    * It is one of the most extensively tested generators in existence.
    * The random() method is implemented in C, executes in a single Python step,
      and is, therefore, threadsafe.

Here’s the source code of random.randbytes from its repository.

    ## -------------------- bytes methods ---------------------

    def randbytes(self, n):
        """Generate n random bytes."""
        return self.getrandbits(n * 8).to_bytes(n, 'little')

The random.randbytes function calls random.getrandbits, which we can predict using mt19937predictor. We need to collect at least 19937 bits of random state (in this case, 19968 bits) to predict the next random state. We can collect this state using option 2, which prints the random.randbytes value as r_bytes in hex.

After predicting the next random state, we can exploit the AES-CFB8 bug. This means the generated password will be one of 256 possible byte values. This AES-CFB8 bug works similarly to the Zerologon vulnerability or CVE-2020-1472.

Zerologon vulnerability

Implementation

#!/usr/bin/env python3
from mt19937predictor import MT19937Predictor
from pwn import *

io = remote('01.linux.challenges.ctf.thefewchosen.com', 60711)

def access_flag(password):
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Password > ', password.encode())
    return io.recvline(0).decode()

def change_password(token):
    assert len(token) == 64
    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'Token > ', token.encode())
    io.recvuntil(b'XORing with: ')
    xoring_with = io.recvline(0).decode()
    return bytes.fromhex(xoring_with)

predictor = MT19937Predictor()

for i in range(624 // 8):
    xw = change_password('0' * 64)
    state = int.from_bytes(xw, 'little')
    print(f'state {i+1}: {hex(state)}')
    predictor.setrandbits(state, 256)

state = predictor.getrandbits(256)
token = state.to_bytes(256 // 8, 'little')
xw = change_password(token.hex())
assert token.hex() == xw.hex()

for i in range(256):
    pload = bytes([i]) * 16
    flag = access_flag(pload.hex())
    if flag.startswith('TFCCTF'):
        print(flag)
        break
$ python3 solve.py
[+] Opening connection to 01.linux.challenges.ctf.thefewchosen.com on port 60711: Done
state 1: 0x29195298f5c49b2576615d2d32efed14d7a01be2e4127c98226caf051a2f9fbd
state 2: 0xfd6fe17dd278b40868ce89b89669c5ddfe6d331dab6f5a53e334212a3be375d4
state 3: 0x72ca0ac9ead9de9ec56bc1d4c33fb87ca3499642c13ad68ae8a2fad2bc9a0b1b
...
state 76: 0xc48747adf5517da734307d38f09e46cbdcdbb0c6fc18180e494b647a6e628a55
state 77: 0x38b0b0411084870b4b7953fa078ae1d6b9b089443ea2b024ae14e45871db8fa8
state 78: 0xa9bb1664193d964d655904fda332fbc0a0f6e5971c7ac2a18fda1c1297643cb1
TFCCTF{4pp4r3ntly_sp4ces_br34ks_th3_0ld_0ne}

Flag

TFCCTF{4pp4r3ntly_sp4ces_br34ks_th3_0ld_0ne}