Skip to main content

All weeks · Worksheet · Overview

Week 2 · Lecture slides

Week 2

Contents16 sections

Hash Functions (password storage / cracking)

Security & Cryptography · Nutthakorn Chalaemwongwan


Today

  • What a cryptographic hash actually guarantees (and doesn't)
  • Why MD5/SHA-1 are "broken" — and why that barely matters here
  • The real failure: fast hash + no salt = passwords fall in ms
  • The fix: salting + slow KDFs (bcrypt/argon2/scrypt)
  • 🔓 Game: Crack the Leaked DB

Recap — Week 1

  • Week 1: adopt an attacker's mindset; threat-model a crypto system before trusting it
  • A system can use a perfectly sound primitive and still fail — because of how it's used
  • This week: the first concrete case study — a hash function, misused

What a cryptographic hash guarantees

  • Any input, any size → fixed-size digest (deterministic)
  • One-way: cheap to compute forward, expensive to invert
  • Small input change → unrecognizably different output (avalanche effect)
  • Used for: integrity checks, digital signatures, password storage (if done right)

Three security properties

PropertyAn attacker can't...Prevents
Preimage resistancefind any m with H(m) = h, given hreversing a leaked hash
2nd-preimage resistancefind m₂ ≠ m₁ with H(m₂) = H(m₁), given m₁swapping your file for a same-hash fake
Collision resistancefind any m₁ ≠ m₂ with H(m₁) = H(m₂)forging two same-hash documents from scratch

MD5 / SHA-1: broken — but where does it matter?

  • MD5 (2004) and SHA-1 ("SHAttered," Google/CWI, 2017) — practical collision attacks exist
  • Collision resistance is what digital signatures rely on → broken badly (attacker forges a second document with the same signature)
  • Password storage relies on something else entirely: the attacker not being able to guess fast enough

The real failure: passwords aren't "reversed"

  • An attacker with a leaked hash file does not invert the hash
  • They guess and hash — try a candidate, compare, repeat, at billions/sec
  • A fast hash (MD5, even plain SHA-256) is a gift to that attacker
  • Speed is a feature for integrity checks — it's a bug for password storage

CWE-916 — Use of Password Hash With Insufficient Computational Effort


Worked example: the vulnerable store

# vulnerable_app.py — /login
stored = lookup(username)          # md5 hex, no salt (users_vulnerable.csv)
ok = hashlib.md5(password.encode()).hexdigest() == stored
  • Same password → same hash, for every user, every time
  • One precomputed wordlist × MD5 table cracks the whole file at once
  • CWE-916 (fast primitive) + CWE-759 (no salt at all)

The attack: dictionary & rainbow tables

  • Dictionary attack: hash each candidate word, compare against the leaked hash
  • Unsalted + fast → one precomputed table cracks every matching row in the leak
  • Rainbow table: precomputed hash chains — trade storage for time, invert fast unsalted hashes at internet scale
  • Verified in this lab: admin's password cracked from a 101-word list in ~0.1 ms

Fix 1 — salting

  • Salt: a random value per user, stored alongside the hash — not secret
  • Store H(salt ‖ password) — identical passwords → different hashes
  • Defeats: precomputed rainbow tables (attacker needs a fresh table per salt) and cross-user correlation ("did two users pick the same password?")
  • A reused or constant salt gives none of this protection — it's just a longer password

CWE-759 / CWE-760 — one-way hash without a salt / with a predictable salt


Fix 2 — slow KDFs & work factor

  • bcrypt / argon2 / scrypt — deliberately, tunably slow
  • The cost / work factor controls how long one hash takes to compute
  • Raising cost: the defender pays once per real login (negligible); the attacker pays per guess, per row — the cost compounds against them
  • bcrypt buys work factor, not invincibility

The corrected construction

# fixed_app.py — /login
stored = lookup(username)      # $2b$<cost>$<salt><hash>  (users_fixed.csv)
ok = bcrypt.checkpw(password.encode(), stored)
  • One-line diff from the vulnerable version: bcrypt.checkpw(...) vs. md5(...) ==
  • Same md5-precompute-over-wordlist technique → 0 / 8 rows match (verified)
  • But: admin's real password (sunshine2021) is in the wordlist — a patient, slow, per-hash bcrypt dictionary attack would still find it, just at orders-of-magnitude higher cost

🔓 Game — Crack the Leaked DB

Two identical services, same users/passwords, different hashing:

ServicePortStoreCrackable by fast offline dictionary?
vulnerable_app.py:8094unsalted MD5Yes — milliseconds
fixed_app.py:8095bcrypt, per-user salt + costNo (to this technique)
  1. Exfil the leaked users_vulnerable.csv, crack admin's MD5 with wordlist.txt
  2. POST /login the recovered password, then GET /admin on the same session → flag
  3. Run the same technique against users_fixed.csv → prove it matches nothing
docker compose up -d      # vulnerable_app.py :8094, fixed_app.py :8095
python exploit.py         # PASS :8094 (flag) · PASS :8095 (0 bcrypt matches)

Lab today

📋 Worksheet 2 — labs/week02-hash/worksheet.md · kickoff: docker compose up -d

  • Part 2 (Q1–Q8): properties, broken hashes, fast-vs-slow, salts, rainbow tables, pepper
  • Part 3 (Tasks 0–5): crack the MD5 store, capture the flag, confirm bcrypt resists it
  • 🤖 Audit the AI: critique an AI's "secure" store_password — bare SHA-256 + a hardcoded constant "salt", framed as a benefit
  • 🧠 EiPE + Prompt Problem: explain the mechanism in plain English; stress-test an AI's password-storage advice

Key takeaways

  • Hash functions are textbook-secure — the math (preimage/collision resistance) holds
  • The real-system failure: using a fast, general-purpose hash where a slow, purpose-built KDF belongs
  • The attacker doesn't break the math — they guess and hash, fast, at scale
  • Fix = salt (kills precomputation) + slow KDF (raises cost) — still pair with a strong-password policy

Questions?

Next week: (see course roadmap)

All weeks in Security & Cryptography