AES / Block-Cipher Modes (CBC bit-flipping, malleability)
Security & Cryptography · Nutthakorn Chalaemwongwan
Today
- AES: the primitive (block cipher) vs. the mode wrapping it
- Why unauthenticated CBC is malleable — bit-flip an encrypted token
- The fix: AES-GCM / AEAD — encryption + integrity, together
- 🏁 Game: Flip Your Way to Admin — forge
role=adminwithout the key
Recap — Week 3
- A keyed hash is not automatically a MAC — length-extension forges valid tags without the key
- HMAC fixed that gap: a construction designed to resist it
- Same theme, one level up: last week broke integrity without a real MAC; today we break integrity without any authentication at all
AES: the primitive vs. the mode
- AES itself: a textbook-secure block cipher — 128-bit blocks, 128/192/256-bit keys, no known practical break
- A block cipher only encrypts one 16-byte block
- Real messages need a mode of operation to chain blocks together: ECB, CBC, GCM…
- The mode — not AES — decides whether the resulting scheme is actually secure
ECB leaks patterns (Q1)
- ECB encrypts each block independently with the same key
- Identical plaintext blocks → identical ciphertext blocks
- The "ECB penguin": encrypt a bitmap image in ECB — the outline is still visible in the ciphertext
- AES is unbroken here — the mode leaks structure, the cipher doesn't
CBC: chaining fixes ECB's leak
- Each plaintext block is XOR-ed with the previous ciphertext block before encrypting
C_i = E(P_i XOR C_{i-1})— block 0 uses a random IV instead of a previous block- Decrypt:
P_i = D(C_i) XOR C_{i-1} - Fixes ECB's pattern leak... but the XOR-chaining opens a different door
The gap: encryption ≠ integrity (Q3, Q9)
- CBC (and most classic modes) give confidentiality only
- Nothing stops an attacker from editing ciphertext — the receiver still decrypts to something and trusts it
- No MAC / no auth tag = no way to detect tampering
- CWE-353 (Missing Support for Integrity Check) / CWE-649 (Reliance on Encryption Without Integrity)
- Today's lab exploits exactly this gap to become
admin
The target: an unauthenticated CBC token
vulnerable_app.py :8096 issues base64(IV ‖ C0 ‖ C1) — AES-256-CBC, no MAC. Fixed 32-byte plaintext, two blocks:
| Block | Plaintext | Token bytes |
|---|---|---|
| 0 | comment=FILLER!! | IV = token[0:16] |
| 1 | role=guest;xpad0 | C0 = token[16:32], C1 = token[32:48] |
/loginissues the token,/whoamidecrypts and reports the role,/adminchecks it
Why flipping C0 changes the role
- CBC decrypt:
P1 = AES_decrypt(C1) XOR C0 - Flipping byte k of C0 flips byte k of P1 — deterministically, without the key
guestsits atP1[5..9];adminis also 5 bytes → same-length swapC0[5+i] ^= guest[i] ^ admin[i]fori = 0..4→ edit token bytes21..25- Side effect: block 0's plaintext (
comment=...) turns to garbage — harmless, the app never reads it
Cashing in the flip
docker compose up -d # vulnerable_app.py :8096, fixed_app.py :8097
python exploit.py # PASS :8096 (flag), PASS :8097 (rejected)
- No AES library needed for the attack itself — pure byte math (XOR)
- Replay the tampered token cookie against
/admin→ flag - The attack never touches the key — swapping
AES_KEYstill works, proving it's pure ciphertext malleability, not a key leak
The fix: authenticated encryption (Q4)
fixed_app.py :8097issuesbase64(nonce ‖ AES-256-GCM(plaintext) ‖ tag)- GCM = AES-CTR (confidentiality) + GHASH authentication tag computed over the ciphertext
- Decrypt checks the tag first — any edited ciphertext byte → tag mismatch → reject
- Attacker can't recompute a valid tag without the key — no equivalent of the CBC trick exists
Proving it empirically
- Apply the same "flip a ciphertext byte" idea to a
:8097token - Result:
403, no flag — GCM rejects the tamper every time - The concrete difference: CBC silently accepts a coherent forged plaintext; GCM raises on decrypt
- Q10's trap: GCM only helps if the app actually checks the tag and doesn't swallow the exception (an improper-verification bug, in the spirit of CWE-347)
The wider family of mode pitfalls
- ECB — pattern leakage (Q1)
- CBC, reused IV — reveals whether two messages share a prefix (Q2)
- GCM, reused nonce — catastrophic: keystream and auth key both reused → plaintext XOR leaks and tags become forgeable (Q5/Q6)
- Disk encryption (LUKS/BitLocker) uses AES-XTS, not GCM — no room for a per-sector nonce+tag, so it trades away authentication for a tweakable cipher (Q8)
- Mobile/IoT: AES-GCM where hardware AES acceleration exists (x86 AES-NI, ARMv8 crypto ext.); ChaCha20-Poly1305 otherwise — avoids timing side channels without hardware accel (Q7)
🏁 Game — Flip Your Way to Admin
| Service | Port | Scheme | Tamperable? |
|---|---|---|---|
vulnerable_app.py | :8096 | AES-256-CBC, no MAC | Yes |
fixed_app.py | :8097 | AES-256-GCM, AEAD | No |
GET /loginon:8096→ capture thetokencookie- XOR the 5-byte delta into
token[21..25]— no key needed - Replay against
/admin→ flag - Try the identical trick on
:8097→ rejected
Lab today
📋 Worksheet 4 —
labs/week04-aes-modes/worksheet.md· kickoff:docker compose up -d
- Part 1: Q1–Q10 written questions (ECB/CBC/GCM/XTS/nonces)
- Part 2A: Tasks 0–5 — capture a token, locate the bytes, flip, cash in the flag, confirm GCM rejects, explain why
- Part 2C — Audit the AI: an AI claims CBC's random IV + secret key means "the client can't change the role" — find the flaw, rewrite with AEAD, verify against the lab
- Part 2D: EiPE + Prompt Problem · Part 2E: viva spot-check (3 questions, pass/fail gate)
Key takeaways
- AES the cipher is textbook-secure — the mode is where real systems fail
- Encryption alone is not integrity: unauthenticated CBC is malleable
- AEAD (GCM / ChaCha20-Poly1305) binds confidentiality + integrity — but only if the tag is actually checked
Questions?
Next week: Key Exchanges — MITM-ing an unauthenticated Diffie-Hellman handshake