Skip to main content

All weeks · Worksheet · Overview

Week 6 · Lecture slides

Week 6

Contents15 sections

Authenticated Encryption / AEAD (the CBC padding oracle)

Security & Cryptography · Nutthakorn Chalaemwongwan


Today

  • What AEAD guarantees, in one primitive (RFC 5116)
  • Encrypt-then-MAC vs the two fragile alternatives
  • Why unauthenticated CBC + a padding check = a padding oracle
  • Why AES-GCM has no equivalent hole
  • 🔓 Game: Read the Secret Without the Key

Recap — Week 5

  • Diffie–Hellman gives Alice & Bob a shared secret no eavesdropper can compute
  • But raw DH authenticates nobody — an active attacker (relay) ran two independent handshakes and sat invisibly in the middle
  • Fix: authenticate the exchange (HMAC over the public keys) — one added check closed it, no crypto swapped

What is AEAD?

Authenticated Encryption with Associated Data — three guarantees, one primitive:

GuaranteeStops
Confidentialityeavesdropping — ciphertext reveals nothing about plaintext
Integrityundetected tampering — any bit-flip is caught
Authenticityforgery — only a key-holder could have produced this ciphertext

Safe composition: how NOT to build it yourself

  • Encrypt-then-MAC (EtM) — MAC over the ciphertext, verified before decryption is even attempted. The provably safe generic composition.
  • MAC-then-Encrypt (MtE) — MAC over plaintext, then encrypt the (plaintext‖MAC). Verifier must decrypt first — old TLS CBC cipher suites did this.
  • Encrypt-and-MAC (E&M) — MAC over plaintext, sent alongside ciphertext (legacy SSH). MAC itself can leak plaintext info.

If the verifier has to touch the plaintext before the MAC check passes, you have a problem.


Bridge: CBC is malleable (Week 4, recap)

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

  • Flip a bit in ciphertext block C_{i-1} → the same bit flips in plaintext block P_i
  • No MAC, no signature — nothing stops the attacker from tampering
  • Week 4: bit-flipping. Today: malleability weaponized into full decryption.

The vulnerability: a padding oracle

vulnerable_app.py — unauthenticated AES-256-CBC, POST /decrypt:

if pkcs7_valid(plaintext):
    return jsonify({"status": "ok"}), 200
return jsonify({"error": "bad padding"}), 403
  • The endpoint never returns the plaintext
  • But 200 vs 403 is a distinguishable signal — that's the oracle
  • "We don't leak the plaintext" is not the same as "we leak nothing"

The attack: byte-by-byte, without the key

Attacker submits a forged previous block + the real last block as a fake IV‖ct:

  • Server computes P = D(C_t) ⊕ forged_prev, checks only: is P's padding valid?
  • Try every forged_prev[15] until the server says 200 → last byte of P is (almost always) 0x01
  • D(C_t)[15] = winning_guess ⊕ 0x01 — one byte of the intermediate value, per query
  • Repeat forcing …0x02 0x02, …0x03 0x03 0x03, … → recover the whole block
  • False-positive case (P ends …0x02 0x02 by accident) → re-query a neighbour byte to disambiguate

The fix: AES-GCM has no padding to probe

fixed_app.py — AES-256-GCM, POST /decrypt:

try:
    _aesgcm.decrypt(nonce, ct_and_tag, None)   # raises on ANY tamper
except Exception:
    return jsonify({"error": "decryption failed"}), 403   # uniform, always
return jsonify({"status": "ok"}), 200
  • No padding — GCM is a stream mode (CTR-based); there's no "valid vs invalid padding" question to ask
  • The 128-bit tag is checked first — no plaintext is released until it passes
  • Every failure — bad base64, wrong length, bad nonce, forged tag — returns the identical 403

Associated data — the "AD" in AEAD

  • Data you want authenticated but NOT encrypted — stays readable, but tamper-evident
  • Example: a protocol header, sequence number, or recipient/context field a router needs to read
  • If that data is left unauthenticated, an attacker can splice a valid ciphertext onto a different header — a context-confusion forgery, even though the ciphertext itself is untouched

This is not a toy bug

  • CWE-347 — Improper Verification of Cryptographic Signature (the tag/MAC isn't checked first)
  • CWE-757 — Selection of Less-Secure Algorithm (unauthenticated CBC where AEAD belongs)
  • CWE-208 — Observable Timing/Response Discrepancy (the 200-vs-403 side channel itself)
  • Real CVEs, not hypothetical: POODLE (2014, SSLv3 CBC padding oracle), Lucky-13 (TLS CBC timing variant) — deployed in production TLS for over a decade

🔓 Game — "Read the Secret Without the Key"

  • Two live targets, same secret (contains your flag): :8098 = AES-CBC (vulnerable), :8099 = AES-GCM (fixed)
  • Attacker only ever sees 200/403 from /decrypt — never the plaintext, never the key
  • Run the padding-oracle attack on :8098 → recover msg:FLAG{...} byte by byte
  • Run the identical attack on :8099 → zero usable signal, nothing recovered — AEAD holds

Lab today

📋 Worksheet 6 — labs/week06-aead/worksheet.md · kickoff: docker compose up -d (:8098 CBC, :8099 GCM) → python exploit.py

  • Tasks 0–5: read the oracle branch, capture ciphertext, derive D(C_t)[15] = guess ⊕ 0x01, recover the flag, confirm GCM gets no signal
  • Deliverable: recovered flag + which byte you forced to 0x01/0x02 0x02 and why
  • + Audit the AI (a "safe" decrypt endpoint that reintroduces the oracle via 400/422/200), EiPE, Prompt Problem
  • Part 2: 8 conventional written questions (Q1–Q8), no AI-resilience layer — answer yourself

Key takeaways

  • Textbook-secure primitive, real-system failure: AES-CBC is a fine cipher — "AES-CBC + tell me if the padding failed" is not
  • The fix isn't hiding the error message — it's composing correctly (encrypt-then-MAC) or reaching for a real AEAD cipher (AES-GCM)
  • Any distinguishable response to an attacker-controlled input is a potential oracle — ask what your error codes reveal

Questions?

Next week: Review — consolidating Weeks 1–6 before the midterm

All weeks in Security & Cryptography