9位AES密钥案例
The case of the 9 bit AES key

原始链接: https://v12.sh/blog/the-case-of-the-9bit-aes-key

多种 PDF 工具使用弱随机性生成 AES-256 文件密钥。MuPDF 1.28.5 之前用 Unix 时间戳作为 drand48 的种子,仅留下九个有效比特;攻击者可暴力破解 512 个候选密钥,并使用 /ID 进行校验。Foxit 将密钥派生为 32 位时间戳、32 个零字节和 "there" 的 SHA-256;/Perms 已知明文允许快速验证,且时间戳可被快速搜索。OpenPDF 和 iText 用毫秒时间戳加上 JVM 空闲内存来种子化 ARC4,明文盐值让攻击者可验证猜测。因此,尽管密码哈希较慢,AES-256 保护仍可被绕过。厂商已收到通知;MuPDF 已修复该问题,但此前加密的 PDF 可能仍然脆弱。MuPDF 和 Foxit 的解密器已发布。

Hacker News 最新 | 历史 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 9 位 AES 密钥之谜 ( v12.sh ) 17 分 由 ledoge 发布于 1 小时前 | 隐藏 | 历史 | 收藏 | 2 条评论 帮助 paulpauper 59 分钟前 | 下一条 [–] 这就像弱硬件钱包熵漏洞 回复 jeffbee 14 分钟前 | 上一条 [–] 反正要用用 C 写的 PDF 工具,你得是完全疯了。 回复 考虑申请 YC 2027 年冬季批次!申请开放至 11 月 2 日。 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

We found that several PDF implementations generate their AES-256 file keys from state spaces small enough to enumerate. In MuPDF, only 9 bits of random state affect the actual AES-256 key. Other PDF editors like Foxit base their AES-256 key on time based randomness and thus also lead to keys that are trivial to recover.

MuPDF AES-256 key recovery worksheets on a desk

Introduction

PDF encryption protects the strings and datastreams in a PDF document, including metadata unless it is explicitly left unencrypted. The password protection mechanism has gone through a bunch of revisions over the years, moving from RC4 to AES and hardening password hashing against brute force attacks. The current version, revision 6, uses AES-256.

When a PDF is encrypted with AES-256, the password does not encrypt the document directly. The implementation generates a random 32-byte file key, which encrypts the content. The password and a salt are then hashed to derive a separate key, which encrypts this random 32-byte file key. Revision 6 makes this hashing deliberately slow; the older AES-256 revision 5 does not.

A PDF can have a user password for viewing and an owner password for changing permissions. Either unlocks the same file key, so the restrictions are up to the reader: with the viewing password, you can also make an unrestricted copy.

We found that this randomness is often generated very insecurely, allowing passwordless decryption of protected PDFs.

MuPDF

MuPDF versions before 1.28.5 use an implementation of the drand48 family to generate their random numbers. This generator is seeded once, using the current epoch in seconds:

static void fz_init_random_context(fz_context *ctx)

fz_srand48(ctx, (uint32_t)time(NULL));

The generator is a 48-bit linear congruential generator:

sn+1=(𝟶𝚡𝟻𝚍𝚎𝚎𝚌𝚎𝟼𝟼𝚍sn+𝟶𝚡𝚋)mod248

fz_srand48() writes the state as three 16-bit words. The low word is the constant 0x330e, and the 32-bit timestamp occupies state bits 16 to 47:

void fz_srand48(fz_context *ctx, int32_t seed)

uint16_t p[3] = { 0x330e, seed, seed>>16 };

When MuPDF needs to generate a key, it calls fz_memrnd(), which will repeatedly take a random number, step the random number generator forward, and then use the lowest byte of this random number as a random byte.

int32_t fz_nrand48(fz_context *ctx, uint16_t s[3])

return fz_rand48_step(s, ctx->seed48+3) >> 17;

*data++ = (unsigned char)fz_lrand48(ctx);

The shift discards state bits 0 to 16, and the cast to unsigned char discards everything above bit 24. Every output byte therefore comes from bits 17 to 24 of the LCG state.

The issue is that the higher bits of the state never affect the lower bits. Multiplication and addition can carry bits upward, but not downward. Since each output byte comes from bits 17 to 24, anything above bit 24 can be ignored, even after stepping the generator forward.

That leaves 25 bits that can affect the output. The bottom 16 are always initialized to 0x330e, so only nine bits from the timestamp matter. Timestamps that differ by 512 seconds produce exactly the same byte stream.

Since the possible keyspace is so small, we can just brute-force this by generating all possible 32-byte keys and trial decrypting:

state = (seed << 16) | 0x330e

state = (0x5deece66d * state + 0xb) & ((1 << 48) - 1)

out.append((state >> 17) & 0xff)

candidates = [stream(seed, 64)[32:64] for seed in range(512)]

MuPDF uses the same generator for the document /ID, which is stored in the trailer in plaintext. When writing a new document, it will first draw 32 bytes from the same LCG and then immediately sample the decryption key after, so /ID can be used as an additional check.

Searching all 512 streams in a very unoptimized python implementation using pure brute force takes about 11 milliseconds. On a test document:

/ID[0] = 7628523958da31d6c21c90e39344b28e

/ID[1] = 8be910bddaa1f1a542e5a27ceb115346

key = c3888ecc8ff554ac93acd689da2ecd6223cb760168622e8a0c44a533a037c212

That key decrypts the document without either password.

Foxit

Foxit PDF Reader 2026.1.3.36551 derives the file key as a hash of the current timestamp, 32 uninitialized bytes, and a constant. In practice these uninitialized bytes are zero filled and do not add any randomness.

SHA-256(uint32_le(time(NULL)) || <32 uninitialized bytes> || "there")

Or as an equivalent python snippet:

from hashlib import sha256

key = sha256(timestamp.to_bytes(4, "little") + bytes(32) + b"there").digest()

The /Perms permissions block contains known plaintext, so the candidate check is one hash and one AES block decryption, with no password hashing and no document decryption:

let mut material = [0u8; 41];

material[..4].copy_from_slice(&timestamp.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::<Aes256>::from(self.perms);

cipher.decrypt_block(&mut plain);

// Check for some known magic values to indicate successful decryption

(plain[..4] == self.permissions

&& plain[4..8] == [0xff; 4]

&& plain[8] == self.metadata

&& plain[9..12] == *b"adb")

Our Rust implementation searches timestamps downward from the current time, using Rayon for multithreading. On a Ryzen 9 7950X with 32 threads it sustains 370 to 376 million timestamps per second:

Target timestamp Candidates tested Search time
Ten years ago 315,573,571 0.84 s
Unix epoch 1,792,610,110 4.85 s

The second row covers every timestamp from 1970 to the present. In practice, for a document that was relatively recently generated, this will be in the order of milliseconds.

key = 41474b490265500f9727f21f1dde34be31bcdfc3830e2dc027daabe5b5984081

/Perms = fcfbffffffffffff54616462f67f0000 (decrypted)

Where did this key derivation come from

This exact pattern popped up in a couple different PDF editing suites, so we were curious where they got it from. It was already present in PDFium’s initial public commit from May 2014. The file header credits Foxit for the original code, so this code appears to have come from Foxit. Other editors may have picked it up through PDFium:

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);

The 32-byte middle value is m_EncryptKey, which is the uninitialized contents of the key buffer being hashed into itself. Assuming these bytes are zero filled, this is exactly the key derivation Foxit still uses today.

OpenPDF and iText

OpenPDF 3.0.5 and iText 9.7.1 both route cryptographic randomness through an IVGenerator class, an ARC4 stream seeded with a millisecond timestamp and the JVM’s free memory:

arcfour = new ARCFOUREncryption();

long time = System.currentTimeMillis();

long mem = Runtime.getRuntime().freeMemory();

String s = time + "+" + mem;

arcfour.prepareARCFOURKey(s.getBytes());

The same stream generates the file key and 32 bytes of salts stored in plaintext in /U and /O. We can check guesses for the timestamp and free memory against these salts, then recover the key from the adjacent bytes.

While there is significantly more variance here than in the MuPDF and Foxit examples, this once again shows that the promise of 256 bit security is undermined by insecure randomness sources and lower-than-advertised state space.

Conclusion

In each of these implementations, the file key is generated from a non-cryptographically secure source of randomness. What was supposed to be a security upgrade over older file keys derived from the user’s password instead led to weaker keys in practice. These issues were disclosed to the vendors and, in MuPDF’s case, fixed. Previously published copies cannot be retroactively protected, however, so these bugs may still have an impact.

We published our decryptors for MuPDF and Foxit.

联系我们 contact @ memedata.com