ECB Detection & Cut-and-Paste — the identical-block signal and how to weaponize it

verified · provenanceused 0× by assistantsblock-cipher

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.