Windows RDP 脆弱性 CVE-2019-0708 (BlueKeep) のエクスプロイト: Refresh Rect PDU と RDPDR Client Name Request PDU を使いシステム権限でリモートコード実行

By and

Category: Unit 42

Tags: , , ,

The conceptual image illustrates the concept of an exploit, such as that described here for Windows RDP Vulnerability CVE-2019-0708 (BlueKeep).

This post is also available in: English (英語)

概要

2019年5月、Microsoftは定例外のセキュリティ更新プログラムを公開しました。 リモートコード実行(RCE)脆弱性CVE-2019-0708に対応するためで、同脆弱性は「BlueKeep」という名前でも知られ、Remote Desktop Services(RDS)に存在します。昨年リサーチャーたちは同脆弱性のエクスプロイトが可能であることを実証したうえでその検出・防止策を提案してきましたが、今日にいたるもRDPは攻撃者がもっとも好んで悪用する攻撃ベクトルの1つとなっています。本稿では、攻撃者がどのようにしてWindows RDPエンドポイントでBlueKeepをエクスプロイトしうるかについて詳しく解説します。RDPの攻撃成功を防ぎ、Windowsユーザーとお客様環境を適切に保護していくのにぜひお役立てください。

さて、2019年8月、Unit42のリサーチャーはCVE-2019-0708に関するブログを公開し、そのなかでBitmap Cache PDU(Protocol Data Unit)、Refresh Rect PDU、RDPDR Client Name Request PDUを使ってWindowsのカーネルメモリにデータを書き込む方法を解説しました。さらに2019年10月には、MicrosoftのセキュリティカンファレンスBlueHat Seattle 2019にて、RDP PDUを使用した3つの新しいWindowsカーネルプール風水(ヒープ風水)テクニックとBlueKeepの2つの異なるエクスプロイトテクニックについてプレゼンテーションを行いました。本稿は、Refresh Rect PDUとRDPDR Client Name Request PDUを組み合わせてシステム権限でリモートコードの実行(RCE)権限を得る方法について解説します。

パロアルトネットワークスの次世代ファイアウォールで脅威防止セキュリティサブスクリプションをご利用のお客様は本脆弱性から保護されています。またCortex XDRをご利用のお客様はWindows XP、Windows 7、Windows Server 2003、Windows Server 2008上での本脆弱性のエクスプロイトを防止できます。

Refresh Rect PDU、RDPDR Client Name Request PDUについての簡単なおさらい

前回のBlueKeepに関するブログでは、Windows RDPプロトコルのRefresh Rect PDUとRDPDR Client Name Request PDUについて詳しくご紹介しました。まずはこれら2つのPDUについて簡単におさらいしておきたいと思います。

Refresh Rect PDUは、PDUを複数回送信することによって、サイズ0x828のカーネルプールを多数スプレーする用途に使えます。これらサイズ0x828のカーネルプールは、0x1000のオフセットごとに整列されていて、サイズ0x828の各カーネルプール内には、0x2cのオフセットで、RDPクライアントが制御する8バイト分のデータが存在しています。このRDPクライアントが制御する8バイトのデータの開始アドレスは0x8xxxx02cという形をしています。VMware仮想マシン上でのテストでは、サイズ0x828のカーネルプールがアドレス0x86000000から0x8a000000までサイズ0x1000のカーネルプールオフセット間隔できっちり連続して並びました。エクスプロイトの概念実証(PoC)では、関数ポインタを格納する固定アドレスとして0x86c1002cを使用して、エクスプロイトを繰り返し行い、システムを再起動しても非常に高い成功率を得られました。このほか、アドレス0x88xxx02c0x89xxx02cもメモリ消費量の多い標的マシン(アプリケーションを多数同時実行しているRDPサーバーなど)で非常に優れたパフォーマンスを発揮するよい候補となります。

