※本記事は2016年10月17日に米国で掲載されたブログ記事の抄訳を基にしています。

先日、私たちはAdobe Readerのゼロデイ脆弱性を2個発見しました。Adobeはその後これらの脆弱性を修正するパッチをリリース(2016年10月6日)しており、脆弱性はCVE-2016-6957およびCVE-2016-6958と命名されました。これらの脆弱性により、攻撃者はJavaScript API実行形式ファイルに関する制限を回避し(CVE-2016-6957)、また、Pythonなどで記述されたスクリプトの任意の実行を防ぐセキュリティ対策を回避する(CVE-2016-6958)ことで、Adobe Readerに対してセキュリティ侵害をもたらすことができます。このブログ記事では、これらの脆弱性に関するテクニカル ウォークスルーや、どのようなエクスプロイト方法があるのか、またPalo Alto Networksのお客様はどのように保護されるのかについてお伝えします。

PDFファイルの中にあるJavaScript

PDFファイルはフォーマットが極めて豊富なので、テキスト、図、そして3Dオブジェクトまでも表示することができます。ドキュメント内に埋め込まれたスクリプトを表示するJavaScriptエンジンも含まれています。そうしたスクリプトはユーザーとの対話を行う動的コンテンツを作成するのに使われます。AdobeのJavaScript APIマニュアルには、そのようなスクリプトで使うのに便利な関数やグローバル変数の大部分が文書化されています。

PDFファイル内のJavaScriptは、2つの異なるコンテキスト「権限付き」と「権限なし」のうち、どちらかの下で実行されます。「権限付き」コンテキストにおいて、呼び出し可能なAPI関数のセットは豊富で、一部の関数は十分慎重に扱わないと危険なものになる恐れがあります。そのような関数はセキュアな関数と呼ばれ、JavaScript APIマニュアルでは赤い丸囲みの「S」の印が付いています。

「JavaScript権限昇格」と呼ばれる一連の脆弱性があります。これらのバグにより、権限なしのコンテキスト下で実行されるJavaScriptが権限付きのコンテキスト下で任意のスクリプトを実行することが可能になり、所望のセキュアな関数をどれでも呼び出すことができてしまいます。このケースの例について考察してみましょう。

実行段階

通常、JavaScriptコードは権限なしのコンテキスト下で実行されますが、Adobeのエンジンによりコンテキストが権限付きコンテキストに自動的に昇格されてしまう例外が3つあります。

  1. バッチ アプリケーションの初期化—ドキュメントがロードされる際に実行されるJavaScript。これは攻撃者がいかなるJavaScriptコードもインジェクトすることができない極めて初期の段階で行われます。この段階に関する詳細については後述します。
  2. コンソール—Adobe Readerのデバッグ用コンソールから実行されるJavaScript。
  3. アプリケーション初期化イベント—あるイベントに関するコールバック。

バッチ段階

この段階においてAdobe ReaderはC:\Program Files\Adobe\Reader 11.0\Reader\Javascriptsディレクトリ下のあらゆるJavaScriptファイルを実行します。何らかの特定用途向けのスクリプトやプラグインが導入されていないかぎり、このディレクトリにはJSByteCodeWin.binファイルしか存在しないはずです。

JSByteCodeWin.binはSpiderMonkey 1.8 XDRバイトコードから構成されており、Gabor Molnar氏が作成したツールを使えば逆コンパイルすることができます。ツールはGabor Molnar氏のgithubにあります。JSByteCodeWin.binを逆コンパイルしてみると、Adobeがたくさんの関数をネイティブ コードでなくJavaScriptで実装していることが分かります。これらの関数の一部はセキュアな関数を実行する必要があるため、初期の段階または特有の段階だけでなく、遅い段階でセキュアな関数の実行を可能にする何らかのメカニズムが必要とされます。

信頼済み関数

この問題を解消するためAdobeは信頼済み関数という概念を導入しました。信頼済み関数はセキュアな関数app.trustedFunctionによって定義することができます。そのような関数は、実行段階を問わずセキュアな関数と呼ばれます。

AdobeはJSByteCodeWin.binの中にバッチ段階の信頼済み関数として関数を多数定義しており、それらは後の段階で呼び出されます。

一例としてANSendApprovalToAuthorEnabled関数があります。

信頼済み関数だからといって、権限付きのコンテキスト下でコード全体が実行されるわけではありません。これは、信頼済み関数がapp.beginPrivを呼び出すことでコンテキストを変更できることを示しているのにすぎず、app.beginPrivとapp.endPrivで挟まれたブロック内で実行されるコードにしか権限が付与されません。なお、権限付きのコンテキストは、beginPriv/endPrivブロック内から呼び出される関数間で「引き継ぎ」されず、そうした関数も権限レベルの昇格を行うためにはapp.beginPrivを呼び出す必要があります。関数のコンテキストが権限レベルを下げる(app.endPrivを呼び出す)ことをせずに終了した場合、スコープの終わりで自動的に権限が下がります。

