CVE-2021-1732についての技術的分析


はじめに

20212月、Dbappsecurityは、Windows 10 x64のゼロデイ脆弱性を悪用したサンプルが出回っていることを確認しました。

この脆弱性CVE-2021-1732は、win32kのウィンドウオブジェクト型の混乱がOOB(境界外)書き込みを発生させるので、Windowsカーネル内で任意のメモリのリード/ライトが可能になります(ローカルEoP(Elevation of Privilege))。メモリの悪用には、一般に、読み取り、書き込み、実行の各プリミティブが必要です。Windows 10 などのハード化されたオペレーティングシステム上でDEP、ASLR、CFGなどの最新のエクスプロイト軽減策を回避することができます。データオンリーアタック は、メモリ上で悪意のあるコードを実行しないためリード/ライトプリミティブのみ必要です。オペレーティングシステムが使用するデータ構造を有利に操作します(つまり、昇格した特権を獲得します)。

通常、Windowsカーネルと直接やりとりを行うカーネルエクスプロイトは最も高度な攻撃です。攻撃者に高い特権を与え、エクスプロイト・チェーン全体の影響力を高めるために利用できるので、この攻撃が成功した場合は致命的です。今回のエクスプロイトでは、64bitWindows 10 バージョン1909を対象としたLPE(ローカル特権の昇格)が行われます。発見されたオリジナルサンプルは、2020年5月にコンパイルされ、2020年12月にマイクロソフトに報告されました。私たちは、新しい情報を探る中で、ある研究者が2021年3月に発表したパブリックエクスプロイトを調べました。このコードが公開されていることで、さらなる脅威の攻撃者の可能性が出てくるかもしれません。POC(概念実証)の悪意ある使用を示す明確な証拠は見つかっていませんが、VirusTotalにアップロードされたいくつかの亜種がテストされていることを発見したのです。

この記事で、McAfee Advanced Threat Research (ATR)は、検出と保護するためのプリミティブを特定するために脆弱性の分析について深く掘り下げて解説します。この脆弱性は、GetMenuBarInfo API を使用した win32k の任意のカーネルメモリリードプリミティブを使用するという点において、斬新です。当社の知る限りでは、これまで一般的に知られていなかったものです。


CVE-2021-1732 の段階

CVE-2021-1732の悪用は、プロセスの権限をシステムに昇格させることを最終ゴールとして、6つの段階に分けることができます。下の図はその段階を示したものです。

1 – CVE-2021-17326つのステージ

詳細を説明する前に、CVE-2021-1732の悪用に使用されるwin32k悪用プリミティブの背景を説明する必要があります。


Win32Kのバックグラウンド

Win32kはMicrosoft Windows Subsystemのグラフィカル(GUI)コンポーネントで、パフォーマンス上の理由から、その多くはカーネルに存在します。Windows OSのデスクトップのグラフィカルプリントに使用されます。ただし、win32kアーキテクチャのため、win32kのカーネルコンポーネントはウィンドウの作成と管理を容易にするために、ユーザーモードのコールバック関数を通じてユーザーモードに呼び出すことができる必要があります。

カーネルのユーザーモードコールバックは、2008年と2010年の時点でよく研究されています。2011年にMandtが非常に包括的な分析を行いました。xxxCreateWindowExなどのwin32kカーネル関数は、コールバック関数を作ります。xxxClientAllocWindowClassExtraBytes などのユーザープロセスの PEB KernelCallbackTableを介して実行されます。.

ユーザーモードコールバックが完了すると、NtCallbackReturnが実行され、期待されるリターンパラメータがカーネルに戻されます。これらのコールバックはステートレスであるため、オブジェクトのロック機構に関連した多くの脆弱性が発見され、use-after-free (UAF) の悪用に繋がります。

Win32kWindowsカーネルの中でも非常に多く悪用されたコンポーネントであり、2010年から2018年において脆弱性の63%を占めています。ntdllのシステムコールと比較して、そのシステムコールの攻撃対象が大きいためです。Win32kの脆弱性は、一般的にtagWNDデータ構造と呼ばれるデスクトップオブジェクトを使用し、カーネルプリミティブの読み込みと書き出しによってデータオンリーアタックへと変化します。

データオンリーアタックには、2つの側面があります:

  1. 脆弱性を発見する。
  2. cbWndExtraなどのオブジェクトフィールドの特定のOS APIを使用して、既存または新規のリード/ライトプリミティブを活用する。