RDPDR Client Name Request PDUは、PDUを複数回送信することにより、制御されたデータで解放済みチャネルオブジェクトを再使用するのに使えます。RDPサーバーがClient Name Request PDUをパースするさい割り当てられるカーネルプールのサイズとデータはいずれも制御可能で、RDPサーバーにサイズ0xd0のカーネルプールを割り当てさせ、解放済みMS_T120チャネルオブジェクトを利用することができます。カーネルプールに特別に細工したデータをスプレーすることで、制御可能な関数ポインタで関数呼び出し実行の再使用フローをコントロールし、それによってEIP (Extended Instruction Pointer) の制御を奪うことができます。このほかにも、RDPクライアントでClient Name Request PDUのサイズとデータを両方制御できることから、Client Name Request PDUはシェルコードをカーネルプールに書き込むのにも使えます。これら2つのPDUがエクスプロイト内でどのように使用されるかについては、次のセクションで詳しく説明します。

脆弱性の概要

エクスプロイトをわかりやすくするため、まずCVE-2019-0708の根本原因について簡単にご紹介します。CVE-2019-0708はダングリングオブジェクトであるMS_T120仮想チャネルに関連したUse After Free(UAF: 解放済みメモリ使用)脆弱性です。MS_T120仮想チャネルというのは、RDPサーバーが内部的に使用する2つのデフォルトチャネル(MS_T120およびCTXTW)の1つで、RDP接続確立のタイミングで初期化されます。ただしRDPクライアントは「MS_T120」という名前のカスタマイズされた仮想チャネルを作成することもできます(図1参照)。そのためには、「MS_T120」という項目をClient MCS Connect Initial PDUのchannelDefArrayに追加します。

図1は青で name: MS_T120 を強調表示している。ここでは、RDPクライアントがchannelDefArrayにカスタマイズされたMS_T120チャネルを作成する方法を示している
図1 カスタマイズしたMS_T120チャンネルをchannelDefArrayに作成

RDPサーバーはこの要求を受信するとChannelPointerTableオブジェクトにMS_T120オブジェクトへの参照を作成します。図2はChannelPointerTableオブジェクトに2つの「MS_T120」チャネルオブジェクト参照があるようすを示しています。1つ目はClient MCSConnect Initial PDUの要求用に作成され、2つ目はRDPサーバーが内部で使用するために作成されます。

これらのスクリーンショットは、ChannelPointerTableオブジェクトに2つの「MS_T120」チャネルオブジェクト参照があることを示しています。
図2 カスタマイズしたMS_T120チャンネルを作成

カスタマイズしたMS_T120チャネルをMCS Channel Join Requestで登録すればRDPクライアントはMS_T120チャネルをオープンできるようになります。そしてRDPクライアントが細工したデータをMS_T120チャネルに送信すると(図3参照)、rdpwx.dllモジュールのMCSPortData 関数が呼び出されます。

RDPクライアントが細工したデータをMS_T120チャネルに送信するとrdpwx.dllモジュールのMCSPortData関数が呼び出されます。
図3 PDUを使ってMS_T120チャネルオブジェクトを解放

関数MCSPortDataの1つ目の引数 lpAddend のオフセット0x74から始まるデータは、RDPクライアントに制御されているもので、変数v2の値が2に等しければMCSChannelCloseが呼び出され、MS_T120チャネルオブジェクトが解放されます(図4参照)。

変数v2の値が2に等しければMCSChannelCloseが呼び出され、MS_T120チャネルオブジェクトが解放されます。赤い矩形はediがRDPクライアントが細工したデータをポイントしていることを示します。
図4 MCSPortData内でチャネルをクローズ

MS_T120チャネルオブジェクトの解放後も、解放済みMS_T120チャネルオブジェクトを指すダングリングポインタが1つ、ChannelPointerTableオブジェクトのスロット0x1fに残っています(図5参照)。

MS_T120チャネルオブジェクトの解放後も、解放済みMS_T120チャネルオブジェクトを指すダングリングポインタが1つ、ChannelPointerTableオブジェクトのスロット0x1fに残っています。
図5 カスタマイズされたMS_T120チャネルオブジェクトを解放したところ。ediがRDPクライアントが細工したデータを指している

RDPクライアントの接続が切断されると、関数RDPWD!SignalBrokenConnectionが呼び出され、次に関数termdd!IcaChannelInputが呼び出されてスロット0x1fにある解放済みMS_T120オブジェクトにアクセスします(図6参照)。

RDPクライアントの接続が切断されると、関数RDPWD!SignalBrokenConnectionが呼び出され、次に関数termdd!IcaChannelInputが呼び出されて、スロット0x1fにある解放済みMS_T120オブジェクトにアクセスします。
図6 解放済みMS_T120オブジェクトのコールスタックを再使用

