2015年6月1日に米国で掲載されたブログ記事の抄訳です。

弊社がCVE-2015-Xと呼んでいる脆弱性の根本的な原因に関する詳細な分析を以下に示します。また、その脆弱性を引き起こす方法についても順を追って説明します。Flashの脆弱性の詳細については、「エクスプロイト キットにおける最近のUAF脆弱性」(投稿者: Tao Yan、投稿日: 5月28日)も参照してください。

それほど前ではありませんでしたが、弊社は、CVE-2015-0359を悪用したものと思われる、Anglerエクスプロイト キット(MD5:049ff69bc23f36a78d86bbf1356c2f63c)からのサンプルを検出しました。この隠蔽されたSWFには、エンコード済みのSWF (MD5: d45808cfa6f3cbfb343fdea269fdc375)が含まれており、ディスクに保存せずに、後でFlashでデコードしてロードすることができます。このプロセスを改変したサンプルは次のとおりです。

埋め込まれたSWFは入念に隠蔽されたものですが、このコードとAngler EKのCVE-2015-0313エクスプロイトのソース コードとの間には多くの共通点があります。

まず最初に、このエクスプロイトが本当にCVE-2015-0359を対象としたものなのかどうかを調べる必要があります。研究者の中には、そう考えていない人もいます。埋め込まれたSWFには、CheckEnvironmentという関数が含まれており、この関数は最後に下記の処理を行います。

ここで、playerVerOkがtrueになるのがFlash Playerのバージョンが13.0.0.250以上で13.0.0.269よりも古い場合、またはFlash Playerのバージョンが15.0.0.189以上で16.0.0.305よりも古い場合であることに注目してください。しかし、関数は前述の各バージョンに加え、バージョン13.0.0.277と17.0.0.134に対してもtrueを返します。

つまり、それらのどのバージョンも存在しない場合は、以下のエクスプロイトが発生しないことがわかります。

さらに興味深いことに、(脆弱性を引き起こす)エクスプロイト自体が、次に示すように、Flashのバージョンに基づいて選択されています。

Flashバージョン13.0.0.250以上で13.0.0.269より古い場合、およびFlashバージョン15.0.0.189以上で16.0.0.305より古い場合は、CVE-2015-0313が使用されます。CVE-2015-0313の説明には、次のように記述されています。

「Windowsでは、13.0.0.269より前、および14.xから16.x (16.0.0.305より前)のAdobe Flash PlayerにおいてUse-After-Free脆弱性があり…」

Flashバージョンが13.0.0.277または17.0.0.134のときは、別のエクスプロイト(トリガー)が使用されます。これは、互いのエクスプロイト パスが異なっている唯一の行であるため、CVE-2015-0359のエクスプロイトであれば、CVE-2015-0313のものと類似した説明を期待できるはずですが、その代わりに、次のように説明されています。

「Windowsでは、13.0.0.281より前、および14.xから17.x (17.0.0.169より前)のAdobe Flash PlayerにおいてDouble-Free脆弱性があり…」

これは、CVE-2015-0359ではないと断言できます。また、Double-Free脆弱性(実際の証拠については、「技術分析」を参照)ではなく、Use-After-Free (UAF)脆弱性であると推測できます。したがって、この脆弱性はCVE-2015-Xと呼ぶことにします。

CVE-2015-Xのイベント履歴は次のとおりです。

2月2日:実世界でのゼロデイ エクスプロイトに付随して、CVE-2015-0313が検出される。

2月4日、5日:CVE-2015-0313に対応した自動/手動ダウンロード用のパッチ適用済みバージョンが、Adobeによってリリースされる。パッチ適用済みバージョンは16.0.0.305で、CVE-2015-Xも阻止する。

2月25日:CVE-2015-0313を対象とした完全に機能するエクスプロイト コードが公開される。数行のコードを変更するだけで、CVE-2015-Xが作成されることが判明。

3月12日:Flash Player 17.0.0.134がリリースされる。CVE-2015-Xに対して脆弱であることが判明。

