Description
They added a custom CBC-like chaining layer on top of AES-ECB: each encrypted block has the previous encrypted block added to it (mod 2^128). Undo the addition to strip the chaining, then ECB's pattern-preserving property makes the image readable.
Setup
Download the Python source and the encrypted PPM file.
Read the source to understand the AES-ABC custom chaining.
Solution
Walk me through it- Step 1Understand the AES-ABC chainingThe AES-ABC mode: encrypt each 16-byte block with AES-ECB, then add the previous encrypted block (mod UMAX = 2^128) to produce the output. To reverse this, go through each block and subtract the previous block (mod UMAX). The result is still ECB-encrypted, but ECB preserves patterns - so the resulting file is a visually readable image.
Learn more
Why ECB leaks structure. In Electronic Codebook (ECB) mode, AES is applied to each 16-byte block independently with no chaining:
C_i = AES_K(P_i). AES is a deterministic permutation, so identical plaintext blocks produce identical ciphertext blocks:P_i = P_j => C_i = C_j. The ciphertext therefore preserves the equality pattern of the plaintext blocks - it acts like a "codebook" lookup, hence the name.The ECB penguin. Encrypt an image of Tux with ECB and the silhouette is still visible. Why: the image has large uniform regions (e.g., the white belly), which means many adjacent blocks contain identical pixel patterns. Those blocks all encrypt to the same ciphertext, preserving the shape exactly. With CBC each block also XORs the previous ciphertext, so uniform plaintext produces wildly varying ciphertext.
Worked example with two-block plaintext. Suppose
P_1 = "AAAAAAAAAAAAAAAA"andP_2 = "BBBBBBBBBBBBBBBB":ECB: C_1 = AES_K(P_1) C_2 = AES_K(P_2) C_1 != C_2 only because P_1 != P_2 If P_1 == P_2, then C_1 == C_2 byte for byte. Attacker can detect repeated plaintext without the key.This challenge. The ciphertext is a BMP image whose pixel data was encrypted with AES-ECB. Because BMP stores raw pixel rows, repeated colors (background, text foreground) become repeated 16-byte blocks. Re-saving the file with a valid BMP header (the same 54-byte BMP header from any image with matching width/height) and viewing the image makes the flag legible directly - no XOR or block re-ordering needed; the patterns are visible because ECB preserved them.
- Step 2Modify the source to subtract instead of addTake the provided Python encryption script. Remove the AES key reference (you don't have it), but keep the block parsing logic. Rewrite the encrypt method as a decrypt method: for each block, compute new_block = (UMAX - prev_block + current_block) % UMAX. Write the result to a .ppm output file.python
python3 << 'EOF' # Modified from the challenge source - removes AES (no key) and reverses the ABC chaining UMAX = pow(2, 128) BLOCK_SIZE = 16 def remove_abc(ciphertext_blocks): new_blocks = [] prev = int.from_bytes(b'\x00' * BLOCK_SIZE, 'big') for block in ciphertext_blocks: curr = int.from_bytes(block, 'big') new_curr = (UMAX - prev + curr) % UMAX new_blocks.append(new_curr.to_bytes(BLOCK_SIZE, 'big')) prev = curr return b''.join(new_blocks) with open('body.enc', 'rb') as f: raw = f.read() # Parse the file: header (unencrypted), then encrypted blocks # Header is the PPM header ending at the pixel data start header_end = raw.index(b'\n', raw.index(b'\n', raw.index(b'\n') + 1) + 1) + 1 header = raw[:header_end] body = raw[header_end:] blocks = [body[i:i+BLOCK_SIZE] for i in range(0, len(body), BLOCK_SIZE)] plaintext_body = remove_abc(blocks) with open('flag.ppm', 'wb') as f: f.write(header + plaintext_body) print("Wrote flag.ppm - open it to read the flag") EOFLearn more
The custom AES-ABC mode XORs ECB-encrypted blocks with the previous ECB-encrypted block via addition mod 2^128. Since we don't have the AES key, we cannot fully decrypt back to plaintext - but we can strip the chaining by subtracting previous blocks. The result is still ECB-encrypted pixel data, which is fine: ECB preserves patterns, so the image is visually readable even when the individual pixel bytes are encrypted.
- Step 3Open the resulting PPM file to read the flagOpen flag.ppm with an image viewer (GIMP, or install a PPM viewer). Because ECB encryption applies the same transformation to equal blocks, the visual patterns of the original image survive. The flag text is visible in the image even though the pixel values are encrypted.bash
gimp flag.ppmLearn more
This is the classic ECB penguin demonstration: encrypt an image with AES-ECB and the silhouette is still recognizable because uniform regions (same pixel color) produce identical ciphertext blocks. The AES-ABC chaining was designed to hide this flaw, but stripping the chaining via subtraction exposes it again.
Why not to roll your own crypto: The designers tried to improve ECB by adding block chaining via arithmetic, but the fix was reversible without the key. Real CBC mode XORs the previous ciphertext block into the plaintext before encryption - this is not reversible without the key because you cannot reconstruct the plaintext XOR mask.
Flag
picoCTF{...}
Strip the custom block chaining by subtracting previous blocks (mod 2^128), then the ECB-encrypted image is visually readable - the flag is visible in the resulting PPM.