ECB Lasagna
b01lersc
Task: AES-ECB used as a 256-entry lookup table D[c]=AES(c*16); plaintext is character-doubled and each D[flag[i]] is laid down at offset i (cyclic), then all layers XORed. Solution: the doubling makes pairs of indices (k=2i, k=2i+1) alias to the same original char, collapsing the 16-term XOR into 8 pair-XOR filters E_i, which gives a linear recurrence recovering m[p+7] from m[p..p+6]; bootstrap with the known bctf{ prefix + 2 brute-forced chars.
$ ls tags/ techniques/
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
ECB Lasagna — b01lers CTF 2026
Description
I think I dropped something into the lasagna, can you untangle it for me?
We are given chall.py and output.txt (base64 of a 360-byte blob).
import base64 from Crypto.Cipher import AES from Crypto.Util.strxor import strxor flag = open("../flag.txt").read().strip() s = "" for c in flag: s += c * 2 flag = s cipher = AES.new(b"lasagna!" * 2, AES.MODE_ECB) result = b"\0" * len(flag) for i in range(len(result)): ciphertext = cipher.encrypt(flag[i].encode() * 16) layer = b"\0" * i + ciphertext if len(layer) < len(result): layer += b"\0" * (len(result) - len(layer)) if len(layer) > len(result): layer = layer[len(result):] + layer[len(layer)-len(result):len(result)] result = strxor(result, layer) print(base64.b64encode(result).decode())
Goal: recover the 180-character plaintext flag from the 360-byte ciphertext.
Reconnaissance
A few facts drop out immediately:
- The AES key
b"lasagna!" * 2is constant and known, so AES-ECB acts as a fixed 1-byte → 16-byte lookup table:D[c] = AES_ECB(bytes([c]) * 16)forc ∈ [0, 256). - The flag is first character-doubled: if the original flag is
m[0..179], the encrypted buffer usesf[2p] = f[2p+1] = m[p], soL = len(f) = 360. - For each position
i, the code dropsD[f[i]]starting at offsetiin a buffer of lengthL. The tail-reordering branch (layer[len(result):] + ...) is precisely a cyclic wrap-around modL. - All 360 "layers" are XOR-accumulated into the output.
Putting it together, the output byte at position j is:
result[j] = XOR_{k=0..15} D[f[(j - k) mod L]][k]
Each layer i contributes D[f[i]][k] to position (i+k) mod L, which by a change of variable is the formula above.
Math — collapsing by doubling
This is where the plaintext doubling becomes a fatal structural weakness.
...