解放済みメモリ使用の脆弱性の悪用はもうできない? IEの新たなメモリプロテクターがその答えを示す

2014年6月公開のMicrosoft社の月例セキュリティ更新プログラム(Microsoft Patch Tuesday)に含まれていたDOMオブジェクトに隔離されたヒープを用意する改善策は、use-after-free(UAF、解放済みメモリ使用)の脆弱性の利用をより困難にすることを目的とした単なる防火訓練でしかありませんでした。しかし2014年7月のパッチは、エクスプロイトの開発者たちにとってかなり衝撃的だったことでしょう。この時のリリースでMicrosoftは解放済みメモリ使用の脆弱性と闘う意志を見せ、解放済みメモリ使用の脆弱性の悪用を非常に困難にする新たなメモリプロテクターをInternet Explorerに導入しました。

変更の概要

2014年7月のアップデートでは、合計14のMSHTML!MemoryProtection::*という関数と1つのグローバル変数が、HTMLおよびDOM要素のメモリ解放機能の改善のために加えられました。


図1. MemoryProtection関数

このアップデートでは、ほとんどの要素のデコンストラクタにも重要な変更が加えられています。

  • HeapFree関数がMemoryProtection::CMemoryProtector::ProtectedFreeに差し替えられました。
  • HeapFree 関数がProcessHeapFree(MemoryProtection::CMemoryProtector::ProtectedFreeのfastcall版)に差し替えられました。
  • 直接MemoryProtection::CMemoryProtector::ProtectedFreeを使って同じメカニズムを実行するデコンストラクタもあります。


図2. HeapFreeに取って代わったProtectedFree関数

こうした新たな関数はどのように機能するのでしょうか? そして、こうした変更がどのように解放済みメモリ使用の脆弱性の利用やその他のエクスプロイトを阻止するのでしょうか? 以下の技術的解析で解説しましょう。

技術的解析

メモリ割り当ての際には通常、同一サイズの解放済みメモリチャンクの参照が行われます。この動作を利用し、解放済みメモリ使用の脆弱性を悪用する従来の方法では次のプロセスを実行します。

1) プログラムがオブジェクトAを割り当て、その後それを解放。
2) アタッカーが解放されたメモリ部分にオブジェクトBを割り当て、後々使用されるデータを慎重に制御。
3) プログラムが解放されたオブジェクトAを使用し、アタッカー制御のデータを参照。

最も効果的な緩和策は上のステップ2の部分、つまりオブジェクトの入れ替えを困難あるいは不可能にすることです。これを行う方法はいくつかあります。

A) 究極の方法は、アタッカーが異なるタイプのオブジェクトを用い、解放されたメモリのリクラメーションを行うのを防止する安全なメモリ管理システムを使用することです。同じタイプのオブジェクトを使わなければメモリの差し替えは不可能となり、非常に厄介です。

B) より安価な方法としては、現在のメモリ管理システムを使って、アタッカーがメモリ割り当てと解放の動作を制御できない状態にすることです。

具体的に言うと、Bの手法は2014年6月にはメモリ割り当てに隔離されたヒープを用いる方法が追加される形で改善され、7月には保護下で解放を行うという改善策が実行されました。

隔離されたヒープという改善策が導入されたことで、アタッカーたちをある程度手こずらせることになると思いますが、これだけに頼った対策では比較的簡単に迂回されてしまいます。特にメモリチャンクが結合した場合には、アタッカーは異なるタイプのものを使って解放されたオブジェクトを参照することができるでしょう。

2014年7月のアップデートで新たに追加された保護下で開放を行う改善策で、状況は一変しました。IEプロセスの各スレッドに、その名の通り現在のスレッドを保護するためのMemoryProtector構造が導入されたのです。


図3. MemoryProtector構造

この構造内のBlockArray が解放されることになる要素のサイズとアドレスを格納します。そして新たな関数MemoryProtection::CMemoryProtector::ProtectedFreeがMemoryProtector構造を利用し、要素のメモリの解放の安全性を高めます。


図4. MemoryProtection::CMemoryProtector::ProtectedFree関数

一般的に、保護されたメモリの解放を行うこの関数は、シンプルな保守的ガベージコレクタの役割を果たします。未使用領域を即座に解放してしまうと、アタッカーにスペース再割り当てのチャンスを与えてしまいます。それをする代わりに、 MemoryProtection::CMemoryProtector::ProtectedFreeは数あるいは合計サイズの値が特定の閾値に達するまで、こうした未使用領域を保持します(中身をゼロで埋める)。閾値に達しても、まだすぐにはすべての格納された要素のメモリを解放することはしません。「マークとスイープ」プロセスを実行する必要があるからです。図4のMarkBlocksとReclaimUnmarkedBlocksの呼び出しでこれを確認することができます。

下はスタック内でまだ参照されている各要素をスキャンし、マークするマーキングのプロセスを示しています。これらのマークされた要素はスイーピングのプロセスでは解放されません。こうすることで、スタック内の解放されたオブジェクトの利用を防ぐのです。


図5. MemoryProtection::CMemoryProtector::MarkBlocks関数

最終的にマークされていない要素がスイーピングプロセスで解放されます。


図6. MemoryProtection::CMemoryProtector::ReclaimUnmarkedBlocks関数

では、攻撃する側の観点から見てみましょう。上のような新たな条件下で解放済みメモリ使用の脆弱性を利用するため、まず解放されたオブジェクトと同じヒープにおいて制御下で割り当てを行う方法を見つけ出す必要があります。そしてオブジェクトを解放する際にはメモリの状態(実際の解放をトリガーする前に何回の解放が必要か)を予測しなければなりません。その後、解放済みオブジェクトの空間が実際に解放されていることを確実にするため、完璧に解放のシーケンスを構築する必要があります。これだけでもすでに難しそうですが、それだけではありません。メモリの結合を予期し、その他の未知の割り当てによるコンフリクトに対処し、適切なタイミングでメモリの再構築を行わなければなりません。そしてようやく、解放済みの要素の空間を制御することが可能になるかもしれないというわけです。この時点で、解放済みメモリ使用の脆弱性を利用しようとしていた人たちのほとんどはすでにギブアップしているでしょう。

その他見解

  • 図4の関数MemoryProtection::CMemoryProtector::ProtectedFree の冒頭部分でわかるように、もし変数MemoryProtection::CMemoryProtector::tlsSlotForInstanceが -1 であれば、あるいはTlsGetValue()が失敗すると、以前の関数 HeapFreeが直接呼び出されますが、これは明らかに達成困難です。
  • 隔離されたヒープと保護下での解放の組み合わせにより、影響を受けるヒープの操作方法もあります。こうした操作方法では脆弱なバッファ用の穴を作るためにいくつかの要素を解放しなければならないからです。メモリの解放を遅延させる仕組みは攻撃側にとってはある程度の頭痛の種となるでしょうが、同じヒープ内において制御下で割り当てを行う方法を見つけ出し、ある程度の要素を解放することができれば操作は不可能ではありません。
  • シンプルな保守的ガベージコレクタは、有名な DionによるASLRの迂回のように新たな攻撃対象領域を発生させることになるでしょうか? スタックに整数値を置き、解放されることになる要素のアドレスを総当たりで推測することは可能です。しかし、もしこのアドレスを推測できたとしても、メモリの中身はすでにゼロで埋められているため、DLLのベースアドレスをリークさせるvftableのような有用なポインターを入手することはできないでしょう。

結論として、IEで確実な解放後メモリ使用のエクスプロイトを構築することは、現在は非常に困難だと言えます。Microsoft Security Response Center(MSRC)の方々、お見事です!

特別協力: Margarette Joven