Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

加密 ≠ 安全:系統級失敗案例

| , 8 minutes reading.

1. 為什麼要關心這個問題?

你的應用使用 AES-256-GCM。你的 TLS 配置在 SSL Labs 獲得 A+。你的密碼使用 Argon2id 雜湊。

然後你還是被攻破了。

加密是門上的鎖。如果窗戶開著、鑰匙在門墊下面、或者牆是紙糊的,鎖就沒用了。

2. 安全思維差距

開發者的想法

開發者的心智模型:
「我加密了資料,所以它是安全的。」

現實:
┌─────────────────────────────────────────────────────────────┐
│                        攻擊面                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   明文資料 ───────────────────────┐                          │
│         ↓                        │                          │
│   [ 加密 ] ← 金鑰管理 ────────────┼── 金鑰暴露               │
│         ↓                        │                          │
│   加密資料 ──────────────────────┼── 儲存洩露               │
│         ↓                        │                          │
│   [ 解密 ] ← 存取控制 ────────────┼── 認證繞過               │
│         ↓                        │                          │
│   明文資料 ───────────────────────┘                          │
│                                                             │
│   加密只保護中間部分!                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

真實的威脅模型

大多數洩露不是破解加密。它們:
- 竊取金鑰
- 在加密前/後存取資料
- 繞過認證
- 利用配置錯誤
- 使用社會工程
- 發現邏輯缺陷

強加密 + 弱運維 = 弱系統

3. 明文暴露

案例 1:記錄敏感資料

# 災難:在加密前記錄明文
import logging

def process_payment(card_number, amount):
    logging.info(f"處理支付: card={card_number}, amount={amount}")
    encrypted_card = encrypt(card_number)
    # 卡號現在永遠在日誌檔案中
    # 日誌聚合服務
    # 開發者筆記型電腦
    # 備份系統

# 加密的資料庫是安全的。
# 日誌在 47 個不同的地方,未加密。

案例 2:錯誤訊息

# 災難:包含敏感資料的例外
def decrypt_user_data(encrypted_data, key):
    try:
        return decrypt(key, encrypted_data)
    except DecryptionError as e:
        # 錯誤訊息包含金鑰!
        raise Exception(f"使用金鑰 {key} 解密失敗: {e}")

# 錯誤追蹤服務(Sentry、Bugsnag)現在有你的金鑰了

案例 3:記憶體傾印和核心傾印

當行程崩潰時,作業系統可能將記憶體傾印到磁碟:
- /var/crash/
- Windows 錯誤報告
- Docker 容器日誌

記憶體包含:
- 解密的資料
- 加密金鑰
- 傳輸中的密碼

「加密的」資料以明文形式存在於記憶體中。
崩潰 = 永久的明文記錄。

案例 4:交換分區和休眠

作業系統可能將記憶體寫入磁碟:

交換空間:
- RAM 溢位寫入磁碟
- 包括解密的金鑰
- 重新開機後可能仍然存在

休眠:
- 整個 RAM 寫入磁碟
- 所有金鑰未加密儲存
- 可透過磁碟存取恢復

雲端虛擬機:
- 虛擬機管理程式可以讀取客戶機記憶體
- 即時遷移複製所有 RAM
- 快照包含記憶體狀態

4. 金鑰管理失誤

案例 1:原始碼中的金鑰

# GitHub 搜尋:「AES_KEY」或「encryption_key」或「secret_key」
# 數百萬個結果

# 在真實儲存庫中發現:
AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"
ENCRYPTION_KEY = "super_secret_key_12345"
DATABASE_PASSWORD = "admin123"

# 一旦提交,即使刪除:
# - 仍在 git 歷史中
# - 被 GitHub 搜尋快取
# - 儲存在開發者機器上
# - 被多次備份

案例 2:環境變數中的金鑰(洩露的)

# docker-compose.yml 提交到儲存庫
services:
  app:
    environment:
      - ENCRYPTION_KEY=aGVsbG8gd29ybGQK
      - DATABASE_URL=postgres://user:password@db/prod

# CI/CD 日誌經常列印環境變數
# 容器檢查會暴露環境變數
# 行程列表會顯示環境變數

案例 3:跨環境金鑰重用

常見反模式:
- 開發、測試和生產使用相同的金鑰
- 開發者筆記型電腦有生產金鑰
- 測試資料庫使用生產金鑰加密

開發環境洩露 = 生產環境洩露

案例 4:不輪換金鑰

