TJCTF 2025: crypto/double-trouble
Twice the encryption, half the security.
Author: tmm
We’re given some ciphertext and the source code that genrated it.
out.txt
7125383e330c692c75e0ee0886ec7779
9ecba853742db726fb39e748a0c5cfd06b682c8f15be13bc8ba2b2304897eca2
enc.py
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import random
def gen():
myrandom = random.Random(42)
k1 = myrandom.randbytes(8)
choices = list(myrandom.randbytes(6))
k2 = b''
for _ in range(8):
k2 += bytes([ choices[random.randint(0, 3)] ])
return k1, k2
def enc(data, k1, k2, k3, k4):
key1 = k1+k2
cipher = AES.new(key1, mode=AES.MODE_ECB)
ct1 = cipher.encrypt(pad(data, 16))
key2 = k4+k3
cipher = AES.new(key2, mode=AES.MODE_ECB)
ct2 = cipher.encrypt(ct1)
return ct2
k1, k2 = gen()
k3, k4 = gen()
pt = b"example"
with open('flag.txt') as f:
flag = f.read().encode()
with open('out.txt', "w") as f:
f.write(enc(pt, k1, k2, k3, k4).hex())
f.write("\n")
f.write(enc(flag, k1, k2, k3, k4).hex())
A gen
function is used to generate two 8-byte key parts, and an enc
function encrypts a plaintext twice (using AES ECB), using 4 of the key parts.
Crucially, we’re actually given two ciphertexts, one with the known plaintext b"example"
, the other being the flag.
Hence, we can employ a meet-in-the-middle attack. If the keys are actually random though, this isn’t practical (we’d be doing operations), so let’s look at gen
…
First, gen
uses a PRNG seeded with a constant to generate the first 8-byte key:
myrandom = random.Random(42)
k1 = myrandom.randbytes(8)
…Which means the k1
and k3
passed to enc
are always the same, and we can trivially generate them. This cuts the bits in half, bringing us down to , assuming the second part is ideal.
Which, well, it isn’t. The second key part is generated by concatenating 8 random… choices among 4 constant (generated by the seeded PRNG) bytes:
choices = list(myrandom.randbytes(6))
k2 = b''
for _ in range(8):
k2 += bytes([ choices[random.randint(0, 3)] ])
We can go through all of the “keys” that can possibly be generated this way with a quick and simple loop. There’s still two keys, but operations is pretty much nothing.
Let’s do those things:
import random
import itertools
badrng = random.Random(42)
k_constant = badrng.randbytes(8)
k_choices = list(badrng.randbytes(6)[:4])
def biasedkeys():
for p in itertools.product(k_choices, repeat=8):
yield bytes(p)
Then comes the actual meet-in-the middle attack. First, encrypt the known plaintext with all possible keys.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
pt = pad(b"example", 16)
middles = dict()
for k2 in biasedkeys():
ct = AES.new(k_constant + k2, mode=AES.MODE_ECB).encrypt(pt)
middles[ct] = k2
And second, decrypt the ciphertext with all possible keys until we hit one which we’ve figured out as a middle:
key1 = b""
key2 = b""
ct2 = bytes.fromhex("7125383e330c692c75e0ee0886ec7779")
for k4 in biasedkeys():
ct = AES.new(k4 + k_constant, mode=AES.MODE_ECB).decrypt(ct2)
if (v := middles.get(ct)) is not None:
key1 = k_constant + v
key2 = k4 + k_constant
Now that we’ve got the keys, decrypt and win!
flagct = bytes.fromhex("9ecba853742db726fb39e748a0c5cfd06b682c8f15be13bc8ba2b2304897eca2")
aes2 = AES.new(key2, mode=AES.MODE_ECB)
aes1 = AES.new(key1, mode=AES.MODE_ECB)
flagpt = aes1.decrypt(aes2.decrypt(flagct))
print(flagpt)
Flag: tjctf{m33t_in_th3_middl3}
Home About Contact