信頼済み関数の悪用

Adobeが以下のように、‘バッチ’段階で信頼済み関数を定義しているものとします。

つまり、後から、app.beginPrivを呼び出すことで、コンテキストを権限付きに昇格できます。vulnerableFunc関数は引数‘doc’を受け入れ、それが‘Doc’オブジェクト タイプであると想定します。

JavaScriptにはタイプセーフ チェックがないため、攻撃者はdoc引数として任意のオブジェクトを渡すことができます。このため、攻撃者は上記のコードを悪用して、必要な保護された関数を実行できます。

‘fakeDoc’という名前の新しいオブジェクトを作成します。その後、呼び出し対象のいくつかの保護された関数を参照するように、メンバー‘displayText’を設定します。さらに、‘vulnerableFunc’を呼び出して、引数として‘fakeDoc’を渡します。

vulnerableFunc関数はコンテキストを権限付きに変更し、app.beginPrivを呼び出します。vulnerableFuncは、それ以前に(たとえば、バッチ段階)信頼済み関数として定義されているため、このことが可能となります。その後、doc.displayTextを呼び出します。この時点では、保護された関数someSecuredFunctionを参照しています。そして、保護された関数の呼び出しをトリガします。これは、現在、コンテキストが権限付きのため実行可能です。

この例では、信頼済み関数が引数としてコールバックを受け取るという事実を悪用しました。しかし、JavaScriptは非常に動的な言語であるため、コールバックが意図せずに呼び出される他の多数のケースがあります。getter関数とsetter関数が定義されたり、グローバル変数が上書きされたりといったことがあります。

このような脆弱性を修正するためにAdobeが採ったアプローチは、特定の動作を防ぐようにJavaScriptエンジンを調整することです。例としては、getter/setter、eval()などからの権限付き関数への呼び出しの防御があります。

ANSendApprovalToAuthorEnabled

手動ですべての信頼済み関数を監査しているうちに、次の関数が見つかりました。

これは、任意の保護された関数を呼び出すための関数の候補として、かなり可能性が高いように見えます。引数としてdocを取り、doc.requestPermissionを2回呼び出しています。これは、完全に制御されています。唯一気になるのは、doc.requestPermissionがANSendApprovalToAuthorEnabledの権限付きの部分の中で呼び出されていないことです(app.beginPriv/app.endPrivの間ではない)。これをバイパスするのは非常に簡単です。実行する必要があるのは、権限レベルを昇格させるapp.beginPrivを参照するようにrequestPermissionを設定することだけです。

hasAddr条件をバイパスするには、doc.Collab.initiatorEmailを‘true’に設定する必要しかありません。これは関数呼び出しではなく、メンバー参照であるため、そこから権限付きコードを実行することはできない点に注意してください。

その後は、doc.requestPermissionへの最初呼び出しが、app.beginPrivへの呼び出しに変換され、コンテキストが権限付きに昇格されます。さらに、戻り値がpermisison.grantedと比較され、それによって、私が作成したカスタムgetter関数がトリガされます。その関数は、fakeDoc.requestPermissionを、呼び出し対象の保護された関数に変更します。その後、ANSendApprovalToAuthorEnabledがdoc.requestPermissionを再度呼び出し、今度は、権限付きのコンテキストから任意の保護された関数を呼び出します。

私がAdobe Readerを調べたときには、間違えて最新バージョンを調べませんでした。アップグレードした後に、この脆弱性はパッチが適用され、既にMWR Labs (CVE-2015-4451)によって検出されていることがわかりました。

CVE-2015-4451パッチ

まだ、私のエクスプロイト方法が失敗したことに驚いています。例外は、app.beginPriv関数への参照をfakeDoc.requestPermissionにコピーしたときに発生しました。

つまり、Adobeは、攻撃者がapp.beginPrivへの参照をコピーするのを阻止することで、バグを修正したのです。その関数への参照なしでは、コンテキストを権限付きに昇格できないため、保護されていない関数しか呼び出せなくなり、脅威にはなりません。その他にも、バグはまだ存在しています。app.beginPrivへの参照を取得する代替の方法を見つけることができれば、このバグは引き続きエクスプロイト可能です。

JavaScriptが助ける番です!

JavaScriptを悪用してapp.beginPrivへの参照をコピーする独創的な方法を見つけようとして、次の解決策を思い付きました。

グローバルなアプリ オブジェクトから直接app.beginPrivへの参照をコピーするのではなく、プロトタイプを別の新たに作成したオブジェクトにコピーしました。オブジェクトのプロトタイプをコピーするとは、そのメンバーとメソッドをすべてコピーするということです。その後、コピーしたインスタンスからbeginPrivへの参照を取得しました。Adobeによって阻止されませんでした。つまり、私は、引き続きANSendApprovalToAuthorEnabledを悪用して、任意の保護された関数を実行できます。

