Browse CTFs New CTF Sign in

AES-ECB Block Alignment Attack: Exploiting Deterministic Encryption for Oracle Leakage

cloud_container_security Difficulty 1–5 30 min certifiable

Theory

Why This Matters

The "ECB penguin" — the image of Tux the Linux mascot encrypted with AES in ECB mode, where the outline of the penguin remains perfectly visible in the ciphertext — is one of the most widely reproduced illustrations of a cryptographic failure in existence. Beyond the visual demonstration, ECB mode has been exploited in real systems: in 2013, security researchers demonstrated that an e-commerce platform's cookie contained user role information encrypted with AES-ECB, allowing an attacker to swap encrypted blocks from a "premium" account cookie into a "free" account cookie to escalate privileges. The byte-at-a-time ECB oracle attack is a standard component of PortSwigger Web Security Academy labs and appears with high frequency in intermediate-level CTF cryptography challenges.

Core Concept

ECB (Electronic Codebook) mode encrypts each 16-byte (128-bit) block of plaintext independently under the same key: Cᵢ = Eₖ(Pᵢ). There is no initialisation vector (IV), no chaining, and no state carried between blocks. The consequence is the fundamental ECB weakness: identical plaintext blocks produce identical ciphertext blocks. This is a violation of semantic security (IND-CPA): an adversary who can observe two ciphertexts produced by the same key can determine whether two plaintext blocks are equal without knowing the key or the actual plaintext values.

The byte-at-a-time ECB decryption attack (also called the chosen-plaintext oracle attack against ECB) exploits a scenario where an attacker can submit arbitrary prefix data to an encryption oracle that appends a fixed secret before encrypting. The attack works block by block, byte by byte:

  1. The attacker pads their input so that exactly one unknown byte sits at the end of a block boundary.
  2. The attacker submits 256 candidate inputs (one for each possible value of the unknown byte) and records which produces a ciphertext block matching the oracle's output for that position.
  3. The matched value is the unknown byte. The process repeats, shifting the boundary by one byte at a time, until the entire secret is recovered.

Block size detection exploits the staircase property of ECB ciphertext length: under a prefix-appending oracle, adding bytes one at a time causes the ciphertext length to jump by 16 bytes at the first block boundary. The number of bytes added before the first jump equals 16 minus the current partial block fill, revealing the block size and the length of the unknown secret.

The cryptanalytic precondition is chosen-plaintext: the attacker must be able to submit chosen inputs to an encryption oracle that uses a fixed key. This is common in web application contexts where a cookie or token is generated by encrypting attacker-controlled data concatenated with a server-side secret.

Technical Deep-Dive

from Crypto.Cipher import AES
import os

# Simulated oracle: encrypts attacker_prefix || secret under fixed key
SECRET = b"flag{ecb_oracle_demo}"
KEY = os.urandom(16)

def oracle(attacker_prefix: bytes) -> bytes:
    plaintext = attacker_prefix + SECRET
    # Pad to block boundary (PKCS#7)
    pad_len = 16 - (len(plaintext) % 16)
    plaintext += bytes([pad_len] * pad_len)
    cipher = AES.new(KEY, AES.MODE_ECB)
    return cipher.encrypt(plaintext)

def detect_block_size(oracle_fn) -> int:
    base_len = len(oracle_fn(b""))
    for i in range(1, 33):
        new_len = len(oracle_fn(b"A" * i))
        if new_len > base_len:
            return new_len - base_len
    return -1

def byte_at_a_time_ecb(oracle_fn, block_size: int = 16) -> bytes:
    """Recover the secret suffix one byte at a time."""
    secret_len = len(oracle_fn(b"")) - block_size  # approximate
    recovered = b""
    for i in range(secret_len):
        # Pad so unknown byte is last in a block
        block_idx = (len(recovered)) // block_size
        pad_len = block_size - (i % block_size) - 1
        pad = b"A" * pad_len
        target_block = oracle_fn(pad)[block_idx*block_size:(block_idx+1)*block_size]
        # Build dictionary: all 256 candidates for next byte
        for candidate in range(256):
            test_input = pad + recovered + bytes([candidate])
            test_block = oracle_fn(test_input)[block_idx*block_size:(block_idx+1)*block_size]
            if test_block == target_block:
                recovered += bytes([candidate])
                break
    return recovered

# Detection: identical ciphertext blocks reveal repeated plaintext
def detect_ecb(ciphertext: bytes, block_size: int = 16) -> bool:
    blocks = [ciphertext[i:i+block_size] for i in range(0, len(ciphertext), block_size)]
    return len(blocks) != len(set(blocks))