解放済みオブジェクトの再使用

ここまでは、ChannelPointerTableオブジェクト内にMS_T120チャネルオブジェクトへのダングリングポインタを残した状態で、MS_T120チャネルオブジェクトを解放する方法について説明してきました。ここからは、解放済みオブジェクトを再使用する方法について説明していきます。関数termdd!IcaChannelInput内で、関数IcaFindChannelが呼び出されて、チャネルオブジェクトを見つけます。RDPクライアントの接続が終了すると、2つ目の引数slot_baseは0x05になり、3つ目の引数slot_indexは0x1fになるので、関数IcaFindChannelは、戻り値に解放済みMS_T120チャネルオブジェクトを設定します。偽のMS_T120チャネルオブジェクトで解放済みMS_T120チャネルオブジェクトを再使用すれば、この後の関数の実行経路を、関数呼び出し(call [eax])によって制御できます。そのさいは、関数呼び出し内で関数ポインタ(レジスタeax)を偽のMS_T120チャネルオブジェクトから取得します(図7参照)。

偽のMS_T120チャネルオブジェクトで解放済みMS_T120チャネルオブジェクトを再使用すれば、つづく関数の実行フローを関数呼び出し(call [eax])によってコントロールできます。つまり、この関数呼び出し内で、関数ポインタ(レジスタeax)を偽のMS_T120チャネルオブジェクトから取得するのです。赤い矩形はコードの重要部分を強調表示しています。緑の矩形はクラッシュを回避するコードを強調表示しています。青い矩形はバイパスチェックを強調表示しています。
図7 IcaChannelInputInternalの経路を再使用

RDPDR Client Name Request PDUで解放済みMS_T120チャネルオブジェクトを再使用してEIPの制御を奪取

ここからは解放済みMS_T120チャネルオブジェクトを再使用し、EIPの制御を奪う方法について詳しく見ていきます。図5に示すように、解放済みMS_T120チャネルのサイズは0xc8(0xd0から8バイトのプールヘッダを引いた値)です。こちらのブログの図19で示したとおり、termdd!IcaChannelInputInternal関数は、サイズがchannel_data_size+0x20のカーネルプールを割り当てます。つまり関数termdd!IcaChannelInputInternalにサイズが0xc8のカーネルプールを割り当てたければ、Client Name Request PDUのサイズは0xa8(0xc8から0x20を引いたもの)に設定する必要があり、結果的にComputerNameLenフィールドは0x98に設定する必要があります。また、このプール割り当ての成功後、メモリコピーオペレーションが行われることを考慮して、これらのClient Name Request PDUを複数回送信して、解放済みMS_T120のプールスロットがClient Name Request PDUのデータで占有されるようにしておきます。なおサイズ0xc8のカーネルプールの最初の0x20バイトはtermddモジュールの内部用ですから最初の0x20バイトは制御できません。つづく0x10バイトもClient Name Request PDUのヘッダ用なので制御できません。したがって、制御可能なデータサイズは合計0x98(0xc8から0x20と0x10を引いたもの)となります。図8はRDPクライアントがRDPDR Client Name Request PDUを作成する方法を示しています。

これはRDPクライアントがRDPDR Client Name Request PDUを作成する方法を示しています。
図8 RDPDR Client Name Requestで解放済みMS_T120チャネルオブジェクトを再使用

図9は、RDPDR Client Name Request PDUを送信して作成した偽のMS_T120チャネルオブジェクトのメモリダンプを示しています。偽のMS_T120チャネルオブジェクトの重要フィールドをいくつかいろんな色でラベル付けしてあります。緑で示す4バイト(DWORD)値0x00000000は、ExEnterCriticalRegionAndAcquireResourceExclusiveから呼び出すhal!KeAcquireInStackQueuedSpinLockRaiseToSynch内でサービスがクラッシュするのを防ぐために設定されています。水色で示すDWORD値0x00000000は、状態チェックをバイパスすることで、制御下においた関数ポインタeaxによる関数呼び出し(call [eax])まで、関数実行経路を確実に生かしておくためのものです(図7参照)。

これは、RDPDR Client Name Request PDUを送信して作成した偽のMS_T120チャネルオブジェクトのメモリダンプを示しています。偽のMS_T120チャネルオブジェクトのいくつかの重要なフィールドをさまざまな色でラベリングしてあります。
図9 偽のMS_T120チャネルオブジェクト

