0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AES-256のはずが9ビットで足りる——MuPDFの暗号化PDFが軽く破られる背景

0
Posted at

「AES-256なら安心」の直感が外れる瞬間

画像: v12.sh より
画像: v12.sh より

PDF の暗号化というと、つい「AES-256 だから強い」と思いがちです。けれど実際の安全性は、暗号アルゴリズムそのものだけでは決まりません。どんな鍵を使うか、どう作るか、どこから乱数を取るかで、同じ AES-256 でも強度はまるで変わります。今回の話は、その差が極端に出た例です。

MuPDF は PDF の書き込み・読み込みで AES-256 を扱います。問題は、文書の中身を守る 32 バイトのファイル鍵が、十分に広い空間から生まれていなかったことです。見かけ上は 256 ビットでも、実際に変化するのはごく一部だけ。結果として、候補を片っ端から試すだけで開ける状況が生まれました。

ここで大事なのは、暗号を「解いた」のではない点です。AES-256 の計算を破るのではなく、鍵が作られる入口が細かった。セキュリティ事故ではよくある構図ですが、利用者からは見えにくい。見た目は堅牢でも、実装の都合で小さな穴が空いていると、その穴が全体を支配します。

PDF の暗号化は少し入り組んでいます。パスワードはそのまま本文を暗号化しません。まずランダムな 32 バイトのファイル鍵を作り、それをコンテンツの暗号化に使います。さらにパスワードと salt から別の鍵を導き、その 32 バイト鍵を包みます。つまり、守るべき要は「パスワードの強さ」だけでなく、「この 32 バイトがどれだけ予測しにくいか」に移るわけです。

どこで小さくなったのかを、乱数生成の素性から見る

MuPDF の古い実装では、乱数は drand48 系の 48 ビット線形合同法で作られていました。これは Open Group の仕様にもある古典的な疑似乱数生成器です。使いやすく高速ですが、状態空間や初期化の扱いを誤ると、出力に思ったほどの多様性が出ません。長く使われてきた分、弱点もよく知られています。

実装では、この生成器が一度だけ現在時刻で seed されていました。しかも seed に使うのは秒単位の Unix 時刻です。48 ビットの内部状態があるように見えても、実際に動かす起点は「今の秒」だけ。srand48() では上位 32 ビットに時刻が入り、下位 16 ビットは固定値 0x330e になります。ここで既に、自由度はかなり削られています。

さらに重要なのは、MuPDF が鍵を作るときにこの生成器から byte を順に抜いていたことです。lrand48() は 48 ビット状態から高位側のビットを使って値を返しますが、上位の情報は下位に伝播しません。線形合同法の掛け算と足し算では、上のビットは下のビットの影響を受けても、その逆は起きない。結果として、ある範囲のビットだけが出力に効く構造になります。

この性質は、生成器の理屈を知っていれば不思議ではありません。Wikipedia の解説にもある通り、2 の冪を法とする LCG は、低いビットほど周期が短く、上位ビットだけが比較的まともに見えることがあります。MuPDF の場合は、1 バイトを切り出す位置が状態の比較的低い側だったため、seed の自由度の大半が出力に届かず、鍵空間が小さく縮んでしまったわけです。

512 通りしか残らない、という見え方

ここで効いてくるのが、9 ビットという数字です。生成器の状態は 48 ビットありますが、低い 16 ビットは固定、出力に影響しない上位側のビットも無視できます。最終的に、時刻由来の 32 ビットのうち、実質的に鍵へ反映されるのは 9 ビットだけになる、というのが調査の要点でした。9 ビットなら候補は 2 の 9 乗、つまり 512 通りです。

512 通りというのは、人間の感覚でも少なすぎます。しかも PDF のファイル鍵は 32 バイトあるので、一見すると「32 バイト全部を総当たりする」ように見えてしまう。けれど実際には、候補生成器が 512 通りしかないなら、やることは 512 個の鍵候補を作って試すだけです。AES-256 の長さに惑わされると、本質を見失います。

調査では、純粋な brute force でも 512 通りを試すのに 11 ミリ秒程度だったと報告されています。計算量の感覚をつかむために雑に見積もると、1 候補あたり約 22 マイクロ秒です。もちろん、実装や環境が変われば前後しますし、この値だけで一般化はできません。それでも「暗号を割る」のではなく「候補を試す」だけで済む重さとしては、十分に軽い数字です。

しかも MuPDF は同じ生成器を文書の /ID にも使っていました。/ID はトレーラに平文で入るので、鍵候補の照合材料になります。ここが面白いところで、候補の正しさを確かめるために、わざわざ本文全体を解読しなくてもよくなる。候補の絞り込みがさらに簡単になる仕掛けが、同じ乱数系列の使い回しの中にあったのです。

なぜこの手の不具合は現場で見落とされるのか

日本の開発現場でも、PDF 出力は外部ライブラリに任せきりになりやすい分野です。帳票、見積書、契約書、監査ログ。どれも「開けること」が重要で、「どんな方法で鍵を作っているか」は後回しになりがちです。しかも暗号化機能は、普段は正常に動く限り問題が表に出ません。開けないより、開けてしまう方が見えにくい失敗です。

似た問題は、他の PDF 実装にも見つかっています。Foxit では時刻ベースの乱数や未初期化領域が鍵に混ざっていた例があり、OpenPDF や iText でも別の乱数経路から同種の脆さが見つかっています。つまり、MuPDF だけの特殊事故ではない。PDF の暗号化は仕様上の構造が複雑で、実装者が「どこまでを本当にランダムにすべきか」を見誤ると、似た穴が何度も生まれます。

ここで気をつけたいのは、「AES-256 なら安全」という表現が、現場で判断を鈍らせることです。暗号方式名は強さの象徴として扱われますが、実際に必要なのは、鍵導出・乱数・初期化・保存形式まで含めた全体設計です。特にライブラリ側が PDF の保護を肩代わりしている場合、呼び出す側は中身をほとんど意識しません。だからこそ、脆さが混じっても長く放置されます。

MuPDF は後に ChaCha20 ベースの暗号学的 PRNG へ置き換えられています。これは、単に「より新しい方式に変えた」という話ではありません。乱数の入口を、古典的で予測しやすい系列から、暗号用途を前提にした生成器へ切り替えた、という意味があります。では、どの程度まで候補空間が広がったのか。11 ミリ秒で開いた鍵は、なぜそこで止まったのか。そこを追うと、話は AES ではなく別の層に入っていきます。

👉 続き(核心はここから)は PaPoo で読めます: https://papoo.work/doc/bcba9d519dcf1aa2

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?