5.2 MD5 與 SHA 家族#
概念說明#
雜湊函數家族的演進史,就是「輸出長度與內部結構的軍備競賽史」:
| 演算法 | 年代 | 輸出位元 | 現況 |
|---|---|---|---|
| MD4 | 1990 | 128 | 已死(數分鐘內可碰撞) |
| MD5 | 1991 | 128 | 已破(2004 年實際碰撞;2012 年 Flame 惡意軟體用 MD5 偽造微軟簽章) |
| SHA-0 | 1993 | 160 | NSA 發布後即自行撤回 |
| SHA-1 | 1995 | 160 | 已破(2017 年 SHAttered 實際碰撞;Google/CWI 約 6500 年單機算力的工作量、以雲端數小時完成) |
| SHA-2 | 2001 | 224/256/384/512 | 安全,現行主力 |
| SHA-3 | 2015 | 224~512 | 安全,與 SHA-2 結構完全不同,作為備援 |
MD5 的崩潰:差分分析與選擇前綴碰撞#
MD5(Merkle–Damgård 結構,見 5.3)輸出僅 128 位元,理論碰撞成本只有生日界 2^64;更糟的是其內部壓縮函數存在嚴重差分弱點,Wang 等人 2004 年發表可構造的碰撞(兩個不同檔案同 MD5),選擇前綴碰撞後續也被工程化——攻擊者可構造「兩份完全不同的文件/憑證/程式,MD5 相同」,讓可信第三方簽了無害文件、攻擊者拿同一簽章套在惡意文件上。
SHA-2 與 SHA-3 的差異#
- SHA-2(SHA-256/512):仍是 Merkle–Damgård 結構,壓縮函數基於 64 輪的 ARX 運算(與 ChaCha20 同族的加減移互斥或),至今無實際攻擊。輸出長度對應安全度:SHA-256 ⇒ 碰撞 128 位元安全。
- SHA-3(Keccak):採用完全不同的海綿結構(sponge construction)——內部 1600 位元狀態,重複「吸收(absorb)輸入、擠出(squeeze)輸出」,速率 r 與容量 c 的取捨給出理論上更廣的證明框架,且可衍生出可變長度輸出 SHAKE128/256。設計動機之一正是「萬一 SHA-2 被破,世上還有一個結構不同的標準」。
Python 實作#
import hashlib
data = b"hello"
for name in ["md5", "sha1", "sha256", "sha3_256"]:
h = hashlib.new(name, data)
print(f"{name:8} {h.hexdigest()[:32]}... ({h.digest_size*8} bits)")
# SHAKE 可變長度輸出
shake = hashlib.shake_128(data).hexdigest(16) # 16 bytes
print("shake128:", shake)
# MD5 碰撞的現成例子(CVE 常用的 demo pair 內容不同但 MD5 相同)
import binascii
a = bytes.fromhex(
"d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f89")
# 完整 128 位元組在網路上可取得「fastcoll」「md5collgen」產生
print("示意:兩個不同檔案可以 md5 相同——這就是為何 MD5 不能再用於簽章")
安全性分析#
- 選擇指引:新系統一律 SHA-256 以上;簽章、憑證、區塊鏈用 SHA-256/384;需要抗「結構性風險」可併用 SHA-3。
- MD5/SHA-1 的殘存用途:僅限「非對抗性」場合(如本機快取鍵值),且要明白連 TLS 憑證都已全面淘汰它們。
- 雜湊不是密碼雜湊:
sha256(password)速度極快,正是 GPU 字典攻擊的最愛;密碼儲存請用 Argon2id/bcrypt/scrypt/PBKDF2(慢函數),詳見 9.3 節。
練習#
- 用 Python 驗證 SHA-256 的雪崩效應:隨機產生 1000 對「只差一個 bit 的輸入」,統計輸出平均翻轉位元數,是否接近 128。
- 上網查「SHAttered attack」頁面提供的兩個 PDF 樣本,用
hashlib驗證它們 SHA-1 相同但內容不同;再檢查它們的 SHA-256 是否不同。 - 解釋為什麼輸出 128 位元的雜湊「碰撞安全只有 64 位元」?用生日悖論(一年中兩人生日相同的機率)做直覺推導。