Cryptanalysis Methodology

  1. Confirm ECB mode — Submit a 48-byte repeated-character input (e.g., 48 × 'A'). If any two consecutive 16-byte blocks in the ciphertext are identical, ECB mode is confirmed. Use detect_ecb().
  2. Detect block size — Call the oracle with 1, 2, 3, … bytes until ciphertext length jumps. The jump size is the block size (16 for AES). The number of bytes before the jump reveals the current partial block occupancy.
  3. Confirm chosen-plaintext oracle — Verify that attacker-controlled input is prepended (or appended) to the secret before encryption. Submit known prefixes and observe which block positions are affected.
  4. Execute byte-at-a-time recovery — Run byte_at_a_time_ecb(). Each byte requires at most 256 oracle queries; a 32-byte secret requires at most 8192 queries. This is feasible over HTTP.
  5. Handle prefix-before-secret vs. secret-before-prefix — If the secret is prepended to attacker input (harder variant), pad until the secret fills whole blocks, then shift the oracle window. The methodology adjusts to align block boundaries at the secret/attacker boundary.
  6. Verify block-swapping attacks — For structured ciphertexts (cookies, tokens), attempt to swap blocks from one ciphertext into another to escalate privileges or modify encoded values.
  7. Confirm with CyberChef or pwntools — CyberChef's "AES Decrypt" with ECB mode and the recovered key (if available) confirms the decryption. pwntools' remote() facilitates automating oracle queries over a network connection.

Secure Implementation Note — Never use AES-ECB for any application data. Use AES-GCM (authenticated encryption with random nonce per message) or, if authentication is handled separately, AES-CBC with a random IV. The AES-GCM nonce must be unique per encryption; GCM with nonce reuse is also catastrophic but for different reasons.

Common Cryptanalysis Errors

  • Miscounting the block boundary offset — When the oracle prepends a random prefix of unknown length, the attacker must first determine the prefix length (by observing when a submitted repeated block causes two identical ciphertext blocks to appear) before the byte-at-a-time attack can be aligned correctly.
  • Not accounting for PKCS#7 padding in secret length estimation — The last block of the ciphertext contains padding. Naively treating all ciphertext bytes as secret content overcounts by up to 15 bytes. Strip padding after recovery.
  • Stopping the attack when a non-printable byte is encountered — The recovered bytes may include null terminators or binary data. Continue through the full estimated secret length; do not stop at the first non-ASCII byte.
  • Querying more than one block at a time — The byte-at-a-time attack requires the target block to contain exactly the bytes being attacked. Querying with oversized padding misaligns the block window and causes incorrect matches.
  • Assuming the oracle is stateless — Some oracles embed a timestamp or session ID in the plaintext that changes between requests. If the "identical known byte" and "unknown byte" queries are not made in the same session, the oracle output is not comparable.
  • Confusing ECB detection with CBC detection — Submitting 48 identical bytes to a CBC oracle with a fixed IV will produce three blocks of which only the first two may match (since CBC chains blocks). Two identical blocks from 48 repeated input bytes confirms ECB; non-matching blocks do not confirm CBC without further investigation.

NICE Framework Alignment

Code Knowledge/Skill/Task Statement How This Card Develops It
K0018 Knowledge of encryption algorithms Defines ECB mode precisely and contrasts it with chained modes; identifies the determinism property as the root cryptographic failure
K0019 Knowledge of cryptography and cryptographic key management concepts Demonstrates why IV/nonce usage and block chaining are non-optional in symmetric encryption
K0305 Knowledge of data concealment Illustrates that ECB mode provides pattern-preserving pseudo-confidentiality, not real confidentiality
S0138 Skill in using public-key encryption and PKI Develops oracle-based chosen-plaintext attack methodology applicable to real web application encryption
T0259 Conduct vulnerability assessment using established frameworks Trains systematic ECB detection and byte-at-a-time exploitation as a structured assessment technique

Further Reading

  • A Graduate Course in Applied Cryptography (Chapter 5: Chosen Plaintext Attacks) — Dan Boneh and Victor Shoup, online draft 2023
  • Practical Cryptography (Chapter 4: Block Ciphers) — Niels Ferguson and Bruce Schneier, Wiley, 2003
  • PortSwigger Web Security Academy: ECB byte-at-a-time decryption lab writeup — PortSwigger research documentation

Challenge Lab

Reinforce your learning with a hands-on generated challenge based on this card's competency.