Clouds picoCTF 2021 Solution

Published: April 2, 2026

Description

Recover the AES key from the provided files. The key generation has a weakness that makes it recoverable.

Download the provided files (encrypted data, possibly key generation code).

bash
wget https://mercury.picoctf.net/static/.../clouds.tar.gz && tar xvf clouds.tar.gz
  1. Step 1Extract the tarball and identify the ciphertext
    Unpack the tarball. Typically you'll get a key generation script (.py) plus an encrypted file. Skim the script for how the key is built and check the file timestamps as a seed-window hint.
    bash
    tar -xzf clouds.tar.gz
    bash
    ls -la clouds/
    bash
    stat clouds.tar.gz             # mtime hints at the seed window
    bash
    less clouds/*.py                # how was the key generated?
    Learn more

    Why the timestamp matters. If the script seeds Python's random module with time.time() at key-generation time, you only need to scan timestamps within the window when the challenge files were created. stat on the tarball gives you the upload time; the picoCTF release calendar gives you the season. Together that's usually a few-million-second window, not a year.

  2. Step 2Analyze the key generation
    Skim the script for the weakness: time-seeded RNG (predictable seed), short effective key length, reused IV, or a padding oracle. The fix you'll exploit follows directly from which one you spot.
    bash
    less clouds/*.py
    bash
    grep -nE 'random|seed|time' clouds/*.py
    Learn more

    Common AES key generation weaknesses:

    • Time-seeded PRNG: If the key was generated using random.seed(int(time.time())), the seed is the Unix timestamp at generation time. If you know approximately when the challenge was created, you can try all timestamps in a small window.
    • Short key derived from password: If the key is MD5 or SHA1 of a password, dictionary attacks may work.
    • ECB mode: AES in ECB mode encrypts identical blocks identically. Pattern analysis reveals information about the plaintext without needing the key.
    • Padding oracle: If the server reports decryption errors differently for padding errors vs. authentication errors, a padding oracle attack can decrypt the ciphertext byte by byte.
  3. Step 3Exploit the identified weakness
    Based on the weakness found, craft the appropriate attack. For a time-based PRNG, try timestamps in a range. For a padding oracle, use the pycryptodome PaddingOracle helper.
    python
    python3 - <<'EOF'
    # Example: time-seeded PRNG key recovery
    import time
    import random
    from Crypto.Cipher import AES
    
    ciphertext = bytes.fromhex('PASTE_CIPHERTEXT_HEX')
    
    # Try timestamps from a reasonable window around challenge creation
    start_time = int(time.time()) - 86400 * 365  # 1 year ago
    end_time = int(time.time())
    
    for seed in range(start_time, end_time):
        random.seed(seed)
        key = bytes([random.randint(0, 255) for _ in range(16)])
        try:
            cipher = AES.new(key, AES.MODE_CBC, iv=ciphertext[:16])
            pt = cipher.decrypt(ciphertext[16:])
            if b'picoCTF' in pt:
                print(f"Seed: {seed}, Flag: {pt.decode(errors='replace')}")
                break
        except Exception:
            pass
    EOF
    Learn more

    Why time-seeded PRNGs are a death sentence. Python's random module uses the Mersenne Twister, a fast non-cryptographic PRNG. Once seeded, every output is deterministic. If the seed is the Unix timestamp at key-generation time (a common bug), the keyspace collapses from 2^128 (a real AES-128 key) to roughly 2^25 (the number of seconds in a year). That is 33 million possibilities - a few minutes of wall-clock work on a laptop.

    Worked sketch. If the challenge tarball was uploaded at 2021-03-15 12:00:00 UTC (Unix epoch 1615809600), and the key was generated with random.seed(int(time.time())) sometime in the preceding year, you only need to scan ~31.5 million seeds. For each seed:

    for seed in range(start_epoch, end_epoch):
        random.seed(seed)
        key = bytes(random.randint(0, 255) for _ in range(16))
        iv  = ciphertext[:16]
        pt  = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext[16:])
        if b'picoCTF' in pt:
            print(seed, pt)
            break

    Mersenne Twister state recovery (when seed brute-force isn't the right tool). If the script doesn't expose time.time() as a seed but instead leaks PRNG outputs - e.g. it prints a "debug" random number alongside the ciphertext, or uses random for both a public nonce and the key - you can collect 624 consecutive 32-bit outputs and reconstruct the entire internal state. randcrack does this in a few lines:

    from randcrack import RandCrack
    rc = RandCrack()
    for n in observed_outputs[:624]:    # 624 32-bit ints from random.getrandbits(32)
        rc.submit(n)
    # Now rc.predict_*() returns the same values random would
    next_key_byte = rc.predict_getrandbits(8)

    Use the seed-bruteforce path when only the seed is broken. Use randcrack when the script leaks enough PRNG outputs to seed the state machine directly. For this challenge, check the script first: if it prints anything random-looking before the ciphertext, randcrack is the cleaner attack.

    The real fix.

    • os.urandom(16) reads from the kernel CSPRNG (/dev/urandom, ChaCha20 or Fortuna under the hood) - not seeded by anything attacker-observable.
    • secrets.token_bytes(16) (Python 3.6+) is a thin wrapper around os.urandom with a name that makes its purpose obvious.
    • Never use random.seed(time.time()), random.seed(getpid()), or any seed derived from process state for security-sensitive randomness.

    Real-world lesson. In 2008, Debian shipped an OpenSSL patch that accidentally reduced the seed entropy to 15 bits. SSH keys generated on those systems for two years were brute-forceable in seconds. The same lesson applies here: AES is unbreakable, but only if the key is. For more on AES modes and the surrounding key-handling pitfalls, see AES for CTF; for the Python plumbing, see Python for CTF.

Flag

picoCTF{...}

AES security depends entirely on key generation quality - a time-seeded PRNG or other weak key generation method makes even AES trivially breakable by reconstructing the PRNG state.

Want more picoCTF 2021 writeups?

Useful tools for Cryptography

Related reading

What to try next