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のコードを参照します。この図ではコード内の空白を削除してクリーンアップしています。
このread64という関数を分析する前に、spmenuオブジェクトについて説明しておきます。なぜこのオブジェクトについての説明が重要かというと、pmbi構造体内に返された情報を、GetMenuBarInfoが引き出している場所が、このオブジェクトだからです。そこでspmenuオブジェクトの全体的なレイアウトを知っておくことが重要です。
Microsoftはここで取り上げるどの構造体のデバッグ シンボルも提供していないので、重要な構造体のオフセットとポインターの関係を特定するには、PoCと多少のリバース エンジニアリングに頼らざるを得ません。図57のPoCコードで、偽のspmenuが作成された箇所(355〜385行目)を見てみると、spmenuオブジェクトの重要なオフセットと構造体のサイズを推測できます。
図58は上記のPoCコードにもとづく偽のspmenuレイアウト図です。
ここで、以前ステップ12でWnd1の正規のspmenuが g_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 に示します。
メニューバー GetMenuBarInfo関数はパラメーターを4つ取ります。最初のパラメーター(hwnd)は、情報を照会する対象となっているメニュー バーを所有しているウィンドウへのハンドルです。2つめのパラメーター(idObject)は、照会対象となるメニュー オブジェクトです。これはポップアップ メニュー、メニュー バー、システム メニューのいずれでもかまいません。ただし、このPoCでは、メニュー バーに相当する-3 (0xFFFFFFFD)だけを使っています。
3つめのパラメーター(idItem)には情報の取得対象となる項目を指定します。このパラメーターが0の場合、この関数はメニュー自体の情報を取得します。このパラメーターが1の場合、この関数はメニューの1つめの項目の情報を取得します。以下は同様となります。
このPoC内では、すべての呼び出しが値1を使っています。したがって、メニューの最初の項目を参照します。最後のパラメーター(pmbi)は返された情報を格納する構造体です。この構造体は PMENUBARINFO型です(図60)。
ここで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に示します。
最後に、グローバル変数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のどのオフセットに対応するかがわかります。
したがって上記の分析にもとづいた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のメモリー ダンプです。この図から上記の計算が正しいことが確認できます。
上記の変数から、図64に示したPoC 249行目で、g_pmbi_rcBar_leftがpmbi.rcbar.left、つまり0x40に等しくなるよう設定されたことがわかります。
図65に示したPoC 261行目では、qwDestAddrからg_pmbi_rcBar_leftの値を引いています。qwDestAddrはread64へ渡された入力パラメーター、つまり私たちがデリファレンスしようとしているアドレスであることがわかっています。したがって、PoCはこのアドレスから0x40を引いてref_g_pMem5に割り当てています。
ぱっと見では混乱するかもしれません。ただ、GetMenuBarInfoがpmbiを計算する方法から、pmbiの最初の2つの値は、ref_g_pMem5[0x40]とref_g_pMem5[0x44]から得られることがわかっているわけです。したがって、pmbi.rcbar.leftとpmbi.rcbar.topを使ってGetMenuBarInfoを呼び出すことでqwDestAddrをデリファレンスする場合、GetMenuBarInfoがこれらの値の計算にref_g_pMem5[0x40]とref_g_pMem5[0x44]を参照している点を考慮せねばなりません。それがPoC 261行目が行っていることです。
注: pmbi.rcbar.leftとpmbi.rcbar.topの両方を使う必要があるのは、私たちは64ビットの値を格納する必要があるのに、それぞれの値が32ビットしかないためです。
これでref_g_pMem5がqwDestAddr - 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の上位ビットが含まれていることがわかります。
私たちはすでに、spmenu+0x50が親のtagWND構造体を指しているはずと知っています。また図67からは、0xffff8e82008427e0が実際に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プリミティブを提供するのか、これらについて説明しました。