cryptofreemedium

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/
ecb_as_lookup_tabledoubling_symmetry_collapsepair_xor_filterconvolutional_decodingknown_prefix_bruteforcebacktracking

$ 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!" * 2 is constant and known, so AES-ECB acts as a fixed 1-byte → 16-byte lookup table: D[c] = AES_ECB(bytes([c]) * 16) for c ∈ [0, 256).
  • The flag is first character-doubled: if the original flag is m[0..179], the encrypted buffer uses f[2p] = f[2p+1] = m[p], so L = len(f) = 360.
  • For each position i, the code drops D[f[i]] starting at offset i in a buffer of length L. The tail-reordering branch (layer[len(result):] + ...) is precisely a cyclic wrap-around mod L.
  • 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.

...