Confusion
by vympel · 17 Jul 2026
Introduction
Confusion was a crypto CTF from Srdnlen CTF 2025.
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
# Local imports
FLAG = os.getenv("FLAG", "srdnlen{REDACTED}").encode()
# Server encryption function
def 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 block
def 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 ct
Since 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: srdnlen{I_h0p3_th15_Gl4ss_0f_M1rt0_w4rm3d_y0u_3n0ugh}