TFCCTF 2022 - Crypto

TFCCTF is a CTF event hosted by a Hungarian-based CTF Team called The Few Chosen. I participated as an individual team and managed to solve all crypto challenges. There is one challenge called Admin Panel but Harder and Fixed which had the highest point among all crypto challenges. Here’s the write-up. Enjoy!

Admin Panel but Harder and Fixed

Challenge

This challenge was running 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 gives us two options:

  1. Guess password
  2. Reset password from token

In the 2nd option, the decrypt function is called, where random.randbytes xor input is passed as an argument. These random bytes are generated by python’s buildin module, which is implemented using Mersenne Twister pseudorandom.

$ 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 repo.

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

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

As you can see, random.randbytes calls random.getrandbits, which we can predict the random value from some known states by using mt19937predictor. The idea is that we need to collect at least 19937-bit random state generated by random.randbytes (in this case, we’re collecting 19968-bit random state), so that our predictor can guess the next random state generated by random.randbytes. We can obtain this random state by using 2nd option, which will print the value of random.randbytes as r_bytes in hex.

After successfully guessed the next random state, now we can take the advantage of the AES-CFB8 bug. Therefore, the generated password will be the same set of bytes (256 possibilities). This AES-CFB8 bug can be exploited in the same way as 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}