tagWNDデータストラクチャは、カーネルメモリ内で読み取り/書き込みを行うための、格好の標的となる2つのフィールドを持っています; tagWND.cbWndExtraとtagWND.ExtraBytesです。window class登録時にWNDCLASSEXAストラクチャのcbWndExtraフィールドを通じてメモリ上のtagWNDオブジェクトを取得した後、CreateWindowExでウィンドウを作成時に、直接メモリの追加バイトを要求することが可能です。

追加のバイト数は、cbWndExtra フィールドによって制御され、割り当てられた追加のメモリアドレスはExtraBytesフィールドに位置します。リード/ライトプリミティブは、以下のように作成されます:

  1. UAFなどの脆弱性を発見、その脆弱性を利用してWND0と呼ばれるメモリ上のtagWNDオブジェクトに書き込む。
  2. メモリ内の既に破損したWND0の近くにWND1と呼ばれる別のtagWNDオブジェクトを割り当てる。
  3. cbWndExtraに0xFFFFFFFなどの大きな値を上書きする。
  4. WND0SetWindowLongPtrなどのAPIを呼び出して、WND1内のフィールドにOOBを書き込む。

Windowsカーネル内のtagWNDの読み書き機能を利用した特権の昇格を利用し、Win32kカーネルのユーザーモードコールバックは何度も悪用されている。(CVE-2014-4113CVE-2015-0057MS15-061CVE-2016-7255, CVE-2019-0808


Win32k プリミティブの悪用

攻撃者が使用したCVE-2021-1732のエクスプロイトには、いくつかのプリミティブが確認されています。さらに、その中にはこれまでは見つけられていなかった新しいものも含まれているということも、付け加えておくべき情報でしょう。

Windows RS4以前は、複数の手法でtagWNDカーネルアドレスをリークすることは容易でした。例えば、HMValidateHandleを呼び出して、tagWNDオブジェクトをカーネルからユーザーデスクトップヒープにコピーするようなケースです。最新版のWindows 10では、このような些細なテクニックに対しても対策が施されています。

しかし、spmenuカーネルアドレスリーク技術と相対的なtagWNDデスクトップヒープオフセットを使用して、一度tagWND.cbWndExtraフィールドを上書きする脆弱性が発見されると、実際のtagWNDカーネルアドレスをリークせずにカーネル読み取り/書き込み機能を実現することが可能です。このエクスプロイトにおけるspmenuのテクニックは、こちらこちらで使われていますが、過去にGetMenuBarInfo APIがwin32kエクスプロイトで使われたことはないと認識しています。

下の図はCVE-2021-1732で使用されているプリミティブです。

2 – CVE-2021-1732 プリミティブ


既存のWindows OSの緩和措置

EoP攻撃に対するwin32kのセキュリティを強化するために、マイクロソフトOSRチームによる新しい緩和策と改良が行われました。 MandtGoogle Project ZeroSchenk そして Dabah です。これらの緩和策には、以下のようなものがあります:

  1. タイプアイソレーション(全て同じタイプのオブジェクト tagWND が使用されます)。
  2. Win32kフィルタリング(Edgeブラウザに限定され、プロセス全体ではないが、この調査Win32k APIのフィルタリング機能には多くの改良が加えられています。例えば、_stub_UserSetWindowLong, sys の_stub_UserSetWindowLongPtr _stub_UserGetMenuBarInfo の追加です。
  3. カーネルデスクトップヒープの断片化と、ユーザーデスクトップヒープ内のカーネルアドレスの削除(ブログで後述するユーザーヒープとデスクトップヒープ内の相対オフセットが使用可能です)
  4. win32kドライバからデータ型シンボルを削除(緩和というより難読化)。

悪意のあるプロセスが CVE-2021-1732 を悪用した場合、上記の緩和策では保護されません。ただし、Google Chromewin32kの呼び出しを禁止しているため(Windows 8以上)、またMicrosoft Edgeは該当するAPIにwin32kフィルタリングを適用するため、影響はありません。


脆弱性のトリガーとパッチの解析

CreateWindowEx APIを使用してウィンドウを作成すると、Windows OSによってtagWNDオブジェクトが作成されます。上記で説明したように、このウィンドウは、cbWndExtraを使用して余剰メモリを確保するためのパラメータを使用して作成されます。

ウィンドウ作成処理(CreateWindowEx API)中に、xxxClientAllocWindowClassExtraBytesというコールバックが起動し、tagWND.cbWndExtraoffset 0xc8)の値サイズごとに、ユーザモードのデスクトップヒープにtagWND.ExtraBytesoffset 0x128)のスペースを確保します(WND1については以下の図3および4を参照ください)。

3 – WND1 カーネル tagWND – オフセット 0x28 に位置するユーザー・モード・コピー

