Skip to content
cryptoSrdnlen2025479 words · 3 min read

Looks like our cryptographers had one too many glasses of mirto! Can you sober up their sloppy AES scheme, or will the confusion keep you spinning?

#crypto

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:

  1. b0:=Rbi:=E(mi)mi1i1\quad b_0 := R \\\quad b_i := E(m_i) \oplus m_{i-1} \quad i \ge 1
  2. c0:=Rci:=D(bi)i1\quad c_0 := R \\\quad c_i := D(b_i) \quad i \ge 1
  3. ct0:=Rcti:=E(ci)ci1i1\quad ct_0 := R \\\quad ct_i := E(c_i) \oplus c_{i-1} \quad i \ge 1

Where mm is our input message, padded, split into blocks and prefixed with the random block RR, meanwhile DD and EE are AES decryption and encryption. Notice how cti=bici1ct_i = b_i \oplus c_{i-1} since E(ci)=E(D(bi))=biE(c_i) = E(D(b_i)) = b_i.

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 D(0)D(0) and set FiF_i to be the iith block of the flag, with F0F_0 being the random block at the start, we can write each block of the flag's ciphertext we received at the start as: Ci:=E(Fi)Fi1D(E(Fi1)Fi2)C_i := E(F_i) \oplus F_{i-1} \oplus D(E(F_{i-1}) \oplus F_{i-2})

Let's take a look at the fourth block after asking the oracle to encrypt  F0F1D(0)\ F_0 \mid F_1 \mid D(0): ct3=b3D(b2)=E(D(0))F1D(E(F1)F0)=F1D(E(F1)F0)ct_3 = b_3 \oplus D(b_2) =E(D(0)) \oplus F_1 \oplus D(E(F_1) \oplus F_0) = F_1 \oplus D(E(F_1) \oplus F_0) T:=ct3C2=F1D(E(F1)F0)E(F2)F1D(E(F1)F0)=E(F2)T := ct_3 \oplus C_2 = F_1 \oplus D(E(F_1) \oplus F_0) \oplus E(F_2) \oplus F_1 \oplus D(E(F_1) \oplus F_0) = E(F_2)

Let's then generate a random block VV and ask for the encryption of  TD(0)V\ T \mid D(0) \mid V: ct3=b3D(b2)=E(V)D(0)D(E(D(0))T)=E(V)D(0)D(T)=E(V)D(0)D(E(F2))=E(V)D(0)F2ct_3 = b_3 \oplus D(b_2) = E(V) \oplus D(0) \oplus D(E(D(0)) \oplus T) = E(V) \oplus D(0) \oplus D(T) = E(V) \oplus D(0) \oplus D(E(F_2)) = E(V) \oplus D(0) \oplus F_2

We know both E(V)E(V) and D(0)D(0) and can therefore recover F2F_2. 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}

ByteTheCookies

Hello, we are ByteTheCookies (BTC), a passionate CTF team formed by students from the University of Salerno after participating in the CyberChallenge 2024.

CTFtime #309819

We play

blockchaincancryptohardwaremiscmobileppcpwnreverseweb
© 2026 ByteTheCookies. All wrongs reversed.We don't eat cookies. We byte them.