脆弱性

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

Clock Icon 9 min read
Related Products

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

目次

6. ウィンドウ間のオフセットを計算する
7. 最も下位のウィンドウのアドレスでNtUserConsoleControlを呼び出す
8. 3つめの(magic)ウィンドウを作成する
9. NtUserConsoleControlとNtCallbackReturn呼び出す悪意のあるバージョンでxxxClientAllocWindowClassExtraBytesをフックしてから正規のxxxClientAllocWindowClassExtraBytesに戻る

図表の目次

図12. PoC 472~499行目
図13. Wnd1のHMValidateHandleの戻り値(rax)をWinDbgでダンプしたところ
図14. Wnd0のHMValidateHandleの戻り値(rax)をWinDbgでダンプしたところ
図15. PoC 501行目
図16. NtUserConsoleControl呼び出し前後のWnd0のpExtraBytesの値
図17. WndMagicの作成
図18. WndMagic作成直後のメモリー レイアウト
図19. PoC 522~530行目
図20. ポインター上書き前のKernelCallbackTable
図21. ポインター上書き後のKernelCallbackTable
図22. PoC 170~190行目
図23. PoC 496行目
図24. PoC 151~164行目

6. ウィンドウ間のオフセットを計算する

472行目から499行目(図12)では単純に、最初に作成された2つのウィンドウのうち、カーネル デスクトップ ヒープのベースに対するオフセットが小さい方を決定しています。その後、tagWNDポインターとtagWND.OffsetToDesktopHeapを、メモリー内でのウィンドウの順番にもとづいて_minまたは_maxのタグを付けた変数に代入します。

画像12は、PoC 472〜499行目までのスクリーンショットです。ポインターと、ポインターが変数にどのように代入されるかを示しています。
図12. PoC 472~499行目

今回の場合、最初に作成されたウィンドウは、実はメモリー上で最も下位にはなかったので、Wnd1と呼ぶことにします。2つめのウィンドウは、メモリー上ではより下位にあるため、今後はこちらをWnd0と呼んで、メモリー上での位置を反映することにします。

図13で示した内容から、ユーザーモード デスクトップ ヒープは0x15ba5028390 - 0x38390、つまり0x15ba4ff0000になければならないことがわかります。

画像13は、WND1のHMValidateHandleの戻り値をWinDbgでダンプしたようすを表すスクリーンショットです。
図13. Wnd1HMValidateHandleの戻り値(rax)をWinDbgでダンプしたところ

Wnd1tagWND.OffsetToDesktopHeapを取り出して上記で計算したデスクトップ ヒープ アドレスに加算すると、0x15ba4ff0000 + 0x2ad30、つまり0x15ba501ad30が得られます。図14に示すように、これがまさにWnd0tagWND構造体に対してHMValidateHandleが返した内容です。

画像14はWND0のHMValidateHandleの戻り値をWinDbgでダンプしたようすを表すスクリーンショットです。
図14 Wnd0HMValidateHandleの戻り値(rax)をWinDbgでダンプしたところ

PoCのこの部分では、単にこの計算を行い、tagWNDのポインターとデスクトップ ヒープへのオフセットを以下のトラッキング用の変数に代入しています。

  • kernel_desktop_heap_base_offset1
  • kernel_desktop_heap_base_offset2
  • kernel_desktop_heap_base_offset_Min
  • tagWndMin_offset_0x128
  • tagWndMin_offset_0x128
  • kernel_desktop_heap_base_offset_Min
  • hWndMin
  • hWndMax
7. 最も下位のウィンドウのアドレスでNtUserConsoleControlを呼び出す

次にこの作者はhWndMin(Wnd0)に対してNtUserConsoleControl(PoC 501行目、図15に示す)を呼び出します。

画像15はPoC 501行目です。g_pfnNtUserConsoleControl(6, &hWndMin, 0x10); という内容です。
図15. PoC 501行目

これでWnd0はコンソール ウィンドウに変換されます。その結果、tagWND.pExtraBytesがユーザーモード アドレス ポインターからオフセットへと変更されます。Windowsはこの後、このオフセットをカーネルモード デスクトップ ヒープのベースへのオフセットとして扱います。なぜそうなるかと言えば、NtUserConsoleControltagWND.dwExtraFlagの値に0x800を追加したことにより、ウィンドウ マネージャーに対して、「このウィンドウはコンソール ウィンドウなのでpExtraBytesフィールドをカーネル デスクトップ ヒープのベース アドレスからのオフセットとして扱ってください」と指示しているからです。

NtUserConsoleControlを呼び出す前と後のWnd0tagWND構造体を図16に示します。

画像16は、NtUserConsoleControlを呼び出す前と後のWnd0のpExtraBytesの値のスクリーンショットです。コードのなかには2つ黄色の長方形でハイライトされている領域があります。
図16. NtUserConsoleControl呼び出し前後のWnd0pExtraBytesの値

tagWND.pExtraBytesは、ユーザーモードの仮想アドレス0x15ba4b74370から、カーネルモード デスクトップ ヒープのオフセットである0x2ae80に変更されています。tagWND.dwExtraFlagは、0x100100018から0x100100818(0x100100018 + 0x800)に変更されています。

これでtagWND.dwExtraFlagはカーネルモード デスクトップ ヒープへのカーネル オフセットを指すようになったので、tagWND.pExtraBytes内のオフセットは、カーネル デスクトップ ヒープにオフセットを加算した結果、つまり0xff8e8201000000 + 0x2ae80 = 0xff8e820102a80を指すことになります。このアドレスは現時点では意味がありませんが、その使いかたについてはステップ9で詳しく説明します。

8. 3つめの(magic)ウィンドウを作成する

次に3つめのウィンドウが作成されます。このウィンドウは上記のステップ4で登録したmagicClassクラスに属します。また、最初の2つのウィンドウであるWnd0Wnd1と同様にsomewndという名前が与えられています。今後はこのウィンドウをWndMagicと呼ぶことにします。

CreateWindowExWの呼び出しは、図17に示すとおりです。

17の画像はCreateWindowExWを呼び出しているコードのスクリーンショットです。これはmagicClassの中にあります。
図17. WndMagicの作成

今回のサンプルの、WndMagicウィンドウを作成後の全ウィンドウのメモリー レイアウトを図18に示します。Wnd0tagWND.dwExtraFlag(0x100100818に対して0x10010018)が、tagWND.pExtraBytes(0x2ae80)がカーネル デスクトップ ヒープへのオフセットになったことを示している点にご注目ください。

この図からは、Wnd0のカーネルのtagWND構造体とそのユーザーモード コピーの両方が、カーネル デスクトップ ヒープ内の同じオフセットを指していること、その一方でほかの2つのtagWND構造体のpExtraBytesがユーザー ランド内のメモリーを指していることがわかります。

画像18は、WndMagic作成直後のメモリーレイアウトを表した図です。左側がユーザー ランドとそのレイアウト、右側がカーネル ランドです。
図18. WndMagic作成直後のメモリー レイアウト

: ウィンドウの作成順序がそのままメモリー上の配置順序と呼応するわけではないことが分かります。今回の場合、Wnd0は2番めに作成されたウィンドウですが最も下位のアドレスに配置されていて、WndMagicは最後に作成されましたが、下位から2番めのアドレスに配置されています。果たしてステップ5で最初に作成するウィンドウ数を増やせばウィンドウのメモリー レイアウトがより予測しやすくなり、ステップ6での各ウィンドウのメモリー配置順序決定に必要な計算量が減るのかどうか、確認してみるのも一興でしょう。

9. NtUserConsoleControlとNtCallbackReturn呼び出す悪意のあるバージョンでxxxClientAllocWindowClassExtraBytesをフックしてから正規のxxxClientAllocWindowClassExtraBytesに戻る

WndMagicの作成後は図19に示したコードが実行されます(PoC 522〜530行目)。

画像19はPoC 522〜530行目までのスクリーンショットです。このコードがWndMagicの生成後に実行されます。
図19 PoC 522~530行目

まず、xxxClientAllocWindowClassExtraBytesxxxClientFreeWindowClassExtraBytesKernelCallbackTableエントリーへのメモリー保護が、VirtualProtectの呼び出しによってPAGE_READONLY(0x2)からPAGE_EXECUTE_READWRITE(0x40)に変更されています。次にxxxClientAllocWindowClassExtraBytesxxxClientFreeWindowClassExtraBytesのカーネル コールバック テーブルのポインター エントリーがそれぞれ、攻撃者の定義する関数であるg_newxxxClientAllocWindowClassExtraBytesg_newxxxClientFreeWindowClassExtraBytesへのポインターによって上書きされることがわかります。

図20は、関数ポインターが上書きされる前のKernelCallbackTableを示しています。

画像20は、関数ポインターが上書きされる前のKernelCallbackTableのスクリーンショットです。
図20. ポインター上書き前のKernelCallbackTable

図21は2つの関数をフックした後のKernelCallbackTableを示しています。

画像21は2つの関数がフックされてポインターが上書きされた後のKernelCallbackTableを示しています。
図21. ポインター上書き後のKernelCallbackTable

これで正規の関数に悪意のある関数g_newxxxClientAllocWindowClassExtraBytesg_newxxxClientFreeWindowClassExtraBytesがうまくフックされたので、正規の関数が呼び出されればいつでもg_new関数に実行がリダイレクトされます。

なぜこれらの関数をフックしたがる人がいるのかを理解するには、正規の関数が何をするものかを理解するとよいでしょう。正規のxxxClientAllocWindowClassExtraBytes関数はtagWND.cbWndExtraの値をパラメーターとして受け取って、そのバイト数をデスクトップ ヒープに割り当てます。その後、NtCallbackReturnの呼び出しにより、割り当て先へのポインターが返されます(これが後で重要になります)。そしてこのポインターは、ウィンドウの構造体のtagWND.pExtraBytesフィールドに格納されます。

xxxClientAllocWindowClassExtraBytesをフックできれば、最低でも、参照されるウィンドウのtagWND.pExtraBytesフィールドに書き込まれるポインターのアドレスを、任意のものにできることがわかります。ただしこのアドレス ポインターはユーザー モード ポインターなので、カーネル内でコードを実行してSystemトークンを盗み、それによって特権を昇格させることが目的なら、あまり意味がありません。したがって、カーネル メモリーへのアクセスを可能にするには何か別のものが必要になるのですが、これについては後で説明します。

図22は、悪意のあるg_newxxxClientAllocWindowClassExtraBytesの関数定義を示しています。

画像22は、PoC 170〜190行目までのスクリーンショットです。この図には悪意のあるg_newxxxClientAllocWindowClassExtraBytesの関数定義が含まれています。
図22. PoC 170~190行目

g_newxxxClientAllocWindowClassExtraBytes関数の主な目的は、NtUserConsoleControlを呼び出して現在参照されているウィンドウのハンドルをコンソール ウィンドウのハンドルに変更することです。ステップ7で説明したように、コンソール ウィンドウは、デスクトップ ヒープのユーザーモード コピー内では追加のバイト フィールドを管理しておらず、カーネル デスクトップ ヒープ内でこれを直接管理しています。このためコンソール ウィンドウのtagWND.pExtraBytesフィールドは、ユーザーモードのデスクトップ ヒープ ポインターではなくカーネル デスクトップ ヒープへのオフセットとして扱われます。

通常、NtUserConsoleControlは正規xxxClientAllocWindowClassExtraBytes関数のコール スタック内にはありません。このためWindowsはNtUserConsoleControltagWND.dwExtraFlag (0x800を追加)やtagWND.pExtraBytesフィールドに変更を加えることを予期しておらず、プログラム的にも想定されていません。この結果、予期せぬカーネル メモリー アクセスへとつながる型の取り違え(type confusion)バグ(CVE-2022-21882)が発生します。

標準のGUIウィンドウはpExtraBytesフィールドにユーザーモード デスクトップ ヒープ ポインターが入った状態で初期化されていますが、今ではウィンドウ マネージャーがこの値をカーネル デスクトップ ヒープへのオフセットとして扱うようになったので、このフィールドの値は変更されねばなりません。新しい値には、今存在するもっと大きなポインターの値とは対照的に、カーネル デスクトップ ヒープのベースからの有用なオフセット値を反映させる必要があります。

正規のxxxClientAllocWindowClassExtraBytes関数の場合、要求されたメモリーの割り当てが終わると、割り当てられたメモリーへのポインターをNtCallbackReturn関数に渡して、実行をカーネルに戻します。この結果、最終的にはpExtraBytesのメモリー割り当てへのポインターが、tagWND.pExtraBytesフィールドに格納されることになります。

g_newxxxClientAllocWindowClassExtraBytesNtCallbackReturnを呼び出す直前、qwRetkernel_desktop_heap_base_offset_Minの値に設定されます。この値は前述のステップ6の図12で見たWnd0tagWND.OffsetToDesktopHeapの値です。図23はkernel_desktop_heap_base_offset_MinWnd0tagWND.OffsetToDesktopHeapを割り当てるPoCのコードを示しています。

画像23は、kernel_desktop_heap_base_offset_Minで始まるPoC 496行目です。
図23. PoC 496行目

おさらいすると、NtUserConsoleControlを呼び出すと、pExtraBytesフィールドがカーネルへのオフセットとして解釈されます。NtCallbackReturnの呼び出しは、そのウィンドウのpExtraBytesフィールドを、Wnd0tagWND.OffsetToDesktopHeapへのオフセットで上書きします。つまりこれで、「あるウィンドウのpExtraBytesフィールドをWnd0tagWND構造体のカーネル アドレスを指すように変更できるようになった」ということです。これは、ユーザーモード コールバックのxxxClientAllocWindowClassExtraBytesを呼び出す関数が見つかることを前提としています(次のステップで詳述)。

図24は、フックされた2つめの関数であるg_newxxxClientFreeWindowClassExtraBytesのコード(151〜164 行目)です。

画像24はPoC 151〜164行目までのスクリーンショットです。茶色でg_nRandomをハイライトしています。
図24. PoC 151~164行目

この関数がフックされる理由は、NtUserConsoleControlの呼び出しが後でxxxClientFreeWindowClassExtraBytesを呼び出すからです。ただしせっかくxxxClientAllocWindowClassExtraBytesをフックしてtagWND.dwExtraFlagtagWND.pExtraBytesをエクスプロイトしやすい値に設定しても、xxxClientFreeWindowClassExtraBytesを呼び出してこれらの値を解放してしまったのでは効果がありません。

これを回避するため、正規の関数がフックされ、悪意のある関数は、g_newxxxClientAllocWindowClassExtraBytesに渡されたパラメーター(tagWND.cbExtraBytesフィールド)とランダムな値g_nRandomとを比較します。WndMagictagWND.cbExtraBytesはPoCの最初の方でこの値に設定されているので(ステップ4参照)、その値で比較することで、正規のxxxClientFreeWindowClassExtraBytesWndMagicウィンドウへの参照中に呼び出されないようにしています。

値が一致すればg_newxxxClientFreeWindowClassExtraBytesは単に1を返します。値が一致しなければ、実行がg_oldxxxClientAllocWindowClassExtraBytesか正規のxxxClientAllocWindowClassExtraBytes関数にリダイレクトされます。

これはこの作者側のミスの可能性があります。というのも、g_oldxxxClientFreeWindowClassExtraBytesへの呼び出し経由で意図した関数(xxxClientFreeWindowClassExtraBytes)に制御を戻す方が理にかなっているからです。この呼び出しは、PoC 304行目で正規xxxClientFreeWindowClassExtraBytesへのポインターとして定義されていました。

ただし、これらのフックされた関数は、ほかのウィンドウを入力として呼び出されることがないので、このエラーがこのPoCのエクスプロイト成功に影響することはありません。続く第4部ではステップ10〜11を見ていきます。

続きを読む ➠ セクション 4 – 詳細分析ステップ10-11

トップに戻る

Enlarged Image