図 4 – WND1 User Mode tagWND

このメモリのロケーションは、デスクトップヒープへのユーザーモードメモリポインタとして格納され、tagWND.ExtraBytesに配置されます。その後、NtUserConsoleControlを使用して、通常のウィンドウをコンソールウィンドウに変換することができます。これは、tagWND.ExtraBytesのユーザーモードポインタを、カーネルのデスクトップヒープを指すオフセット値に変換します(WND0については、以下の図5を参照してください)。このtagWND.ExtraBytesの値の変更(ウィンドウタイプの混乱)は、xxxClientAllocWindowClassExtraBytesコールバックウィンドウの最中にOOBの書き込みに悪用されます。

図 5 – WND0 User Mode tagWND

6 – win32kfull!xxxCreateWindowEx 内のタイプ混乱脆弱性のトリガー

上の図6では、脆弱性のトリガーには、次の手順が必要です。

  1. dll 内の HMValidateHandle インライン関数へのポインタを取得
  2. PEB KernelCallBackテーブル内のxxxClientAllocWindowClassExtraBytesをフック
  3. CreateWindowEx APIを使用して複数のウィンドウを作成し(ここでは最初に作成したWND0WND1だけを使用)、2つのウィンドウをメモリ上で近接した場所に作成
  4. WND0WND1に対してHMValidateHandleを呼び出し、そのオブジェクトをカーネルのデスクトップヒープからユーザーのデスクトップヒープにコピー。tagWND+0x8でデスクトップヒープにオフセットを格納;このオフセットは、ユーザーとカーネルのデスクトップヒープと同じ。エクスプロイトは、これらのオフセット値を使用してカーネル デスクトップ ヒープ内の WND0 WND1 間の相対距離を計算。これは、後でOOBの読み取りと書き出しに必要。下の表1では、オフセットに関連して読み取りと書き出しできるため、このオフセットを使用することで実際のWND0WND1のカーネルアドレスをリークする必要はない。(ユーザーとカーネルのデスクトップヒープのオフセットは同じ) 1 – ユーザーとカーネルのデスクトップヒープが同じオフセットを持つ
表1 – ユーザーとカーネル・デスクトップヒープが同じオフセットを持つ

5. WND0は、NtUserConsoleControlを呼び出すことによってコンソールウィンドウに変換され、WND0.ExtraBytesがユーザーデスクヒープポインターからカーネルデスクヒープのオフセットに変換される。これは、WND0WND1OOB を書き込むために、後で必要になる

6. CreateWindowEx APIを使用して、悪意のあるウィンドウWND_Maliciousを作成。

    • ウィンドウ作成中にcallback xxxClientAllocWindowClassExtraBytes APIが実行され、ユーザーモードにcbWndExtraのメモリを割り当て、カーネル関数 win32kfullxxxCreateWindowExに戻るよう、ユーザーデスクトップヒープポインタを要求。
    • xxxClientAllocWindowClassExtraBytesがフックされたので、win32kfull!xxxCreateWindowEx: に戻る前に以下を実行
      • NtUserConsoleControlを呼び出してWND_Maliciousをコンソールウィンドウに変換し、そのcbWndExtraをユーザーのデスクトップヒープへのポインタからカーネルのデスクトップヒープ内のオフセットに変換。
      • 最後にNtCallbackReturnを呼び出し、xxxClientAllocWindowClassExtraBytesにコールバックと単一の値を返すことを完了させる。xxxClientAllocWindowClassExtraBytesが期待するユーザーデスクトップヒープポインタをカーネルに渡す代わりに、以下の図7に示すように、WND0+0x08の値、つまりカーネルのデスクトップヒープをWND0へのオフセットとして渡す。これで、WND_MaliciousSetWindowLongWを呼び出すと、いつでもWND0に書き込まれる。
図 7 – WND_Malicious

パッチの解析

この脆弱性は、win32kfull!xxxCreateWindowExが、xxxClientAllocWindowClassExtraBytesを開始してからNtCallbackReturnから応答を得るまでの間に、ウィンドウタイプが変化したかどうかをチェックしないことに起因しています。

上記のフックでWND_Maliciousを指定してNtUserConsoleControlを呼び出すと、xxxConsoleControltagWND+0xE8フラグが0x800に設定されているかどうか、つまり下図のようにコンソールウィンドウであることをチェックするのですが、このとき、WND_Malicious0x800であれば、コンソールウィンドウであることを示します。WND_Maliciousは通常のウィンドウとして作成されているため、xxxConsoleControlはカーネルデスクトップヒープ内のオフセットにメモリを確保し、WND_Malicious.ExtraBytesにあるユーザーデスクトップヒープポインタを解放します(0ffset 0x128)。 そして、WND_Malicious.ExtraBytes (0ffset 0x128)にカーネルヒープ内のこの新しい割り当てへのオフセットを置き、tagWND+0xE8フラグを0x800に設定して、コンソールウィンドウであることを示します。

 

