9 比特 AES 密钥事件

Hacker News Top 新闻

摘要

研究人员发现多个 PDF 实现(如 MuPDF 和 Foxit)在生成 AES-256 文件密钥时使用了极弱的随机性——MuPDF 的密钥仅受 9 比特随机状态影响,可通过暴力枚举实现无密码解密受保护的 PDF 文档。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/10/28 22:29

# 9位AES密钥之谜 来源: https://v12.sh/blog/the-case-of-the-9bit-aes-key 我们发现,多个PDF实现所生成的AES-256文件密钥,其状态空间小到可以被穷举。在MuPDF中,实际影响AES-256密钥的随机状态只有9位。其他PDF编辑器如Foxit的AES-256密钥基于时间随机性生成,因此同样导致密钥可以轻易恢复。MuPDF AES-256 密钥恢复工作表放在桌上 ## 引言 https://v12.sh/blog/the-case-of-the-9bit-aes-key#introduction PDF加密保护文档中的字符串和数据流,除非明确指定不加密,否则也包括元数据。密码保护机制多年来经历了多次修订,从RC4到AES,并不断强化密码哈希以抵御暴力破解攻击。当前版本为修订版6(Revision 6),使用AES-256。 当PDF使用AES-256加密时,密码并不直接加密文档。实现会生成一个随机的32字节文件密钥(file key),用它来加密内容。然后将密码与一个盐值(salt)进行哈希,派生出另一个独立密钥,用该密钥加密这个随机的32字节文件密钥。修订版6刻意使该哈希过程变慢;而较早的AES-256修订版5则没有这样做。 PDF可以有一个用于查看的用户密码(user password)和一个用于修改权限的所有者密码(owner password)。任一密码都能解锁同一个文件密钥,因此限制的执行取决于阅读器:拥有查看密码,你也可以制作一份不受限制的副本。 我们发现,这种随机性往往生成得非常不安全,使得受保护的PDF可以在没有密码的情况下被解密。 ## MuPDF https://v12.sh/blog/the-case-of-the-9bit-aes-key#mupdf MuPDF 1.28.5之前的版本使用 [drand48家族](https://pubs.opengroup.org/onlinepubs/7908799/xsh/drand48.html) 的一个实现来生成随机数。该生成器仅初始化一次种子,使用当前的秒级时间戳: ```c static void fz_init_random_context(fz_context *ctx) { /* ... */ fz_srand48(ctx, (uint32_t)time(NULL)); } ``` 该生成器是一个48位线性同余发生器(LCG):`s_{n+1} = (0x5deece66d * s_n + 0xb) mod 2^48`。`fz_srand48()` 将种子写入为三个16位字。最低位字为常量 `0x330E`,32位时间戳占据了状态的第16至47位: ```c void fz_srand48(fz_context *ctx, int32_t seed) { uint16_t p[3] = { 0x330e, seed, seed>>16 }; fz_seed48(ctx, p); } ``` 当MuPDF需要生成密钥时,会调用 `fz_memrnd()`,该函数会反复获取一个随机数,推进随机数发生器,然后取该随机数的最低字节作为随机字节。 ```c int32_t fz_nrand48(fz_context *ctx, uint16_t s[3]) { return fz_rand48_step(s, ctx->seed48+3) >> 17; } /* in fz_memrnd() */ while (len-- > 0) *data++ = (unsigned char)fz_lrand48(ctx); ``` 右移操作丢弃了状态的第0至16位,而转换为 `unsigned char` 则丢弃了第24位以上的所有内容。因此每个输出字节都来自LCG状态的第17至24位。 问题在于,状态的高位永远不会影响低位。乘法和加法只能产生向上进位,不能向下传递。由于每个输出字节来自第17至24位,即使在推进发生器之后,第24位以上的任何内容都可以忽略。这样只剩下25位可以影响输出。底部16位始终初始化为 `0x330E`,因此只有来自时间戳的9位是有效的。相差512秒的时间戳会产生完全相同的字节流。 由于可能的密钥空间如此之小,我们可以直接暴力破解:生成所有可能的32字节密钥并尝试解密: ```python def stream(seed, count): state = (seed << 16) | 0x330e out = bytearray() for _ in range(count): state = (0x5deece66d * state + 0xb) & ((1 << 48) - 1) out.append((state >> 17) & 0xff) return bytes(out) candidates = [stream(seed, 64)[32:64] for seed in range(512)] ``` MuPDF将同一生成器用于文档的 `/ID`,而 `/ID` 以明文存储在文件trailer中。在写入新文档时,MuPDF会先从同一个LCG中提取32字节,然后紧接着采样解密密钥,因此 `/ID` 可以用作额外的校验手段。 在一个非常未优化的纯暴力Python实现中,搜索所有512个流大约需要11毫秒。对于一个测试文档: ``` time(NULL) mod 512 = 80 /ID[0] = 7628523958da31d6c21c90e39344b28e /ID[1] = 8be910bddaa1f1a542e5a27ceb115346 key = c3888ecc8ff554ac93acd689da2ecd6223cb760168622e8a0c44a533a037c212 ``` 该密钥无需任何密码即可解密文档。 ## Foxit https://v12.sh/blog/the-case-of-the-9bit-aes-key#foxit Foxit PDF Reader 2026.1.3.36551 将文件密钥派生为当前时间戳、32个未初始化字节和一个常量的哈希值。实际上这些未初始化字节被零填充,并未贡献任何随机性。 ``` SHA-256(uint32_le(time(NULL)) || <32 uninitialized bytes> || "there") ``` 或者等价的Python代码片段: ```python from hashlib import sha256 key = sha256(timestamp.to_bytes(4, "little") + bytes(32) + b"there").digest() ``` `/Perms` 权限块包含已知明文,因此候选验证只需一次哈希和一次AES块解密,无需密码哈希,也无需解密文档: ```rust let mut material = [0u8; 41]; material[..4].copy_from_slice(&stamp.to_le_bytes()); material[36..].copy_from_slice(b"there"); let key: [u8; 32] = Sha256::digest(material).into(); let cipher = Aes256::new(&key.into()); let mut plain = Block::from(self.perms); cipher.decrypt_block(&mut plain); // 检查一些已知的魔法值以确认解密成功 (plain[..4] == self.permissions && plain[4..8] == [0xff; 4] && plain[8] == self.metadata && plain[9..12] == *b"adb") .then_some(key) ``` 我们的Rust实现使用Rayon进行多线程,从当前时间向下搜索时间戳。在Ryzen 9 7950X上使用32线程,可以维持每秒3.7亿至3.76亿个时间戳的搜索速度: | 目标时间戳 | 测试候选数 | 搜索耗时 | |---|---|---| | 十年前 | 315,573,571 | 0.84秒 | | Unix纪元(1970年) | 1,792,610,110 | 4.85秒 | 第二行覆盖了从1970年至今的每一个时间戳。在实践中,对于相对较新生成的文档,恢复时间仅为毫秒级别。 ``` timestamp = 1787583328 key = 41474b490265500f9727f21f1dde34be31bcdfc3830e2dc027daabe5b5984081 /Perms = fcfbffffffffffff54616462f67f0000 (已解密) ``` ### 这种密钥派生方式从何而来 https://v12.sh/blog/the-case-of-the-9bit-aes-key#where-did-this-key-derivation-come-from 这种模式出现在多个不同的PDF编辑套件中,因此我们好奇它们是从哪里获得的。它已经存在于PDFium的[首次公开提交](https://github.com/chromium/pdfium/blob/5110c4743751145c4ae1934cd1d83bc6c55bb43f/core/src/fpdfapi/fpdf_parser/fpdf_parser_encrypt.cpp#L504-L531)中,该提交日期为2014年5月。[文件头](https://github.com/chromium/pdfium/blob/5110c4743751145c4ae1934cd1d83bc6c55bb43f/core/src/fpdfapi/fpdf_parser/fpdf_parser_encrypt.cpp#L1-L5) 注明原始代码来自Foxit,因此这段代码似乎源自Foxit。其他编辑器可能是通过PDFium获得的: ```c if (m_Revision >= 5) { int t = (int)time(NULL); uint8_t sha[128]; CRYPT_SHA256Start(sha); CRYPT_SHA256Update(sha, (uint8_t*)&t, sizeof t); CRYPT_SHA256Update(sha, m_EncryptKey, 32); CRYPT_SHA256Update(sha, (uint8_t*)"there", 5); CRYPT_SHA256Finish(sha, m_EncryptKey); /* ... */ ``` 中间的32字节值是 `m_EncryptKey`,也就是将密钥缓冲区的未初始化内容哈希进了它自身。假设这些字节被零填充,这正是Foxit至今仍在使用的密钥派生方式。 ## OpenPDF与iText https://v12.sh/blog/the-case-of-the-9bit-aes-key#openpdf-and-itext OpenPDF 3.0.5 和 iText 9.7.1 都将加密随机数通过一个 `IVGenerator` 类来生成,该类使用一个以毫秒级时间戳和JVM空闲内存为种子的ARC4流: ```java arcfour = new ARCFOUREncryption(); long time = System.currentTimeMillis(); long mem = Runtime.getRuntime().freeMemory(); String s = time + "+" + mem; arcfour.prepareARCFOURKey(s.getBytes()); ``` 同一个流生成文件密钥和存储在 `/U` 和 `/O` 中的32字节明文盐值。我们可以针对这些盐值校验对时间戳和空闲内存的猜测,然后从相邻字节中恢复密钥。 虽然这里的变异度比MuPDF和Foxit的案例大得多,但这再次表明,256位安全性的承诺被不安全的随机源和低于标称的状态空间所破坏。 ## 结论 https://v12.sh/blog/the-case-of-the-9bit-aes-key#conclusion 在所有这些实现中,文件密钥都来源于非密码学安全的随机源。本应作为旧版从用户密码派生文件密钥的安全升级,在实践中反而导致了更弱的密钥。 这些问题已向相关厂商披露,MuPDF的漏洞也已修复。然而,此前已发布的副本无法被追溯性地加以保护,因此这些漏洞仍可能产生影响。 我们公开了针对MuPDF和Foxit的[解密工具](https://github.com/v12-security/pocs/tree/main/pdf_decryptors)。

相似文章

PDF 伪造文件出人意料地罕见(2022年)

Hacker News Top

这篇博客文章分析了在互联网欺诈中,尽管PDF编辑相对容易,但PDF伪造文件却出人意料地罕见,并探讨了诸如验证实践以及流通的多为新文件而非篡改文件等原因。

How hackers reverse Math.random()

Lobsters Hottest

本文演示如何逆向常见的伪随机数生成器(如线性同余生成器、XOR shift、Flash的RNG),并通过预测扫雷游戏中的地雷位置展示实际攻击,强调不要将普通随机数用于安全场景。

@leaf_sanren: https://x.com/leaf_sanren/status/2073069608437764266

X AI KOLs Timeline

作者详细介绍了如何通过逆向工程破解微信Mac版的加密系统,使用Frida动态插桩捕获系统函数CCKeyDerivationPBKDF生成的密钥,从而解密29个加密数据库,实现了自动获取群聊记录的目标,并计划开源该项目。

@geekbb: 阅后急焚,发一段加密文本,收到的人打开一次就自动销毁,服务器本身也看不到内容。 一个零知识加密的 Pastebin,前端用 Web Crypto API 做 AES-256-GCM 加密,密文才上传,密钥留在 URL 的 # 片段里不发给…

X AI KOLs Timeline

binthere is a zero-knowledge, end-to-end encrypted pastebin that self-destructs after one read. It uses AES-256-GCM encryption in the browser, stores only ciphertext on Cloudflare Workers, and includes a CLI client.