Athena Wiki

ECB Mode Attacks

cryptointermediate

Exploit AES-ECB's deterministic block encryption - byte-at-a-time oracle and cut-and-paste attacks.

cryptoaesecboraclebyte-at-a-timecut-and-paste

ECB Mode Attacks

AES-ECB encrypts each 16-byte block independently with the same key. Identical plaintext blocks produce identical ciphertext blocks - a fatal property that enables several powerful attacks.


What is ECB?

  • Electronic Codebook (ECB) is a very simple way to use a block cipher: the input message is split into fixed-size blocks (16 bytes for AES), and each block is encrypted independently using the same key.
  • Analogy: imagine a stamp that imprints a pattern for each 16-byte block - identical blocks get the same stamp. That makes repeated patterns visible in the encrypted output.

Why this is dangerous

  • Pattern leakage: identical plaintext blocks produce identical ciphertext blocks, so structured data (images, repeated fields) can be partially recovered or recognized.
  • No integrity: ECB provides no authentication; attackers can rearrange ciphertext blocks without detection.
  • Not recommended: modern systems use chaining or streaming modes (CBC, CTR, GCM) that hide patterns and provide stronger guarantees.
ECB encryption result
ECB encryption (pattern leakage)
ECB decryption / other modes
ECB decryption / other modes

Detecting ECB Mode

This small snippet demonstrates a practical oracle-based test for ECB: send two identical consecutive blocks and see if the ciphertext blocks match. If they do, the encryption is deterministic per-block (ECB).

Key points:

  • Uses a fixed block size (16 bytes for AES).
  • Works even when there is additional unknown prefix or secret, as long as two identical plaintext blocks are aligned.
from Crypto.Cipher import AES
import os

# Oracle that encrypts our input (prepended or appended to secret)
def oracle(plaintext):
    key = b'\x00' * 16  # fixed key (example)
    cipher = AES.new(key, AES.MODE_ECB)
    return cipher.encrypt(plaintext + secret)

# Detection: send two identical blocks (32 bytes = 2 blocks of A's)
ct = oracle(b'A' * 32)
blocks = [ct[i:i+16] for i in range(0, len(ct), 16)]
if blocks[0] == blocks[1]:
    print("ECB detected!")

Explanation: If two identical plaintext blocks map to identical ciphertext blocks, the cipher is using ECB (or another deterministic per-block mode). This check is simple and often used as a first step during challenge reconnaissance.


Finding Block Size and Prefix Length

These helpers determine the cipher block size (usually 16 for AES) and how many unknown bytes may be prepended by the oracle (a random prefix). Knowing the prefix and block size is necessary to align attacker-controlled input with block boundaries for later byte-at-a-time attacks.

def detect_blocksize(oracle):
    initial_len = len(oracle(b''))
    for i in range(1, 256):
        new_len = len(oracle(b'A' * i))
        if new_len > initial_len:
            return new_len - initial_len

block_size = detect_blocksize(oracle)  # typically 16

def find_prefix_length(oracle, block_size):
    """Find how many bytes of prefix are prepended to our input."""
    for pad_len in range(block_size * 2):
        ct = oracle(b'A' * pad_len)
        blocks = [ct[i:i+block_size] for i in range(0, len(ct), block_size)]
        # Two identical consecutive blocks → we've padded into alignment
        for j in range(len(blocks) - 1):
            if blocks[j] == blocks[j+1]:
                # Block j starts at j*block_size
                # Alignment padding = pad_len, our two blocks start at j
                prefix_len = j * block_size - pad_len
                return prefix_len
    return 0

Notes:

  • detect_blocksize finds the block increment as you increase plaintext length.
  • find_prefix_length tries varying padding until two identical consecutive ciphertext blocks appear - that indicates alignment into block boundaries, from which the prefix length can be derived.

Byte-at-a-Time ECB Decryption

This attack recovers an unknown secret appended to attacker-controlled input when encryption uses encrypt(attacker_input || secret) under ECB. The idea is to align the unknown byte at the end of a block and brute-force it by comparing ciphertext blocks.

Before running this, ensure you know block_size and prefix_len (use the helpers above). The algorithm:

  • Pad so the unknown byte is the last byte of a target block.
  • For each unknown byte position, craft 256 test inputs with the candidate byte appended.
  • The candidate that produces the same ciphertext block as the oracle reveals the correct byte.
def ecb_byte_at_a_time(oracle, block_size=16, prefix_len=0):
    """
    Decrypt secret by sending crafted inputs that align one unknown byte
    at the end of a known block, then brute-force that byte.
    """
    # Calculate alignment padding to neutralize prefix
    align_pad = (block_size - prefix_len % block_size) % block_size

    secret = b''
    secret_len = len(oracle(b'A' * align_pad)) - prefix_len - align_pad

    for i in range(secret_len):
        # How many A's to push the target byte to end of a block
        # We need (align_pad + block_size - 1 - i%block_size) A's
        pad_count = align_pad + block_size - 1 - (i % block_size)
        padding = b'A' * pad_count

        # Target block index
        target_block_idx = (prefix_len + pad_count + i) // block_size

        # Get the target block (contains unknown byte at end)
        target = oracle(padding)
        target_block = target[target_block_idx*block_size:(target_block_idx+1)*block_size]

        # Try all 256 possible last bytes
        for byte_val in range(256):
            # Craft input: padding + known_secret_so_far + byte_val
            test_input = padding + secret + bytes([byte_val])
            test_ct = oracle(test_input)
            test_block = test_ct[target_block_idx*block_size:(target_block_idx+1)*block_size]

            if test_block == target_block:
                secret += bytes([byte_val])
                print(f"\rDecrypted: {secret}", end='', flush=True)
                break

    print()
    return secret

result = ecb_byte_at_a_time(oracle)
print(result)

Practical tips:

  • Run this when the oracle appends the secret. If the secret is prepended, the method changes.
  • Large secrets can be slow; add logging and checkpointing.

ECB Cut-and-Paste Attack

When an application encrypts structured data (e.g., key-value pairs) under ECB and later parses the decrypted plaintext, you can mix-and-match ciphertext blocks to create a forged plaintext. This is possible because ECB leaks block boundaries and encrypts each block independently.

High-level steps:

  • Create a ciphertext block that represents the value you want (e.g., admin with correct PKCS#7 padding).
  • Create another ciphertext whose block alignment places the role= field at the start of a new block.
  • Replace the target ciphertext block with your crafted admin block and submit the forged ciphertext to the server.
# Server encrypts: "email=USER_EMAIL&uid=10&role=user"
# We want:         "email=????&uid=10&role=admin"
#
# Attack: craft two requests whose encrypted blocks can be rearranged
#
# Block 0: "email=aaaaaaaaaa"  (pads to 16 bytes)
# Block 1: "admin\x0b\x0b..."  (admin + PKCS7 padding for "admin" as a standalone block)
# Block 2: "&uid=10&role=us"
# Block 3: "er\x0e\x0e..."

# Step 1: Get encryption of "admin" block
# email = "AAAAAAAAAAadmin" + PKCS7 pad
admin_block_input = b'A' * 10 + b'admin' + b'\x0b' * 11
ct1 = oracle_encrypt(f"email={admin_block_input.decode()}&uid=10&role=user")
admin_block = ct1[16:32]  # block 1

# Step 2: Get encryption with email length that aligns role= to block boundary
# email = exactly 13 chars → "email=AAAAAAAAAAAAA&uid=10&role=" = 32 bytes = 2 blocks
ct2 = oracle_encrypt("email=" + "A"*13 + "&uid=10&role=user")

# Step 3: Combine: first two blocks of ct2 + admin_block
forged = ct2[:32] + admin_block
# Decrypt: "email=AAAAAAAAAAAAA&uid=10&role=admin"

Caveats:

  • This relies on predictable formatting and no integrity checks (no HMAC or authenticated encryption).
  • Modern apps use authenticated modes (GCM) or sign data to prevent this.


Last updated on

On this page