ECB Detection & Cut-and-Paste — the identical-block signal and how to weaponize it
ECB (Electronic Codebook) encrypts every block independently and deterministically, so equal plaintext blocks always produce equal ciphertext blocks — the leak an attacker detects and then abuses.
The signal that gives it away
Two structural facts, checked together, are what actually confirms ECB in a challenge — neither alone is conclusive:
- Ciphertext length is a multiple of the block size (8 bytes for DES/3DES, 16 for AES). This is necessary but not sufficient: CBC and other padded modes also produce block-multiple lengths, so length alone never proves ECB.
- Repeated ciphertext blocks appear when you feed repeated plaintext blocks. This is the actual proof. If the endpoint lets you submit chosen plaintext (even indirectly — a username field, a comment that gets encrypted and returned), send two identical 16-byte chunks back to back, e.g. b'A'*32. If two ciphertext blocks come out byte-identical, the cipher is running in ECB.
Passive observation of ciphertext alone (no chosen-plaintext) is a weaker signal: short messages, or messages without any 16-byte-aligned repeated substring, will show no repeated blocks even under ECB — a false negative. The repeated-block test only becomes reliable once you control (or can guess/align) enough of the plaintext to force a deliberate repeat across a block boundary. See Block Cipher Modes of Operation — the mode carries the CTF vulnerability, not the primitive for the broader mode-identification triage this signal feeds into.
How it's exploited
1. Detect. Send at least two identical 16-byte blocks (b'A'*32 is the standard probe) and diff the ciphertext in 16-byte chunks. A match at any chunk boundary confirms ECB.
2. Byte-at-a-time decryption, when the oracle encrypts attacker_input || secret (or secret || attacker_input) under a fixed key and returns the ciphertext: you recover the secret one byte at a time without ever learning the key.
- Pad your input so the next unknown byte of the secret lands as the last byte of some target block. The padding length needed is (-len(known) - 1) % block_size — i.e. just enough filler so the boundary sits exactly one byte before the next unrecovered secret byte.
- Record that target block: oracle(pad)[blk*bs:(blk+1)*bs].
- Brute-force all 256 candidate byte values by appending pad + known_so_far + candidate_byte and checking whether the same target block now matches. Only one candidate (the real secret byte) reproduces it, because ECB re-encrypts that exact same 16 plaintext bytes to the exact same ciphertext every time.
- Slide the window one byte forward and repeat. This is a purely mechanical loop — no cryptanalysis, no key material, just exploiting determinism.
``python
def byte_at_a_time(oracle, bs=16):
known = b''
for _ in range(bs * 8):
pad = b'A' * ((-len(known) - 1) % bs)
blk = (len(pad) + len(known)) // bs
target = oracle(pad)[blk*bs:(blk+1)*bs]
for c in range(256):
if oracle(pad + known + bytes([c]))[blk*bs:(blk+1)*bs] == target:
known += bytes([c])
break
else:
break # no byte matched: hit padding or end of secret
return known
``
The loop's natural termination is the else branch: once you're past the real secret and into its padding, no candidate byte reproduces the target block, so the search stops there — that's also how you detect you've recovered the whole secret rather than running off into garbage.
3. Cut-and-paste (block splicing). Because each ciphertext block decrypts independently, you can rearrange or splice blocks from *different* encryptions (under the same key) and the result still decrypts block-by-block into valid plaintext for each spliced piece. Classic use: forging a token like ...role=user into ...role=admin.
- Craft an input so that an attacker-controlled block — e.g. the bytes admin followed by valid PKCS#7 padding (admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b, padding the remaining 11 bytes of a 16-byte block to the value 0x0b) — lands exactly on a block boundary. You control this by padding earlier fields so your target string starts at offset 16, 32, etc.
- Encrypt that crafted input to obtain a ciphertext block that decrypts to a clean admin + valid-padding block.
- Take a *separate* encryption of a normal token (e.g. one ending ...role=user with its own padding) and swap in your crafted block at the position where the role field falls, dropping the original last block(s).
- Reassemble: the forged ciphertext, decrypted block-by-block under the real key, produces a token whose role field reads admin — without ever knowing the key.
Related: Block Cipher Modes of Operation — the mode carries the CTF vulnerability, not the primitive CBC Bit-Flipping — controlled tampering with no oracle needed Permutation Cipher — frequencies survive intact, only position is scrambled CBC Padding Oracle — leak the intermediate decryption value one byte at a time
What does NOT work
- Judging ECB from ciphertext length alone. Block-aligned length is produced by every padded block mode (CBC included); it only narrows "this is a block cipher with padding," not which mode.
- Expecting the repeated-block test to work on unmodified, short, or attacker-blind ciphertext. Without the ability to inject a controlled, repeated 16+16-byte chunk, real-world messages rarely contain a naturally repeating 16-byte-aligned substring — you'll see no match and wrongly conclude "not ECB."
- Byte-at-a-time decryption without correctly computing the alignment padding. Get the (-len(known)-1) % bs offset wrong by even one byte and every candidate check misses the target block; the loop silently fails to recover anything (or recovers garbage) rather than throwing an obvious error, so the padding arithmetic has to be exact, not approximate.
- Cut-and-paste without a valid, in-context padding byte on the spliced block. If the crafted block's tail bytes are not correct PKCS#7 padding, the decrypted plaintext fails padding validation (or displays garbage padding bytes) even though the block content itself decrypted "correctly" — the splice has to respect both the block boundary *and* the padding scheme.
Verified against
17 claims checked against these sources
Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.