
CVE-2015-0057(win32k.sys の use-after-free 脆弱性)に関する詳細な技術分析とエクスプロイト実装。Windows XP から 8.1 までの 32 ビット版および 64 ビット版 Windows システムを対象としています。
著者: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について何も知らない状態では、私が始めたときと同じように、これは自分にとって冒険でした。