
2026/10/29 5:51
9 ビット AES キーの事例
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
主要な PDF レンダリングライブラリ(MuPDF、Foxit Reader、OpenPDF、iText)に存在する深刻な脆弱性により、攻撃者は有効なパスワードなしでパスワード保護付きドキュメントを解読できてしまいます。根本原因は、AES-256 のセキュリティ基準を満たさない不安全なランダムソースの使用にあります。具体的には以下の通りです:
- MuPDF:
生成器を用いて鍵を生成しており、現在のエポック(秒単位)のみでシードされており、9 ビットのランダム状態しか作られません。これにより可能な鍵の組み合わせはわずか 512 個に限られ、ブルートフォース攻撃によって解読にミリ秒単位の時間しかかからなくなります。drand48 - Foxit Reader:現在のタイムスタンプと定数からハッシュ化して鍵を導出します。特定の平文フィールドが既知であるため、攻撃者は正しい鍵を検証する速度で毎秒約 4 億回の試行が可能となり、たとえ 10 年前のドキュメントであっても即座に復元できてしまいます。
- OpenPDF および iText:鍵生成を IVGenerator を通して経由させ、ミリ秒単位のタイムスタンプとシステムメモリでシードするとともに、決定的に PDF ヘッダー内への結果としての塩(salt)の平文保存を行います。これによりフルスペースのブルートフォースを行わずとも即時の鍵検証が可能になります。
この欠陥は 2014 年の PDFium コードベースに起源を持ち、これらのベンダーが採用しました。MuPDF はバージョン 1.28.5 で問題を修正していますが、旧バージョンは依然として脆弱であり、また他の影響を受けたソフトウェアに対する研究者らが公開した解読ツールも同様です。これらの脆弱性は、計算上の難易度ではなく予測可能な数学的模式を通じて機密データを露見させることで、業界全体におけるパスワード保護の根本を揺るがしています。
本文
PDF 暗号化における AES-256 鍵の脆弱性と復号化可能性調査
複数の PDF 実装で、AES-256 ファイル鍵が極めて小さな状態空間から生成されている事実が発見されました。具体的には以下の通りです。
- MuPDF: ランダムな状態に影響を与えるのは実際の AES-256 鍵に対してわずか 9 ビット に過ぎません。
- Foxit など: 鍵は時間ベースのランダム性に依存しており、結果として鍵は極めて容易に復号可能となります。
序論:PDF 暗号化の仕組みと問題点
PDF の暗号化は、メタデータやコンテンツストリームを保護するものであり(例外を除く)、パスワード保護機構は RC4 から AES へと進化し続けています。
改訂 6 の仕組み
- AES-256 が採用されています。
- ユーザー入力は直接的にドキュメントを暗号化せず、以下の手順を経ます。
- ランダムな 32 バイトのファイル鍵 を生成する。
- コンテンツをこの鍵で暗号化する。
- パスワードとソルトをハッシュ化して別の鍵(ユーザー鍵)を導出し、ファイルをさらに暗号化する。
- 改訂 6 ではハッシュ計算が意図的に遅く設定されていますが、旧来の AES-256(改訂 5 など)ではそうではありませんでした。
- ユーザーパスワードと所有者パスワードは同一のファイル鍵を解読するため、閲覧パスワードだけで無制約なコピー作成が可能になるリスクがあります。
発見された問題
- ランダム性の生成方法に重大なセキュリティ不備が含まれており、パスワードなしで PDF を復号可能な事例が多発しています。
MuPDF の脆弱性解析
MuPDF バージョン 1.28.5 以前は、
drand48 ファミリの関数を用いて乱数を生成していました。
実装の概要
- 種付け(Seed)は現在のエポック秒数のみを用いています。
で状態を 3 つの 16 ビット語に書き込みます:fz_srand48()// 下位: 定数 0x330e // 上位: 時刻情報 (ビット 16〜47) uint16_t p[3] = { 0x330e, seed, seed>>16 };- 鍵生成時は
が呼び出され、線形合同ジェネレータ(LCG)の状態を進めて最下位バイトを取得します:fz_memrnd()// 状態の上位ビットが下位ビットに影響しない構造 // 出力は常に LCG 状態のビット 17〜24 から得られる while (len-- > 0) *data++ = (unsigned char)fz_lrand48(ctx);
致命的な問題点
- キャリー(繰り上がり)がない: 乗算や加算は上位ビットへの影響はあるが、下位ビットへ逆転しないため、出力バイトに利用される低位部分と高位部分は独立しています。
- 有効な鍵空間の縮小:
- 状態の上位ビットが下位に影響を与えないため、25 ビット分のみに影響されます。
- そのうち下位 16 ビットは常に入出力に意味を持たない定数 (
) で初期化されるため、実際には意味を持つのは 時刻情報の上位 9 ビットだけです。0x330e
- 結果: 時刻が最大 512 秒 (約 8.5 分) 異なっても生成されるバイトストリームは完全に同じになります。
ブルートフォース攻撃の容易さ
非常に小さい鍵空間のため、全候補を網羅して試行復号すれば十分です:
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) # 512 つの候補(時刻の変化分)を生成 candidates = [stream(seed, 64)[32:64] for seed in range(512)]
実証事例
MuPDF はドキュメントトレーラーの
/ID フィールドにも同一のジェネレータを使用するため、これを確認することで追加の検証が可能です。
- 検索コスト: Python での単純なブルートフォースでも、512 つのストリームを検索するのに約 11 ミリ秒 しかかからない。
- 復号例: テストドキュメントにおいて、
の条件で以下のような鍵を取得し、パスワードなしで復号できました。time(NULL) mod 512 = 80
/ID[0] = 76 /ID[1] = 8b key = c3888ecc8ff554ac93acd689da2ecd6223cb760168622e8a0c44a533a037c212
Foxit PDF Reader の脆弱性解析
Foxit PDF Reader (バージョン 2026.1.3.36551) では、ファイル鍵は以下の式で導出されます。
鍵生成式
- 現在時刻と定数からなるハッシュを使用しています。
- 実際には「未初期化データ」部分はゼロ埋めされておりランダム性はありません:
SHA-256(uint32_le(time(NULL)) || <32 未初期化バイト> || "there")
Python での表現
from hashlib import sha256 key = sha256(timestamp.to_bytes(4, "little") + bytes(32) + b"there").digest()
復号化の効率性
ブロックに既知の平文が含まれるため、全ドキュメントの復号やパスワードハッシュ化は不要です。/Perms- 以下の Rust コードのように、1 つのハッシュ計算と AES ブロック復号のみで検証可能です:
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); // 復号成功の確認 (既知のマジック値チェック) (plain[..4] == self.permissions && plain[4..8] == [0xff; 4] && plain[8] == self.metadata && plain[9..12] == *b"adb") .then_some(key)
パフォーマンス比較 (Ryzen 9 7950X: 32 コア)
マルチスレッド処理(Rayon)を用いた逆算による検索結果:
| ターゲットタイムスタンプ | 検証候補数 | 検索時間 |
|---|---|---|
| 10 年前 | 315,573,571 | 0.84 s |
| Unix エポック(1970 年〜現在) | 1,792,610,110 | 4.85 s |
- 比較的新しいドキュメントの場合、数ミリ秒で鍵を取得できます。
timestamp = 1787583328 key = 41474b490265500f9727f21f1dde34be31bcdfc3830e2dc027daabe5b5984081 /Perms = fcfbffffffffffff54616462f67f0000 (復号済み)
鍵導出パターンの起源
この脆弱なパターンは、Foxit が起源である可能性が高いです。
- 2014 年 5 月に公開された最初のコミットに元コードが含まれており、ヘッダーで Foxit にクレジットされています。
- 他の多くの PDF エディタ(PDFium など)はこのコードを引き継いでいます。
歴史的なコード例 (Foxit 系)
中央の 32 バイト (
m_EncryptKey) はハッシュ化対象の未初期化バッファです。これをゼロ埋めと仮定すると、現代の Foxit の手法と一致します:
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); }
OpenPDF および iText の脆弱性
OpenPDF 3.0.5 と iText 9.7.1 では、
IVGenerator クラスを通じて以下の方法で暗号学的ランダム性を生成しています。
Java での実装例
arcfour = new ARCFOUREncryption(); long time = System.currentTimeMillis(); long mem = Runtime.getRuntime().freeMemory(); String s = time + "+" + mem; arcfour.prepareARCFOURKey(s.getBytes());
- 生成内容: 現在時刻(ミリ秒)と JVM のフリーメモリを種として ARC4 ストリムを作成。
- 利用箇所: このストリームからファイル鍵、およびドキュメント内に格納されたソルト (
,/U
) を生成します。/O - 攻撃ベクトル: 推測可能な「時刻 + メモリ量」に対して検証を行うことで鍵を復元できます。
結論としての問題点
- MuPDF や Foxit と比べると状態空間に分散がありますが、それでも256 ビットというセキュリティ強度が、実効的な状態空間(暗号学的に安全でない乱数源)によって損なわれています。
まとめと対策
すべての調査対象実装において、以下の事実は確認できました。
- ファイル鍵は暗号学的に安全ではない乱数源から生成されています。
- ユーザーパスワードのハッシュ化によるセキュリティ向上という本来の目的とは裏腹に、より弱い鍵へと繋がっています。
- MuPDFについてはベンダーへ開示され、修正済みですが、既公開のバージョンは保護できません。
- Foxitについても同様の脆弱性があり、復号ツールを公開しています。
これらの欠陥により、パスワードなしで PDF を閲覧・複製するリスクが現実化しています。