脆弱性

インサイドWin32kエクスプロイト: CVE-2022-21882とCVE-2021-1732の分析

Clock Icon 9 min read
Related Products

This post is also available in: English (英語)

目次

CVE-2022-21882の詳細解析
1. HMValidateHandleを見つける
2. NtUserConsoleControl、NtCallbackReturnをロードする
3. KernelCallbackTableを見つけてユーザーモード コールバックxxxClientAllocWindowClassExtraBytesとxxxClientAllocWindowClassExtraBytesへのポインターを保存する
4. ウィンドウ クラスをいくつか定義する
5. ヒープ グルーミングを行う

図表の目次

図1. FindHMValidateHandle関数呼び出し
図2. IsMenu関数のIDAによる逆アセンブル
図3. FindHMValidateHandleのコードスニペット
図4. PoC 285~288行目
図5. PoC 297~304行目
図6. WinDbgによるPEBの出力
図7. KernelCallbackTableの最初の16個のエントリー
図8. KernelCallbackTable + 0x3d0のWinDbgによるメモリー ダンプ。ここに興味の対象となる2つの関数が配置されている
図9. PoC 312~325行目
図10. 最初に作成されたウィンドウでHMValidateHandleを呼び出したときの戻り値
図11. ユーザーモードとカーネルモードで共有されるtagWND構造体のカーネルモードのコピー

CVE-2022-21882の詳細解析

1. HMValidateHandleを見つける(図1)
画像1は数行のコードのスクリーンショットです。FindHMValidateHandle関数に対する呼び出しをしています。
図1. FindHMValidateHandle関数呼び出し

前述のように、歴史的にエクスプロイトの作者はFindHMValidateHandle関数を使ってこの関数にハンドルを渡したオブジェクトのカーネル アドレスを漏えいさせてきました。この関数の関数プロトタイプはHMValidateHandle(HANDLE h, BYTE type)です。ここでこのハンドルhは、検証対象オブジェクトへのハンドル、typeはオブジェクトの型(type)を表す数値定数です。

本稿ではこの型が0x001というウィンドウ型を表すものとします。Windows 10 バージョン 1803以降の場合、これは、渡されたウィンドウ ハンドルと対応するtagWND構造体への、ユーザーモードにマッピングされたデスクトップ ヒープ ポインターを返します。

HMValidateHandleはエクスポートされた関数ではないので、単純にGetProcAddressを使ってこの関数を解決させることはできません。そこでエクスプロイトの作者は通常User32.dll内のIsMenu関数を解決させ、このエクスプロイト コードが最初のE8オペコードを検索することになります。その理由はIsMenuが呼び出す唯一の関数がHMValidateHandleであるためです。E8オペコードはCALL命令 (IsMenuの唯一のCALL命令)に対応しています。

図2はIsMenu関数を逆アセンブルしたものです。E8オペコードがHMValidateHandleに対する呼び出しを行う最初で唯一のオペコードであることがわかります。

画像2は、IsMenu関数を逆アセンブルしたスクリーンショットです。HMValidateHandleに対する呼び出しを行う最初で最後のオペコードが含まれています。
図2. IsMenu関数のIDAによる逆アセンブル

この作者はこのPoC 58行目にFindHMValidateHandleという名前の関数を定義することで、上記の内容を実現しています。図3はこのFindHMValidateHandleという関数からのコードスニペットです。この関数はE8オペコードを探してIsMenuを検索し、その位置をポインターとしてg_pfnHmValidateHandleというグローバル変数に保存します。この位置は、後でユーザーモードにマップされたtagWND構造体のアドレスを漏えいさせるのに使われます。

画像3は、FindHMValidateHandleのコード スニペットのスクリーンショットです。
図3. FindHMValidateHandleのコード スニペット
2. NtUserConsoleControl、NtCallbackReturnをロードする

CVE-2022-21882とCVE-2021-1732のトリガーには、NtUserConsoleControlNtCallbackReturnの両関数が悪用されます。これらはいずれも文書化されていない関数で、それぞれwin32u.dllntdll.dllに存在しています。PoC 285~288行目(図4)は、これらの関数を解決してポインターを保存し、後で使えるようにしています。

画像4はPoC 285〜288行のスクリーンショットです。これらの行は、前述の関数を解決し、ポインターを保存して後で使用できるようにします。
図4. PoC 285~288行目

NtUserConsoleControlNtCallbackReturnの両関数についてはそれぞれステップ7と9で詳しく説明します。

3. KernelCallbackTableを見つけてユーザーモード コールバックxxxClientAllocWindowClassExtraBytesとxxxClientAllocWindowClassExtraBytesへのポインターを保存する

297〜304行目までのコード(図5)は、KernelCallbackTableの場所を特定し、正規のxxxClientAllocWindowClassExtraBytes関数とxxxClientFreeWindowClassExtraBytes関数のアドレスのポインターをローカル変数に保存しています。xxxClientAllocWindowClassExtraBytes関数と xxxClientFreeWindowClassExtraBytes関数(ステップ9で説明)をフックするには、各関数へのポインターを見つける必要があるのです。これらの関数はいずれもユーザーモード コールバックで、Windows API内での利用のためにエクスポートされていないのがその理由です。

画像5は、PoC 297〜304行目までのスクリーンショットです。KernelCallbackTableの位置を特定してアドレスのポインターを保存しています。
図5. PoC 297~304行目

プロセス環境ブロック(PEB)をパースすることによりKernelCallbackTableの場所が特定されます。このPEBのオフセット0x58の位置にKernelCallbackTableへのポインターが含まれています。

: GS[0x60]レジスターは、Windows x64システムではPEBへのポインターを含んでいます。このコードが__readgsqword(0x60u)を参照しているのはそのためです。KernelCallbackTableは、Windowsカーネルが使用するすべてのカーネル コールバック関数へマッピングするポインターを含むテーブルです。このPEBのKernelCallbackTableエントリーの表示にはWinDbgを使っています(dt nt!_peb @$pebコマンドを使って現在のPEBをダンプした)。

図6に示したWinDbgの出力にもとづくと、PEB+0x58KernelCallbackTableアドレスへのポインターが含まれていることがわかります。

画像6はWinDbgの出力のスクリーンショットです。KernelCallbackTableへのポインターが含まれています。
図6. WinDbgによるPEBの出力

カーネル コールバックの最初のいくつかのエントリーを図7に示します。

画像7は、KernelCallbackTableの最初の16個のエントリーのスクリーンショットです。
図7 KernelCallbackTableの最初の16個のエントリー

このPoCはKernelCallbackTableg_oldxxxClientAllocWindowClassExtraBytesg_oldxxxClientFreeWindowClassExtraBytesに、2つのポインター(オフセット0x3d80x3e0)をそれぞれ保存しています。

KernelCallbackTable+0x3d8には xxxClientAllocWindowClassExtraBytesへのポインターが、次のエントリー(0x3e0)にはxxxClientFreeWindowClassExtraBytesへのポインターが保存されています。そのようすを図8に示します。

画像8は、WinDbgでKernelCallbackTable + 0x3d0をメモリー ダンプしたさいのスクリーンショットです。
図8. KernelCallbackTable + 0x3d0のWinDbgによるメモリー ダンプ。ここに興味の対象となる2つの関数が配置されている
4. ウィンドウ クラスをいくつか定義する

図9のコード(312〜325行目)には見覚えがあるでしょう。本連載の第1部で取り上げたように、2つのウィンドウ クラスを定義し、そのうち1つ(wndClass)を登録しています。一方にはnormalClassというクラス名が与えられ、もう一方にはmagicClassというクラス名が与えられています。

またこのmagicウィンドウ クラスには、ランダムなcbWndExtra値が与えられているように見えます。この値は、後でフックされた関数を呼び出すさいに、2つのウィンドウ クラスを区別するために使用されます(詳細分析は後述)。

画像9はPoC 312〜325行目までのスクリーンショットです。g_nRandomを黄色でハイライトしています。
図9. PoC 312~325行目
5. ヒープ グルーミングを行う

PoC 413〜467行目まではnormalClassというクラス タイプのウィンドウを10個作成するdo whileループを定義しています。各ウィンドウにはsomewndというウィンドウ名が与えられます。

このエクスプロイトの作者は、0から9まで10個のウィンドウを作成した後、2から9までのウィンドウを削除しています。これはおそらく、ここで残した2つのウィンドウのすぐ後ろに後からPoCのこの部分で作成することになるmagicウィンドウを割り当てるためにヒープ グルーミングを行おうとしたのだろうと思われます。ただし後述するように、この実行サンプルでは、magicウィンドウは最初の2つのウィンドウの真ん中に割り当てられます。

各ウィンドウの作成時、この作者は各ウィンドウへのハンドルをarrhwndNoraml[]という名前の配列に格納します。次に、各ウィンドウのtagWND構造体へのポインターを、arrEntryDesktop[]という名前の別の配列に格納します。この処理はHMValidateHandleを呼び出すことで行われます。すでに説明したようにこの呼び出しでウィンドウの各tagWND構造体のユーザーモード コピーへのポインターが返されます。

図10はHMValidateHandleを最初に呼び出した後(つまり1つめのウィンドウを作成した後)の戻り値(rax)です。

画像10は、最初に作成したウィンドウでHMValidateHandleを呼び出したさいの戻り値のスクリーンショットです。戻り値はraxです。
図10. 最初に作成されたウィンドウでHMValidateHandleを呼び出したときの戻り値

PoC分析で重要となるラベルをtagWND構造体に追加してあります。tagWND.cbWNDExtraの値が32 (0x20)であることに留意してください。この値は上記図9のnormalClassの登録時に宣言された値と同じです。

またtagWND.dwExtraFlagsにも注目してください。この値がNtUserConsoleControlの呼び出し中に変化する値で、tagWND.pExtraBytesフィールドの値は、ユーザーモード アドレスではなく、カーネルに対するオフセットを示すようになります。ただしウィンドウの作成直後は、この値がユーザーモード アドレス(0x0000015ba4b73fb0)であることが明確にわかります。

同じウィンドウのカーネルモード デスクトップのtagWND構造体を図11に示します。この構造体を見つけるため、CreateWindowExW関数を静的に解析してメモリーが割り当てられた場所を特定し、実行中その場所にブレーク ポイントを設定しました。

カーネル内のtagWND構造体が実際にHMValidateHandleの呼び出し後にユーザーモードで返されるものと同じであることがわかります。前述のように、ユーザーモード デスクトップ ヒープはカーネルモード デスクトップ ヒープの単なるコピーで、Win32kは実際にはこのコピーを使ってウィンドウを管理しています。

後述しますが、実はカーネル内には親のtagWND構造体が配置されています。この親の構造体には関連するカーネル アドレスがすべて格納されていて、Microsoftはウィンドウ構造体へのユーザーモードのアクセスはすべて、ユーザーモード セーフなtagWND構造体を介して行われるようにしてきました。Microsoftはユーザーモード アプリケーションからのカーネル ポインター漏えい防止にひとかたならぬ努力をしており、ユーザーモードからはデスクトップ ヒープ アドレスにしかアクセスできません。このため、この制約を回避する方法が少し後で必要になります。

画像11は、ユーザーモードとカーネルモードで共有されるtagWND構造体のカーネルモード コピーのスクリーンショットです。
図11 ユーザーモードとカーネルモードで共有されるtagWND構造体のカーネルモードのコピー

tagWND構造体が存在する実際のアドレスを確認すれば、さらにtagWND.OffsetToDesktopHeapが明確になる点にも留意が必要です。上記のtagWND.OffsetToDesktopHeapの値は0x38390で、カーネルのtagWNDのアドレスは0xffff8e8201038390です。tagWNDのアドレスからtagWND.OffsetToDesktopHeapの値を引くと、カーネル デスクトップ ヒープのアドレスである0xffff8e8201000000を特定できます。同じことがユーザーモード tagWND構造体についても当てはまります。

後で任意のwriteプリミティブを取得すれば、これらのオフセットがカーネル メモリー空間内のナビゲーションに使われることになります。続く第3部ではステップ6~9を見ていきます。

続きを読む ➠ セクション 3 – 詳細分析ステップ6-9

トップに戻る

Enlarged Image