上記のNtCallbackReturnを発行してコールバックから戻った後、xxxCreateWindowExはウィンドウタイプが変わったことを確認せず、以下の図9のようにWND0+0x08をWND_Malicious.ExtraBytesに配置します。RedirectFieldpExtraBytes は、WND_Malicious.ExtraBytes が初期化した 値 をチェックしますが、WND0+0x08 がすでに WND_Malicious.ExtraBytes (offset 0x128) に書き込まれているので手遅れです。

図 9 – win32kfull!xxxCreateWindowEx (vulnerable version)

下の図10では、パッチを適用した win32kfull.sysは、ユーザーモードから戻り値を書き込む前にExtraBytes初期化された値をtagWND-ExtraBytes (オフセット0x128)にチェックするためxxxCreateWindowExを更新しました。

図 10 – win32kfull!xxxCreateWindowEx (patched version)

下の図11では、tagWND. ExtraBytes は、通常のウィンドウ生成時に xxxCreateWindowEx 内で0に初期化されます。

図 11 – tagWND. ExtraBytes initialization for normal window

以下の図12は、tagWND. ExtraBytes は、コンソールウィンドウの作成時に xxxConsoleControl 内のカーネルデスクトップヒープ内の新しいオフセット値に初期化されます。RedirectFieldpExtraBytesは、この初期化された値をチェックするだけで、ウィンドウの種類が変更されたかどうかを判断します。さらに、マイクロソフトは、パッチ適用後のバージョンで、ウィンドウタイプフラグの変更を検出するためのテレメトリーも追加しました。

図 12 – tagWND. ExtraBytes initialization for console window

tagWND OOB 書き込み

xxxCreateWindowEx APIに脆弱性があり、WND_Malicious.ExtraBytesフィールドに、カーネルデスクトップヒープ内のWND0オフセットが設定される可能性があります。これで、WND_Malicious SetWindowLongW が呼ばれると、いつでも WND0 に書き込まれるようになります。下の図 13、図 14 にあるように、オフセット0xc8を与えることで、関数はWND0.cbWndExtraフィールドを大きな値0XFFFFFFFに上書きします。

つまり、tagWND構造体やカーネルメモリ内のExtraBytesを超えて、WND1内のフィールドに書き込むことができるのです。また、WND0.ExtraBytesも自分へのオフセットで上書きされるため、WND0に対するSetWindowLongPtrAの呼び出しは、WND0の先頭に対するカーネルのデスクトップヒープ内のオフセットに書き込まれることになります。

Figure 13 – OOB Write from WND_Malicious to WND0
図 14 – WND0 cbWndExtra overwritten with 0xFFFFFFF by WND_Malicious OOB write

カーネルアドレスのリーク

WND0.cbWndExtraフィールドに非常に大きな値(0xFFFFFFF)が設定されたので、WND0上でSetWindowLongPtrAがいつでも呼び出されると、以下の図15のようにカーネルメモリ内のWND1に書き込まれることになります。WND1の特定のフィールドに書き込むことで、以下のようにカーネルアドレスのメモリリークを発生させることができます。

  1. WND1スタイルフィールドに0x400000000000000を書き込み、一時的に下記の図1516のように子ウィンドウに変更します。
  2. WND0SetWindowLongPtrA APIを値:-12GWLP_ID)で呼び出すと、以下の図1517のように子ウィンドウに変更したため、WND1spmenuフィールド(タイプtagMENU)を偽のspmenuデータ構造で上書きすることができるようになりました。
  3. SetWindowLongPtrA API documentationによると、戻り値は上書きされたオフセットの元の値、つまりカーネルメモリアドレスであるspmenuデータ構造体ポインタを与えてくれます。そこで、カーネルメモリ内のspmenu(タイプtagMENU)データ構造へのポインタをリークし、spmenu内のポインタを、以下の図17に従ってユーザーデスクヒープ内の偽spmenuデータ構造で置き換えたのです。

15 – WND0 から WND1 への OOB 書き込みによるカーネル・アドレスのリーク

16 – 0x40000000000000 を書き込む前と後の WND1 スタイルフィールド

17 – spmenuカーネルメモリアドレスポインタがリークされ、その後、偽のspmenuデータ構造を指すユーザーモードアドレスに置き換えられた


カーネルの任意読み込み

