
翻訳記事、CVE-2015-0057脆弱性を32ビットおよび64ビットシステムで悪用する。Exploiting the win32k!xxxEnableWndSBArrows use-after-free (CVE 2015-0057) bug on both 32-bit and 64-bit(Aaron Adams of NCC )
著者:Aaron Adams
翻訳:55-AA
訳注:本文の一部は意訳しています。不明な点は原文を参照してください。
用語:
今年の初め、私はwin32k.sys(CVE-2015-0057)に関する興味深い脆弱性に触れ、32ビットおよび64ビットシステムで安定したエクスプロイトを実現しました。その適用範囲はXPからWindows 8.1(一部例外あり)までに及びます。本稿では、これら2つのプラットフォームでどのようにエクスプロイトを完成させたかを詳細に説明し、最後にいくつかの追加事項も記載します。また、SMEPが有効なWindows 8.1上で、低整合性権限でのエクスプロイトを実現する方法についても説明します。
本稿は長文ですが、この脆弱性の悪用の複雑さを隠すことなく、可能な限り多くの詳細を提供するよう努めています。もちろん、一部の詳細は省略しています。これらの詳細が皆様のお役に立てば幸いです。
2015年2月10日、マイクロソフトはMS15-010の詳細を公開しました。このバグはenSiloのUdi Yavo氏によって最初に発見されました。Udi氏はbreaking malware blogで優れた分析"one bit rule-bypassing windows 10 protections using single bit"を公開しています。このバグを深く理解するために、その記事を熟読することをお勧めします。本稿では脆弱性をトリガーする際に克服しなければならない障害について可能な限り多くの詳細を提供しますが、この脆弱性の悪用は非常に興味深く、多くの詳細はUdi氏のブログに由来しています。以下は彼の声明です:
合理的な開示:このブログは技術的なものですが、技術的な専門家がこの脆弱性の悪用を再現することを防ぐため、コードや完全な詳細は一切公開しません。
この脆弱性を悪用した追加のボーナスとして、ポケモンの進化形「テクニカルポケモン」を手に入れました。Udi氏には、このバグを発見し、ブログで関連状況と悪用の詳細を提供してくれたことに感謝しなければなりません。これらの情報は非常に役立ちました。
これまで、私はwin32k.sysの脆弱性を悪用したことがなく、ユーザーモードコールバックや関連する多くのAPIにも不慣れでした。そのため、Skywing氏、Tarjei Mandt氏、Alex Ionescu氏、j00ru氏など、著名なセキュリティ研究者がオンラインで提供してくれた新しいリソースにも感謝します。これらの方々がこれほど多くの技術情報を公開してくれたことは、賞賛に値します。私が特に参考にしたのは、Tarjei Mandt氏の論文Win32k.sys exploitation paperです。
このエクスプロイトを作成している間に、優秀なリバースエンジニアがCVE-2015-1701の安定したエクスプロイトを実装しました。その中で、ユーザーモードコールバックに関するサンプルコードは非常に有用でした。この作者に感謝します。
なお、以下の分析はWindows 7上で行いました。なぜなら、このバージョンのwin32k.sys内のすべての構造体に対応するシンボルがある唯一のバージョンだからです。これらのシンボルのほとんどは、他のバージョンのWin32k.sysの構造体にも使用できます。何らかの理由で、マイクロソフトはこれらのシンボルをWindows 8から削除しました。
最後に、私の悪用方法はかなり複雑です。もっと簡単な方法がある可能性は十分にあり、私が気づいていないだけです。誰かが別の方法を使用したという話を聞きたいものです。いずれにせよ、これらすべてがwin32k.sysの脆弱性の研究に役立つことを願っています。
以下では、win32k!xxxEnableWndSBArrowsの逆アセンブルコードでこのバグを垣間見ることができます。かなり巧妙なバグです:
未パッチの状態:
.text:FFFFF97FFF1B157D mov r8d, r13d
.text:FFFFF97FFF1B1580 mov rdx, r14
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar ; ユーザーモードコールバックをトリガー
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
[...]
.text:FFFFF97FFF1B1519 mov eax, [rbx] ; チェックなしで tagSBINFO ポインタを参照
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh
.text:FFFFF97FFF1B1520 xor eax, esi
上記のコードでは、Win32K!xxxdrawscrollbar は適切な状況でユーザースペースにコールバックし、ユーザースペースコードで tagSBINFO のポインタが攻撃者によって解放される可能性があります。その後、上記のコードに戻ると、0xFFFFF97FFF1B1519 のコードは無効なポインタを参照することになります。
パッチ適用後:
.text:FFFFF97FFF1D69C3 xor r8d, r8d
.text:FFFFF97FFF1D69C6 mov rdx, rbp
.text:FFFFF97FFF1D69C9 call xxxDrawScrollBar ; ユーザーモードコールバックをトリガー
.text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h] ; tagSBINFO ポインタが正しいかチェック
---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; 正しければ元のフローを続行
| .text:FFFFF97FFF1D69D7 mov rcx, rbp
| .text:FFFFF97FFF1D69DA call _ReleaseDC
| .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958 ; 関数の終了へジャンプ
|->.text:FFFFF97FFF1D69E4 mov eax, [rbx] ; 正しい tagSBINFO ポインタを安全に使用
.text:FFFFF97FFF1D69E6 xor eax, r14d
上記のパッチバージョンでは、tagSBINFO ポインタを使用する前にヌルチェックが行われています。関連する構造体の情報は後述します。
このエクスプロイトを実現するにあたり、複数の破壊(corruption)を実行しました。そして、そのうちの1つの破壊でこの脆弱性をトリガーします。
このバグの技術的な根本原因は、デスクトップヒープにおけるUAF(use-after-free)です。当初、これは私を困惑させました。なぜなら、私はwin32k.sysのユーザーモードコールバックメカニズムに詳しくなく、その動作を理解していなかったからです。そのため、これはロックの競合状態によるUAFだと考えました。実際には、その構造体のロックは正しく使用されており、そのフローは期待通りでした。簡単に言えば、問題の本当の原因は次のとおりです:
以上です。ユーザーモードコールバックを考慮しなければ、このフェーズは非常に明快です。
しかし、どのように破壊を行い、なぜ破壊するのでしょうか? Udi氏のブログで述べられているように、特定の位置で2ビットを設定またはクリアできます。その位置は、システムコードが tagSBINFO 構造体の WSBflags フィールドと見なす場所です。これは通常のUAFの悪用方法ではありませんが、記事ではその操作方法についてのヒントが提供されています。それについては後続のセクションで説明します。まず、これらのビットをどのように操作して制御するかを理解しましょう。
tagSBINFO 構造体(32ビットと64ビットで共通):
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
このUAF脆弱性は、win32k!xxxEnableWndSBArrows() 関数にあります。この関数は、1つまたは2つ(水平または垂直)のスクロールバーコントロールの矢印を有効または無効にするために使用されます。スクロールバーコントロールは、スクロールバーを操作するための特別なウィンドウです。CreateWindow() 関数に組み込みの "SCROLLBAR" ウィンドウクラスをパラメータとして渡すことで作成できます。
win32k!xxxEnableWndSBArrows() の関数プロトタイプ:
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);
パラメータ WSBflags の意味は WinUser.h で定義されているものと同じで、どのスクロールバーが操作されるかを示します:
#define SB_HORZ 0
#define SB_VERT 1
#define SB_CTL 2
#define SB_BOTH 3
パラメータ wArrows は、矢印の状態が有効か無効かを示します。セットされていると矢印は無効、そうでなければ有効です。wArrows の下位2ビットは水平スクロールバーを、次の2ビットは垂直スクロールバーを表します。残りのビットは今回のエクスプロイトの目的とは関係ありません。
以下のコードは win32k!xxxEnableWndSBArrows() 関数から抜粋したもので、SB_HORZ または SB_BOTH がセットされている場合、水平矢印の関連ビットを設定またはクリアします:

このバグは、水平スクロールバーと垂直スクロールバーのフラグを設定する際に存在します。水平スクロールバーをリフレッシュした後、そのスクロールバーに対応するウィンドウがデスクトップ上で表示可能になると、win32k!xxxEnableWndSBArrows() 関数は win32k!xxxDrawScrollBar() を呼び出します。以前の記事で述べたように、これは潜在的なユーザーモードコールバックをトリガーする可能性があります。
ユーザーモードコールバックについて説明する前に、win32k!xxxDrawScrollBar() の呼び出し後に何が起こるかを引き続き説明します。これは実際には水平スクロールバーと同じロジックで、数ビットの違いだけです。垂直スクロールバーを無効にすることを選択し、UAFをトリガーすると仮定すると、2ビットが tagSBINFO ヒープチャンクのどこかに書き込まれます。したがって、元の値が 0x2 であれば、0xe になります。次の図に示すとおりです。

この1ビットの変更は、最終的なコード実行につながるのに十分です。ビットをクリアしてエクスプロイトを完成させる方法については深く掘り下げていませんが、可能性はあります。
上記の要点は、水平スクロールバーと垂直スクロールバーの両方を同時に操作するために、これらの2つの要素を持つスクロールバーコントロールを作成する必要があるということです。これは、CreateWindow() を WS_HSCROLL と WS_VSCROLL フラグを指定して呼び出すことで実現します。コードは次のとおりです:
g_hSBCtl = CreateWindowEx(
0, // 拡張スタイルなし
"SCROLLBAR", // クラス
NULL, // 名前
SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // 垂直+水平
10, // x
10, // y
100, // 幅
100, // 高さ
g_hSpray[UAFWND], // モードレス親ウィンドウ
(HMENU)NULL,
NULL, // ウィンドウ所有者
NULL // 追加パラメータ
);
以下のコードで可視であることを確認できます(通常はデフォルトですが、明示的に呼び出しています):
result = ShowWindow(g_hSBCtl, SW_SHOW);
スクロールバーはデフォルトで有効です。脆弱性コードをトリガーしようとする準備ができたら、スクロールバーを無効に設定して必要なビットを破壊できます:
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);
上記でバグの詳細と関連コードをトリガーする方法を説明しましたが、最も重要なステップである、win32k!xxxDrawScrollBar() によって開始されるユーザーモードコールバックをインターセプトして、win32k!xxxEnableWndSBArrows() の実行が続行される前にヒープの内容を変更する方法をまだ説明していません。実際にこのバグをトリガーする必要がありますが、Win32k.sys および関連APIについて何も知らない状態では、私が始めたときと同じように、これは自分にとって冒険でした。
以前の記事には、このプロセスを詳細に示す有用なコールスタックの図があります。win32k!xxxDrawScrollBar() を介してトリガーし、ClientLoadLibrary() が呼び出され、KeUserModeCallback() を介してディスパッチ処理されます。KeUserModeCallback() の呼び出し状況を正確に把握して、自分のプロセス内でフックできるようにする必要があります。
ユーザーモードコールバックに関する資料をいくつか見つけました。win32k に関する部分は非常に役立ちます:
通常、各プロセスにはユーザーモードコールバック関数のポインタテーブルがあり、PEB->KernelCallBackTable がこのテーブルを指します。カーネルがユーザーモード関数を呼び出したいときは、関数インデックスを KeUserModeCallBack() に渡します。上記の例では、インデックスはユーザーモードの __ClientLoadLibrary() 関数を指します。
KeUserModeCallBack() は、PEB->KernelCallBackTable 内のインデックスに基づいて対応する関数を検索して実行し、最終的にユーザーモードで KiUserModeCallbackDispatch() を呼び出します。
指定されたエントリポイントをフックするには、PEB->KernelCallBackTable 内の __ClientLoadLibrary() のインデックスを検索し、それを独自の関数に置き換える必要があります。このインデックスは OS のバージョンやハードウェアプラットフォームによって異なることに注意してください。
PEB->KernelCallBackTable を表示するには、WinDbg を使用してこのテーブルのアドレスを見つけます。32ビットと64ビットのプラットフォームを比較しても、大きな違いは見られません。
kd> dt !_PEB @$peb
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0 ''
+0x003 BitField : 0x8 ''
+0x003 ImageUsesLargePages : 0y0
[...]
+0x02c KernelCallbackTable : 0x76daf620 Void
kd> dds 0x76daf620
76daf620 76d96443 user32!__fnCOPYDATA
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA
76daf628 76da736b user32!__fnDWORD
76daf62c 76d9d603 user32!__fnNCDESTROY
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG
76daf634 76ddf1be user32!__fnINOUTDRAG
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL
[...]
76daf724 76da3962 user32!__ClientLoadLibrary
kd> ?? (0x76daf724-0x76daf620)/4 int 0n65
上記の例では、__ClientLoadLibrary のインデックスは 65 であり、これがフックする場所です。フックした後、__ClientLoadLibrary が win32k の関連コードによって何度も呼び出されていることがわかりました!最初に行うべきことは、興味のある呼び出しが発生する前にフックコードに通知して、実際に目的の場所でフックが行われていることを確認することです。そのため、フックコードではグローバル変数を使用してフラグを設定し、このフラグがセットされている場合にのみ関連操作を実行します。
現在、2つの障害があります:
したがって、フック関数は次のようになります:
void ClientLoadLibraryHook(void * p)
{
CHAR Buf[PGSZ];
memset(Buf, 0, sizeof(Buf));
if (g_PwnFlag)
{
dprintf("[+] __ClientLoadLibrary hook called\n");
if (++g_HookCount == 2)
{
g_PwnFlag = 0; // 1回だけ実行..
ReplaceScrollBarChunk(NULL);
}
}
fpClientLoadLibrary(&Buf); // 元の関数を呼び出す
}
現在が win32k!xxxDrawScrollBar() 関数からの呼び出しであることを確認したら、バグをトリガーすることを試みます。今はトリガーのみを考え、DestroyWindow(g_hSBCtl) を呼び出すだけで十分です。これにより、ウィンドウの tagSBINFO 構造体が解放されますが、ウィンドウ構造体自体は参照カウントがまだ元の呼び出しによって使用されているため、すぐには解放されません。しかし、tagSBINFO にはそのような参照カウントメカニズムがないため、即座に解放されます。
この時点で、バグをトリガーしました。tagSBINFO を含むヒープチャンクを再割り当てしていないにもかかわらず、無効を表す2ビットを解放済みのヒープに書き込むことができます。次のステップは、この解放されたヒープチャンクを目的のもので置き換えることであり、数ビットを設定するよりも興味深いことを行えるようにすることです。そのためには、デスクトップヒープに関する背景知識が必要です。
win32k.sys は、デスクトップヒープを介して、特定のデスクトップに関連するGUIオブジェクトを格納します。これには、ウィンドウオブジェクトとその関連構造体(プロパティリスト、ウィンドウテキスト、スクロールバーなど)が含まれます。Tarjeiの論文で言及されていますが、特に注意すべき点は、デスクトップヒープは実際にはユーザーモードバックエンドアロケータの簡略版であり、RtlAllocateHeap() および RtlHeapFree() を使用して操作されることです。デスクトップヒープは _HEAP 構造体によって管理され、フロントエンドアロケータがないため、低フラグメンテーションヒープ(LFH)やサイドバイサイドリストなどはありません。
デスクトップが作成されるたびに、対応するデスクトップヒープがそのデスクトップにサービスを提供します。つまり、新しいデスクトップを割り当てることで、「クリーンな」デスクトップヒープを取得でき、そのヒープ上での操作はより予測可能になります。ただし、これは低整合性権限のプロセスでは意味がありません。なぜなら、そのようなプロセスは新しいデスクトップを作成できないからです。
現在の主な問題は、割り当てプロセスを追跡することです(メタデータなどの詳細については後で詳しく説明します)。
デスクトップヒープの割り当てと解放を監視するために、私は WinDbg スクリプトを使用しています:
64ビットヒープ監視
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";
32ビットヒープ監視
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc";
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"
これらのデバッグスクリプトに加えて、デスクトップヒープは単にユーザーモードバックエンドアロケータの簡略版であるため、WinDbg の !heap コマンドも使用できます。
このバグを悪用するには、最後に解放された tagSBINFO ヒープチャンクを置き換える必要があります。また、これらの典型的なバグをどのように悪用するかもわかっています。それは、近隣のデータを破壊することです。これにより、破壊したい構造体の近くにヒープチャンクを事前に割り当てるという基本的な要件が生じます。ヒープチャンクがどこに割り当てられるかを予測するには、ヒープ全体のレイアウトを制御する(または可能な限り制御する)必要があります。これを満たすための実行可能な方法は、解放されたヒープチャンクを埋めるためにできるだけ多くのヒープチャンクを割り当て、新しく割り当てられたヒープチャンクが連続するようにすることです。穴が必要なときは、割り当て済みのヒープチャンクを解放することで、予測可能な位置に穴を開けることができます。
この部分は、割り当てに影響を与えるいくつかの要因を簡単に理解することです。上記の WinDbg スクリプトが役立ちます。Tarjei 氏は win32k に関する PPT で、デスクトップヒープ上で割り当てられる主要なオブジェクトについて言及しており、それは私が見たものと完全に一致しています。それらは:
デスクトップヒープは非常に興味深いもので、ほとんどの割り当てはウィンドウオブジェクトに直接関連し、tagWND 構造体を介して管理されます。つまり、任意のサイズのヒープチャンク(いわゆる小さなチャンクで小さな穴を埋める)を割り当てたい場合、まずそれに関連するウィンドウを割り当てる必要があります。ウィンドウの構造はヒープ割り当てのインターフェースであると考えることができます。もう1つの興味深い点は、ウィンドウ操作を介して割り当てられた多くのヒープチャンクは、ウィンドウ自体が破棄されない限りすぐには解放できないことです。これは明らかにヒープに影響を与えます。最後に、ウィンドウを介してサイズ N のヒープチャンクを割り当てるとします。上記の例のように、多くのサイズ N のヒープチャンクを割り当てたい場合、割り当てられるウィンドウ構造のサイズはリンクリストに格納されていません。したがって、各ウィンドウはサイズ N のヒープ割り当てを1つ制御できます。つまり、多数のサイズ N のヒープチャンクを割り当てる必要がある場合は、まず多数のウィンドウを作成し、ウィンドウを使用してヒープチャンクの割り当てを支援する必要があります。
デスクトップヒープ上で割り当てられるもう3つの重要なデータ型があり、ウィンドウオブジェクトを介して間接的に利用して、ヒープ上のデータを制御できます。これらのデータ型を多用して、エクスプロイトの実現とヒープスプレーの構築を行います。これら3つのデータ型は次のとおりです:
図2は、これらのデータ型間の相互関係を示しています:

ヒープを初期化するために、私は多数の tagWND 構造体(ウィンドウオブジェクトを作成することで)を作成しました。これにより、ヒープ上の多くの大きな穴を埋めることができ、他の必要なヒープチャンクを割り当てるためのインターフェースも提供されます。Windows 8 および Windows 8.1 では、新しいウィンドウを割り当てると tagPROPLIST 構造体が自動的に割り当てられます(前述の WinDbg スクリプトで観察できます)。Windows 7 およびそれ以前のバージョンでは、新しい tagPROPLIST を自分で割り当てて、小さな穴を埋めます。
ここでは、スプレーするすべてのウィンドウオブジェクトにウィンドウテキスト文字列はありません。ただし、必要であれば、それを利用して任意のサイズのヒープチャンクを割り当てたり解放したりすることも可能です。作成後は、ウィンドウが破棄されない限り、既存のプロパティリストを削除することはできません。しかし、新しいプロパティを収容するためにリストの再割り当てを制御することができ、このメカニズムを使用して以前の場所に穴を開けることができます。必要なのは、以前のリストに存在しない新しいプロパティ(atomKey で区別)を設定することだけです。
興味深いことに、デスクトップヒープはユーザースペースにマッピングされていますが、読み取り専用です。つまり、構築したスプレーレイアウトを検証し、正しく動作することを確認できます。まず、デスクトップヒープがユーザーモードのどこにマッピングされているかを特定する必要があります。Tarjei氏のwin32kの論文でこれについて言及されています。TEB には未公開の構造体 Win32ClientInfo があり、これに関連しています。そのおおよその定義は次のとおりです:
typedef struct _CLIENTINFO {
ULONG_PTR CI_flags;
ULONG_PTR cSpins;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
DWORD dwTIFlags;
PDESKTOPINFO pDeskInfo;
ULONG_PTR ulClientDelta; // 不完全。ReactOS を参照
} CLIENTINFO, *PCLIENTINFO;
ここで、PDESKTOPINFO 構造体の定義は次のとおりです:
typedef struct _DESKTOPINFO {
PVOID pvDesktopBase;
PVOID pvDesktopLimit; // 不完全。ReactOS を参照
} DESKTOPINFO, *PDESKTOPINFO;
最初のフィールド pvDesktopBase は、デスクトップヒープのカーネルモードアドレスを指します。とりあえずこれを覚えておいてください。Win32ClientInfo の ulClientDelta フィールドは、カーネルモードアドレスとユーザーモードアドレスの差であり、これらの情報から必要なものを得ることができます。
ただし、ヒープ構造を自分で解析するのではなく、HWND の値のような user32 ハンドルを取得し、それをユーザーモードマッピングのアドレスに変換して、他のヒープ割り当てに関連しているかどうかを判断したいところです。このハンドルを見つけるには、gShared と呼ばれる構造体を見つける必要があります。通常、これは user32.dll にあり、Windows 7 以降ではエクスポートされているため、簡単に見つけることができます。
ほとんどのシステムでは、この構造体は次のように定義されています:
kd> dt !tagSHAREDINFO
win32k!tagSHAREDINFO
+0x000 psi : Ptr32 tagSERVERINFO
+0x004 aheList : Ptr32 _HANDLEENTRY
+0x008 HeEntrySize : Uint4B
+0x00c pDispInfo : Ptr32 tagDISPLAYINFO
+0x010 ulSharedDelta : Uint4B
+0x014 awmControl : [31] _WNDMSG
+0x10c DefWindowMsgs : _WNDMSG
+0x114 DefWindowSpecMsgs : _WNDMSG
上記の構造体では、aheList は _HANDLEENTRY の配列を指し、各 _HANDLEENTRY はカーネルモードアドレスへのハンドルを含んでいます。「カーネルモードアドレスとユーザーモードアドレスの差」を使用して、使用可能なユーザーモードアドレスを取得できます。残念ながら、これは Windows 7 以前のバージョンでは不可能です。なぜなら、gSharedInfo がエクスポートされていないからです。Tarjei 氏の論文によると、未公開関数 CsrClientConnectToServer を使用して gSharedInfo のコピーを取得できるとのことですが、実行可能な例を見つけられませんでした。厄介なことに、この関数を実装するために必要な構造体のサイズはシステムによって異なるため、私の経験では、ReactOS で見られるものを完全に信頼することはできません。
マッピングされた位置を計算したら、ウィンドウオブジェクトがデスクトップヒープ上のどこにあるかを教えてくれる関数を構築できます。そして、対応するプロパティリストやテキスト文字列のヒープチャンクがどこに割り当てられたかを知りたい場合は、ユーザーモードの構造体を解析するだけです。
さて、いよいよ脆弱性の悪用に近づいてきました。ヒープチャンクを制御する方法、ヒープチャンクの位置が正しいことを確認する方法、そしてバグをトリガーする方法を手に入れました。これで、解放された tagSBINFO ヒープチャンクを選択した tagPROPLIST プロパティリストで置き換えることが最終的に確定できます。tagPROPLIST は単なる大きなリストの先頭であるため、リストのサイズをスクロールバーヒープチャンクのサイズに一致させることができることに注意してください。tagPROPLIST の後半部分は基本的に tagPROP 構造体の配列、つまりプロパティリストです。したがって、配列とリストという用語は区別しません。tagPROPLIST 構造体は64ビットシステムで次のように定義されています:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
前述のように、ウィンドウオブジェクトにはそれに関連するプロパティリストがあります。このリストは SetProp() 関数によって作成されます。これは、atomKey を一致させて既存のプロパティを検索し、プロパティが存在しない場合は、プロパティリストに新しいプロパティエントリを作成します。プロパティリストがまったく存在しない場合は、プロパティリストを作成して tagWND 構造体に接続します。
バグのある tagWND をスプレーし、関連する tagPROPLIST エントリを作成した場合、最終的なレイアウトは図3のようになります:

これが設定されたら、悪用するスクロールバーコントロールを割り当てます。これにより、図4のような結果になります:

次に、スクロールバーを操作してユーザーモードコールバックのフックをトリガーし、フック関数内で DestroyWindow を呼び出して tagSBINFO 構造体を解放しようとします。これにより、図5の状況になります:

64ビットでは、tagSBINFO 構造体は 0x28 バイト、tagPROPLIST 配列エントリは 0x18 バイトで、そのうち 0x10 バイトがデフォルトの tagPROP です。したがって、2つの配列エントリを持つプロパティリストは 0x28 バイト(0x8 + 0x10 + 0x10)となり、これは偶然の一致です。メモリをスプレーして穴を埋める準備ができていると仮定します。プロパティリストを持つウィンドウを1つだけ必要とし、そのウィンドウで tagSBINFO 構造体を解放した後(前図のように)、すぐに新しいプロパティリストエントリを追加します。このプロセスは、最初に以前の 0x18 バイトの tagPROPLIST ヒープチャンクを解放します。ヒープはスプレーされているため、前後には空きヒープチャンクがなく、ヒープチャンクの結合も発生せず、新しく割り当てられる 0x28 バイトを収容するのに十分な大きさの領域はありません。これにより、解放されたばかりの tagSBINFO の位置が使用されます(そのサイズはちょうど 0x28 バイトです)。この状況を図6に示します:

フックしたコールバック関数から戻ると、UAF がトリガーされ、数ビットが tagPROPLIST の cEntries フィールドに書き込まれます。元の cEntries の値は 0x2 で、2つのプロパティリストエントリを作成したことを示します。オーバーフロー後は 0xe になり、第3、4ビット(1から数えて)が 1 にセットされます。
この時点で、新しいヒープのオーバーフローが完了し、このプロパティリストのエントリ数が増加し、エントリ数は 0xc より大きくなります。次に、隣接するヒープチャンクをオーバーフローさせます。これをフェーズ2の破壊と呼びます。
Udi 氏のブログでは、ここまでが説明されています。これ以前は「典型的なヒープオーバーフロー」と呼ばれていましたが、私の経験では、この点から任意アドレス読み取り/書き込みやコード実行を実現するのは困難です。64ビットの tagPROPLIST 構造体をもう一度見てみましょう:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
フェーズ1の破壊により、破壊された tagPROPLIST 配列が得られ、配列エントリ tagPROP を追加できるようになりました。tagPROPLIST には2つのフィールドしかありません:
新しいリストエントリがリストに挿入されると、まず各エントリをスキャンして適切な iFirstFree インデックス値を見つける関数が呼び出されます。見つからない場合は、iFirstFree の値が cEntries より大きいかどうかを確認します。atomKey に対応するエントリがリストにない場合は、iFirstFree != cEntries かどうかを確認します。等しくない場合は、iFirstFree インデックスの位置にエントリを挿入します。等しい場合は、新しいプロパティリストを割り当てて挿入するプロパティを収容し、元のエントリがコピーされ、新しいエントリが挿入されます。
atomKey フィールドは LPCTSTR lpString に対応します。MSDN の SetProp() のドキュメントにあるように、呼び出し元は文字列ポインタまたは16ビットの atom 値を渡すことができます。文字列ポインタを渡すと、プロパティリストに格納される前に自動的に atom 値に変換されます。SetProp() 関数に任意の atom 値を渡すことができるため、これによりこれら2バイトを制御する能力が得られますが、いくつかの制約があります。つまり、atomKey の破壊データが重複してはいけません。重複すると、新しいプロパティエントリを設定するときに、同じ atom 値を持つ既存のプロパティエントリが置き換えられるからです。さらに、fs フィールドは制御できません。その値は、atomKey の値が < 0xBFFF の場合は 0 で、これは整数 atom 値に該当します。fs フィールドの値が 2 の場合は、atomKey の値が >= 0xC000 であることを示します。
もう1つ注意すべき点は、tagPROP はわずか 0xc バイトであることです。この構造体は64ビットシステムでは 0x10 バイト境界にアラインされるため、tagPROP エントリを挿入すると、追加で4バイトが破壊されないことになります。最後の重要な点は、tagPROPLIST ヒープチャンクの先頭8バイトのデータがリストエントリのサイズを定義していることです。つまり、新しく挿入された各 tagPROP エントリは常に8バイト境界に書き込まれます。
64ビットシステムで挿入される各 tagPROP について、状況は次のとおりです:
* Offset 0x0: 8バイトの任意制御可能データ (hData)
* Offset 0x8: 2バイトのほとんどの制御可能データ (atomKey)
* Offset 0xa: 2バイトの制御不可能データ (fs)
* Offset 0xc: 4バイトの修正不可能データ (パディング)
この状況は2ビットよりははるかに優れていますが、それでも完璧ではありません。最初の8バイトで何かを上書きできる場合を除き、これら8バイトは完全に制御可能な hData フィールドから得られるため、制限があります。隣接する構造体のより深いフィールドに書き込む必要がある場合、特定の値が制御不能な破壊を避けられません。私はデスクトップヒープ上のさまざまなオブジェクトを探すのに時間を費やしました。前述の破壊の制限を考えると、任意アドレス読み取り/書き込みを実現するためにこの制限を回避する唯一の方法は、tagWND の strName フィールドを破壊することだと考えました。これは _LARGE_UNICODE_STRING 構造体です:
kd> dt !_LARGE_UNICODE_STRING
win32k!_LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr64 Uint2B
この構造体の Buffer フィールドを破壊できれば、ウィンドウテキストを操作することで、指定されたアドレスから MaximumLength バイトを読み書きできるようになります。これが私の取ったアプローチです。以前の章で、デスクトップヒープ上に任意のサイズと値のヒープチャンクを作成する方法について言及しましたが、同じ状況がここでも当てはまります。
これで、tagPROPLIST リストエントリを使用してデータを破壊する方法、制御できる部分、そして直面する制限、特に32ビットと64ビットで異なる点がわかりました。64ビットで行ったことは32ビットでは機能しません。次に、フェーズ2の破壊(つまり、tagPROP 構造体を介したデータ書き込み)から、完全に制御可能なデータを書き込む別の破壊「操作プリミティブ」に移行します。これをフェーズ3の破壊と呼びます。
目標は、隣接する tagWND 内の strName フィールドを破壊することです。それが LARGE_UNICODE_STRING 構造体であることは既にわかっていますが、tagWND 構造体の詳細をさらに見てみましょう。次のようになります:kd> dt !tagWND win32k!tagWND +0x000 head : THRDESKHEAD +0x028 state : Uint4B +0x028 bHasMeun : Pos 0, 1 Bit +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit [省略されたフラグ] +0x028 bDestroyed : Pos 31, 1 Bit +0x02c state2 : Uint4B [省略されたフラグ] +0x02c bWMCreateMsgProcessed : Pos 31, 1 Bit +0x030 ExStyle : Uint4B +0x030 bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit +0x030 bUnused1 : Pos 1, 1 Bit +0x030 bWS_EX_NOPARENTNOTIFY : Pos 2, 1 Bit [省略されたフラグ] +0x030 bUIStateFocusRectHidden : Pos 31, 1 Bit +0x034 style : Uint4B +0x034 bReserved1 : Pos 0, 16 Bits [省略されたフラグ] +0x034 bWS_POPUP : Pos 31, 1 Bit +0x038 hModule : Ptr64 Void +0x040 hMod16 : Uint2B +0x042 fnid : Uint2B +0x048 spwndNext : Ptr64 tagWND +0x050 spwndPrev : Ptr64 tagWND +0x058 spwndParent : Ptr64 tagWND +0x060 spwndChild : Ptr64 tagWND +0x068 spwndOwner : Ptr64 tagWND +0x070 rcWindow : tagRECT +0x080 rcClient : tagRECT +0x090 lpfnWndProc : Ptr64 int64 +0x098 pcls : Ptr64 tagCLS +0x0a0 hrgnUpdate : Ptr64 HRGN +0x0a8 ppropList : Ptr64 tagPROPLIST +0x0b0 pSBInfo : Ptr64 tagSBINFO +0x0b8 spmenuSys : Ptr64 tagMENU +0x0c0 spmenu : Ptr64 tagMENU +0x0c8 hrgnClip : Ptr64 HRGN__ +0x0d0 hrgnNewFrame : Ptr64 HRGN__ +0x0d8 strName : LARGE_UNICODE_STRING +0x0e8 cbwndExtra : Int4B +0x0f0 spwndLastActive : Ptr64 tagWND +0x0f8 hImc : Ptr64 HIMC_ +0x100 dwUserData : Uint8B +0x108 pActCtx : Ptr64 _ACTIVATION_CONTEXT +0x110 pTransform : Ptr64 _D3DMATRIX +0x118 spwndClipboardListenerNext : Ptr64 tagWND +0x120 ExStyle2 : Uint4B +0x120 bClipboardListener : Pos 0, 1 Bit [省略されたフラグ] +0x120 bChildNoActivate : Pos 11, 1 Bit
上記は 64 ビット環境での構造体です。今回上書きしたい _LARGE_UNICODE_STRING 構造体のオフセットが 0xd8 であることがわかります。また、この構造体の先頭近くにある重要なフィールドにも気づくでしょう。私はこれを存分に悪用したいところでしたが、_THRDESKHEAD には大量のポインタがあり、注意が必要です。そして残念ながら、書き込み先を制御できないという制限が以前述べた理由により存在します。
_THRDESKHEAD 構造体の定義:
kd> dt !_THRDESKHEAD
win32k!_THRDESKHEAD
+0x000 h : Ptr64 Void
+0x008 cLockObj : Uint4B
+0x010 pti : Ptr64 tagTHREADINFO
+0x018 rpdesk : Ptr64 tagDESKTOP
+0x020 pSelf : Ptr64 UChar
_THRDESKHEAD の問題は混乱を招くだけでなく、アライメント制約を再検討させるものでもあります。新しい tagPROP リスト項目がどのオフセットに置かれても、書き込み操作は _LARGE_UNICODE_STRING の先頭を直接上書きします。
win32k!_LARGE_UNICODE_STRING
+0x000 Length <-- hData (完全に制御可能) がここを上書き
+0x004 MaximumLength <-- およびここ
+0x004 bAnsi <-- およびここ
+0x008 Buffer <-- atomKey と fs (部分的に制御可能) がここを上書き
明確に言えば、Buffer ポインタを上書きして任意のアドレスのメモリにアクセスしたいのです。しかし、この構造体の他のフィールドを安全に攻撃できたとしても、必要なポインタを制御することはできません。
任意データの破壊は不可能です。この問題の解決はもはや tagPROPLIST の破壊ではなく、まったく別の破壊メカニズムへと変わります。
Windows XP 以降では、ユーザーモードのバックエンドアロケータ(例えばカーネルデスクトップヒープ)のヒープブロックヘッダ(_HEAP_ENTRY)はヒープ上に格納され、ブロックの実際の内容の直前に位置します。デスクトップヒープ自体は _HEAP 構造体によって管理されており、これによりヒープブロックを悪用する際にある程度の自由度が得られます。
_HEAP_ENTRY 構造体の定義は次の通りです。
kd> dt !_HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B
+0x00a Flags : UChar
+0x00b SmallTagIndex : UChar
+0x00c PreviousSize : Uint2B
+0x00e SegmentOffset : UChar
+0x00f UnusedBytes : UChar
ヒープブロックヘッダは合計 0x10 バイトで、先頭の 8 バイトは PreviousBlockPrivateData であり、要求されたサイズが通常の 0x10 を超える場合(8 バイト未満の場合は 8 バイトアライメント)、以前の実際のブロックデータを収容するために使用されます。これについては Leviathan blog entry で簡単に説明されており、また初期のユーザーモードヒープに関する記事でも説明されています。Size と PreviousSize は現在のブロックサイズと前のブロックサイズを表し、単位は 0x10 バイトです。Flags はブロックが空きかどうかなどを示します。_HEAP で _HEAP_ENTRY のセキュリティモードが有効になっている場合、SmallTagIndex にはブロック内データの XOR チェックサムが含まれます。
アライメント制約は私たちにとって不利ですが、確かに存在します。tagPROPLIST は常に少なくとも 0x18 バイトであり、さらに 0x10 バイトの tagPROP が追加されます。2 つのリスト項目を持つ 0x28 バイトの tagPROPLIST は、0x20 バイトのヒープブロックに配置され、PreviousBlockPrivateData によって表される余分なバイトは隣接するヒープブロックを使用します。つまり、3 つ目のリスト項目を追加すると、隣接するヒープブロックが破壊され、制御可能な 8 バイトの hData が _HEAP_ENTRY の先頭を上書きします。
私たちがやりたいことは、これを利用して、何らかの方法で Buffer の先頭位置に任意のデータを書き込むことです。まず、ヒープレイアウトを変更して、破壊した tagPROPLIST ヒープブロックの近くに、制御プロセス内にウィンドウに関連するテキスト文字列を含む小さなヒープブロックを配置します。これを「オーバーライドヒープブロック」と呼びます。この「オーバーライドヒープブロック」に隣接して、破壊対象となる tagWND を配置します。以下の図 7 はこのプロセスを示しています。なお、以前にスプレーしたヒープブロックは省略しています。

次に、3 つ目の tagPROP を tagPROPLIST のリストに挿入します。これにより、_HEAP_ENTRY の後半 8 バイトと「オーバーライドヒープブロック」の前半 8 バイトが上書きされます。これにより、「オーバーライドヒープブロック」の _HEAP_ENTRY を変更し、そのサイズを実際のサイズよりも大きくし、隣接する tagWND 構造体を含めるのに十分な大きさにします。
ここで、破壊された「オーバーライドヒープブロック」を解放し、ヒープマネージャーがそれを空きリストに配置するようにします。この空きリストは、実際の「オーバーライドヒープブロック」のサイズよりも大きなブロックサイズに対応します(各空きリストは固定サイズに対応)。その後、ウィンドウテキスト(完全に制御可能)を変更して、このブロックを再利用します。ただし、ここで小さな問題が発生します。「オーバーライドヒープブロック」が解放されるとき、ヒープマネージャーは破壊された Size フィールドに基づいて前の隣接ブロックを検索します。ヒープマネージャーは、この隣接ブロックが空きかどうかを確認し、空きであれば結合しようとします。いずれにせよ、私たちはそれを制御したいため、使用中であることを示すフラグを設定します。ヒープレイアウトを少し変更することでこれを実現します。ここで、使用中フラグが設定され、PreviousSize の値が破壊された Size フィールドの値と一致する偽のヒープブロックを配置します。これは、別のウィンドウのウィンドウテキストを割り当てるだけで簡単に実現できます。新しいヒープレイアウトは以下の通りです(図 8)。

これで、破壊された「オーバーライドヒープブロック」を解放できます。これには、関連するウィンドウのテキスト文字列を、元の 0x10 バイトよりも長い長さに更新します。これにより、破壊された「オーバーライドヒープブロック」が最初に解放され、空きリストに置かれます。ただし、そのサイズは破壊されたものであり、実際のサイズよりも大きいと宣言されています。このサイズは実際のニーズに応じて調整できます。こうして、文字列データがこの「オーバーライドヒープブロック」に書き込まれ、この「オーバーライドヒープブロック」を使用して隣接する tagWND を任意のデータで破壊します。以下に示します(図 9)。

これがフェーズ 3 の破壊です。これで、任意のデータで strName.Buffer ポインタを上書きできるようになりました。ただし、tagWND の他のデータを破壊するには依然としていくつかの問題がありますが、それは問題ではありません。なぜなら、デスクトップヒープはユーザー空間にマッピングされているからです!したがって、すべてを破壊する前に、tagWND のすべての内容を読み取り、strName 構造体の内容を希望するものに変更し、ウィンドウテキストを変更してすべてのデータを送信します。
strName により、任意の読み取り/書き込み「プリミティブ」が得られるだけでなく、strName を繰り返し変更することも可能です。これはウィンドウテキストの変更メカニズムによって許可されています。書き込む文字列の長さが MaximumLength の値を超えなければ、同じヒープブロックを引き続き使用できます。したがって、strName のアドレスを変更してどこかの値を読み取りたいときはいつでも、新しい文字列を追加し、そのデータで「オーバーライドヒープブロック」を更新します。この再利用を図 10 に示します。なお、各破壊を詳細に示すため、鳥瞰図の粒度を再度拡大しています。

これは、最初の tagPROPLIST リスト項目に加えて、最終的に 2 つの追加のオブジェクトだけを破壊する必要があることを意味します。
さて、メモリのどこかから数バイトを読み取りたい場合、InternalGetWindowText() 関数を介してウィンドウテキストを照会します。そこには破壊された strName のエントリがあります。Length フィールドで宣言されたバイト数を読み取ることができます。同様に、メモリ内の任意の場所に書き込みたい場合は、破壊されたウィンドウテキストを NtUserDefSetText() 関数で更新しますが、書き込む量は MaximumLength フィールドで宣言された値(これも設定可能)を超えてはなりません。これにより、既存のバッファが再利用され、目的のメモリアドレスを指すようになります。
Windows Vista 以降、ユーザーモードのバックエンドアロケータはヒープエンコーディングを使用していますが、デスクトップヒープは Windows 8 になるまでこれを有効にしていませんでした。そのため、Windows 8 以降では、「オーバーライドヒープブロック」を上書きする際に障害が発生します。しかし、ヒープを格納する _HEAP 構造体にはこの cookie が含まれており、それを使用してヒープヘッダ全体をエンコードしています。したがって、ユーザー空間にマッピングされたデスクトップヒープからこの cookie を読み取り、それを使用して「オーバーライドヒープブロック」のヘッダをエンコードできます。エンコード方法はアロケータコードのリバースエンジニアリングにより取得され、その操作を模倣することでアロケータはこれを受け入れます。
まず注意すべき点として、32 ビットシステムの tagPROP 構造体は 8 バイトであり、64 ビットシステムの 0xc バイトではありません。また、制御可能な hData フィールドは 4 バイトのみであり、64 ビットシステムの 8 バイトではありません。さらに、余分なパディングバイトはなく、64 ビットシステムでは 8 バイトのパディングがあったため、構造体全体が正確に 8 バイトになります。これは、データを部分的にしか制御できない場合、隣接するヒープブロックヘッダを完全に破壊できないことを意味します。一部の Windows バージョンでは、最も重要なフィールドを制御できるため可能ですが、Windows 8 および 8.1 ではヒープヘッダがエンコードされており、最終的には fs フィールドを介してヒープヘッダの一部を安全でない方法で上書きすることになります。32 ビットシステムの _HEAP_ENTRY ヘッダは似ていますが、PreviousBlockPrivateData フィールドがありません。
それでも、切り詰められたポインタを避けることができないため、tagWND のすべての部分を破壊することはできません。そして、これに適したオブジェクトもまだ見つかっていません。_LARGE_UNICODE_STRING が 64 ビットシステムでうまく機能することを考えると、32 ビットシステムでもそれを使用したいと考えました。
私のアイデアは、tagPROPLIST 構造体の iFirstFree フィールド(プロパティリスト内で最初に解放されたプロパティ項目のインデックス)を、インデックス値を増やす方法で破壊できれば、ヒープ上のさらに遠くの位置を指すようにできるということです。例えば、tagWND.strName の先頭を指すようにできます。図 11 にこのアイデアを示します。

プロセスを明確にするため、ここでは 2 つの tagPROPLIST 構造体を使用します。1 つは UAF 用の「プロパティリストA」、もう 1 つは「プロパティリストB」です。挿入された「プロパティリストA」の tagPROP のどの部分が「プロパティリストB」の iFirstFree フィールドを上書きするかを正確に知る必要があります。また、一度に 8 バイトしか書き込めないことも覚えておかなければなりません。そのため、まず隣接するヒープヘッダを破壊するために「プロパティリストA」に余分な tagPROP を少なくとも 1 つ挿入し、次に「プロパティリストB」の tagPROPLIST フィールドを狙うために 2 つ目を挿入する必要があります。これらはオペレーティングシステムやヒープブロックサイズによって異なる可能性があり、私のエクスプロイトではさまざまなヒープレイアウトに適応する必要があります。図 12 は、どのように破壊するかを示しています。図では、最初の tagPROPLIST は個別のフィールドに分割されておらず、因此 tagPROP[0] は暗黙的です。しかし、2 つ目の tagPROPLIST では、破壊プロセスを示すために内部メンバが分割されています。これが tagPROP[0] が表示される理由です。

まず、各 tagPROP に 8 バイトを書き込むと、iFirstFree への上書きを部分的にしか制御できないことに注意してください(atomKey と fs フィールドから得られるため)。これは最も重要な部分です。atomKey の値を通じて少なくとも 2 つの重要なバイトを完全に制御でき、その値が十分に小さい場合、fs フィールドは 0 になります。したがって、hData の値を使用して cEntries を妥当な値で上書きし、atomKey を使用して iFirstFree が tagWND を指すようにし、上書きしたい strName.Buffer ポインタが tagWND 内に存在するようにします。Length と MaximumLength の値を直接上書きできない場合は、ターゲットウィンドウにあらかじめ文字列を割り当てて、その長さが既に設定された値になるようにすることができます。
32 ビットの tagWND 構造体を見て、何が得られるか確認しましょう。今回は -b パラメータを使用して、strName 内の Buffer のオフセットを簡単に計算できるようにします。
kd> dt -b !tagWND
win32k!tagWND
+0x000 head : _THRDESKHEAD
+0x000 h : Ptr32
+0x004 cLockObj : Uint4B
+0x008 pti : Ptr32
+0x00c rpdesk : Ptr32
+0x010 pSelf : Ptr32
+0x014 state : Uint4B
+0x014 bHasMeun : Pos 0, 1 Bit
[省略されたフラグ]
+0x014 bDestroyed : Pos 31, 1 Bit
+0x018 state2 : Uint4B
[省略されたフラグ]
+0x018 bWMCreateMsgProcessed : Pos 31, 1 Bit
+0x01c ExStyle : Uint4B
+0x01c bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit
[省略されたフラグ]
+0x01c bUIStateFocusRectHidden : Pos 31, 1 Bit
+0x020 style : Uint4B
+0x020 bReserved1 : Pos 0, 16 Bits
[省略されたフラグ]
+0x020 bWS_POPUP : Pos 31, 1 Bit
+0x024 hModule : Ptr32
+0x028 hMod16 : Uint2B
+0x02a fnid : Uint2B
+0x02c spwndNext : Ptr32
+0x030 spwndPrev : Ptr32
+0x034 spwndParent : Ptr32
+0x038 spwndChild : Ptr32
+0x03c spwndOwner : Ptr32
+0x040 rcWindow : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x050 rcClient : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x060 lpfnWndProc : Ptr32
+0x064 pcls : Ptr32
+0x068 hrgnUpdate : Ptr32
+0x06c ppropList : Ptr32
+0x070 pSBInfo : Ptr32
+0x074 spmenuSys : Ptr32
+0x078 spmenu : Ptr32
+0x07c hrgnClip : Ptr32
+0x080 hrgnNewFrame : Ptr32
+0x084 strName : _LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr32
+0x090 cbwndExtra : Int4B
+0x094 spwndLastActive : Ptr32
+0x098 hImc : Ptr32
+0x09c dwUserData : Uint4B
+0x0a0 pActCtx : Ptr32
+0x0a4 pTransform : Ptr32
+0x0a8 spwndClipboardListenerNext : Ptr32
+0x0ac ExStyle2 : Uint4B
+0x0ac bClipboardListener : Pos 0, 1 Bit
[省略されたフラグ]
+0x0ac bChildNoActivate : Pos 11, 1 Bit
strName のオフセットは 0x84、Buffer のオフセットは 0x8c です。tagPROP リスト項目のインデックスを持ち、8 バイト書き込めることを考慮すると、iFirstFree が MaximumLength を指すウィンドウのオフセット 0x88 を指すかどうかは簡単にわかります。Buffer の 2 バイトしか制御できないため、書き込み操作は不可能であり、これを任意の読み取り/書き込み「プリミティブ」として使用することが目標であるため、この結果は受け入れられません。次のインデックスを設定して 0x90 を指すようにすると、cbwndExtra を上書きすることになり、これは求めるものではありません。
以前にヒープ風水で制御できたものを振り返り、tagWND 内に制御可能で興味深いオフセットがないか確認します。tagWND のオフセット 0x70 には pSBInfo フィールドがあります。このオフセットは 8 で割り切れるため、偽の tagPROP の hData の一部のデータを使用してこのポインタを上書きできます。
pSBInfo を直接同じ tagWND 構造体内の strName を指すように上書きできるでしょうか?おそらく、スクロールバーの API を使用して strName を破壊し、目的を達成できるでしょう。
pSBInfo は tagSBINFO 構造体を指しており、これは最初の UAF プロセスで言及されています。
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
WSBflags はあまり制御できませんが、スクロールバーが有効な場合は 1 に設定され、無効な場合は 0 に設定されることが少なくともわかっています。このフラグフィールドを任意の値に設定することはできません。関連する機能関数をリバースエンジニアリングすると、スクロールバーの状態を変更しない限り、このフラグフィールドは変更されないことがわかります。tagSBDATA 構造体の値はより興味深いように見えます。SetScrollInfo() のドキュメントを読めば、これらの値の意味をよく理解できます。SCROLLINFO 構造体を介して SetScrollInfo() にパラメータを設定できるようです。破壊したいウィンドウの近くにスクロールバーコントロールがあれば、pSBInfo ポインタを直接操作できます(関連するウィンドウコントロールに特別なウィンドウメッセージが送信されます)。明らかに、posMin と posMax の値は無条件に制御できます。page と pos フィールドは少し厄介で、一定の範囲内に制限されるため、今はこれらを避けるようにします。SCROLLINFO 構造体に SIF_RANGE フラグを設定して、最大値と最小値を設定する場所を宣言します。
Buffer を任意のデータで上書きしたいため、posMin でそれを上書きできるようにします。そのため、pSBInfo を strName.MaximumLength を指すように上書きします。スクロールバーを有効または無効にしない限り、WSBflags フィールドは上書きされず、strName.MaximumLength の整合性が保たれます。つまり、posMin(SCROLLINFO の nMin を介して)をどのように設定しても、Buffer が上書きされ、posMax が cbwndExtra に書き込まれます。これは大きな問題ではありません。64 ビットシステムでは、この値を事前に読み取り、後で復元できます。オーバーフローの一般的な考え方を図 13 に示します。

次に、図 14 で 32 ビットシステムの攻撃プロセスを説明します。UAF から離れたアドレスを破壊する前に、関連するヒープブロックとヒープレイアウトを図で確認しましょう。詳細がわかったので、次に行うべきことは明白です。

次に、「プロパティリストA」に 2 つのプロパティ項目を挿入します。これにより、以前の UAF 破壊により「プロパティリストA」の近くのデータが破壊されると同時に、「プロパティリストA」の iFirstFree が pSBInfo を指すようになります。これにより、近くの pSBInfo の値も破壊されますが、事前に読み取っておき、破壊後に復元できます。