紫で示したDWORD値0x86c10030は、偽のMS_T120チャネルオブジェクトのオフセット0x8cに設定されています(図9参照)。図10のデバッグログは、オフセット0x8cで偽のオブジェクトアドレスを取得して関数呼び出し(call [eax])を行い、EIPの制御を奪う方法を示したものです。

このデバッグログは、オフセット0x8cで偽のオブジェクトアドレスを取得して関数呼び出し(call [eax])を行い、EIPをコントロールする方法を示したものです。
図10 IcaChannelInputInternal内で制御下においた関数ポインタを使用して関数呼び出しを行ったところ
EIPをどのように制御するかについてここまでで示せたので、次はeaxがアドレス0x86c10030に設定され、EIPがアドレス0x86c1002cに設定される理由について見ていきます。これまでに説明した手法を使えば、アドレス0x86xxxxxxから0x8axxxxxxまでの間、安定したスプレー結果を得ることができます。図11に示すように、整列された各0x1000アドレスのオフセット0x002cには、8バイト分のコントロール可能なデータが存在しています。

整列された各0x1000アドレスのオフセット0x002cには8バイト分のコントロール可能なデータが存在しています。赤い矩形は重要要素を強調表示しています。
図11 Refresh Rect PDUを使って安定したプール スプレーを行える

図12のコードスニペットは、RDPクライアントがRefresh Rect PDUを作成する方法と、作成したRefresh Rect PDUを何回RDPサーバー送信しなければならないかを示しています。

このコードスニペットは、RDPクライアントがRefresh Rect PDUを作成する方法と、作成したRefresh Rect PDUを何回RDPサーバー送信しなければならないかを示しています。赤い矩形は重要要素を強調表示しています。
図12 RDPクライアントでスプレー用のRefresh Rect PDUを作成する方法

整列されたサイズ0x1000の各カーネルプールにある8バイト分のコントロール可能データでは、オフセット0x0030(0x86c10030)の4バイトを0x86c1002cに設定します。これはステージ0のシェルコード用にハードコードしたアドレスです。オフセット0x002cの残り4バイト(0x86c1002c)は、ステージ0のシェルコード格納に使います。これ以降は、このシェルコードの各ステージについていくつか詳しく見ていきます。

シェルコード

ステージ0のシェルコードに使用できるのは4バイト分しかないので、ここでは4バイトのシェルコードスリッパのトリックを使用してadd bl,al; jmp ebxのかわりにcall/jmp ebx+30hを使ってシェルコードのステージ1にジャンプさせます。termdd!IcaChannelInputInternal がアセンブリ命令call dword ptr [eax]を実行すると、alは0x30となり、ebxは偽のMS_T120チャネルオブジェクトを指します。RDPクライアントは、この偽のMS_T120チャネルオブジェクト内を制御下においたデータで埋め尽くすことができます。ステージ1のシェルコードは、アドレスfaked_MS_T120_channel_object+0x30(0x867b3590)からのカーネルプールに配置されます。このプロセス全体を図13に示します。

これは、トリックを使用してステージ0のシェルコードからステージ1のシェルコードにジャンプするプロセスを示しています。本文で説明する主要項目が赤い矩形で強調表示されています。
図13 ステージ0のシェルコード

言及しておく価値があるのは、2バイトのアセンブリコードadd bl, alが使われているという点です。これはjmp ebx+0x30の実装に使えるシェルコードが4バイトしかないことからこのようにしています。blが0xd0より大きければ、add bl, alでオーバーフローが発生し、jmp ebxが間違ったアドレスにジャンプしてエクスプロイトは失敗してしまうので、完璧な方法とはいえませんが、成功率は理論的には81.25%(0xd / 0x10)ですから、これはこれで十分でしょう。

ステージ1のシェルコードについて説明する前に、ステージ2の最終シェルコードについて説明しておきます。私たちは、RDPDR Client Name Request PDUを使用すれば、任意のサイズの最終カーネルシェルコードをRDPサーバーのカーネルプールに送信できることに気付きました。例として、データ長が0x5c8で、ペイロードを埋め込んだRDPDR Client Name Request PDUを作成しました(図14参照)。

例として、データ長が0x5c8で、ペイロードを埋め込んだRDPDR Client Name Request PDUを作成しました。
図14 RDPDR Client Name Request PDUが送信する最終シェルコード

RDPサーバーにシェルコードが埋め込まれたRDPDR Client Name Request PDUが送信されると、PDMCS - Hydra MCS Protocol Driverは、データをカーネルプールに格納します。興味深いことに、そのカーネルプールにはスタック上で維持されている参照があり、これをステージ1のシェルコードで使用すると、最終シェルコードを見つけることができるのです。具体的には、ここでのECXレジスタはスタックを指していて、アドレスECX+0x28はカーネルプールアドレスを格納しています。最終シェルコードはカーネルプールのオフセット0x434に存在しています(図15参照)。

最終シェルコードはカーネルプールのオフセット0x434に存在します。
図15 ステージ1のシェルコード

このオフセット0x434はWindowsのバージョンによって異なる場合がありますが、最終シェルコードをカーネルプール内で検索して普遍的なシェルコードにするために、ステージ1のシェルコードをエッグハンターとして作っておくことは容易です。

クラッシュ回避のためにカーネルにパッチを当てる

つづく作業は、カーネルのエクスプロイトではルーチン化しているものです。最終シェルコードは、最初に戻り値を修正し、カーネルにパッチをあててシェルコード終了後のクラッシュを回避します。それからカーネルシェルコードを実行してAPCをlsass.exeまたはspoolsv.exeに挿入し、ユーザーモードのシェルコードを実行します。図16は、最終的なシェルコードがChannelPointerTableオブジェクトを修正し、リターンアドレスを変更し、ExReleaseResourceAndLeaveCriticalRegion関数の実行をエミュレートしてKTHREADのWORD値をインクリメントする様子を示しています。

これは、最終的なシェルコードがChannelPointerTableオブジェクトを修正し、リターンアドレスを変更し、ExReleaseResourceAndLeaveCriticalRegion関数の実行をエミュレートしてKTHREADのWORD値をインクリメントする様子を示しています。
図16 最終シェルコードがカーネルにパッチを適用

カーネルへのパッチ適用後にカーネルシェルコードの機能部分が実行されます。エクスプロイトの説明にあたり、私たちはSleepyaがEternalBlueのエクスプロイト用に公開したカーネルシェルコードのテンプレートを利用しました。デモ用のWinExec('calc') のユーザーランドシェルコードを図17に示します。

デモ用のWinExec(‘calc’) のユーザーランドシェルコードを示します。
図17 最終シェルコードであるユーザーモードシェルコード

以上のまとめ

以上、エクスプロイトにおける課題をすべておさえたところで、エクスプロイトチェーン全体については次のようにまとめることができます。

  1. 侵害対象端末との接続を確立
  2. Refresh Rect PDUでスプレー
  3. 細工済みPDUを送信、MS_T120チャネルオブジェクトを強制解放
  4. 複数のRDPDR Client Name Request PDUで解放済みMS_T120チャネルオブジェクトを占有
  5. RDPDR Client Name Request PDUで最終シェルコードを送信
  6. 解放済みMS_T120チャネルオブジェクトを再使用するため接続を終了、EIPの制御奪取、シェルコードを数ステージ分実行

結論

本稿ではWindows RDPの脆弱性CVE-2019-0708 (BlueKeep) について、Refresh Rect PDU と RDPDR Client Name Request PDU を使い、システム特権でリモートコード実行を行えるようにする方法を概説してきました。本シェルコードステージ0の4バイトのシェルコードスライダとシェルコードステージ1のシェルコードエッグハンターは、一般的なエクスプロイト手法なので、どんなWindows RDPの脆弱性エクスプロイトにでも利用される可能性があります。RDP PDUとWindows RDPエクスプロイトテクニックを用いるカーネルプール風水(ヒープ風水)テクニックに関する深い研究は、防御側がWindows RDP攻撃からすべてのWindowsユーザを保護する上で役立ちます。

パロアルトネットワークス製品をご利用中のお客様はBlueKeepから保護されています。

  • Cortex XDR は、Windows XP、Windows 7、Windows Server 2003、Windows Server 2008上でこの脆弱性のエクスプロイトを防止します。
  • 次世代ファイアウォール脅威防御サブスクリプションを利用しているお客様は本脆弱性を検出することができます。