脅威リサーチ

MicrosoftのCVE-2022-21907の分析

投稿者 Tim Lau | 2022年3月14日

FortiGuard Labs Research

影響を受けるプラットフォーム: Windows Server 2022、Windows Server 2019、Windows 10
影響を受けるユーザー:     影響を受けるWindowsシステムを運用しているあらゆる組織
影響:             影響を受けるシステムのサービス妨害
深刻度:            

Microsoftは2022年1月11日の「Patch Tuesday」で、CVE-2022-21907に対するパッチを公開しました。CVE-2022-21907は、ワーム化し得る脆弱性であると言われていたため、業界関係者から特に注目を集めました。この分析では、この脆弱性の原因と攻撃者による悪用方法を検証します。

CVE-2022-21907は、WindowsのIIS(Internet Information Services)コンポーネント内のリモートコード実行の脆弱性です。具体的には、この脆弱性は、IISの中核動作のほとんどを扱うhttp.sys内のカーネルモジュールに影響を与えます。少なくとも、この脆弱性は、オペレーティングシステムをクラッシュさせることで、被害者のマシン上でサービス拒否状態を引き起こす可能性があります。この脆弱性を別の脆弱性と合わせて悪用することで、リモートコード実行が可能になる恐れもあります。

分析のベースとして、Windows 2022 Server 10.0.20348.143を使用しました。IISは、Windows 10にも搭載されています。Windows 10(2H 2021)のhttp.sysも検証して、同じ脆弱なコードパスが存在していることを確認しました。ただし、Windows 10ではIISはデフォルトで有効になっていないため、Windows 10システムが悪用される可能性は大幅に低くなります。

最初に、脆弱なhttp.sysとパッチが適用されたhttp.sys(10.0.20348.469)の間のバイナリ比較を実行しました。Bindiffというプログラムによって、これら2つのバイナリファイルを比較して、変更された関数を強調表示しました。いくつかの関数が大きく変更されていた中で、特に興味を引かれた2つの関数は、http!UlpAllocateFastTracker()およびhttp!UlFastSendHttpResponse()でした。

(参考までに、Windows 10のhttp.sysに関する初期分析を実行したところ、Windows 10上でパッチが適用されていたのは、これら2つの関数だけでした。)

http!UlpAllocateFastTracker()では、以下の違いを確認できました。

図1:元の関数(一番上)とパッチが適用された関数(一番下)の違い

1つ気付いた奇妙な点は、バッファをゼロに設定するためにmemset()が2回呼び出されることです。1回目は、バッファのハードコーディングされた先頭0x1e0バイトが対象であり、2回目は、0x2eから始まる0x50バイトが対象です。

図2:memset(value = 0, size=0x1e0)

図3:memset(value = 0, size=0x50)

これら2つのmemset()呼び出しの違いは、memset(0x1e0)の対象は、nt!ExAllocatePool3()から新たに割り当てられたバッファであるのに対して、memset(0x50)の対象は、nt!ExAllocatePool3()とExpInterlockedPopEntrySList()の両方から割り当てられたバッファであることです。(内部では、この呼び出しではWindowsの単一リンク付きリスト構造体であるLIST_ENTRYが使用され、基本的には以前に割り当てられたバッファが再利用されます。)

http!UlpAllocateFastTracker()に対する変更内容のみに注目した場合は、バッファ(ここではTrackerと呼びます)内の非ゼロエントリによって、いくつかの望ましくない副次的作用が引き起こされる可能性があることを推測できます。さらに、nt!ExAllocatePool3()からバッファを直接割り当て可能であるため、システムでメモリ負荷が生じている場合は、攻撃者の制御下にあるデータをメモリに注入して、その攻撃者の制御下にあるデータを新たに作成されたTrackerバッファに出現させることが可能になります。

同じTrackerバッファに対してmemset()が2回呼び出されることも奇妙です。どうやら開発者は、Tracker()の特定のセグメント(0x2E~0x7E)をゼロに設定する必要があると感じたようであり、バッファがLookAsideリンクリストから取得された場合でも同様です(ただしこの場合は、初期割り当てによって攻撃者の制御下にあるデータすべてがすでにゼロに設定されていたでしょう)。したがって、どのような非ゼロ値がこのバグを引き起こすのかにかかわらず、その非ゼロ値はTracker構造体の0x2e~0x7eの範囲内にあるはずです。