4月14日:CVE-2015-0359およびCVE-2015-Xを両方とも修正するFlash Player 17.0.0.169がリリースされる。

この履歴を見ると、攻撃者には、CVE-2015-Xを標的とした開発を行ったり、Flash Player 17.0.0.134へのゼロデイ エクスプロイトとしてCVE-2015-Xを利用するために、まる1か月の余裕があったことがわかります。CVE-2015-0313およびCVE-2015-Xに対する攻撃は、どちらもパロアルトネットワークスのTrapsによって、エクスプロイトの初期段階で阻止されました。その際に、マシンと組織の双方に被害がもたらされることはありませんでした。脆弱性や攻撃に対する予備知識がないにもかかわらず、防御が提供されたのです。

この状況の重大さを把握した後、「さまざまな」脆弱性を引き起こす同様のエクスプロイトが少なくとも複数存在することを踏まえて、このような脆弱性の根本的な原因について理解を深めていきたいと思います。これらの脆弱性の根本的な原因や脆弱性のトリガー フローを示した詳細な技術分析について、この記事を読み進めてください。

技術分析

この技術分析では、Flash 16.0.0.287のActiveXコントロールを使用します。この分析は、Actionscript Virtual Machine (AVM)のソース コードとも相関しています。

CVE-2015-0313およびCVE-2015-Xのエクスプロイトは、どちらもFlashのByteArrayオブジェクトをdomainMemoryと連携させて使用しており、このdomainMemoryが、ダングリング ポインタを実際に使用します。これから、上記のUse-After-Free状況をどのように実現できるかを説明するとともに、ByteArrayクラスの設計に潜む重大な欠陥を示していきます。

まず、Flashが公開しているクラス名と、AVMで表示されるクラス名との間には、重要な違いがあることを明記する必要があります。例えば、FlashのByteArrayクラスはAVMソース コードのByteArrayObjectクラスによって表されており、バイト配列実装の内部表現である、AVMのByteArrayクラスと間違えないようにする必要があります。このリサーチ記事の残りの部分では、AVMで表示されるクラス名のみを使用します。

AVMはC++で記述されています。他の言語とは異なり、オブジェクトは、静的(実際のオブジェクトが戻り値になる)、動的(新しい演算子を使用して、オブジェクトへのポインタ(参照)が戻り値になる)という、2つの方法で作成されます。バイト配列を表すクラスでは、これら両方の方法を使用してメンバー変数を作成します。

ByteArrayObjectのメンバー変数は次のとおりです。

m_byteArrayは、ByteArrayオブジェクトの静的な割り当てです。Flashのテスト済みバージョンでは、ByteArrayObjectオブジェクトにおけるそのオフセットは0x18になります。

ByteArrayクラスは、DataInputとDataOutputの両方から継承するため、それらのオブジェクトの表現を暗黙的に含みます。弊社は、ByteArrayオブジェクト内にあるDataOutputオブジェクトのオフセットが0x8であることに注目しました。

ByteArrayのメンバー変数は次のとおりです。

ここには、次の3つの興味深いフィールドがあります。

m_buffer:1つのフィールドを持つクラスからなるオブジェクト。このフィールドは、ByteArray::Bufferクラスのオブジェクトをポイントしており、それをたどると実際のバイト配列に到達します。メモリ内では、このフィールドを先行する追加フィールドとして、FixedHeapRefクラスへの仮想関数表ポインタが存在します。m_bufferはByteArrayクラスのオフセット0x24にあるため、対象のフィールドはオフセット0x28にあります。

m_isShareable:これにより、ワーカー(Flashのスレッドに相当)間でバイト配列を共有できるかどうかを判別できます。これは特に、クラスのコピー コンストラクタに影響を及ぼします。バイト配列が共有されると、データをコピーする必要はなくなるからです。その代わりに、m_bufferがポイントしているものとまったく同じByteArray::Bufferオブジェクトをポイントすることになります。このフィールドは、オフセット0x2Cで検出可能です。

m_subscribers:これは、ListDataオブジェクトへのポインタを格納するオブジェクトです。ListDataオブジェクトには、バイト配列の再割り当て時や解放時(つまり、メモリ内における配置場所の変更時)、または単にそのサイズが変更された時の通知先であるエンティティに関する情報が格納されています。バイト配列は動的に拡張または消去できるため、この機能は重要ですが、domainMemoryなどの実際のバイト配列データへの直接的なポインタを有するエンティティのみが対象になります。ListDataオブジェクトへのポインタを有するフィールドは、ByteArrayオブジェクトのオフセット0x18に存在します。

m_bufferから到達可能なByteArray::Bufferオブジェクトには、次のような興味深いフィールドが存在します。

array (オフセット0x8):データを格納する実際のバイト配列へのポインタ。

capacity (オフセット0xC):配列が格納できる最大バイト数(実際の割り当て)。

length (オフセット0x10):プログラムが現在「使用している」配列バイト数。割り当て(capacityフィールドで判別可能)よりも多くのバイト数をプログラムで使用するときは、バイト配列の再割り当てが必要です。つまり、メモリ内の別のアドレスに配列ポインタのポイント先が変更されます。

最後に、m_subscribersから到達可能なListDataオブジェクトには、次の関連フィールドが存在します。

len (オフセット0x4):実際のバイト配列バッファを再割り当て(メモリ内の配置場所を変更)する際に通知する必要があるサブスクライバ数。

次の図は、調査対象のFlashバージョンにおけるCVE-2015-Xの関連オブジェクトと関連フィールドを示しています。

根本的な原因

CVE-2015-Xの根本的な原因として考えられるのは、ByteArrayのコピー コンストラクタに相当する機能が、共有可能なバイト配列を処理する方法です。バイト配列が共有可能な場合、コピー コンストラクタは、ByteArray::Bufferオブジェクトに対するまったく同じ参照を使用しますが、そのバイト配列のサブスクライバは、すべて忘れ去られます。これにより、Use-After-Free状態が発生しやすくなります。この状態では、実際のバイト配列へのポインタが変更されても(古いバイト配列が解放されても)、サブスクライバへの通知は行われないため、そのうちの1つが後でダングリング ポインタを使用しようとしてしまう事態が起きます。

実際には2つのコピー コンストラクタが存在しており、これらは両方とも、同じロジックの欠陥を抱えています。ワーカーと一緒に使用する次のコンストラクタを見てください。

ここでは、m_subscribersが空のサブスクライバ リストとして初期化されることがわかります。これにより、ByteaArray::Bufferオブジェクトの配列メンバーを対象とした変更通知を受信する必要があるサブスクライバの存在が事実上無視されます。上記の図を見ると、この現象がワーカーのみで発生しているように思えるかもしれませんが、他のコピー コンストラクタも同じように動作しており、ワーカーが関与しない状況において同様のバグを生じさせる可能性があります。

つまり、バグを引き起こすには、サブスクライバが存在するバイト配列で、コピー コンストラクタを使ってサブスクライバなしの新しいバイト配列を取得して、元のサブスクライバに通知せずに、コピーしたバイト配列でその配列ポインタを(バイト配列の容量を超えたデータを追加するなどして)変更する必要があります。その後、サブスクライバで、解放されたバッファを参照します。

ワーカー

CVE-2015-0313およびCVE-2015-Xを標的とした実世界のエクスプロイトでは、ワーカーが使用されていました。前述のコピー コンストラクタに関する解説では、ワーカーが使用された理由をある程度示しましたが、脆弱なコードの実行方法についてはまだ何も説明していません。コードをトリガーする方法について詳述する前に、まず、新しいワーカーを作成して実行したときに何が起きるか調べてみましょう。ActionScript 3 (AS3)コードの次の2つの行について考えてみましょう。

AVMソース コードの次のコメントには、問題発生の可能性が示唆されています。

ワーカーと実際の軽量スレッドを区別することの重要性については後述します。

Workerクラスのドキュメントには、ワーカーはバイト配列から作成されると記述されています。その後、次の処理が行われます。

初期化した後は、次の処理が行われます。

ワーカー用に新しいIsolateオブジェクトが作成されたことがわかります。もう一度、詳しく調べてみましょう。

ワーカーに関連付けられているIsolateがShellIsolateタイプ(Isolateから継承)であることがわかります。

startメソッドを使って実行を開始するようにワーカーに要求すると、次の関数が呼び出されます。

これにより、新しいスレッドが作成され、次のように開始されます。

IsolateはRunnableから継承されるので、VMThreadコンストラクタは、(Runnable*経由で)ShellIsolateオブジェクトのm_runnableにポインタを保存します。

VMThreadのstartメソッドを実行した後は、次の処理が行われます。

ネイティブの(プラットフォーム固有の)スレッドが作成され、その実行対象はstartInternal関数になります。この関数はパラメータとしてVMThreadオブジェクトを取得し、関連付けられているIsolateからrunメソッドを呼び出します。

また、別の内部関数が実行されます。

これからが注目箇所です。

ここでは、ワーカーのIsolate用に新しいTopLevelオブジェクトが作成されたことがわかります。setup関数には、次の行が含まれます。

なぜこれが重要なのかを理解するために、このTopLevelクラスについて調べてみましょう。Adobe社の説明は次のとおりです。

「最上位レベルには、中核となるActionScriptクラスとグローバル関数が含まれます。」

つまり、TopLevelクラスのオブジェクトは1回しか存在しないはずです。メイン スレッド(元のワーカー)のみの場合は、この条件が当てはまります。ただし、新しい(バックグラウンド)ワーカーを作成する場合、そのワーカーは新しいIsolateを使って作成され、新しいTopLevelオブジェクトも作成されます。前述の引用文を思い出してください。ワーカーの作成とは、環境のかなりの部分の個別インスタンスを作成することです。

新しいTopLevelオブジェクトがワーカー用に作成されているという事実が、エクスプロイトの成否の鍵となります。具体的に言うと、新しいTopLevelオブジェクトには空の_isolateInternedObjects表が含まれており、この表を利用することで脆弱なコードへのアクセスが可能になります。これについては後述します。

脆弱性のトリガー

ワーカー オブジェクトの仕組みについてある程度理解したら、CVE-2015-Xの脆弱性をトリガーする方法について、段階ごとの詳細な例を考慮する準備が整いました。

ステップ1:共有可能なFlashのByteArrayオブジェクトを作成し、domainMemoryに割り当てます。

AVMソースでは、この割り当ての影響を確認できます。

DomainEnv::globalMemorySubscribeでByteArray::addSubscriberへの呼び出しが生成されるときには、baオブジェクトの実際のデータを保持している、基礎となるバッファに対する変更が通知されるサブスクライバとして、domainMemory (現在のドメイン)が追加されます。

上の箇所をWindbgでデバッグして、ByteArray::addSubscriber (オフセット0x66B210)でブレークすると、次のように出力されます。

ecxレジスタには、thisポインタが格納されます。このポインタは、新しいサブスクライバ(現在のドメイン)を追加するByteArrayオブジェクト(ba)をポイントします。ByteArrayオブジェクトの興味深いフィールドに関する記述のとおり、次の結果になります。

関連するByteArray::Bufferオブジェクトは、アドレス0x79D7230 (オフセット0x28)にあります。

バイト配列は共有可能(オフセット0x2C)です。

サブスクライバ情報は、アドレス0x83050D0 (オフセット0x18)にあります。

ByteArray::Bufferオブジェクトを調べると、次の結果になります。

実際のバイト配列はメモリ内のアドレス0x8317000 (オフセット0x8)に存在しており、そのcapacity (オフセット0xC)とlength (オフセット0x10)は、0x1000 (4096バイト)です。

最後に、サブスクライバのListDataオブジェクトを調べると、次の結果になります。

lengthは0 (オフセット0x4)であるため、まだサブスクライバが存在しないことがわかります。addSubscriber関数の最後には新しいサブスクライバが存在するようになることを期待できますが、実際にそうなります。

ステップ2:引き続きメイン スレッド内でワーカーを作成し、ワーカーと共有するプロパティとして、共有可能なバイト配列を設定します。その後、ワーカー スレッドを開始します。

共有プロパティを設定するコードを見てみましょう。

Dion Blazakisのセミナー資料で説明されているとおり、atomとは、実際にはタグ付けされたポインタのことです。チャネル アイテムは、オブジェクトのタイプに基づいて次のように作成されます。

ByteArrayObjectクラスの適切なメンバー関数が呼び出されることを確認できます。

バイト配列は共有可能なため、共有プロパティとして格納されているチャネル アイテムによってByteArray::Bufferオブジェクト(このオブジェクトには実際の配列データへのポインタ、capacityとlengthなどが保持されている)へのポインタ、およびバイト配列が共有可能であるという事実が保存されることを確認できます。

注目すべきもう1つの重要な事実は、intern関数が使用されているということです。その処理について調べてみましょう。

m_valueは、ChannelItemオブジェクトの一部として保存したばかりのByteArray::Bufferオブジェクトです。これは、TopLevelオブジェクト内のinternObject関数に送信されます。その際には、当該Bufferオブジェクトに関連付けられた(つまり、Bufferオブジェクトを保持している)ByteArrayObjectオブジェクトへのポインタも一緒に送信されます。この関連付けは、TopLevelオブジェクトによって次のように内部保存されます。

したがって、keyはBufferオブジェクトへの参照に、valueはByteArrayObjectオブジェクトへの参照になります。つまり、ワーカー用のバイト配列で共有プロパティを設定すると、対応するByteArrayObject参照の取得キーとして、TopLevelオブジェクト経由でその配列のByteArray::Buffer参照を使用できます。しかし、チャネル アイテムにはBufferオブジェクトへの参照と、共有可能なバイト配列かどうかの判定のみが保存されます。さらに、TopLevelオブジェクトは、ワーカーごとに固有なものであることを思い出してください。そのため、この関連付けは現在のワーカー(この場合は元のワーカー)にのみ該当するものです。

ステップ3:ワーカー スレッドで、共有されているバイト配列プロパティを取得します。

この時点でAVMソース コードを追跡すると、脆弱性の根本的な原因に行き着くことができます。まず、共有プロパティを取得することから始めます。

これにより、Isolate::getSharedPropertyの次のコード断片が見つかります。

したがって、文字列baは、ステップ2で保存したChannelItemオブジェクトへの対応する参照をフェッチするためのキーとして使用されます。続いて、getAtom関数を使用してChannelItemをAtomに変換します。その際には、以前にパラメータとして取得したTopLevel参照(前述のコード断片を参照)を、引数として渡します。

getAtom関数の存在は、脆弱性の発生の前触れになります。この関数は、次のように始まります。

m_valueは、ChannelItemオブジェクトから取得したByteArray::Bufferオブジェクトへの参照であり、ここでは、それに関連付けられたByteArrayObjectオブジェクトが検索されています。

ステップ2では、該当する関連付けを_isolateInternedObjects表に保存しましたが、これは元のワーカーのコンテキストで行われた処理でした。今回は、バックグラウンド ワーカーのコンテキストにあるため、独自のTopLevelオブジェクトと空の_isolateInternedObjects表が存在します。つまり、getInternedObjectによってNULLが返されることになります。元のワーカーでまったく同じ呼び出しを行った場合、共有プロパティとして設定したものとまったく同じByteArrayObjectオブジェクトが取得されて、有効なサブスクライバ リストも存在することになります。

getAtomの例に戻ると、脆弱なコピー コンストラクタが呼び出されていることを明確に確認できます。

バックグラウンド ワーカーには、サブスクライバなしのByteArrayObjectオブジェクトへの参照があることを明記する必要があります。このオブジェクトは、元のワーカーにおける最初のByteArrayObjectオブジェクトが参照しているのとまったく同じByteArray::Bufferオブジェクトを参照します。次のステップでは、そのことの確証が得られます。

ステップ4:ワーカー スレッドで、共有されているバイト配列にバイトを追加して、そのcapacityの受け渡しと、基礎となるバッファの再割り当てを強制的に実行します。

writeBytesコマンドの実行により、domainMemoryへの通知なしにポインタが解放されます(これにより、より大きな配列が割り当てられる)。関数の実行前のバイト配列状態の調査を行う前に、バイト配列にdomainMemoryを設定した後の元のワーカーの値が何であったかを思い出してみましょう。

ByteArrayオブジェクトは、アドレス0x7A8F938にあります。

ByteArray::Bufferオブジェクトは、アドレス0x79D7230にあります。

バイト配列は共有可能です。

サブスクライバを記述するオブジェクトは、アドレス0x83050D0にあります。

1つのサブスクライバ(現在のドメイン)があります。

ここで、バックグラウンド ワーカーのwriteBytes呼び出しに戻ります。この呼び出しは、UAF脆弱性の「自由な」部分に作用します。ByteArrayObject::writeBytesはDataOutput::WriteByteArrayを呼び出し、DataOutput::WriteByteArrayは後でByteArray::Writeを呼び出します。先ほど述べた他のどの関数でブレークしても良いのですが、ここでは、DataOutput::WriteByteArray (オフセット0x6A1F10)でブレークします。

ecxレジスタにはthisポインタが保持されており、この場合、このポインタは、ByteArrayオブジェクト(オフセット0x8)内のDataOutputオブジェクトをポイントしています。元の各値と比較してみると、次のことがわかります。

ByteArrayオブジェクトは、アドレス0x8B0FF10 (0x8B0FF18 – 0x8)にあります。

ByteArray::Bufferオブジェクトは、アドレス0x79D7230 (DataOutputからオフセット0x20を参照)にあります。

バイト配列は共有可能(オフセット0x24)です。

サブスクライバを記述するオブジェクトは、アドレス0x8BE2280 (オフセット0x10)にあります。

したがって、これは、新しいサブスクライバ リストにポイントしつつも、元のワーカーにおける最初のByteArrayオブジェクトと同じBufferを共有する新しいByteArrayオブジェクトであることがわかります。これは、ステップ3の分析で予測したどおりの結果です。

何個のサブスクライバがあるかを調べてみましょう。

新しいByteArrayオブジェクトにはサブスクライバ(オフセット0x4)がないことがわかります。ただし、domainMemoryは、このオブジェクトが(Bufferオブジェクト経由で)ポイントしている実際のバイト配列にロー(raw)アクセスできます。これは、writeBytesメソッドを使用してバッファの容量を超える書き込みを行ったことにより、再割り当てとメモリ内の位置変更が発生した場合、domainMemoryにはそのことが通知されないため、解放済みのバイト配列へのポインタが保持されることを意味します。

以下は、実際に呼び出されても、サブスクライバ リスト(lengthが0を返す)が空であるため、関連するドメインのdomainMemoryへの通知が行われないコードです。

まとめ

以上が、CVE-2015-X脆弱性の根本的な原因に関する詳細な分析と、その脆弱性を引き起こす方法についての順を追った説明です。さらに重要なことに、ワーカーに関する弊社の洞察と、似た性質の脆弱性につながるその他の経路が存在する可能性が示されました。

CVE-2015-0313のエクスプロイトを阻止するパッチとコード変更は、根本的な原因自体を解決するものではないこと、そしてCVE-2015-Xについてもまったく同じことが言えることについて注目しました。つまり、ワーカーのバイト配列の観点からは、サブスクライバの情報が削除されてしまうということです。これにより、まったく同じバグに基づいた脆弱性であるCVE-2015-Xのエクスプロイトの経路が残されることになりました。実際に、このエクスプロイトの存在がすぐに明らかになりました。

同様に、似たような脆弱性がまだ潜んでいる可能性があり、このまったく同じ根本的な原因に起因するものさえあるかもしれません。組織内で、パロアルトネットワークスのTrapsを導入すると、Flashのアプリケーションなどが既存および将来のエクスプロイトから保護されます。