公司使用同一個加密金鑰 10 年:
- 多名離職員工曾有存取權限
- 金鑰可能在未被偵測的情況下洩露
- 如果金鑰被洩露,所有歷史資料都有風險
- 無法限制影響範圍

金鑰輪換提供:
- 有限的暴露視窗
- 撤銷被洩露金鑰的能力
- 合規性
- 密碼學衛生

5. 運維安全失誤

案例 1:備份

生產資料庫:靜態加密 ✓
資料庫備份:未加密,儲存在 S3 ✗

真實事件(2019):
- 公司正確加密了他們的 MongoDB
- 備份指令碼傾印到未加密的 S3 儲存桶
- 儲存桶是公開的
- 2.5 億條記錄暴露

加密是完美的。
備份過程完全繞過了它。

案例 2:開發和除錯存取

# 帶有除錯端點的生產程式碼
@app.route('/debug/user/<user_id>')
def debug_user(user_id):
    if request.args.get('debug_key') == 'supersecret':
        user = get_user(user_id)
        return {
            'encrypted_data': user.encrypted_data,
            'encryption_key': user.encryption_key,  # 😱
            'decrypted_data': decrypt(user.encrypted_data, user.encryption_key)
        }

# 「我們會在生產前刪除這個」
# 旁白:他們沒有。

案例 3:支援和管理員存取

典型企業:
- 50+ 人有生產資料庫存取權限
- 200+ 人有解密金鑰存取權限
- 500+ 人理論上可以存取資料

每個人都是:
- 潛在的內部威脅
- 社會工程的目標
- 筆記型電腦被盜的風險
- 帳戶被入侵的風險

當授權使用者是威脅時,加密沒有幫助。

案例 4:第三方整合

你的安全:
- 端到端加密儲存
- HSM 支援的金鑰管理
- 零信任架構

你的供應商整合:
「只需將客戶資料作為 JSON POST 到我們的 webhook」

// 在生產中發現的實際程式碼
async function sendToVendor(customer) {
    await fetch('https://vendor.com/webhook', {
        method: 'POST',
        body: JSON.stringify({
            ssn: customer.ssn,                    // 😱
            bank_account: customer.bank_account,  // 😱
            password: customer.password           // 😱😱😱
        })
    });
}

6. 側通道洩露

實踐中的時序攻擊

# 認證時序洩露
def authenticate(username, password):
    user = database.get_user(username)

    if user is None:
        return False  # 快:使用者不存在

    if not verify_password(password, user.password_hash):
        return False  # 慢:bcrypt 比較

    return True

# 攻擊者可以列舉有效使用者名稱:
# 無效使用者名稱:1ms 回應
# 有效使用者名稱:500ms 回應(bcrypt)

基於快取的洩露

AES 實現可能有時序變化:
- 表查找取決於快取狀態
- 不同的金鑰位元組 = 不同的快取模式
- 可從同一機器或虛擬機測量

研究已證明:
- 從共置虛擬機擷取 AES 金鑰
- 跨行程金鑰擷取
- JavaScript 快取時序攻擊

你「加密」資料的金鑰透過 CPU 快取時序洩露了。

基於網路的洩露

HTTPS 保護內容,不保護中繼資料:

網路可觀察到的:
- 請求時序(你何時存取資料)
- 請求大小(大約你存取什麼)
- 請求頻率(你多久檢查一次)
- 存取的端點(你使用哪些功能)

流量分析可以揭示:
- 你存取什麼網站(透過封包大小)
- 你在輸入什麼(透過擊鍵時序)
- 你在觀看什麼(透過頻寬模式)

7. 架構失誤

案例 1:客戶端「安全」

// 瀏覽器中「加密的」密碼儲存
function savePassword(password) {
    const encrypted = btoa(password);  // 這是 base64,不是加密!
    localStorage.setItem('password', encrypted);
}

// 即使使用真正的加密:
const key = 'hardcoded_key_in_js';  // 在原始碼中可見
const encrypted = CryptoJS.AES.encrypt(password, key);
// 任何人都可以讀取原始碼並解密

案例 2:透過隱蔽實現安全

物聯網裝置的真實例子:
- 通訊使用互斥或「加密」
- 金鑰是字串 "security"
- 對於更長的訊息重複

「沒人會逆向工程我們的協定」
旁白:只花了 15 分鐘。

案例 3:先加密後認證 vs 先認證後加密

錯誤的順序(易受填充預言攻擊):
1. 加密明文
2. 計算密文的 MAC
3. 攻擊者修改密文
4. 伺服器先解密,再檢查 MAC
5. 解密錯誤洩露資訊!

正確的順序(或使用 AEAD):
1. 計算 MAC
2. 加密(明文 + MAC)
3. 攻擊者修改在解密前被偵測到

或者直接使用正確處理這一點的 AES-GCM。

案例 4:混淆代理人

# 伺服器正確地按使用者加密資料
def get_document(user_id, doc_id):
    doc = database.get_document(doc_id)
    # 使用使用者的金鑰解密
    key = get_user_key(user_id)
    return decrypt(doc.encrypted_content, key)

# 但授權檢查是錯誤的!
def get_document(user_id, doc_id):
    doc = database.get_document(doc_id)
    # 忘記檢查 user_id 是否擁有 doc_id!
    key = get_user_key(user_id)  # 獲取錯誤使用者的金鑰
    return decrypt(doc.encrypted_content, key)  # 解密失敗...或者更糟

# 如果攻擊者用自己的內容替換加密內容會怎樣?
# 如果金鑰意外共享會怎樣?

8. 真實世界洩露案例

Capital One(2019)

他們有的:
- AWS 靜態加密
- AWS 傳輸加密
- 正確的金鑰管理

出了什麼問題:
- WAF 中的 SSRF 漏洞
- 攻擊者存取執行個體中繼資料
- 獲得 IAM 憑證
- 使用憑證存取 S3
- 下載 1 億條客戶記錄

加密無關緊要。
存取控制失誤 = 洩露。

Equifax(2017)

他們有的:
- 加密的資料庫
- 安全團隊
- 合規認證

出了什麼問題:
- 未打補丁的 Apache Struts(CVE 已知數月)
- 攻擊者獲得 shell 存取
- 透過應用程式存取資料(應用程式有解密權限)
- 1.47 億條記錄暴露

加密無關緊要。
應用程式是授權的存取者。

Adobe(2013)

他們有的:
- 加密的密碼(但使用 ECB 模式)
- 所有密碼使用相同的金鑰

出了什麼問題:
- 資料庫洩露(獨立的漏洞)
- ECB 模式:相同密碼 = 相同密文
- 密碼提示以明文儲存
- 交叉參照提示和密文模式

使用者 1:密文 ABC,提示:「我貓的名字」
使用者 2:密文 ABC,提示:「和 fiskers 押韻」
使用者 3:密文 ABC,提示:「whiskers」

攻擊者在不破解加密的情況下解密了數百萬密碼。

9. 什麼真正有效

縱深防禦

第 1 層:網路安全
- 防火牆、隔離、WAF
- 阻止基於網路的攻擊

第 2 層:認證和授權
- 強認證(MFA)
- 最小權限原則
- 阻止未授權存取

第 3 層:應用安全
- 輸入驗證
- 安全編碼實踐
- 阻止應用層攻擊

第 4 層:資料安全
- 靜態和傳輸加密
- 金鑰管理
- 在其他層失敗時阻止資料被盜

第 5 層:監控和回應
- 日誌、警報、事件回應
- 偵測和遏制洩露

每一層捕獲其他層遺漏的。

實用檢查清單

部署前驗證:

[ ] 金鑰不在原始碼或日誌中
[ ] 加密金鑰定期輪換
[ ] 存取控制已測試(嘗試繞過它們!)
[ ] 備份使用不同的金鑰加密
[ ] 除錯端點已刪除
[ ] 第三方整合已保護
[ ] 監控和警報已配置
[ ] 事件回應計劃存在
[ ] 所有團隊成員接受過安全訓練
[ ] 定期安全稽核已安排

10. 本章小結

三點要記住:

  1. 加密只保護一種狀態的資料。 資料存在於加密前、解密後、記憶體中、日誌中、備份中、錯誤訊息中。加密只保護加密的形式。

  2. 存取控制失誤繞過加密。 如果攻擊者可以透過你的應用程式存取資料(應用程式必須解密才能使用),你的加密就無關緊要了。大多數洩露是存取控制失誤,而不是密碼學被破解。

  3. 安全是系統屬性,不是功能。 你不能新增加密就「完成」安全。安全需要持續關注運維、監控、存取控制和人為因素。

11. 下一步

你理解了為什麼單靠加密是不夠的。但你如何像安全工程師一樣思考?你如何建構抵抗攻擊的系統?

在下一篇文章中:建立安全判斷能力——像攻擊者一樣思考、威脅建模,以及知道什麼時候「足夠好」真的足夠好。