跳轉到

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 節。

練習#

  1. 用 Python 驗證 SHA-256 的雪崩效應:隨機產生 1000 對「只差一個 bit 的輸入」,統計輸出平均翻轉位元數,是否接近 128。
  2. 上網查「SHAttered attack」頁面提供的兩個 PDF 樣本,用 hashlib 驗證它們 SHA-1 相同但內容不同;再檢查它們的 SHA-256 是否不同。
  3. 解釋為什麼輸出 128 位元的雜湊「碰撞安全只有 64 位元」?用生日悖論(一年中兩人生日相同的機率)做直覺推導。