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)
前述のように、歴史的にエクスプロイトの作者は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に対する呼び出しを行う最初で唯一のオペコードであることがわかります。
この作者はこのPoC 58行目にFindHMValidateHandleという名前の関数を定義することで、上記の内容を実現しています。図3はこのFindHMValidateHandleという関数からのコードスニペットです。この関数はE8オペコードを探してIsMenuを検索し、その位置をポインターとしてg_pfnHmValidateHandleというグローバル変数に保存します。この位置は、後でユーザーモードにマップされたtagWND構造体のアドレスを漏えいさせるのに使われます。
2. NtUserConsoleControl、NtCallbackReturnをロードする
CVE-2022-21882とCVE-2021-1732のトリガーには、NtUserConsoleControlとNtCallbackReturnの両関数が悪用されます。これらはいずれも文書化されていない関数で、それぞれwin32u.dll、ntdll.dllに存在しています。PoC 285~288行目(図4)は、これらの関数を解決してポインターを保存し、後で使えるようにしています。
NtUserConsoleControl、NtCallbackReturnの両関数についてはそれぞれステップ7と9で詳しく説明します。
3. KernelCallbackTableを見つけてユーザーモード コールバックxxxClientAllocWindowClassExtraBytesとxxxClientAllocWindowClassExtraBytesへのポインターを保存する
297〜304行目までのコード(図5)は、KernelCallbackTableの場所を特定し、正規のxxxClientAllocWindowClassExtraBytes関数とxxxClientFreeWindowClassExtraBytes関数のアドレスのポインターをローカル変数に保存しています。xxxClientAllocWindowClassExtraBytes関数と xxxClientFreeWindowClassExtraBytes関数(ステップ9で説明)をフックするには、各関数へのポインターを見つける必要があるのです。これらの関数はいずれもユーザーモード コールバックで、Windows API内での利用のためにエクスポートされていないのがその理由です。
プロセス環境ブロック(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+0x58に KernelCallbackTableアドレスへのポインターが含まれていることがわかります。
カーネル コールバックの最初のいくつかのエントリーを図7に示します。
このPoCはKernelCallbackTableのg_oldxxxClientAllocWindowClassExtraBytesと g_oldxxxClientFreeWindowClassExtraBytesに、2つのポインター(オフセット0x3d8と0x3e0)をそれぞれ保存しています。
KernelCallbackTable+0x3d8には xxxClientAllocWindowClassExtraBytesへのポインターが、次のエントリー(0x3e0)にはxxxClientFreeWindowClassExtraBytesへのポインターが保存されています。そのようすを図8に示します。
4. ウィンドウ クラスをいくつか定義する
図9のコード(312〜325行目)には見覚えがあるでしょう。本連載の第1部で取り上げたように、2つのウィンドウ クラスを定義し、そのうち1つ(wndClass)を登録しています。一方にはnormalClassというクラス名が与えられ、もう一方にはmagicClassというクラス名が与えられています。
またこのmagicウィンドウ クラスには、ランダムなcbWndExtra値が与えられているように見えます。この値は、後でフックされた関数を呼び出すさいに、2つのウィンドウ クラスを区別するために使用されます(詳細分析は後述)。
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)です。
PoC分析で重要となるラベルをtagWND構造体に追加してあります。tagWND.cbWNDExtraの値が32 (0x20)であることに留意してください。この値は上記図9のnormalClassの登録時に宣言された値と同じです。
またtagWND.dwExtraFlagsにも注目してください。この値がNtUserConsoleControlの呼び出し中に変化する値で、tagWND.pExtraBytesフィールドの値は、ユーザーモード アドレスではなく、カーネルに対するオフセットを示すようになります。ただしウィンドウの作成直後は、この値がユーザーモード アドレス(0x0000015ba4b73fb0)であることが明確にわかります。
同じウィンドウのカーネルモード デスクトップのtagWND構造体を図11に示します。この構造体を見つけるため、CreateWindowExW関数を静的に解析してメモリーが割り当てられた場所を特定し、実行中その場所にブレーク ポイントを設定しました。
カーネル内のtagWND構造体が実際にHMValidateHandleの呼び出し後にユーザーモードで返されるものと同じであることがわかります。前述のように、ユーザーモード デスクトップ ヒープはカーネルモード デスクトップ ヒープの単なるコピーで、Win32kは実際にはこのコピーを使ってウィンドウを管理しています。
後述しますが、実はカーネル内には親のtagWND構造体が配置されています。この親の構造体には関連するカーネル アドレスがすべて格納されていて、Microsoftはウィンドウ構造体へのユーザーモードのアクセスはすべて、ユーザーモード セーフなtagWND構造体を介して行われるようにしてきました。Microsoftはユーザーモード アプリケーションからのカーネル ポインター漏えい防止にひとかたならぬ努力をしており、ユーザーモードからはデスクトップ ヒープ アドレスにしかアクセスできません。このため、この制約を回避する方法が少し後で必要になります。
tagWND構造体が存在する実際のアドレスを確認すれば、さらにtagWND.OffsetToDesktopHeapが明確になる点にも留意が必要です。上記のtagWND.OffsetToDesktopHeapの値は0x38390で、カーネルのtagWNDのアドレスは0xffff8e8201038390です。tagWNDのアドレスからtagWND.OffsetToDesktopHeapの値を引くと、カーネル デスクトップ ヒープのアドレスである0xffff8e8201000000を特定できます。同じことがユーザーモード tagWND構造体についても当てはまります。
後で任意のwriteプリミティブを取得すれば、これらのオフセットがカーネル メモリー空間内のナビゲーションに使われることになります。続く第3部ではステップ6~9を見ていきます。
続きを読む ➠ セクション 3 – 詳細分析ステップ6-9