ECB Mode Attacks
Exploit AES-ECB's deterministic block encryption - byte-at-a-time oracle and cut-and-paste attacks.
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.


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 0Notes:
detect_blocksizefinds the block increment as you increase plaintext length.find_prefix_lengthtries 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
256test 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.,
adminwith 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
adminblock 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