ネイティブコード実行の実現

HPのZDIチームは、DEFCON 2015でのプレゼンテーションでこれらの脆弱性を利用してネイティブコードを実行できることを示しました。この実行は、ディスクへのファイルの書き込みを可能にする、文書化されていない保護されたAPIを使用することで実現できます。

Collab.uriPutData関数を使用すると、任意のファイルをディスクに書き込むことができます。この操作はAdobe Readerのレンダリングプロセスからの実行にすぎませんが、それだけに整合性が低い状態でも実行できます。これは、ファイルへの書き込みを実行できる場所がきわめて限定されることを意味します。ZDIのプレゼンテーションで説明のあった方法は、サンドボックスを使用していないAdobe Acrobat Proエディションでのみ機能します。同チームは、Collab.uriPutDataを使用してディスクにDLLを書き込み、DLLを乗っ取るAdobe Readerのバグを利用してそのDLLをロードしていました。

筆者は、Adobe Readerの無償エディションで機能する別の方法も試してみました。

app.launchURL

これは、ブラウザを起動して、引数で指定されたURLを表示するセキュアな関数です。上記の権限付きJavaScriptの脆弱性を利用することで、攻撃者はこの関数を呼び出すことができます。

この関数を呼び出すと、デフォルトのブラウザが起動することがわかりました。そうなると、起動する実行可能ファイル(chrome.exe、firefox.exe、iexplore.exeなどのブラウザ)をこの関数がどのようにして判別しているのかが疑問点となります。

kernel32!CreateProcessWにブレークポイントを設定して調べてみたところ、shell32!ShellExecuteを使用するプロセスが作成されることがわかりました。

 

shell32!ShellExecute関数は、ファイルとオプションのプロトコルヘッダの指定を受け、的確なプロセスを起動するwinapiです。この関数は、プロトコルと拡張子のレジストリキーを照合し、起動するプロセスを判断しています。app.launchURLを呼び出すとデフォルトのブラウザが起動する理由はここにあります。デフォルトのブラウザは、自身を'http'プロトコルのハンドラとして登録するからです。この関数については、ShellExecuteのMSDNページに詳しい説明があります。

app.launchURLのドキュメントでの説明と異なり、この関数では'file://'で始まるURLを起動できます。'http'の場合と異なる点は、該当のパスからのアプリケーション起動を認めるかどうかの確認を求めるメッセージボックスが表示されることだけです。これは、任意のパスと任意の拡張子でshell32!ShellExecuteを呼び出すことができるということです。そこで、拡張子が'ps1'であるPowerScriptスクリプトを起動しようとしましたが、

起動しませんでした。つまり、PowerScriptスクリプトは実行できません。

拡張子のブラックリスト

Adobeではshell32!ShellExecuteを実行できることが危険であると考えていなかったことから、任意のコードを実行できる余地を攻撃者に与えるすべての拡張子のブラックリストを作成しています。このブラックリストは以下のレジストリキーにあります。

筆者はこのリストを調べ、Adobeがこのリストに追加していない拡張子がないか確認してみました。些細なすべての拡張子を除外するうえでAdobeはしっかりした作業をしています。Pythonスクリプトを除外しないようにしていたことが見て取れます。

しかし、Pythonファイル (コンソールを使用しないスクリプト) に相当する'.pyw'拡張子がブラックリストに入っていません。したがって、どのようなPythonスクリプトでも、インストールしてあれば、攻撃者はshell32!ShellExecuteを使用して実行できます。

結論

Adobeが実装したJavaScriptエンジンには、セキュリティ上の脆弱性につながる可能性のある関数が使用されています。JavaScriptに対するセキュリティ対策を適用するために、Adobeでは保護された関数や信頼できる関数などの各種機能を導入しています。一方で、これらのセキュリティ機能とJavaScript言語を組み合わせると、JavaScript言語の性質を利用して予期しない何らかの動作が引き起こされる可能性があります。これらの問題の軽減に向けたAdobeの取り組みはかなり徹底したものですが、エクスプロイトを可能にする余地が未だに残っています。この投稿で取り上げた問題に対するAdobeの迅速な注目と10月6日付けパッチのリリースに感謝いたします。

当社の次世代セキュリティプラットフォームを展開しているPalo Alto Networksのお客様は、ここで述べたようなゼロデイ脆弱性から保護されています。脅威防御 (当社の次世代ファイアウォールの本質的なサービス。アプリケーション制御IPSアンチマルウェアコマンドアンドコントロール保護などがあります)、WildFire、およびURLフィルタリングの各サービスでは、包括的な保護のほか、わずか5分前には知られていなかった脅威に対する自動更新を提供しています。