前にリークしたspmenuデータ構造のカーネルポインタを使い、このデータ構造のレイアウトとGetMenuBarInfo APIロジックを使った任意のカーネルメモリの読み込みにすることができます。下の図18,19,20のとおりです。

図 18 – Kernel Arbitrary Read using fake spmenu and GetMenuBarInfo
19 – ユーザーデスクトップヒープ内の偽の spmenu データ構造と、GetMenuBarInfo API を使用して任意の読み取りを可能にするために細工された場所にあるオリジナルの spmenu リークカーネルポインタ

20 – WinDbg コマンドにより、xxGetMenuBarInfo により参照される spmneu データ構造内の位置を表示

下の図21と図22xxxGetMenuBarInfo関数からわかるように、リークしたカーネルアドレスを偽のspmenuデータ構造内の正しい位置に配置することで、GetMenuBarInfoを呼び出す際に任意のカーネルメモリの読み取りを作成することができるのです。

図 21 – win32kfull!xxxGetMenuBarInfo

22 – GetMenuBarInfo データ構造には、通常の spmenu と偽の spmenu に応じて返り値が入力されている(カーネル アドレスのリーク) 

カーネルの任意の書き込み

SetWindowLongPtrA on WND0を呼び出すことで、WND1.ExtraBytesフィールドに宛先アドレスを書き込み、OOBWND1に指定したオフセットに相対的に書き込むことで、下の図 23のように任意のカーネルの書き込みプリミティブが容易に達成できます。

この場合、オフセットは0x128であり、ExtraBytesです。そして、WND1に対してSetWindowLongPtrAを呼び出すだけで、WND1.ExtraBytesフィールドに置かれたアドレスに指定の値を書き込むことができます。WND1は通常のウィンドウなので(WND0WND_Maliciousのようにコンソールウィンドウに変換されていない)、WND1.ExtraBytesに書き込んだアドレスに書き込みを行うため、任意の書き込みが実現します。

Figure 23– Kernel Arbitrary Write for What-Write-Where (WWW)

データオンリーアタック

任意のカーネルの読み取りと書き込みのプリミティブを組み合わせて、悪意のあるプロセスのEPROCESSトークンをPID 4のトークンで上書きするというデータのみの攻撃が可能な特権(EoP)の昇格のためのシステムです。

このWND1カーネルアドレスで偽のspmenuデータ構造のGet既にリークしたオリジナルのspmenuカーネルアドレス は、下の図24、25のように オフセット0x50 に WND1 へのポインタを持っています。MenuBarInfoを使用して複数の任意の読み取りを通じて、我々は最終的にPID 4システムEPROCESSトークンを読み取ることができます。

24 – 偽の spmenu GetMenuBarInfo の任意読み込みを組み合わせて、PID 4 トークンを取得

25- オフセット 0x50 WND1 カーネルアドレスポインタを持つオリジナルの spmen

WND1.ExtraBytesに宛先アドレス(悪意のあるプロセスのEPROCESSトークン)を置くことで、その後のSetWindowLongPtrAのコールは、下の図26および27にあるように、そのアドレスに値(PID 4 – システムEPROCESSトークン)を書き込みます。

26 – EPROCESSトークンのスワップ

27 – EPROCESSトークンのアドレスでWND1.ExtraBytesを上書き

そして、このエクスプロイトは、EoPが完了すると上書きされたデータ構造の値を復元し、BSODBlue Screen of Death)を防ぎます。


まとめ

本レポートでは、Windows 10におけるローカル特権の昇格であるCVE-2021-1732について深く分析しました。Windowsカーネルデータオンリーアタックは、一度脆弱性が発見されると、特定のAPIを通じて正当かつ信頼できるコードを使用してカーネルメモリ内のデータ構造を操作するため、防御するのが困難です。

 win32kコンポーネントは、Microsoftの素晴らしい取り組みにより、read/writeプリミティブに対して強化されていますが、その大きな攻撃対象(システムコールとコールバック)と、プロセス単位でのwin32kフィルタリングの欠如により、悪用の機会はまだ残ります。Windows 10でシステム全体のwin32kフィルタリングポリシー機能を見ることができるのも素晴らしいことです。

 脆弱性に対してはパッチ適用が常に最善の解決策ですが、パッチ適用が不可能な場合や、キャンペーンによって使用されている脆弱性/エクスプロイトの亜種を検出するためには、脅威ハンティングなどの強力な防御戦略も必要です。

※本ページの内容は2022年1月5日(US時間)更新の以下のMcAfee Enterprise Blogの内容です。
原文: Technical Analysis of CVE-2021-1732
著者: