脆弱性

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

Clock Icon 9 min read
Related Products

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

目次

14. System権限で新しいプロセスを作成し更新された構造体に変更を復元する
read64関数の分析
結論

図表の目次

図56. PoC 206〜266行目
図57. PoC 355-385行目図58. 偽spmenuオブジェクトのメモリー レイアウト
図59. GetMenuBarInfoの関数プロトタイプ
図60. PMENUBARINFO構造体
図61. 初期化後のref_g_pMem5のメモリー ダンプ
図62. RECT (rcbar)構造体に関するMSDNの文書
図63. GetMenuBarItemの初回呼び出し後のpmbiのメモリー ダンプ
図64. PoC 249行目図65. PoC 261行目
図66. GetMenuBarItemの2回目の呼び出し後のpmbiのメモリー ダンプ
図67. Wnd1の親のtagWND構造体のアドレス ポインターを示すメモリーダンプ

14. System権限で新しいプロセスを作成し更新された構造体に変更を復元する

PoCの残りの行(640〜726)では、現在のプロセスのセキュリティー トークン(Systemトークン)を継承する新しいプロセスを作成するだけです。それから、Windowsが今後これらの構造体のいずれかにアクセスした場合にシステム クラッシュが発生しないよう、これまで更新した構造体を元の状態にリセットします。

read64関数の分析

read64関数の説明では図56のコードを参照します。この図ではコード内の空白を削除してクリーンアップしています。

画像56は、PoC 206〜266行目のスクリーンショットです。この図はコード内にあった空白を取り除いてクリーンアップしてあります。QWORD MyRead64(QWORD qwOestAddr) MENUBARINFO pmbi = O; pmbi.cbSize = sizeof(MENUBARINFO); if (g_blslnit) else QWORD *pTemp = memset(pTemp, ø, øx20e) ; QWORD qwBase = øxeoøoeø4øøøøøøøø; QWORD qwAdd = øxeøøøoeø8eøøøøeø8; for (int i = 0; i < øx4ø; i++) *(pTemp + i) = qwBase + qwAdd*i; = (QWORD)pTemp; -3, 1, &pmbi) ; g_pmbi_rcBar_teft = pmbi. rcBar. left; øx2øø) ;
図56. PoC 206〜266行目

このread64という関数を分析する前に、spmenuオブジェクトについて説明しておきます。なぜこのオブジェクトについての説明が重要かというと、pmbi構造体内に返された情報を、GetMenuBarInfoが引き出している場所が、このオブジェクトだからです。そこでspmenuオブジェクトの全体的なレイアウトを知っておくことが重要です。

Microsoftはここで取り上げるどの構造体のデバッグ シンボルも提供していないので、重要な構造体のオフセットとポインターの関係を特定するには、PoCと多少のリバース エンジニアリングに頼らざるを得ません。図57のPoCコードで、偽のspmenuが作成された箇所(355〜385行目)を見てみると、spmenuオブジェクトの重要なオフセットと構造体のサイズを推測できます。

画像57はPoC 355〜385行目までのスクリーンショットです。偽のspmenuが作成されています。
図57. PoC 355-385行目

図58は上記のPoCコードにもとづく偽のspmenuレイアウト図です。

画像58は、偽のspmenuオブジェクトのメモリー レイアウト図です。左から右に順にg_pMem4、g_pMem3、g_pMem1、g_pMem2が並んでいます。
図58. 偽spmenuオブジェクトのメモリー レイアウト

ここで、以前ステップ12でWnd1の正規のspmenug_pMem4によって置き換えられていたことを思い出してください。図58を見るとわかるように、ここには上図57のコード スニペットで設定した、より大きい偽spmenuへのポインターが含まれています。

高レベルでは、read64関数はカーネル アドレスを受け取り、そのアドレスにあるポインターを返します。ここではこの機能が必要になります。なぜなら、漏えいさせたspmenuのアドレスにもとづいてspmenu + 0x50 (0xfff8e82008218c0 + 0x50 = 0xfff8e8200821910)のアドレスを計算することはできますが、そのアドレスは、私たちがここで必要としているものではないからです。私たちが必要としているのは、そのアドレスにあるポインターです。

ところが、漏えいさせたspmenuのアドレスはカーネル アドレスなので、ユーザーモードからspmenu構造体内のポインターを直接読むことはできません。このread64という関数はこの機能の提供のために設計されました。

このread64という関数の中には、GetMenuBarInfoの呼び出しが2回と、初期化flag (g_bIsInit)menubarの情報構造体(pmbi)、サイズが0x200バイト割り当てられたメモリー チャンクが存在します。これらそれぞれについてはこの後説明します。

GetMenuBarInfo関数を使うと、PMENUBARINFO (pmbi)構造体を介して、指定したウィンドウ(hwnd)のメニューバー情報を取得できます。GetMenuBarInfo関数の関数プロトタイプを図 59 に示します。

