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整数を操作します。一方、SetWindowLongPtrAはSetWindowLongWにかわって32-bitと64-bitのLONG_PTR整数を操作できます。
図33(546行目から549行目)に示すSetWindowLongPtrAへの最初の呼び出しは、Wnd1のtagWND.dwExStyleを0x40000000(WS_CHILD)に変更します。これでこのウィンドウは子ウィンドウになります。パラメーターによく注意して計算をしてみてください。この関数はWnd0のハンドルを与えられています。また私たちはSetWindowLongPtrAがそのウィンドウのpExtraBytesフィールドを操作することを知っています。そしてこのpExtraBytesフィールドは今現在、Wnd0のtagWND構造体を指しています。
Wnd1のデスクトップ ヒープ ベース オフセット(0x38390)からWnd0のデスクトップ ヒープ ベース オフセット(0x2ad30)を引いて(0x18)を足すと、0xd678になります。これをWnd0のtagWND構造体の先頭(0xffff8e820102ad30)に足すと、0xffff8e820102010383a8が得られます。これはWnd1のtagWND構造体へのオフセット0x18、またはそのtagWND.dwExStyleです。
Wnd1のウィンドウのスタイルを子ウィンドウに変更することは、次のSetWindowLongPtrAへの呼び出しにとって重要です。
次の呼び出し(図34)はWnd1のハンドルを直接渡し、nIndexパラメーターを-12に設定し、g_pMem4をdwNewLongパラメーターとして渡しています。
SetWindowLongPtrAのnIndexパラメーター(図35)に関するMSDNの文書を参照すると、この-12は子ウィンドウに対するマクロであるGWL_IDを参照しています。GWL_IDの変更はトップレベル ウィンドウではできないので、その前に行われたSetWindowLongPtrAの呼び出しは、この呼び出しのためにWnd1を準備するために行われています。
ここで何が起こっているかをよく理解するには、hMenu(ステップ8のウィンドウ作成時に定義)は、メニューへのハンドルを指すか、子ウィンドウの識別子を指定することを覚えておくことが重要です。hMenuパラメーターのMSDNによる定義を図36に示します。
ステップ8で、hMenuパラメーターが各ウィンドウに対しCreateMenuを呼び出すために設定されたことを思い出してください。そのため、ウィンドウを作成するつど、メニュー オブジェクトへのハンドル(実質的にはカーネル内のオブジェクトへのポインター)がhMenuパラメーターに割り当てられていました。しかし、現在のWnd1は子ウィンドウなので、エクスプロイトはSetWindowLongPtrAを使ってこの子ウィンドウの識別子をg_pMem4のそれに変更できます。Wnd1が今も親ウィンドウなら、hMenuハンドルの変更はできなかったことでしょう。
最後に、g_qwExploitがSetWindowLongPtrAの戻り値に設定されます。これは呼び出し前のhMenuの値(ウィンドウ作成時に割り当てられたハンドル)と等しくなっています。ハンドルは単にWindowsカーネルがオブジェクトを追跡するために使うメモリー位置でしかありません。このため、g_qwExploitはこの時点でWnd1のspmenuオブジェクトへのカーネル アドレスを含んでいます。
これまではまだ説明していませんでしたが、g_pMem4は前にエクスプロイト コードで定義されていました。355〜385行目では、図37に示す5つのメモリー割り当て(g_pMem1からg_pMem5まで)を設定しています。
これらの割り当てとそれに続くコードは、Wnd1の本物のメニュー オブジェクトを置き換えるために偽のメニュー オブジェクトをセットアップしています。偽のメニュー オブジェクトを図58に示します。これについては後でもう少し詳しく説明します。
また、SetWindowLongPtrAはnIndexの古い値を返すので、hMenuオブジェクトへのポインターが返されます。実際には、返されるポインターは親のtagWND構造体内の*spmenuエントリーで、これはカーネル ポインターです。これは、図38として再掲したtagWND構造体内のオフセット0xa8に存在しています。
GWL_IDを指定してSetWindowLongPtrAを呼び出した場合、図39に示すように、親のtagWND構造体と、子/ユーザーモード コピー(オフセット0x98)のtagWND構造体の両方に対して*spmenuを変更する実行パスをトリガーする(このr15は g_pMem4と等しい)ことになります。
図39ではrsiが親のtagWND構造体です。子でありコピーであるtagWND構造体へのポインターはオフセット0x28の位置にあり、これがraxレジスターにコピーされます。その後、各tagWND構造体のspmenuエントリーがr15内の値 (g_pMem4)に変更されます。
このSetWindowLongPtrAの呼び出し後に返される値は、変更される前の親のtagWND構造体 + 0xa8 (spmenu)の値です。これは上の図39で、0xffff8eac5a15c386にあるrsi+0xa8になります。この時点でWinDbg内にブレークポイントを設定すれば、レジスターの内容とrsi+0xa8(0xfff8e82008218c0)のダンプを確認できます(図40)。
戻り値が実際に私たちが考えている通りの内容なのかを確認してみましょう。図41は、SetWindowLongPtrWの呼び出しが終わったときのレジスターを示しています。
注: 混乱を避けるために申し添えておきます。一部の図でSetWindowLongPtrAではなくSetWindowLongPtrWが使われているのには理由があります。Windowsが直接扱うのはUnicode文字だけなのですが、このUnicode文字を扱う関数の名前はWで終わっている関数です。そして、ASCIIに対応するためにMicrosoftはラッパー関数を作成していて、これらのラッパー関数が名前がAで終わる関数です。ラッパー関数は、単純に変換を行ってからUnicode用の関数を直接呼び出します。したがってSetWindowLongPtrAの呼び出しをデバッグする場合、実際の仕事はSetWindowLongPtrWの中で行われていることになります。
raxが、図40のrsi+0xa8と同じ値であることに注目してください。これは、ステップ8で作成した、元のメニュー項目のカーネル アドレスです。このアドレスは、後のステップで、カーネルのEPROCESS構造体を見つけ、特権昇格用Systemトークンをコピーするのに使われます。このポインターはPoC内の変数g_qwExploitに保存されています(上図34)。
図42に示したSetWindowLongPtrAへの次の呼び出し(566行目)は、Wnd1のtagWND.dwExStyleを以前の値にリセットします。この値はPoC 546行目に保存されます(図43)。したがって、Wnd1はもう子ウィンドウではなくなります。
「SetWindowLong関数を使ってカーネル メモリーに書き込めるなら、なぜこんなこんなに苦労してspmenuポインターを漏えいさせる必要があるの?」という疑問が浮かぶかもしれません。単にGetWindowLongを使ってメニュー オブジェクトのカーネル アドレスを読み取ればよいのではないでしょうか。
そうすればメニュー オブジェクトを得られるわけです。ですがその場合、子のtagWND構造体からの読み取りになり、そこにメニュー オブジェクトのカーネル アドレスは格納されていません。図44は、Wnd1の子のtagWND構造体のWinDbg出力を示しています。オフセット0x98(spmenu)を見るとこれが絶対にカーネル ポインターではないことがわかります。後ですぐ説明しますが、これはデスクトップ ヒープからspmenuオブジェクトへのオフセットなのです。
ステップ7で計算したカーネル デスクトップ ヒープ ベースに、子の構造体のtagWND+0x98内の値を加えると、図45のようになります。
親のtagWND構造体(オフセット0xa8)から漏えいさせたアドレスを確認すると、図46のようになっています。
最初の値が同じであることがわかります。tagWND構造体のユーザーモード フレンドリーなコピーが存在するように、ユーザーモード フレンドリーなspmenuオブジェクトのようなものも存在しているかもしれません。
しかし、このカーネルアドレスが手に入ったとしてこれを使って何ができるのでしょうか。これについては第6部のステップ13で検討します。
続きを読む ➠ セクション 6 – 詳細分析ステップ13