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. 下一步

你理解了为什么单靠加密是不够的。但你如何像安全工程师一样思考?你如何构建抵抗攻击的系统?

在下一篇文章中:建立安全判断能力——像攻击者一样思考、威胁建模,以及知道什么时候”足够好”真的足够好。