画像59は、GetMenuBarInfoの関数プロトタイプのスクリーンショットです。
図59. GetMenuBarInfoの関数プロトタイプ

メニューバー GetMenuBarInfo関数はパラメーターを4つ取ります。最初のパラメーター(hwnd)は、情報を照会する対象となっているメニュー バーを所有しているウィンドウへのハンドルです。2つめのパラメーター(idObject)は、照会対象となるメニュー オブジェクトです。これはポップアップ メニュー、メニュー バー、システム メニューのいずれでもかまいません。ただし、このPoCでは、メニュー バーに相当する-3 (0xFFFFFFFD)だけを使っています。

3つめのパラメーター(idItem)には情報の取得対象となる項目を指定します。このパラメーターが0の場合、この関数はメニュー自体の情報を取得します。このパラメーターが1の場合、この関数はメニューの1つめの項目の情報を取得します。以下は同様となります。

このPoC内では、すべての呼び出しが値1を使っています。したがって、メニューの最初の項目を参照します。最後のパラメーター(pmbi)は返された情報を格納する構造体です。この構造体は PMENUBARINFO型です(図60)。

画像60はPMENUBARINFO型の構造体のスクリーンショットです。この図のfBarFocusedとfFocusedは1の値を示しています。
図60. PMENUBARINFO構造体

ここでread64関数を見直してみると、pmbiという PMENUBARINFO構造体が割り当てられていることがわかります。その後g_bIsInitフラグがチェックされます。このg_bIsInitフラグは、GetMenuBarInfoの初回呼び出し後に設定されます。

PoC(246〜252行目)を見てみると、GetMenuBarInfoの初回呼び出し時は、その呼び出しの後にpmbi.rcbar.leftの値を使ってグローバル変数g_pmbi_rcBar_leftを設定していることがわかります(249行目)。これが完了すると、g_bIsInitフラグがtrueに設定されます(252行目)。

このPoCはまた、サイズが0x200バイトの配列を割り当てて作成します。そしてforループでこの配列を初期化し、各DWORD(32ビット)にインデックスを設定します。このメモリーの部分的なダンプを図61に示します。

画像61は、初期化後のref_g_pMem5のメモリー ダンプのスクリーンショットです。この図では、配列の書式が表示されています。列名は Offset、0、4、8、Cです。最初の行の2列分のセルが青でハイライト表示されています。それぞれ内容は00000000と00000004です。
図61. 初期化後のref_g_pMem5のメモリー ダンプ

最後に、グローバル変数ref_g_pMem5が、割り当てられたこのメモリーを指すように設定されます。このref_g_pMem5は、PoCコードがWnd1の本物のspmenuを置き換えた偽のspmenuの一部であることを思い出してください(図58)。

GetMenuBarInfo関数は、要求されたメニュー バー情報の格納にpmbiを使うことが分かっています。read64関数を見てみると、参照されているpmbiのメンバーはpmbi.rcbar.left (249行目、265行目)とpmbi.rcbar.top (265行目)だけです。したがって、ここで何が起こっているのかを理解するには、GetMenuBarInfoがどうやってこれらの値の計算しているのかを特定する必要があります。GetMenuBarInfo関数をリバース エンジニアリングしてみると、pmbi.rcbarの値は以下のように計算されていることがわかりました。

  • pmbi.rcbar.left = pmbi[0x4] = ref_g_pmem5[0x40] + tagWND[0x58]
  • pmbi.rcbar.top = pmbi[0x8] = ref_g_pmem5[0x44] + tagWND[0x5c]
  • pmbi.rcbar.right = pmbi[0xc] = ref_g_pmem5[0x40] + g_pmem5[0x48]
  • pmbi.rcbar.bottom = pmbi[0x10] = ref_g_pmem5[0x4c] + pmbi[0x8]

図62に示したMicrosoftのMSDNの文書を確認すると、どのrcbar変数がpmbiのどのオフセットに対応するかがわかります。

画像62は、MicrosoftのRECT (rcbar)構造体に関する文書のスクリーンショットです。
図62. RECT (rcbar)構造体に関するMSDNの文書

したがって上記の分析にもとづいたGetMenuItemの初回呼び出し後のpmbi.rcbarは次のようになります。

  • pmbi.rcbar.left = 0x40 + 0x00 = 0x40
  • pmbi.rcbar.right = 0x44 + 0x00 = 0x44
  • pmbi.rcbar.top = 0x40 + 0x48 = 0x88
  • pmbi.rcbar.bottom = 0x4c + 0x44 = 0x90

図63は、GetMenuBarItemの初回呼び出し後のpmbiのメモリー ダンプです。この図から上記の計算が正しいことが確認できます。

画像63は、GetMenuBarItemの初回呼び出し後のpmbiのメモリー ダンプのスクリーンショットです。配列の形式で表示されています。1行目の1列目と2列目の2つのセルが青くハイライトされています。それぞれ内容は00000030と00000040です。
図63. GetMenuBarItemの初回呼び出し後のpmbiのメモリー ダンプ

上記の変数から、図64に示したPoC 249行目で、g_pmbi_rcBar_leftpmbi.rcbar.left、つまり0x40に等しくなるよう設定されたことがわかります。

画像64はPoC 249行目、g_pmbi_rcBar_left = pmbi.rcbar.leftのスクリーンショットです。
図64. PoC 249行目

図65に示したPoC 261行目では、qwDestAddrからg_pmbi_rcBar_leftの値を引いています。qwDestAddrread64へ渡された入力パラメーター、つまり私たちがデリファレンスしようとしているアドレスであることがわかっています。したがって、PoCはこのアドレスから0x40を引いてref_g_pMem5に割り当てています。

画像65は、PoC 261行目のスクリーンショットです。*(QWORD *)ref_g_pMem5 から始まっています。
図65. PoC 261行目

ぱっと見では混乱するかもしれません。ただ、GetMenuBarInfopmbiを計算する方法から、pmbiの最初の2つの値は、ref_g_pMem5[0x40]ref_g_pMem5[0x44]から得られることがわかっているわけです。したがって、pmbi.rcbar.leftpmbi.rcbar.topを使ってGetMenuBarInfoを呼び出すことでqwDestAddrをデリファレンスする場合、GetMenuBarInfoがこれらの値の計算にref_g_pMem5[0x40]ref_g_pMem5[0x44]を参照している点を考慮せねばなりません。それがPoC 261行目が行っていることです。

: pmbi.rcbar.leftpmbi.rcbar.topの両方を使う必要があるのは、私たちは64ビットの値を格納する必要があるのに、それぞれの値が32ビットしかないためです。

これでref_g_pMem5qwDestAddr - 0x40を指すようになったので、次にGetMenuBarInfoを呼び出すと、次のような値が得られます。

  • ref_g_pMem5 = qwDestAddr - 0x40
  • pmbi.rcbar.left = qwDestAddr (下位ビット)
  • pmbi.rcbar.right = qwDestAddr (上位ビット)

図66は、2回目のGetMenuBarItem呼び出し後のpmbiのメモリー ダンプを示したものです。pmbi[0x4]にはqwDestAddrの下位ビットが確かに含まれ、pmbi[0x8]にはqwDestAddrの上位ビットが含まれていることがわかります。

画像66は、2回目のGetMenuBarItem呼び出し後のpmbiのメモリー ダンプのスクリーンショットです。配列の形式で表示されています。1行目の1列目と2列目の2つのセルが青くハイライトされています。それぞれ内容は00000030と008437e0です。
図66. GetMenuBarItemの2回目の呼び出し後のpmbiのメモリー ダンプ

私たちはすでに、spmenu+0x50が親のtagWND構造体を指しているはずと知っています。また図67からは、0xffff8e82008427e0が実際にWnd1の親のtagWND構造体であることがわかります。

画像67は、Wnd1の親のtagWND構造体のアドレス ポインターを示しているメモリー ダンプのスクリーンショットです。配列の形式で表示されています。最初の行の0000000000050330のセルが青でハイライトされています。oxffff8e82008437ee 0000000000000005 oxffff8e82008437f0 ffff8e8202cc0010 ffffd40ff34ea4b0 oxffff8e820084380e ffff8e82008437ee ffff8e8201038390
図67. Wnd1の親のtagWND構造体のアドレス ポインターを示すメモリー ダンプ

これで、read64を呼び出すと、入力パラメーターqwDestAddrの位置にある、デリファレンスされたアドレスが返され、任意のreadプリミティブ提供に成功したことがわかります。

結論

Win32kに関する2部構成の連載は今回で完結です。第1部ではWin32 APIを使ってウィンドウやメニューなどのGUIオブジェクトを作成する方法を説明しました。次に、これらのオブジェクトの管理に使われるユーザーモードとカーネルモードのデータ構造について説明しました。さらに、それらのデータ構造が、ユーザーモードとカーネルモードとの間で最適かつ安全に移行できるよう、長年かけてどのように変化してきたのかを説明しました。

第2部では最近の脆弱性(CVE-2022-21882)と、この脆弱性を悪用して特権を昇格させる方法について分析しました。私たちは公開PoCの内部構造について論じ、それによってMicrosoftが過去20年を費やし入念に実装してきた保護を回避するには今日何が求められるのかを示しました。

そして私たちは、CVE-2022-21882のCVE-2021-1732との類似性や、CVE-2021-1732用の修正プログラムがなぜCVE-2022-21882の防止に不十分だったのかを示しました。最後に、このエクスプロイトがどのように偽のメニュー構造体と組み合わせてGetMenuBarItem関数を使うのかや、それによって特権昇格に必要なSystemトークンを発見・コピーし、任意のreadプリミティブを提供するのか、これらについて説明しました。

トップに戻る

Enlarged Image