脆弱性

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

Clock Icon 9 min read
Related Products

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

目次

12. SetWindowLongPtrAを使ってWnd1のspmenuのカーネル アドレスを漏えいさせ、それを偽のspmenuオブジェクトに置き換える

図表の目次

図33. PoC 546〜549行目
図34. PoC 552行目
図35. SetWindowLongPrtAのnIndexパラメーターに関するMSDNの文書
図36. MSDNによるhMenuパラメーターの定義
図37. PoC 355~385行目
図38. 親のtagWND構造体
図39. xxxSetWindowData (SetWindowLongPtrAから呼び出される)の逆アセンブル
図40. 変更前のspmenu値を示しているWinDbgの出力
図41. SetWindowLongPtrWから戻る直前のレジスターの値
図42. PoC 566行目
図43. PoC 546行目
図44. Wnd1の子のtagWND構造体のWinDbg出力
図45. カーネル デスクトップ ヒープ + 0x3a850
図46. Wnd1の親のtagWND構造体 + 0xa8

12. SetWindowLongPtrAを使ってWnd1のspmenuのカーネル アドレスを漏えいさせ、それを偽のspmenuオブジェクトに置き換える

SetWindowLongWに対する2回の呼び出しの後、SetWindowLongPtrAを複数回呼び出します。SetWindowLongWはより古い、将来廃止が予定されている関数で、32-bitのLONG整数を操作します。一方、SetWindowLongPtrASetWindowLongWにかわって32-bitと64-bitのLONG_PTR整数を操作できます。

図33(546行目から549行目)に示すSetWindowLongPtrAへの最初の呼び出しは、Wnd1tagWND.dwExStyle0x40000000(WS_CHILD)に変更します。これでこのウィンドウは子ウィンドウになります。パラメーターによく注意して計算をしてみてください。この関数はWnd0のハンドルを与えられています。また私たちはSetWindowLongPtrAがそのウィンドウのpExtraBytesフィールドを操作することを知っています。そしてこのpExtraBytesフィールドは今現在、Wnd0tagWND構造体を指しています。

Wnd1のデスクトップ ヒープ ベース オフセット(0x38390)からWnd0のデスクトップ ヒープ ベース オフセット(0x2ad30)を引いて(0x18)を足すと、0xd678になります。これをWnd0tagWND構造体の先頭(0xffff8e820102ad30)に足すと、0xffff8e820102010383a8が得られます。これはWnd1tagWND構造体へのオフセット0x18、またはそのtagWND.dwExStyleです。

画像33は、PoC 546〜549行目までのスクリーンショットです。これはSetWindowLongPtrAの最初の呼び出しです。
図33 PoC 546〜549行目

Wnd1のウィンドウのスタイルを子ウィンドウに変更することは、次のSetWindowLongPtrAへの呼び出しにとって重要です。

次の呼び出し(図34)はWnd1のハンドルを直接渡し、nIndexパラメーターを-12に設定し、g_pMem4dwNewLongパラメーターとして渡しています。

画像34は、PoC 552行目のスクリーンショットです。これは画像33に示した呼び出しの次の呼び出しです。
図34 PoC 552行目

SetWindowLongPtrAnIndexパラメーター(図35)に関するMSDNの文書を参照すると、この-12は子ウィンドウに対するマクロであるGWL_IDを参照しています。GWL_IDの変更はトップレベル ウィンドウではできないので、その前に行われたSetWindowLongPtrAの呼び出しは、この呼び出しのためにWnd1を準備するために行われています。

画像35は、SetWindowLongPrtAのnIndexに関連するMicrosoftの文書のスクリーンショットです。この図には表が含まれており、その1列目は「Value」です。2列目は「Meaning」です。[in] nIndex, Type: int, The zero-based offset to the value to be set. Valid values are in the range zero through the number of bytes of extra window memory, minus the size of an integer. To set any other value, specify one of the following values. Value GWL EXSTYLE -20 GWL HINSTANCE -6 GWL_ID -12 GWL STYLE -16 GWL USERDATA -21 GWL_WNDPROC -4 Meaning Sets a new extended window style. Sets a new application instance handle. Sets a new identifier of the child window. The window cannot be a top-level window. Sets a new window style. Sets the user data associated with the window. This data is intended for use by the application that created the window. Its value is initially zero. Sets a new address for the window procedure. You cannot change this attribute if the window does not belong to the same process as the calling thread.
図35. SetWindowLongPrtAnIndexパラメーターに関するMSDNの文書
ここで何が起こっているかをよく理解するには、hMenu(ステップ8のウィンドウ作成時に定義)は、メニューへのハンドルを指すか、子ウィンドウの識別子を指定することを覚えておくことが重要です。hMenuパラメーターのMSDNによる定義を図36に示します。

Type: HMENU A handle to a menu, or specifies a child-window identifier, depending on the window style. For an overlapped or pop-up window, hMenu identifies the menu to be used with the window; it can be NULL if the class menu is to be used. For a child window, hMenu specifies the child-window identifier, an integer value used by a dialog box control to notify its parent about events. The application determines the child-window identifier; it must be unique for all child windows with the same parent window.
図36 MSDNによるhMenuパラメーターの定義

ステップ8で、hMenuパラメーターが各ウィンドウに対しCreateMenuを呼び出すために設定されたことを思い出してください。そのため、ウィンドウを作成するつど、メニュー オブジェクトへのハンドル(実質的にはカーネル内のオブジェクトへのポインター)がhMenuパラメーターに割り当てられていました。しかし、現在のWnd1は子ウィンドウなので、エクスプロイトはSetWindowLongPtrAを使ってこの子ウィンドウの識別子をg_pMem4のそれに変更できます。Wnd1が今も親ウィンドウなら、hMenuハンドルの変更はできなかったことでしょう。

最後に、g_qwExploitSetWindowLongPtrAの戻り値に設定されます。これは呼び出し前のhMenuの値(ウィンドウ作成時に割り当てられたハンドル)と等しくなっています。ハンドルは単にWindowsカーネルがオブジェクトを追跡するために使うメモリー位置でしかありません。このため、g_qwExploitはこの時点でWnd1spmenuオブジェクトへのカーネル アドレスを含んでいます。

これまではまだ説明していませんでしたが、g_pMem4は前にエクスプロイト コードで定義されていました。355〜385行目では、図37に示す5つのメモリー割り当て(g_pMem1からg_pMem5まで)を設定しています。

画像37はPoC 355〜385行目までのスクリーンショットです。メモリーを5つ割り当てています。
図37 PoC 355-385行目

これらの割り当てとそれに続くコードは、Wnd1の本物のメニュー オブジェクトを置き換えるために偽のメニュー オブジェクトをセットアップしています。偽のメニュー オブジェクトを図58に示します。これについては後でもう少し詳しく説明します。

また、SetWindowLongPtrAnIndexの古い値を返すので、hMenuオブジェクトへのポインターが返されます。実際には、返されるポインターは親のtagWND構造体内の*spmenuエントリーで、これはカーネル ポインターです。これは、図38として再掲したtagWND構造体内のオフセット0xa8に存在しています。

画像38は、親のtagWNG構造体のスクリーンショットです。
図38 親のtagWND構造体

GWL_IDを指定してSetWindowLongPtrAを呼び出した場合、図39に示すように、親のtagWND構造体と、子/ユーザーモード コピー(オフセット0x98)のtagWND構造体の両方に対して*spmenuを変更する実行パスをトリガーする(このr15g_pMem4と等しい)ことになります。

図39ではrsiが親のtagWND構造体です。子でありコピーであるtagWND構造体へのポインターはオフセット0x28の位置にあり、これがraxレジスターにコピーされます。その後、各tagWND構造体のspmenuエントリーがr15内の値 (g_pMem4)に変更されます。

画像39は逆アセンブルしたxxxSetWindowDataのスクリーンショットです。
図39 xxxSetWindowData (SetWindowLongPtrAから呼び出される)の逆アセンブル

このSetWindowLongPtrAの呼び出し後に返される値は、変更される前の親のtagWND構造体 + 0xa8 (spmenu)の値です。これは上の図39で、0xffff8eac5a15c386にあるrsi+0xa8になります。この時点でWinDbg内にブレークポイントを設定すれば、レジスターの内容とrsi+0xa8(0xfff8e82008218c0)のダンプを確認できます(図40)。

画像40は変更前のspmenuの値を示しているWinDbgの出力のスクリーンショットです。この図にはレジスターの内容とrsi+0xa8 (0xfff8e82008218c0)のダンプが含まれています。
図40 変更前のspmenuの値を示しているWinDbgの出力

戻り値が実際に私たちが考えている通りの内容なのかを確認してみましょう。図41は、SetWindowLongPtrWの呼び出しが終わったときのレジスターを示しています。

注: 混乱を避けるために申し添えておきます。一部の図でSetWindowLongPtrAではなくSetWindowLongPtrWが使われているのには理由があります。Windowsが直接扱うのはUnicode文字だけなのですが、このUnicode文字を扱う関数の名前はWで終わっている関数です。そして、ASCIIに対応するためにMicrosoftはラッパー関数を作成していて、これらのラッパー関数が名前がAで終わる関数です。ラッパー関数は、単純に変換を行ってからUnicode用の関数を直接呼び出します。したがってSetWindowLongPtrAの呼び出しをデバッグする場合、実際の仕事はSetWindowLongPtrWの中で行われていることになります。

画像41は、SetWindowLongPtrWから戻る前のレジスターの値のスクリーンショットです。raxは前図と同じ値になっています。. kd> g Break instruction exception - code 8øeeeøe3 (first chance) USER 32 !SetWindowLongPtrW+0xc4 : øe33:eøee7ff8• 664ab814 c3 ret rax=ffff8e820e8218co rdx-eeeeeeøeeeeøøeeo rip-eeee7ff8664ab814 r8-eeeeee36bd4f34e8 ril=eeøeeeøeeeeøø246 r14-eeeeeeøeeeeøøeeo iopl=e nv up rbx=eøeee15ba502839ø rsi-eeeeøøeeøeuaegø rsp=eøeeee36bd4f3528 r9-eøeeeø36bd4f363ø r12=eøeeeøeeøeeeeeeø r15-eøeeeøeeøee3839ø ei pl nz na pe nc rcx=øee07ff86535b4a4 rdi-øee07ff7ed32eeøe rbp=øeeeee36bd4f363e rlo-øeeeeeøøeeøeeøøe r13=øeee7ff866532e7e cs-ee33 ss-ee2b ds=ee2b es-øe2b fs=eø53 gs=ee2b 1 ef1=eøeeme2 USER32 !SetWindowLongPtrW+0xc4 : ee33:eeee7ffC 664ab814 c3 ret
図41. SetWindowLongPtrWから戻る直前のレジスターの値

raxが、図40のrsi+0xa8と同じ値であることに注目してください。これは、ステップ8で作成した、元のメニュー項目のカーネル アドレスです。このアドレスは、後のステップで、カーネルのEPROCESS構造体を見つけ、特権昇格用Systemトークンをコピーするのに使われます。このポインターはPoC内の変数g_qwExploitに保存されています(上図34)。

図42に示したSetWindowLongPtrAへの次の呼び出し(566行目)は、Wnd1tagWND.dwExStyleを以前の値にリセットします。この値はPoC 546行目に保存されます(図43)。したがって、Wnd1はもう子ウィンドウではなくなります。

画像42はPoC 566行目です。この図はSetWindowLongPtrAで始まっています。
図42. PoC 566行目
画像43はPoC 546行目です。この図は g_qwrpdesk への代入から始まっています。
図43. PoC 546行目

SetWindowLong関数を使ってカーネル メモリーに書き込めるなら、なぜこんなこんなに苦労してspmenuポインターを漏えいさせる必要があるの?」という疑問が浮かぶかもしれません。単にGetWindowLongを使ってメニュー オブジェクトのカーネル アドレスを読み取ればよいのではないでしょうか。

そうすればメニュー オブジェクトを得られるわけです。ですがその場合、子のtagWND構造体からの読み取りになり、そこにメニュー オブジェクトのカーネル アドレスは格納されていません。図44は、Wnd1の子のtagWND構造体のWinDbg出力を示しています。オフセット0x98(spmenu)を見るとこれが絶対にカーネル ポインターではないことがわかります。後ですぐ説明しますが、これはデスクトップ ヒープからspmenuオブジェクトへのオフセットなのです。

画像44は、Wnd1の子のtagWND構造体のWinDbg出力のスクリーンショットです。0003a850 00000000が黄色の枠でハイライト表示されています。
図44. Wnd1の子のtagWND構造体のWinDbg出力

ステップ7で計算したカーネル デスクトップ ヒープ ベースに、子の構造体のtagWND+0x98内の値を加えると、図45のようになります。

画像45はカーネル デスクトップ ヒープ + 0x3a850のスクリーンショットです。
図45. カーネル デスクトップ ヒープ+0x3a850

親のtagWND構造体(オフセット0xa8)から漏えいさせたアドレスを確認すると、図46のようになっています。

画像46はWnd1の親のtagWND構造体+0xa8のスクリーンショットです。
図46. Wnd1の親のtagWND構造体 + 0xa8

最初の値が同じであることがわかります。tagWND構造体のユーザーモード フレンドリーなコピーが存在するように、ユーザーモード フレンドリーなspmenuオブジェクトのようなものも存在しているかもしれません。

しかし、このカーネルアドレスが手に入ったとしてこれを使って何ができるのでしょうか。これについては第6部のステップ13で検討します。

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

トップに戻る

Enlarged Image