The case of the 9 bit AES key


We discovered that a number of PDF implementations generate their AES-256 file keys from state areas sufficiently small to enumerate. In MuPDF, solely 9 bits of random state have an effect on the precise AES-256 key. Other PDF editors like Foxit base their AES-256 key on time based mostly randomness and thus additionally result in keys which can be trivial to get well.

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 as soon as, utilizing the present 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 phrases. The low phrase is the fixed 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 must generate a key, it calls fz_memrnd(), which can repeatedly take a random quantity, step the random quantity generator ahead, after which use the bottom byte of this random quantity as a random byte.

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

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

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

The shift discards state bits 0 to 16, and the forged to unsigned char discards every part above bit 24. Every output byte due to this fact comes from bits 17 to 24 of the LCG state.

The subject is that the upper bits of the state by no means have an effect on the decrease bits. Multiplication and addition can carry bits upward, however not downward. Since every output byte comes from bits 17 to 24, something above bit 24 might be ignored, even after stepping the generator ahead.

That leaves 25 bits that may have an effect on the output. The backside 16 are at all times initialized to 0x330e, so solely 9 bits from the timestamp matter. Timestamps that differ by 512 seconds produce precisely the identical byte stream.

Since the attainable keyspace is so small, we will simply brute-force this by producing all attainable 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 vary(512)]

MuPDF makes use of the identical generator for the doc /ID, which is saved within the trailer in plaintext. When writing a brand new doc, it would first draw 32 bytes from the identical LCG after which instantly pattern the decryption key after, so /ID can be utilized as an extra verify.

Searching all 512 streams in a really unoptimized python implementation utilizing pure brute drive takes about 11 milliseconds. On a check doc:

/ID[0] = 7628523958da31d6c21c90e39344b28e

/ID[1] = 8be910bddaa1f1a542e5a27ceb115346

key = c3888ecc8ff554ac93acd689da2ecd6223cb760168622e8a0c44a533a037c212

That key decrypts the doc with out both 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 seems to have come from Foxit. Other editors might have picked it up via 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 center worth is m_EncryptKey, which is the uninitialized contents of the important thing buffer being hashed into itself. Assuming these bytes are zero stuffed, that is precisely the important thing derivation Foxit nonetheless makes use of in the present day.

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.



Source link