この時点で、試すことができるいくつかのアイデアがあります。まず、どのような状況下でhttp!UlpAllocateFastTracker()が呼び出されるのかを知る必要があります。結果的に、このことは非常に簡単に確認できました。Ghidra(UlpAllocateFastTrackerへの参照を検出します)またはIDA(xrefにジャンプします)で1つのコマンドを実行すると、http.sys内の1つの関数のみがUlpAllocateFastTracker()を呼び出せることが分かります。

図4:2つのhttp!UlpAllocateFastTracker()呼び出し

複数のHTTP要求をIISに送信するためのPythonスクリプトをいくつか書いてみると、http!UlFastSendHttpResponse()は、その名のとおりの操作を実行することが分かりました。すなわち、この関数の役割はhttpレスポンスをクライアントに返信することです。前出のTrackerオブジェクトは、このレスポンスに関するさまざまな状態とポインタを追跡管理する構造体です。これらのポインタ内を探せば、そのうちの1つにレスポンスデータを見つけることができます。

図5:Trackerオブジェクトと対応するレスポンスデータ

初期化コードへのアクセス方法を特定した後に、非ゼロ値をTrackerに事前書き込みすることで、このエクスプロイトを「支援」することにしました。Windbg、pykd、およびPythonスクリプトを使用して、http!UlpAllocateFastTracker()が復帰する前に影響を受ける可能性が高いTracker領域に、既定の値を何とか注入できました。

残念ながら、「ファザー(fuzzer)」をどれだけ実行しても、テストシステムは安定状態を保って正常にレスポンスし続けました。ただし、この際に気付いたこととして、http!UlpAllocateFastTracker()呼び出しのほとんど(90%以上)は、UlFastSendHttpResponse+0x2F0からのものであり、UlpAllocateFastTracker+0xe99からの割り当て呼び出しは数件のみでした。Windows 10のhttp.sys上で、http!UlFastSendHttpResponse()に対してbindiffを実行したところ、大幅なコード変更がありました。

図6:パッチが適用されたhttp!UlFastSendHttpResponse()と元のhttp!UlFastSendHttpResponse()の比較

この時点で、クラッシュを引き起こすことができなかったため、Twitter でPoCを探しました。

クラッシュ

新たなPoCを得て、我々は分析を再開し(今回はWindows 2022 Server上で)、パッチ適用対象となっていたクラッシュの原因をすぐに発見しました。

このクラッシュは、無効なメモリにアクセスしようとするnt!MmUnampLockedPages()の呼び出しとともに、http!UlFastSendHttpResponse()のクリーンアップフレーズの最後に発生します。

図7:クラッシュのスタックトレース

Microsoftによると、nt!MmUnmapLockedPages()は、仮想メモリアドレスと物理メモリアドレスの間のマッピングを解除するWindowsカーネルルーチンです。このマッピングは、MDL(Memory Descriptor List)と呼ばれるカーネル構造体によって表現されます。

図8:MmUnmapLockedPages()の関数シグネチャ

nt!MmUnmapLockedPages()の呼び出し上にブレークポイントを設定すると、あらゆる種類の無効なメモリアドレスがBaseAddress(仮想メモリアドレス)として渡されるようになりました。

図9:MmUnmapLockedPages()の無効な引数

しかし今度は、PoCが脆弱性を誘発するために、何か違ったことをしていたのか、が問題になってきました。このことを解明するために、http!UlFastSendHttpResponse()をデコンパイルして、コードを調べる必要がありました。

図10:逆コンパイルされた脆弱なコード

我々はすぐにいくつかの推測を立てることができました。ポインタv19はUlpFreeFastTracker()によって解放されることを確認できました。このことから、v19はTrackerバッファへのポインタであることが分かりました。実際に、スクロールアップして2つのhttp!UlpAllocateFastTracker()呼び出しを確認したところ、v19はこの関数の戻り値であることが分かりました。

図11:返されたTrackerオブジェクトとしてのv19

同時に、MmUnmapLockedPages()の2つ目の引数はMDL構造体であることが分かっていました。MDLの設定を確認すれば(http.sysではMDLの内部構造体設定が使用されますが、これはカーネルのものと同じです)、0x00aフィールドはMdlFlagsであること、および当ルーチンによってフラグの0番目のビットが1かどうかがチェックされたことを確認できます。最後に、0x018(10進数では24)フィールドはMappedSystemVaです。

