Introduction
Confusion was a crypto CTF from Srdnlen CTF 2025.
#!/usr/bin/env python3
from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadimport os
# Local importsFLAG = os.getenv("FLAG", "srdnlen{REDACTED}").encode()
# Server encryption functiondef encrypt(msg, key): pad_msg = pad(msg, 16) blocks = [os.urandom(16)] + [pad_msg[i:i + 16] for i in range(0, len(pad_msg), 16)]
b = [blocks[0]] for i in range(len(blocks) - 1): tmp = AES.new(key, AES.MODE_ECB).encrypt(blocks[i + 1]) b += [bytes(j ^ k for j, k in zip(tmp, blocks[i]))]
c = [blocks[0]] for i in range(len(blocks) - 1): c += [AES.new(key, AES.MODE_ECB).decrypt(b[i + 1])]
ct = [blocks[0]] for i in range(len(blocks) - 1): tmp = AES.new(key, AES.MODE_ECB).encrypt(c[i + 1]) ct += [bytes(j ^ k for j, k in zip(tmp, c[i]))]
return b"".join(ct)
KEY = os.urandom(32)
print("Let's try to make it confusing")flag = encrypt(FLAG, KEY).hex()print(f"|\n| flag = {flag}")
while True: print("|\n| ~ Want to encrypt something?") msg = bytes.fromhex(input("|\n| > (hex) "))
plaintext = pad(msg + FLAG, 16) ciphertext = encrypt(plaintext, KEY)
print("|\n| ~ Here is your encryption:") print(f"|\n| {ciphertext.hex()}")The challenge acts as an encryption oracle in 3 steps:
Where is our input message, padded, split into blocks and prefixed with the random block , meanwhile and are AES decryption and encryption. Notice how since .
Solution
Encryption utlity function:
# encrypt and return the nth blockdef encrypt(r, msg: bytes, block: int = -1): r.sendlineafter(b'x) ', msg.hex().encode()) r.recvuntil(b'n:\n|\n| ') ct = bytes.fromhex(r.recvline().rstrip().decode()) if 0 <= block < len(ct) // 16: return ct[16*block:16*(block + 1)] return ctSince the flag is appended to the end of our input, we can recover the first block with a simple chosen-prefix ECB attack, which I’m doing using my library cryptils.
With dec0 we can calculate a decryption of a 16 long bytestring of zeros, which I’ll use to recover the rest of the flag:
def dec0(r): msg1 = os.urandom(16) enc_msg1 = encrypt(r, msg1, 1) msg2 = os.urandom(16) enc_msg2 = encrypt(r, msg2, 1) val = xor(enc_msg2, msg1)
ct3 = encrypt(r, enc_msg1 + msg1 + msg2, 3)
return xor(ct3, val)Also notice how the second block the oracle gives us is a plain encryption of the first block of input.
Let’s call the output of dec0 simply and set to be the th block of the flag, with being the random block at the start, we can write each block of the flag’s ciphertext we received at the start as:
Let’s take a look at the fourth block after asking the oracle to encrypt :
Let’s then generate a random block and ask for the encryption of :
We know both and and can therefore recover . The process can then be repeated for successive blocks:
def main(): r = remote('confusion.challs.srdnlen.it', 1338) if args.REMOTE else process('./chall.py')
r.recvuntil(b' = ') ct_flag = blockify(bytes.fromhex(r.recvline().rstrip().decode()))
D0 = dec0(r)
flag = chosen_prefix(lambda b: encrypt(r, b, 1), string.printable, length=16) curr, prev = flag, ct_flag[0]
for i in range(2, len(ct_flag)): ct3 = encrypt(r, prev + curr + D0, 3) enc_next = xor(ct_flag[i], ct3)
msg = os.urandom(16) enc_msg = encrypt(r, msg, 1) enc = encrypt(r, enc_next + D0 + msg, 3)
prev = curr curr = xor(enc, xor(enc_msg, D0))
flag += curr
print('flag:', unpad(flag, 16).decode())
r.close()flag: