$ cat writeup.md…
$ cat writeup.md…
metactf
Task: a custom stream cipher XORs the flag against a deterministic keystream whose only random input is a single byte from os.urandom(1). Solution: brute-force all 256 possible seeds and select the candidate whose decryption starts with the known prefix DawgCTF{.
I heard something about streaming before in my crypto book, idk what twitch has to do with crypto, but I decided to add my favorite brainrot to it.
Files provided:
chall.py — encryption scriptoutput.txt — ciphertext as hexSource: https://github.com/UMBCCyberDawgs/dawgctf-sp26/tree/main/Six%20Seven
from Crypto.Util.strxor import strxor from secrets import flag import os assert flag[:8] == b"DawgCTF{" def gen(start): return (((6 * 7) * (start - 6) * 7) + ((start * 6) - 7) * (start ^ 6)) % 255 def encrypt(message): start = os.urandom(1); key = start; for i in range(1, len(message)): key += gen(key[i-1]).to_bytes(1, "big") return strxor(key, message) ct = encrypt(flag) print("ct =", ct.hex())
Ciphertext (67 bytes):
9f2eadbd998e9ca1aab6bfbba9bf85afa9bf85a9bfb9a8bfaea985b3b485a3b5afa885a9aea8bfbbb785b9b3aab2bfa8a985ece3b8bcbfebe3eebbbceee9bceab9bea7
The custom stream cipher draws exactly one byte of entropy from os.urandom(1) as the seed. Every subsequent keystream byte is fully determined by the recurrence key[i] = gen(key[i-1]). Because the seed has only 256 possible values, the entire keystream has only 256 possible instantiations. We brute-force all of them and use the known plaintext prefix DawgCTF{ to recognise the correct one.
Reading chall.py:
gen is the brainrot joke — it packs "six" and "seven" (6, 7, 42) into an arithmetic expression and reduces mod 255. It is deterministic.encrypt seeds the key with one random byte and then chains gen to produce as many keystream bytes as the plaintext has.strxor.The cryptographic weakness is immediate: the random seed space is 2^8 = 256, not 2^128 or similar. No matter how complex gen looks, the total entropy of the keystream cannot exceed that of the seed.
A quick sanity check on plaintext length: the ciphertext is 67 bytes, which matches a typical DawgCTF{...} flag length with a plausible body.
Two independent design flaws collapse to the same break:
assert flag[:8] == b"DawgCTF{" guarantees eight known plaintext bytes at offset 0. These let us reject 255 of the 256 candidates without any human inspection.Combining the two, the attack is purely offline and runs in microseconds.
As an aside, the plaintext itself turns out to be the lesson: use secrets (or at least enough entropy) for cryptographic seeds, never a single os.urandom(1) byte.
Full solver:
#!/usr/bin/env python3 """ Six Seven - DawgCTF SP26 (crypto) The stream cipher uses a single random byte `start` as the seed. All subsequent keystream bytes are deterministic: key[i] = gen(key[i-1]) Since `start` is only 1 byte, there are only 256 possible keystreams. Brute force all 256 and check for known plaintext prefix "DawgCTF{". """ from Crypto.Util.strxor import strxor def gen(start): return (((6 * 7) * (start - 6) * 7) + ((start * 6) - 7) * (start ^ 6)) % 255 def main(): ct = bytes.fromhex( "9f2eadbd998e9ca1aab6bfbba9bf85afa9bf85a9bfb9a8bfaea985b3b485a3b5afa88" "5a9aea8bfbbb785b9b3aab2bfa8a985ece3b8bcbfebe3eebbbceee9bceab9bea7" ) for start in range(256): key = bytes([start]) for i in range(1, len(ct)): key += gen(key[i - 1]).to_bytes(1, "big") pt = strxor(key, ct) if pt[:8] == b"DawgCTF{": print(f"[+] start byte = {start}") print(f"[+] flag = {pt.decode()}") return if __name__ == "__main__": main()
Output:
[+] start byte = 219
[+] flag = DawgCTF{REDACTED}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar