Cisco Japan Blog

エクスプロイト可能なバグかの判断:REVEN を使用して NULL ポインタの参照先の値を取得

1 min read



エグゼクティブ サマリー

バグがエクスプロイト可能かどうかを判断するには、非常に時間がかかる場合があります。今回の記事では、クラッシュを引き起こした実行パスを追跡することで、脆弱性がエクスプロイト可能かどうかを判断する方法をご紹介します。今回はリバース エンジニアリング プラットフォームの「Tetrane REVENpopup_icon」を使用しています。この種のツールを使えば、バグがエクスプロイト可能かどうか迅速に判断できます。

 

ファジングによるソフトウェア脆弱性の調査では、NULL ポインタのデリファレンス(参照先の値を取得)によりクラッシュを多発させる傾向があります。

ファジングでは、多様な入力データを対象プログラムに与え、脆弱性が発生しないかそれぞれの挙動を確認します。ファジングで発見できる種類のバグは、特に重大ではないサービス妨害の脆弱性に留まり、テスト対象のソフトウェアをクラッシュさせるだけに終わる場合もあります。ただし攻撃者が NULL ポインタを制御できる場合は、任意コードの実行を許す脆弱性が存在している可能性もあります。今回はこうした要素を踏まえ、特定のバグがエクスプロイト可能かどうかを判断します。

クラッシュの発生

脆弱性調査ではファジングも多用します。ただしファジングでは NULL ポインタのデリファレンスによるクラッシュが多発し、調査を阻害する傾向にあります。しかしまれに、以下のようなクラッシュが発生します。

クラッシュ時の Windbg 出力

上図のようなクラッシュは興味深いのと同時に、迷惑でもあります。クラッシュの原因が NULL アドレスの読み取りアクセス違反にあるため、NULL ポインタのデリファレンスが起きていることを確認できます。しかしクラッシュ位置が「call」に非常に近いため、攻撃者が「ecx」を制御できれば、任意コードを実行できる可能性があります(ただし、同様のクラッシュの 99 % は単なる NULL ポインタのデリファレンスです)。

 

一部のケースでは、NULL ポインタのデリファレンスがクラッシュ原因であることを容易に確認できます。オブジェクトのフィールドが明示的に NULL に初期化され、有効な値が一度も割り当てられず、アクセス方法がダイレクト(オフセットなし)な場合です。他にも、フィールド初期化からクラッシュまでの間に多くのコードが実行される場合もあります。その際は値がスタックなどからコピー、プッシュ、除去され、ソース コードの分析が困難になります。ここで役立つのが過去にさかのぼるデバッグです。

 

 

「REVEN」では、特定期間に仮想マシン(VM)上で実行された命令をすべて記録してシミュレートし、任意の実行時点における全体像を静的に分析できます。VM の状態も包括的に把握できるため、クラッシュが脆弱性につながるかどうかを確認するのに役立ちます。

クラッシュ発生位置を特定する

最初に、クラッシュ イベントを特定する必要があります。Windows では、アクセス違反のためにプロセスがクラッシュすると KiUserExceptionDispatcher に実行処理が移ります。このため、以下のように KiUserExceptionDispatcher の呼び出しを完全に追跡できます。

上の画面では追跡が完了していませんが、KiUserExceptionDispatcher が 2 回呼び出されていることは見て取れます。最初の呼び出しからスタックを開くと、次のようになります。

スタック上には、ACCESS_VIOLATION の例外コードである「0xc0000005」の値を確認できます。この値は KiUserExceptionDispatcher へ引数として渡されます。画面を見る限り分析対象は正しいようです。それを裏付けているのが、以下の画面にある、KiUserExceptionDispatcher の 1 つ前のシーケンスです。

実行直前のシーケンスは「KiUserExceptionDispatcher」で継続されますが、pagefault または hardware イベントにより停止しています。これは要確認です。

「ecx」の値はゼロです。このため、前述した windbg の出力と同様にアクセス違反が発生します。

クラッシュの分析

これでクラッシュ位置が特定できました。では、それが攻撃者により制御できないクラッシュであることを、どうすれば確認できるでしょうか?そもそも、「ecx」の NULL 値はどこから来たのでしょうか?これらの疑問を解決すべく、クラッシュ位置で「ecx」レジスタを選択して、レジスタの以前の使用法まで追跡します。

分析に関係のない汚染追跡(taint tracking)およびトリム操作を視覚化することで、以下のようにレジスタの分析が容易になります。

ただし、それでもまだ十分に見やすいとは言えないため、Reven の汚染グラフを使用します。

Reven の汚染グラフはすっきりしており、実際のデータをソースまで追跡できます。ここでは「ecx」の NULL 値について、一番下から上へ矢印をたどります。すると、NULL 値が「xor ecx,ecx」から来ており、オブジェクト フィールド値が 23067860 のシーケンス、4 番の命令で初期化されていることがわかります。別の興味深い点は、フィールド値が初期化された時点(23067860)と、最初にアクセスされた時点(24772997)との間で、相当数のコードが実行されていることです。後者は、クラッシュ発生時点(24773092)の比較的近くです。

 

この点について、メモリ アクセス履歴と相互参照します。上図の 2 番目の命令で「edi+0x30」が指すメモリを調べたところ、次のような結果になりました。

上の図では、調査対象のシーケンスで書き込みが 1 度、読み取りが 2 度だけ実行されたことがわかります。1 度目の読み取りでは何も起きていませんが、2 度目の読み取りは(行をさらに下った部分で)クラッシュを引き起こしています。

 

実行パスをさかのぼると、オブジェクト フィールドを NULL に設定したコードがコンストラクタの一部であることがわかります。このため、問題のオブジェクト フィールドは NULL に安全に初期化されており、実際の値は一度も割り当てられておらず、デリファレンスによるクラッシュであるという結論に至ります。クラッシュ時に「ecx」の最終的な値を制御することは不可能、つまりこのバグはエクスプロイト不可能です。

結論

クラッシュを引き起こすバグについて、任意コードの実行を許す脆弱性につながるかどうかを判断するのは困難です。NULL ポインタのデリファレンスでは、クラッシュ時点からポインタ初期化時点まで実行パスを追跡する必要があります。

 

そのため従来の分析方法は時間や手間がかかります。しかし REVEN などのツールを使用すれば実行パスを迅速に特定できます。実行パスとマシンの状態を視覚化することで、分析速度が向上するだけでなく、バグの根本原因を特定することもできます。

本稿は 2018年8月2日に Talos Grouppopup_icon のブログに投稿された「Exploitable or Not Exploitable? Using REVEN to Examine a NULL Pointer Dereference.popup_icon」の抄訳です。

コメントを書く