Skip to main content

All weeks · Worksheet · Overview

Week 3 · Lecture slides

Week 3

Contents18 sections

Cryptography Used Correctly (and Misused)

Software Security · Nutthakorn Chalaemwongwan


Today

  • Hashing vs encryption vs encoding
  • Password storage done right
  • Symmetric / asymmetric basics
  • Common crypto failures (A04:2025)
  • 🔓 Game: Capture the Hash

Recap — Week 2

  • Tools find bug patterns; today we study one bug class deeply
  • Crypto misuse is subtle: code "works" but isn't safe

Three things people confuse

  • Hashing — one-way, fixed-size digest (integrity, passwords)
  • Encryption — reversible with a key (confidentiality)
  • Encoding — reversible without a key (Base64 ≠ security!)

Symmetric vs asymmetric

  • Symmetric (AES, ChaCha20) — one shared key, fast
  • Asymmetric (RSA, ECC) — public/private key pair
  • TLS uses asymmetric to exchange a symmetric session key

Password storage

  • Never store plaintext or plain hashes
  • Use a slow, salted KDF, in order of preference: argon2id → scrypt (if argon2id unavailable) → bcrypt (legacy systems only) → PBKDF2 (FIPS-140 compliance only)
  • Salt is per-user and random
from argon2 import PasswordHasher
ph = PasswordHasher(); hash = ph.hash("correct horse battery staple")

Worked example: why ECB leaks

  • ECB encrypts each block independently → patterns survive
  • The classic "ECB penguin": the image is still recognizable encrypted
  • CBC isn't the fix either — no integrity check means an attacker can flip bits in the ciphertext and the corresponding plaintext bits flip predictably
  • Fix: an authenticated mode (AES-GCM) with a random nonce — confidentiality and integrity together
Why ECB leaks a picture, and what CBC's XOR costs (Week 3) — open full size

ECB in one picture

Two identical 16-byte plaintext blocks encrypted with ECB become two identical ciphertext blocks, so the repetition leaks (the ECB-penguin effect); the same input re-encrypts to the same bytes because there is no nonce. AES-GCM with a random nonce and auth tag produces different, random-looking blocks and detects tampering. Choosing AES is not the fix — AES-ECB leaks and AES-GCM under a hardcoded key is still CWE-798.


Capture the Hash — how cracking works

# strip comments, then crack unsalted MD5 with a wordlist
hashcat -m 0 hashes.txt rockyou.txt
  • Unsalted fast hash → attacker hashes wordlist once, matches millions
  • Salt would force per-guess work; a slow KDF makes it infeasible

Common crypto failures (A04:2025)

  • ECB mode → block patterns leak
  • Hardcoded keys / keys in source (CWE-798)
  • Weak RNG (random instead of secrets/CSPRNG) (CWE-330)
  • MD5 / SHA-1 for security; unauthenticated encryption (no integrity)

Do this instead

  • Authenticated encryption: AES-GCM or ChaCha20-Poly1305
  • Keys from a secrets manager / KMS — never in code
  • CSPRNG: secrets (Python), crypto.randomBytes (Node)
  • SHA-256+ for integrity; argon2id/bcrypt for passwords

CWE mapping

  • CWE-327 — broken/risky crypto algorithm
  • CWE-916 — weak password hash (no/weak KDF)
  • CWE-330 — insufficiently random values
  • CWE-798 — hardcoded key

Four fixes — in the code

# vulnerable_crypto.py                       ->  solution_skeleton.py
md5(pw).hexdigest()           # CWE-916      ->  argon2.PasswordHasher().hash(pw)
AES.new(key, AES.MODE_ECB)    # CWE-327      ->  AES.new(key, MODE_GCM, nonce=os.urandom(12))
HARDCODED_KEY = b"0123..."    # CWE-798      ->  bytes.fromhex(os.environ["ENC_KEY_HEX"])
random.choice("0123456789")   # CWE-330      ->  secrets.token_urlsafe(16)
  • Four separate decisions — KDF · cipher mode · key source · RNG — not one "use better crypto"
  • "Use AES" answers none of them: AES-ECB leaks; AES-GCM under a hardcoded key is still CWE-798
  • Each fix makes the value unguessable / authenticated / unique, not merely "encrypted"

Four decisions, four fixes

Four crypto misuses and their fixes: password storage (md5 to argon2id), cipher mode (hardcoded-key ECB to GCM with a nonce and auth tag), randomness (random.choice to secrets.token_urlsafe), and key source (hardcoded key to an environment variable). One cipher name answers none of these four questions — AES-GCM under a hardcoded key is still CWE-798.


🔓 Game — Capture the Hash

  • Round 1 (speedrun): crack unsalted/MD5 hashes — fastest team wins
  • Round 2: exploit an ECB oracle (identical blocks)
  • Round 3 (defend): run the fixed version (argon2id + AES-GCM + keys from env) and confirm it holds — then author two pieces yourself: migrate a legacy MD5 record to argon2id on next login, and write the decrypt+tamper-check the fixed version doesn't include for you

Lab 3 — deliverable

📋 Worksheet 3 — labs/week03-cryptography/worksheet.md (Part 3) · kickoff: docker compose up (runs the crypto scripts)

  • Cracked hashes + method · ECB-leak proof · predictable-token note
  • Before/after code (misuse → fix → CWE closed) — incl. your rehash-on-login migration + GCM decrypt/tamper-check
  • TLS: read a real cert's issuer/subject/validity + negotiated protocol version
  • Crack NoteVault's own MD5 hashes for the term-project report
  • Every screenshot needs your whoami + student ID + timestamp in-frame — this lab's raw output is identical for the whole cohort by design, so unstamped evidence isn't gradable
  • + Audit the AI / EiPE / Prompt Problem (see worksheet)

Key takeaways

  • Encoding ≠ encryption; hashing isn't reversible
  • Use vetted KDFs + authenticated encryption — never roll your own
  • Randomness & key handling matter as much as the algorithm

Questions?

Next week: Injection & input handling

All weeks in Software Security