図12:MDLの設定

参考までに、Windows SDKに含まれているwdm.hによると、0x0001はMDL_MAPPED_TO_SYSTEM_VAです。すなわち、このMDLによって表現されるメモリマッピングは有効です。

図13:Windows SDKに含まれているMdlFlagsビットフィールドの設定

これら2つの情報に基づいて、疑似コードを作成できました。

図14:脆弱なコードの疑似コード。この分析ではこれ以降、Tracker->80のことを「some_mdl」と呼びます。

ここで、最初に立てた推測を振り返ってみると、かなり当たっていました。我々はTrackerの0x2e-0x7eは非ゼロ値である必要があると推測し、実際に、このifステートメントが最後まで実行されるためには、0x50のポインタは非ゼロ値である必要があります。

そしてこの時点で、次の4つの新たな疑問が生じます(明白さが高い順に示しています)。

  1. 「some_mdl」というMDL構造体データを制御できるのか?
  2. Tracker->member_0x50とは何か?
  3. 我々のドライバーではできなかったのに、PoCではこのコードに到達できるのはなぜか?
  4. リモートコード実行は可能か?

「some_mdl」というMDL構造体データを制御できるのか?

答えはイエスであり、特定の状況下では攻撃者はMDL内のバイトを制御できることが分かりました。http!UlpAllocateFastTracker()に戻って、すべての命令行を1行ずつ実行したところ、複数のMDLポインタがTracker内に存在する一方で、オフセット0x80におけるMDLはまったく初期化されません。割り当てルーチンは、Trackerの構造体位置の後ろにシーケンシャルメモリスペースを単に確保して、TrackerのMDLポインタがこれらのアドレスを指すようにします。このことは理にかなっています。nt!ExAllocatePool3()が呼び出された場合、要求されるバイト数は、Tracker構造体の推測サイズをはるかに上回るからです(パッチが適用されたmemset()はバッファの先頭0x1e0に0を書き込むだけであることに留意してください)。

図15:nt!ExAllocatePool3()によってバッファの0xc85バイトが割り当てられる

図16:http!UlInitializeFastTrackerPool()によってTrackerポインタにアドレスが割り当てられる

0x68、0x70、0x88、0x80はいずれもMDLポインタであることが分かっていますが(IDAヒューリスティックを通じて)、0x68のみが、初期化ルーチン内のMmBuildMdlForNonPagedPool()によって初期化されます。

図17:有効なMDLオブジェクトとして初期化されるTracker->0x68ポインタ

制御権がhttp!UlFastSendHttpResponse()に返されたら、追加のMDLが最終的に初期化されます。ただしPoCでは、初期化がスキップされて深刻な結果がもたらされるコードパスが見つかりました。

Tracker->member_0x50とは何か?

このコードを詳しく調べて、member_0x50が行う操作を明らかにしようとしましたが、このオブジェクトはhttp!UlFastSendHttpResponse()の外部で作成されているため、ポインタ引数としてルーチンに参照渡しされました。

このコードでは、member_0x50が有効な場合は、some_mdlも有効なはずだと想定されているため、MDLのメモリ範囲はおそらくmember_0x50の補助記憶域であり、両方の要素はともに有効(または無効)なはずです。

しかし我々が行ったテスト中は、我々のドライバーとPoCのどちらでも、member_0x50につながる引数は常にNULLです。これ以上は深掘りしないことにしました。

我々のドライバーではできなかったのに、PoCではこのコードに到達できるのはなぜか?

前述のとおり、some_mdlが初期化されない実行パスが使用される必要があります。PoCでは、まったく同じ不正な形式のHTTPパケットを短い間隔で連続して送信することで、このことが利用されます。

http!UlpAllocateFastTracker()は2回呼び出されます。1つ目の呼び出しは、90%以上の比率で使用されます。Tracker構造体が割り当てられると、http!UlFastSendHttpResponse()が引き継いで、構造体の初期化プロセスを継続します。最も重要なこととして、このプロセス中に、member_0x50要素がゼロに設定されることで、通常の実行時に当該バグが引き起こされることが防止されます。

図18:member_0x50ポインタがゼロに設定される

ただし、IISが複数の不正形式パケットをたて続けに受信すると、異なるコードパスが使用されます。我々のコード分析によると、IISは最終的に独自の(我々の推測)キャッシングメカニズムを放棄します。特に、1つ目のhttp!UlpAllocateFastTracker()呼び出しは引き続き行われるのに対して、割り当て済みのTracker構造体はすぐに割り当て解除されます。

その理由は不明ですが、1つ目の呼び出し時に、Tracker->0x148に位置する単一値が0x14(前の呼び出し時)から0x0に変化します。その結果として、新しいTracker構造体が割り当て解除されて、異なる第2引数を使用してhttp!UlpAllocateFastTracker()が呼び出されます。

図19:1つ目の呼び出し:0x200

図20:2つ目の呼び出し:0x0

(ところで、このコードとPDB(プログラムデータベース)から判断すると、他の2つの変数「UlH3ExtraHeaderCount」と「_UX_DUO_COLLECTION」に基づいてこの値が決定されます。これらの変数の役割をご存知の方がいれば、ぜひお知らせください。)

2番目の割り当ての後に、http!UlGenerateFixedHeaders()が呼び出されます。ただし、この呼び出しは途中で終了します。http!UlGenerateFixedHeaders()の6番目の引数、すなわち、割り当て呼び出しと同じ0x0(通常は0x200)変数は、早い段階のチェック不合格を引き起こします。その結果として、0xC000000Dというエラーコードが発行されて、このルーチンはすぐに復帰します。

図21:引数内に0x0という値が含まれていると、http!UlGenerateFixedHeaders()がエラーを伴って復帰する

このルーチンがエラーコードを伴って復帰した後に、実行プロセスはhttp!UlFastSendHttpResponse()の大部分をスキップして、そのままクリーンアップフレーズに進みます。クリーンアップの一環として、脆弱なnt!MmUnmapLockedPages()呼び出しが行われて、システムがクラッシュします。

前述のとおり、このバグを引き起こすには、不正な形式のHTTPパケットが必要です。そこで、他の形の不正形式HTTPパケットによってもこのバグを引き起せるのかという疑問が生じました。意外にも、ほとんどすべてのテストサンプルによってシステムがクラッシュしました。このことは重大な問題をもたらします。被害者のシステムを攻撃するための多くの手段が存在する可能性があるからです。

リモートコード実行は可能か?

攻撃者の制御下にあるメモリのマッピングは我々が制御しているため、リモートコード実行のリスクは存在します。ただし、このようなリモートコード実行を構築するには、Trackerフィールドによって実行される操作をさらに調べる必要があります。攻撃者は、メモリに偽のMDLと偽のTrackerポインタを注入したり(このためにはカーネルアドレス情報を漏洩させる別の脆弱性が必要になることがあります)、適切に初期化されない他のフィールドがTracker内に存在することを悪用したりする必要があります。

我々は、PoCと我々のドライバープログラムを組み合わせて、攻撃者の制御下にあるデータをカーネルメモリに注入しようとしました。しかし、注入されたコンテンツが再び再割り当てされて、脆弱なコード内に現れる可能性はかなり低いです。したがって、リモートコード実行チェーンを成功させるには、メモリにデータを注入するためのさらに正確な方法が必要になる可能性があります。

この分析に基づいて、FortiGuardのリサーチャーは、予想される不正トラフィックに対処するためにIPSシグネチャを変更しました。

結論

このCVEはワーム化し得ると言われているため、CVE-2022-21907は大きな影響を及ぼす可能性があるという懸念が当初はありました。しかし、2つの要因によってこのリスクがある程度低減されます。1つ目は、Windows 10上でIISが有効化されることはほとんどないことであり、2つ目は、カーネルメモリ内に読み取りプリミティブや書き込みプリミティブを作成するための直接的な方法を攻撃者が有していないことです。

フォーティネットのソリューション

FortiGuardのIPSは、次のシグネチャによって、当CVEに関連するすべての既知のエクスプロイトから保護します。

                MS.Windows.HTTP.Protocol.Stack.CVE-2022-21907.Code.Execution

ただし、不正な形式のHTTPパケットには予測不能な側面があるため、サービスの中断を回避するために、対応するパッチを早急に適用することを強くお勧めします。FortiGuard Labsは、当CVEを継続的に監視して、必要に応じて新たな対策を講じる予定です。

付録:

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21907

https://twitter.com/wdormann/status/1488148028317917186

https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmunmaplockedpages