
CVE-2026-54107の根本原因分析:Windows win32kfull.sysのuse-after-free。競合状態のデバッグ、静的解析、MSRCトリアージの洞察、実践的なカーネルエクスプロイト研究を含む。
ValidateHwnd がゲートではないとき: CVE-2026-54107 の根本原因を追う
win32kfull.sysのウィンドウライフサイクル管理における use-after-free —— どのように見つけ、どうやって本物だと確信したか、そして研究者側から見た MSRC プロセスが実際どうだったか。
午前2時近く、ターゲットのVMがデバッガーのハートビートに応答しなくなり、私が数週間にわたって到達可能だと主張してきた命令のまさにその場所でブレークに落ちた。アサーションでも、破損したプールの停止でもない——メッセージディスパッチ経路上の単純なアクセス違反で、別のスレッドが既に破棄したオブジェクトをデリファレンスしていた。
そのブレークは CVE-2026-54107 となり、MSRC ケース 11xxxxx として、2026年7月のセキュリティ更新で27のWindows製品にわたって修正された。
この投稿は、この話のうち非エンバーゴ部分である。根本原因、このバグクラスがなぜそうなのか、そしてそこに至った思考の筋道。悪用の詳細は省く。
Win32k は Windows グラフィカルサブシステムのカーネルモード側の半分である。古く、巨大で、そして決定的なことに、信頼されていないと想定されるコンテキストから到達可能である。この最後の性質こそが、20年にわたるハードニング、フィルタリング、システムコール制限の努力にもかかわらず、win32k が恒久的な研究対象であり続ける理由だ。
win32k の中でも、tagWND オブジェクト (PWND) は、そのライフタイムが複数のメカニズムによって同時に管理されているため、異常に興味深い。ウィンドウは次のようにして参照される:
ValidateHwnd スタイルのルックアップを通じて、独立した参照経路が複数あり、破棄経路が1つしかないオブジェクトは、ゆっくり読む価値がある。これは脆弱性の主張ではない——どこに時間を割くべきかというヒューリスティックだ。
このコンポーネントに腰を据えたきっかけは、インポート面だった。win32kfull.sys は、ntoskrnl から3つの異なるオブジェクト参照プリミティブを取り込んでいる:```c
NTSTATUS ObReferenceObjectByPointer(
void *Object,
uint32_t DesiredAccess,
POBJECT_TYPE ObjectType,
char AccessMode);
NTSTATUS ObReferenceObjectByHandle( HANDLE Handle, uint32_t DesiredAccess, POBJECT_TYPE ObjectType, char AccessMode, void **Object, OBJECT_HANDLE_INFORMATION *HandleInformation);
NTSTATUS ObReferenceObjectByName(/* ... */);
入り口は3つ、出口は `ObfDereferenceObject` 経由の1つだけだ。
それはコードが間違っているという意味ではない。**不変条件が分散している**という意味だ — どの単一の関数も*「このオブジェクトは今まさに生きている」*を所有していない。したがって、正しさは、すべての呼び出し元が、自分が保持している参照がどれであり、それがどのくらいの期間有効であるかについて合意しているかに依存する。分散された不変条件は、レースコンディションが潜む場所である。なぜなら、レースとは単一の関数内で見えるロジックバグではないからだ。それは2つの関数にまたがって保持された前提のバグである。
そこで私が `PWND` に触れるすべての関数に対して問い始めたのは、*「このコードは正しいか?」*ではなく、次の問いだった:
> **このまったく同じ関数本体が、数命令分だけずれて2つのスレッド上で実行された場合、間違っているのはどちらか?**
## 3. 根本原因
この欠陥は、ウィンドウ破棄パスにおける**参照解放とオブジェクト破棄の間の time-of-check / time-of-use(TOCTOU)ギャップ**であり、並行してハンドルを検証するコンシューマに対する適切な同期が欠如している。
その形だけに単純化すると:```c
/* Thread A — teardown */
NtUserDestroyWindow(HWND hwnd)
{
PWND pWnd = ValidateHwnd(hwnd);
if (pWnd) {
ObfDereferenceObject(pWnd); /* reference released */
/* <-- race window: object may become reclaimable here */
FreeWindowObject(pWnd); /* teardown proceeds on a pointer
no longer guaranteed live */
}
}
The input chunk is empty — no content was provided to translate. Please supply the Markdown text for chunk 5 of 13.```c /* Thread B — consumer, concurrent / NtUserMessageCall(HWND hwnd, UINT msg, ...) { PWND pWnd = ValidateHwnd(hwnd); / may resolve a handle whose object is mid-teardown */
if (pWnd->fnid == FNID_BUTTON) /* use-after-free */
...
}
これが問題になるには2つの条件が真でなければならず、その両方が成立していた:
**(a) その時間窓は本物である。** `ValidateHwnd` はハンドルベースのアクセスを安全にするためのゲートである。既に破棄が始まっているオブジェクトに対して検証が成功し得るのなら、そのゲートはゲートではなく、単なる提案に過ぎない。
**(b) 解放されたメモリは攻撃者が影響を与えられる。** 検証直後に読み取られるフィールドには、メッセージディスパッチを駆動する `fnid` が含まれる。再利用されたメモリから下されたディスパッチの決定は、*"信頼できないクラッシュ"* と *"セキュリティ境界の侵害"* の違いを生む。この区別こそが、これが安定性バグではなく、EoP 影響を伴う CWE-362 であるすべての理由である。
> 観測された **破壊(corruption)** は use-after-free であり、**原因** は CWE-362、すなわち不適切な同期による共有リソースの並行実行である。この2つは別の命題であり、MSRC が重視するのは後者である。**症状だけでなく原因を報告せよ。**
### win32k の競合状態が構造的に難しく見える理由
他のサブシステムでバグを競わせた経験があるなら、win32k は苛立たしいだろう。なぜなら、アーキテクチャが3つの具体的な方法で敵対してくるからだ。
**ウィンドウにはスレッド親和性がある。** ウィンドウは、それを生成したスレッドに属する。サブシステムの多くは、所有スレッドがオブジェクトに触れるという前提の上に構築されている。つまり、素朴に「2つのスレッドを回して同じ API を呼ぶ」というアプローチは、しばしば何も重なり合わない——競合しているのではなく、キューイングしているのだ。2つのパスを同じオブジェクト上で本当に衝突させるには、どの操作が呼び出し元スレッドで実際に実行され、どの操作が所有者側にマーシャリングされるのかを理解する必要がある。
**メッセージディスパッチは部分的に直列化される。** Send と Post は動作が異なり、クロススレッドと同一スレッドのディスパッチもまた異なる動作をする。並行処理の機会に見えるものの一部は、関心のあるコードに到達する前に、静かに順序付けられた操作に変換される。自分のトリガーがどのカテゴリに該当するかを知らなければ、本当の競合状態は到達不能だと結論してしまう——これは「ここにバグはない」と見分けがつかない偽陰性である。
**クリティカルセクションは呼び出し元側に隠れている。** サブシステムの多くは、注視している関数よりずっと上位で取得される粗いロックの下で実行される。これは win32k 監査における最大の時間の無駄の源泉である。可視的な同期がなくても、そこへのすべての経路が既に直列化されているため完全に安全な関数、というものが存在するのだ。**ロックの適用範囲は手続き間の性質である。** 関数を読むだけでなく、コールグラフを遡って調べなければならない。
この3点目こそ、*"この関数にはロックがない"* というシグナルがほぼ無価値である理由であり、今回の探索作業のほとんどが欠陥そのものではなく到達可能性に費やされた理由である。
## 4. 影響度評価がこの値になる理由
MSRC はこれを **重要(Important)、特権昇格(Elevation of Privilege)、CVSS 8.8、攻撃ベクトルはローカル、認証済み** と評価した。これを決定づけたのは2つの性質である:
**低整合性レベルからの到達可能性。** Win32k のメッセージ呼び出し面は、SYSTEM よりはるかに低いコンテキストから到達可能である。これこそがサンドボックスエスケープチェーンに関連する理由であり、サンドボックス内で既にコード実行を達成したレンダラープロセスでも、この面には到達できる。カーネルバグの深刻度は、ほぼ専ら *誰が触れることができるか* の関数であり、破壊がどれほど巧妙かではない。
**ディスパッチに影響を与える破壊。** `switch` が実行対象とするフィールドを破壊することは、ログに記録されるだけのフィールドを破壊することよりも質的に悪い。前者はメモリバグを制御フローの問題に変える。
ここで1つ正確に述べておきたい。なぜなら、初めての CVE を報告する投稿がこれを誇張するのを見たことがあるからだ: **私は競合状態と use-after-free を実証した。兵器化された SYSTEM レベルのエクスプロイトを納品したわけではない。** サンドボックスエスケープという枠組みは、このバグタイプが属する *チェーンのクラス* と、その面が価値を持つ理由を説明するものであり、到達可能性についての議論であって、私がエクスプロイトを構築したという主張ではない。影響度の誇張はベンダーとの信頼を失う最短経路であり、重要なのは私の評価ではなく MSRC の評価である。
## 5. 反証が先——ほとんどの候補は脱落した
誰も書かない部分: これは最初の候補ではなかった。生き残った唯一の候補だったのだ。
私の作業規則は、**候補は有罪が証明されるまで有罪とみなす** というものだ。有望なパターンにはすべて、*悪用できるはずがない* 具体的な理由を書き留め、トリガーを試す前にその理由を確立しに行く。今回の候補より前に私が切り捨てた候補には以下があった:
- 同期がないように見えたが、1フレーム上のロックによって直列化されていたパス、
- 「解放された」オブジェクトが実際には解放ではなくキャッシュされていたパス、
- 実際に競合状態ではあるが、低権限ユーザーが駆動できる呼び出し元からは到達不能なパス。
そのすべては、私が MSRC *に送らなかった* 発見である。そこが要点だ。研究者のスループットとは、生成する候補の数ではなく、誤った候補をどれだけ速く殺せるか——午前2時までそれらを抱え続けなくて済むか——である。
**ほとんどの候補を殺した3つの質問:**
1. **自分より上流で誰かがロックを保持しているか?** ローカルではなく手続き間で見る。関数内にロックがないことは何も意味しない。
2. **非特権の呼び出し元が実際に両方の経路に到達できるか?** 異なる権限レベルを必要とする2つのパス間の競合は、競合ではなく思考実験である。
3. **解放されたメモリは、自分が影響を与えられる時間窓の中で再利用可能か?** 実用上、破棄がアトミックに完了するなら、報告する価値のあるバグは存在しない。
## 6. 検証: 静的解析は仮説を与え、デバッガーは真実を与える
`win32kfull.sys` の静的解析は私に仮説を与えた。しかし、バグそのものを与えることは決してなかった。**競合状態は逆コンパイラには見えない。** 欠陥は命令の中にあるのではなく、インターリーブ(交錯)の中にあるからだ。
### 実験環境
| 役割 | 構成 |
| --- | --- |
| ホスト / デバッガー | Windows 11、WinDbg |
| ターゲット | Windows Server 2022、Build 20348.2159 |
| 解析 | Kali Linux + Windows 11 VM |
| デバッグ転送 | VMware シリアル COM、ホスト → ターゲットのカーネルデバッグ |
| 静的解析 | GhidraMCP 経由の Ghidra |
| トリアージ補助 | 逆コンパイル出力に対する AI 支援パス |
実際の作業を行ったのは3つのインストルメンテーション層である。
### スペシャルプールと Driver Verifier
カーネル UAF 調査における単一の最高レバレッジ手順。デフォルトでは、解放されたプールメモリは、同じサイズの次の割り当てによってほぼ即座に再利用される。つまり use-after-free は通常 *フォルトを起こさない*。他人の有効なデータを読み、実行を継続し、数分後に無関係な場所で爆発する。そしてあなたは無実の関数の監査に3日を費やすことになる。
スペシャルプールはそれを変える。各割り当ては、隣接するガードページを持つ専用ページを取得し、解放されたページは再利用される代わりにアクセス不可としてマークされる。その結果、問題の逆参照は、下流ではなく **それを実行する命令の箇所で** フォルトを起こす:```
!verifier 0x1 win32kfull.sys ; special pool on the target driver
!verifier 0x8 win32kfull.sys ; pool tracking
プールタグのフィルタリングと組み合わせることで、これこそが*「負荷時の断続的なバグチェック」*を、再現可能で原因を特定できる障害へと変えるものです。
この投稿で一つだけ覚えておくなら: スペシャルプールは、行き詰まった後ではなく、始める前に有効にしましょう。
障害が発生したら、問題は破損なのかライフタイムバグなのかです。それぞれ異なるレポートが必要です。プールメタデータがその答えを示します。``` kd> !pool
もっともらしいタグとガベージ内容を持つ*割り当て済み*ブロックは、**破損**を示しています。*解放済み*のブロック、または特殊プールのアクセス不可ページ上にあるブロックは、**ライフタイムバグ**を示しています — オブジェクトの消滅後もポインタを保持していた何かがあるということです。これが*"攻撃者がここに書き込んだ"*と*"このオブジェクトは到達可能であるべきではなかった"*の違いであり、ヒープ破損レポートとCWE-362レポートの違いです。
どちらに断定する前に、オブジェクトの種類を照合してください。`PWND`には見分けられる形状があります。フォールトしたメモリにその残骸がまだ残っているなら、ランダムな上書きではなく、ほぼ間違いなくウィンドウ・ライフタイム問題です。
### インタリーブのライブカーネルデバッグ
特殊プールを使用しても、レースはスケジューリングの問題であり、**デバッガはスケジューリングを変えてしまいます。** これがレース作業の中心的な悩みです。計測器が測定対象を乱してしまうのです。破棄パス上のブレークポイントは、オーバーラップさせようとしている正確に2つのスレッドを直列化し、バグは綺麗に消えてしまいます。
これを回避するには、ブレークポイントでレースを捉えようとするのをやめ、代わりに次のようにします。
- **人為的にウィンドウを広げる** — 参照解放と破棄の間の間隔を長くするものは何でも、通常のスケジューリングレートで衝突を到達可能にします。
- **精度を上げるのではなく衝突試行回数を増やす** — 2つのパスを継続的に実行し、確率に任せます。
- **条件付きブレークポイントとワンショットブレークポイントを使用する** — 対象となる状態が存在して初めて有効になり、すべてのエントリで停止するのではありません。
- **フォールト後に、スレッド状態とスタックからインタリーブを確認する** — ライブで観察しようとするのではなく。
フォールト自体は、いったん捉えてしまえば、地味なものです。オブジェクトが別のスレッドで既に破棄を経たメッセージディスパッチパス上の`PWND`の逆参照であり、`!pool`がそのブロックが上書きではなく解放されたことを確認します:```
kd> !analyze -v
EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - Access violation
FAULTING_MODULE: win32kfull
(オフセット、アドレス、再現の詳細は、協調的な情報開示に基づき伏せられています。)
影響はクラッシュによって証明されるわけではない。それを証明するのは、誰がそのクラッシュを引き起こせるかだ。すべてのトリガー実行は、対象マシン上の標準の非管理者ユーザーアカウントから行った。なぜなら、既に特権を持つコンテキストからしか到達できないカーネル障害は、セキュリティバグではなく安定性のバグだからだ。衝突を引き起こしたプロセスの整合性レベルを確認するのは30秒で済む作業であり、それがバウンティ対象のケースになるのか、Windows Feedback Hubへの投稿事項になるのかを左右する。
重要な規律:私は単発のクラッシュを信じなかった。 レース条件における単発のクラッシュはノイズにすぎない。報告可能にしたのは、制御されたタイミングでの再現性—*「この2つのパス、この順序、この時間窓」*と言って、同じ障害が返ってくること—だった。それが、MSRCが対応できるレポートと、再現不能としてクローズされるレポートとの違いだ。
私はAI支援のトリアージを使って逆コンパイル済みの出力をより速く処理した。そして、それが何に役立ったのか、何に役立たなかったのかを率直に述べる。
役に立った点:攻撃面を広くカバーすること。 大量のHLILを読み、*「これらの関数は可視的な同期なしに共有オブジェクトに触れている」*と指摘するのはパターンマッチングであり、大量のパターンマッチングこそ、まさにこうしたツールが得意とする分野だ。数週間分のスキミングを数日間に圧縮してくれた。
役に立たなかった点:判断。 存在しない攻撃チェーンを自信満々に説明し、立証もしていない到達可能性を主張し、実在しないバグについて美しく構成されたレポートを作り出す。すべての結論は、レポートに載せる前にWinDbgでの手動検証を通過する必要があった。
恐れるべき障害モードは、ツールが間違っていることではない。正しくあってほしいと願う午前2時に、ツールが間違ったまま流暢に語ることだ。
捏造された発見をMSRCに送れば、彼らのエンジニアの貴重な時間を奪い、すぐには取り戻せない評判を失うことになる。
| 日付 | イベント |
|---|---|
| 2026年5月7日 | 提出 — VULN-186460 |
| 2026年5月7日 | ケース開設 — MSRCケース 11xxxxx |
| 2026年6月11日 | マイクロソフトが動作を確認、バウンティ審査開始 |
| 2026年6月27日 | 修正は7月リリースに予定、CVE-2026-54107を割り当て(プレリリース) |
| 2026年6月30日 | バウンティ授与 — US$8,000、Windows Insider Preview Bounty Program |
| 2026年7月14日 | パッチ配信、CVE公開 |
提出から確認までの5週間の沈黙。それがあなたを試す部分だ。他人のカーネルについての主張を書き留めたが、自分の推論が正しいのか、重複なのか、あるいは彼らのビルドで再現するのかどうかは、まだ全く分からない。確認メールが届いた瞬間、それはあなたが抱く理論ではなく、実在する脆弱性になる。
自分自身に笑ってしまった小さな出来事:私の現地時間で7月14日、CVEがまだ公開されていない理由をケースに問い合わせた。返答は丁寧に、シアトルでは7月13日ですというものだった。マイクロソフトのリリースカレンダーは太平洋時間で動いている。今ではそれが分かっている。
| 項目 | 詳細 |
|---|---|
| CVE | CVE-2026-54107 |
| MSRCケース | 11xxxxx (VULN-186460) |
| コンポーネント | win32kfull.sys — ウィンドウオブジェクトのライフサイクル |
| 分類 | 競合状態 → use-after-free |
| CWE | CWE-362 |
| 影響 | 特権の昇格 |
| 深刻度 | Important (MSRC) |
| CVSS v3.1 | 8.8 (High) |
| 攻撃ベクトル | ローカル、認証済み |
| プログラム | Windows Insider Preview Bounty Program |
| 報奨金 | US$8,000 |
| 修正 | 2026年7月 セキュリティ更新プログラム |
バグではなく不変条件を探して読む。 *「このコードは、強制していない何かをどこで仮定しているのか?」という問いは、「オーバーフローはどこか?」*よりも多くのものを見つける—特に、簡単なバグクラスがすでに排除された、成熟して徹底的に監査されたコンポーネントでは。
レース条件はプロシージャ間をまたぐ主張である。 単一の関数からそれを立証したり反駁したりすることはできない。解析が関数の境界で止まってしまうなら、決して結論を出せない候補を生み出すことになる。
デバッグ環境こそが仕事そのものだ。 私は実際の探索よりも、不安定なシリアルCOMリンクで多くの時間を失った。壊れたパイプラインは、「ここには何もない」と見分けがつかない偽陰性を生み出す。私はCOMポートの設定が原因で、このターゲットをほとんど放棄しかけた。
クラッシュではなく原因を報告する。 MSRCにはクラッシュが届くものだ。ケースを前進させるのは、どの不変条件が破られ、なぜその強制が存在しなかったのかについての一貫したストーリーなのだ。
確認されたからといって完了ではない。 確認からパッチ配信までの間には、再現性のフォローアップ、Canaryの動作、レビューがある。関与し続けること。
同じ方法論、異なる攻撃面—tcpip.sys、afd.sys、clfs.sys。私は一発の幸運な発見よりも、仕事の積み重ねで知られたい。それを実現する唯一の方法は、候補を生み出すよりも速く潰し続けることだ。
あなたが1年前の私と同じ立場にいるなら—ウェブのバウンティから来て、カーネルに興味を持ち、自分にできるのか確信が持てない—答えは実際にやってみることで見つかる。ドライバを1つ選べ。デバッガを接続しろ。ゆっくり読め。2回実行したらどうなるかを問い続けろ。
それが、本当にすべての始まりだ。
執筆:Pravin Choudhary(@pr4v1nx)—独立系オフェンシブセキュリティ研究者。協調的な情報開示に基づきマイクロソフトに開示。悪用の詳細、オフセット、再現コードは意図的に伏せられています。