「プロパティリストB」に新しい tagPROP を挿入します。この tagPROP の atom 識別子はリスト内の既存のものとは異なるため、次の空きインデックス項目に挿入されます。これにより、pSBInfo が破壊され、同じ tagWND 内の strName.MaximumLength を指すようになります。

最後に、スクロールバーを更新して strName.Buffer フィールドを破壊します(図 17)。

64 ビットの場合とは異なり、strName の長さの値を破壊できないことに注意してください。適切な長さのウィンドウテキスト文字列を事前に割り当てて、その値が既に使用されているようにすることができます。その後、カーネルアドレスからデータを読み書きする必要がある場合は、SetScrollInfo() を呼び出してターゲットウィンドウの Buffer の値を更新し、ウィンドウテキストの API を使用して操作します。
これで、32 ビットシステムでも再利用可能な任意のアドレス読み取り/書き込み「プリミティブ」が得られました。
ここからは、任意の読み取り/書き込み「プリミティブ」があることを前提とします。したがって、何かをリーク/読み取りしたり、上書きしたりと言うときは、以前の破壊フェーズで確立されたこの「プリミティブ」を実行することを意味します。この「プリミティブ」は両方のプラットフォームでほぼ同じです。残っているのは、関数ポインタを上書きして、シェルコードペイロードをどこかに指すようにすることだけです。一般的な方法は、nt!HalDispatchTable の 2 番目のエントリを上書きすることです。このエントリは HalQuerySystemInformation() 関数に対応しています。その後、ユーザーモードで NtQueryInternalProfile() 関数を呼び出してトリガーします。
カーネルモジュールのロードベースアドレスを知る必要があります。これにより、nt!HalDispatchTable のカーネルアドレスを計算できます。これを実現するには、ユーザーモードで NtQuerySystemInformation() を呼び出してモジュール情報を取得します。この情報にはモジュールベースアドレスが含まれています。
// 列挙値 11 は SystemModuleInformation に対応、これは未ドキュメントです...
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);
その後、ユーザーモードで ntoskrnl.exe をロードして nt!HalDispatchTable のオフセットを見つけ、そのカーネル空間アドレスを取得します。次に、「読み取りプリミティブ」を使用して HaliQuerySystemInformation() のカーネルアドレス(この関数はエクスポートされていません)を読み取り、それを変更し、「書き込みプリミティブ」を使用してその関数ポインタをシェルコードのアドレス(カーネルアドレス空間またはユーザーアドレス空間のいずれか、後述)で破壊します。読み取り/書き込みのバイト数は 32 ビットと 64 ビットで同じです。
Windows 8 および 8.1 では SMEP のサポートが導入され、一部のセキュリティ製品は Windows 7 でもこれを有効にできるため、SMEP が存在することを前提とします。SMEP は、カーネル権限でユーザー空間のコードを実行することを防ぐため、nt!HalDispatchTable のエントリをユーザー空間アドレスを指すように変更することができなくなります。したがって、カーネル空間内の制御可能な場所を指すようにし、その場所のコードが cr4 レジスタの値を変更して SMEP を無効にし、ユーザーアドレス空間にジャンプできるようにする必要があります。MWR の記事 では、64 ビット向けの興味深いテクニックが紹介されています。これは、ページテーブルエントリを自己マッピングすることで、任意の仮想アドレスに対してカーネルモードで有効なアドレスを取得するものです。その後、「書き込みプリミティブ」を使用してページテーブルエントリを直接変更し、マスクビットを変更します。このテクニックを 32 ビットシステムに移植しましたが、PAE が有効な場合と無効な場合でいくつかの違いがあります。
これを実現する明白な方法は、ユーザー空間のアドレスをカーネル空間にマッピングし、「書き込みプリミティブ」を使用してページテーブルエントリにシステム権限(ユーザー権限ではなく)を持たせることです。これが最初のステップです。Windows 8 で実装したときに、興味深い問題に遭遇しました。Windows 8 以降のデスクトップマネージャー (dwm.exe) は定期的にデスクトップ上のウィンドウをスキャンし、その名前を照会します(理由は調査していません)。この操作はウィンドウにメッセージを送信しませんが、対応するウィンドウプロシージャがあり、その中で GetInternalWindowText() が呼び出されます。したがって、問題は、シェルコードを含むメモリのページテーブルエントリ(これは自身のプロセス空間のページテーブルに属する)を、ウィンドウ構造体の strName フィールドを使用して書き換えることです。dwm.exe がカーネルからウィンドウ名を取得するとき、変更されたページテーブルエントリにより、カーネルが strName.Buffer が null でないかチェックし、そのアドレスを間接参照することになります。そのアドレスが無効であればシステムクラッシュが発生します。
dwm.exe のクエリを満たすために、ペイロードとしてカーネルアドレスを使用しました。これにより、現在のプロセスがどのようにロードされていても、そのアドレスに関連するページテーブルエントリは常に有効です。私はこれをデスクトップヒープに配置することにしました。これは、前述の方法でそのカーネルアドレスを計算できるためです。それでも、ページテーブルエントリを自己マッピングするテクニックを使用します。このとき、ページテーブルは既に高権限としてマークされていますが、実行可能としてマークされていません。したがって、実行ビットを設定するだけで済みます。
手順は次のとおりです。
Windows 8.1 ではさらに別の問題があります。NtQuerySystemInformation() は低整合性 SID 値をチェックするため、中整合性以上でなければカーネルベースアドレスを取得できません。これは、よく知られている sidt テクニックを使用して簡単に実現できます。IDT のアドレスをユーザーモードに保存し(権限チェックは不要)、「読み取りプリミティブ」を使用して必要な IDT インデックスを読み取ります。これは通常、カーネルアドレス空間を指しているため、割り込みハンドラのカーネルアドレスをリークでき、その後カーネルモジュールに対応する PE ファイル内のオフセットを検索します。
カーネルロードベースアドレスが得られれば、nt!HalDispatchTable のアドレスを計算できます。
通常の方法は、ntoskrnl.exe ファイルをロードし、そのシンボルのオフセットを解釈し、リークしたカーネルロードベースアドレスを加算することです。ただし、強化モードのサンドボックスでは、ファイルシステム自体の制限により C:\windows\system32\ntoskrnl.exe を読み取ることができないため、これは機能しません。この制限をバイパスするには、「リークプリミティブ」を使用して、メモリ内のカーネル PE イメージから必要なシンボルアドレスを解析します。
以上がすべての資料です。お読みいただきありがとうございます。本稿で提示した技術を使用して、32 ビットおよび 64 ビットの Windows XP、Vista、7、8、8.1、Server 2012 で安定したエクスプロイトを実現できました。Windows 2003 および 2008 ではデフォルトでは動作しません。これは、ユーザーモードコールバックをフックできないためであり、必要な条件を満たさない限りこれらのシステムを攻撃することはできません。エクスプロイトプロセスはかなり複雑で、多くの障害を克服する必要がありましたが、それにより多くの楽しみと学びが得られました。本稿で使用している多くの実用的な方法や研究成果は、他の研究者の記事でも言及されています。私の知る限り、win32k.sys の悪用を防ぐ唯一の緩和策は、Google Chrome サンドボックスが使用しているものであり、これはランタイム時に win32k のカーネルシステムコールを効果的にブロックします。改善点やフィードバックがあれば歓迎します。私が提示した技術に不備があればお知らせください。この文書を更新します。Twitter (@fidgetingbits) またはメール ([email protected]) で連絡できます。