アドバンストテクノロジーラボの竹迫です。

リチェルカセキュリティさんより公開されたCVE-2019-0708 (BlueKeep) pre-auth RCE POC on Windows7のエクスプロイトコードと英語版の解説記事が大変興味深い内容だったため、日本語訳をこちらのブログに翻訳許可いただきました。

Ricerca Security

(日本語訳) CVE-2019-0708 (BlueKeep) pre-auth RCE POC on Windows7

(英語版) https://github.com/RICSecLab/CVE-2019-0708

このリポジトリは、Windowsリモートデスクトップサービス(RDS)のリモートコード実行のバグを実証します。

注:重要な脆弱性についてアナリストがより深く理解できるように、以前開発したBlueKeepの脆弱性に関するPOCコードとテクニカルレポートを公開します。

使用方法

前提

このエクスプロイトはPython3で書かれており、PyRDP ライブラリが必要です。 PyRDPのインストールガイドに従い導入してください。

使い方

現時点でのエクスプロイト対象・テスト環境はVirtualBox上のWindows 7 SP 1(6.1.7601) x64です。

あなたのコンピュータのIPアドレスが192.168.56.1、攻撃対象となるRDPサーバーがexample.com:1234の場合、次のように入力してください:

スクリプトがサーバーに対するエクスプロイトを成功した場合、サーバーのシェルコードが192.168.56.1:4444にTCP接続を行います。netcatで接続を待つ例は次のようになります:

サーバーのシェルコードの接続先ポートを変更するには-bpオプションが利用できます:

レポート

脆弱性

2019年5月、マイクロソフトはリモートデスクトップサービス (旧称: ターミナルサービス) における重大な遠隔コード実行脆弱性であるCVE-2019-0708を発表しました。この脆弱性は認証前に存在するため、ワーム化可能であり広範囲に及ぶ損害をもたらす可能性があります。 攻撃者は細工したRemote Desktop Protocol (RDP)のメッセージを対象のサーバーに送り脆弱性を利用することで、管理者権限での任意コード実行が可能です。

RDP仮想チャネル

Microsoft リモートデスクトップ サービスはユーザーがリモートから利用できるWindowsの対話セッションを提供します。このサービスはWindowsデスクトップを表示するために、TCPポート3389番上のRemote Desktop Protocol (RDP)を使用しクライアントと通信を行います。 RDPプロトコルでは仮想チャネルと呼ばれるソフトウェア拡張を利用できます。拡張機能の例としては、特殊なハードウェアのサポートやオーディオ、コア機能の追加などがあります。 仮想チャネルにはMicrosoft提供のチャネルである"rdpdr" (リダイレクト)、"rdpsnd" (サウンド)、"cliprdr" (クリップボードの共有)などが含まれます。また、ユーザーがRDP APIを使ったモジュールを作ることで他のチャネルをサポートすることも可能です。前述のチャネルに加えて、MicrosoftはMS_T120 (RDP自身で使用)とCTXTW (Citrix ICAで使用)の2つのチャネルをデフォルトで作成します。 脆弱性は"MCS Connect Initial and GCC Create"リクエスト中における、MS_T120の仮想チャネルのバインド手続きに関するものです。 さらなる背景情報についてはZDIをご覧ください。 ZDIの記事で述べられているように、仮想チャネル作成のリクエストは termdd!IcaCreateChannel() を使って作成されます。 RDPクライアントとの接続が確立したとき、MS_T120を含む全ての静的仮想チャネルがWindows RDPサーバーにより作成され、ChannelPointerTable から参照されます。 MS_120とCTFTWの作成クエリは rdpcore!WDLIB_IcaVirtualQueryBindings() によって行われます。

Untitled.png

図.1: MS_T120とCTXTWの作成クエリの生成

termdd!IcaBindVirtualChannels() にクエリが渡された後、 termdd!IcaAllocateChannel() で仮想チャネルの構造体が作られ、ChannelPointerTableに登録されます。

IcaAllocateChannel.png

図.2: 仮想チャネル構造体の作成と登録

ChannelPointerTable への仮想チャネルの構造体の登録は termdd!IcaBindChannel() が行います。

Windowx 7 x64上で第1引数を"MS_T120"、第3引数を0x1Fとして termdd!IcaBindChannel() を呼び出した時のスタックトレースがこちらになります。

StackTrace31.png

図.3: 初期化リクエスト中、MS_T120がスロット0x1Fにバインドされる

この時 ChannelPointerTable は次のようになっています。MS_T120が既にスロット0x1Fに存在することに注意してください。

ChannelPointerTable.png

図.4: 初期化リクエスト中のChannelPointerTable

根本原因解析

Windows RDPカーネルドライバであるtermdd.sysにUse-after-free脆弱性が存在します。

問題が発生するのは"MCS Connect Initial and GCC Create"の間にクライアントがMS_T120\\x00という名前のチャネルを指定した時です。termdd!IcaCreateChannel()termdd!IcaFindChannelByName() を呼び出し、スロット0x1Fに存在する MS_T120チャネルの構造体を返します。この構造体は"MCS Attach User Request"において新しい仮想チャネルのエントリとしてみなされ、別のスロット (例: スロット2) に保存されます。

Windowx 7 x64上で第1引数を"MS_T120"、第3引数を0x2として termdd!IcaBindChannel() を呼び出した時のスタックトレースがこちらになります。

StackTrace2.png

図.5: アタッチリクエスト中、MS_T1209がスロット0x2にもバインドされる

つまり、MS_T120チャネルの構造体は0x1Fと0x2の2つのスロットから参照されます。

ChannelPointerTable2.png

図.6: アタッチリクエスト中のChannelPointerTable

攻撃者がMS_T120チャネルに無効なデータを送ると、termdd.sysは termdd!IcaCloseChannel() でこのチャネルを閉じ、スロットにあるポインタをクリアします。 (この例ではスロット0x2) しかし、スロット0x1Fにある同じポインタはクリアされません。 その後コネクションが終了する時、RDPWD!HandleDisconnectProviderUlt() が呼び出されます。そしてこの関数は termdd!IcaChannelInputInternal() を呼び出し、スロット0x1Fのポインタを使って既に解放されているMS_T120チャネルの構造体を再び破棄しようとします。

破棄手続きはチャネルの構造体内のvtableポインタを使って呼び出されるため、use-after-freeの状態となります。

VTableDereferenced_disas.png

図.7: vtableの参照外し

ヒープスプレー

前節で説明したように、RDPWD!HandleDisconnectProviderUlt() は解放済みのチャネル構造体のvtableポインタを使って関数呼び出しを行います。攻撃者がチャネル構造体の値を制御できれば、vtableポインタを上書きし、カーネルの権限で任意コード実行が可能になります。 しかし、これを実現するには2つの難題が存在します。 一つは そもそも解放されたチャネル構造体の値をどうやって制御するか です。 脆弱性がuse-after-freeであるため、攻撃者が典型的にとる着実な手段は解放された構造体が存在した場所にメモリを確保することです。 しかし今回の場合、攻撃者が望んだ場所にメモリを確保する決定的な方法は存在しません。

その理由は、カーネルにおいては多くのスレッドが動作しており、(実質的に) 同時にメモリが確保されるためです。メモリがどこに確保されるかはスレッドの実行される順序に依存します。ほとんどの場合、メモリ確保が攻撃者にとって上手くいくことは期待できません。

もう一つの難題はvtable自体のアドレスとvtable内のポインタのアドレスをどこにするかです。前節で述べたように、攻撃者はvtableのアドレスをセットする必要があります。任意コード実行を行いたいので、攻撃者は 偽のvtableが実行したい (シェルコードやガジェットの) アドレスを持つようにアドレスをセットする必要があります。しかしながら、前述したカーネルヒープとKASLRの無作為性のために適切なアドレスを知ることはほぼ間違いなく不可能です。

  1. おそらくカーネルヒープにメモリを確保し、この場所にシェルコードのアドレスを書き込むことは可能です。しかし、確保されたメモリのアドレスを知ることは上述した無作為性のためできません。
  2. 可能性の低い別の選択肢としては、 偶然に 有用なガジェットのアドレスを含んでいる (ヒープではない) 静的なメモリ領域 を使うという方法がありますが、これも上手くいきません。Windows 7が緩和策として備えているKASLRにより、そのようなメモリのアドレス領域のアドレスも無作為化されているためです。

これらの事実により、カーネルのアドレスをリークする他の脆弱性を利用しない限り、攻撃者はvtableポインタを制御できても直ちに任意コード実行を達成できません。お気付きかもしれませんが、加えて「シェルコードやガジェットのアドレス」も攻撃者は知ることができません。

彼らの作成したエクスプロイトではヒープスプレーと呼ばれる手法一つでこれらの難題を解決しています。ヒープスプレーはこのような無作為性を突破するための手法で、大きなサイズのメモリ確保を大量に行います。

PoolusedBeforeSpraying.png

図.8: ヒープスプレー前のヒーププール使用量

PoolusedAfterSpraying.png

図.9: ヒープスプレー後のヒーププール使用量

攻撃者は巧妙なメモリ確保を何度も繰り返すことで、確保したメモリの一部が解放されたチャネル構造体と同じ場所に配置される可能性を高めることができます。 カーネルヒープ内オブジェクトの大部分が攻撃者が作成したものになると、ヒープ上の適当なアドレスが攻撃者の作成したオブジェクトに当たる可能性が高くなるため、偽のvtableアドレスとして指定することができます。ここで、カーネルヒープのベースアドレスはKASLRで無作為化されないことを特筆しておきます。基本的にヒープの無作為性はスレッド実行の順序のみによって引き起こされます。 これは最も重要なことですが、幸運にもWindows 7では非ページカーネルプールのNX bitが有効化されていません。 そのため攻撃者はカーネルヒープに偽のvtableだけでなくシェルコードも直接保存することができます。これにより、return-oriented programmingを使用する必要が無くエクスプロイトの作成がかなり容易になります。

NonPagedPoolPte.png

図.10: ページテーブル エントリ (PTE) のパーミッション

ヒープスプレーを行うには当然、カーネルヒープにメモリを確保し、そこに入力を書き込む機能が必要です。彼らはThe report of Unit 42BlueKeep exploit in Metasploitをもとに、そのような機能がある処理をカーネルドライバから探しました。そして多くのPDUをテストした結果、最も信頼性がある有用な手段は、Metasploitのエクスプロイトが利用しているようにVirtual Channel PDUをrdpsndチャネルに送ることであると結論づけました。

参考までに、なぜUnit 42のレポートで紹介されている3つのPDUが適さなかったかを説明します。

  • Bitmap Cache PDU: まず、攻撃者は最初のハンドシェイクでのみこのPDUを送ることができます。ハンドシェイクが終了してからuse-after-freeが発生するため、これはvtableの書き換えには使用できません。さらに、このPDUでは0x2b5240バイト (<3MB) のメモリしか確保できずヒープスプレーには適していません。
  • Client Name Request PDU: このPDUはヒープスプレーに利用できると思われましたが、彼らがテストした限りこのPDUは通常の方法では複数回送信 (受信) することができませんでした。詳細情報が不足していたため断定はできませんが、このPDUを利用するには攻撃者が細工した複雑なパケットを送る必要があるか、ルーチンが変更されそもそも64-bit環境では動かないと考えられます。
  • Refresh Rect PDU: このPDUでは攻撃者が実際に送るサイズよりもかなり大きなサイズのメモリを確保できるため、ヒープスプレーには効率的です。しかし、このPDUで確保されるメモリのうち8バイトしか制御できないため有意義に利用することは困難です。詳細は省きますが、攻撃者がこの類のPDUを実用するには少なくともデータの内13バイト (vtableに8バイト、"jmp $+0x1000"に5バイト) が制御可能である必要と考えられます。

Virtual Channel PDUは、名前が示唆する通り、データを静的仮想チャネルに送るためにクライアント・サーバー間でやりとりされます。

PDUのデータがどのように処理されるかはチャネルによって異なります。Microsoftが拡張として提供している良く知られたチャネルの中でも、rdpsndチャネルは任意の入力を受け取り、その入力のためメモリ確保を行うという独特な機能を持っています。

以上が彼らのproof of conceptにおける任意コード実行までの要点となります。

ControlPC.png

図.11: 制御されたvtableアドレス

SuccessfulShellcodeExecution.png

図.12: vtableのアドレスを、シェルコードを指す悪意あるアドレスに書き換えることに成功 (ud2)

コード実行

前節ではシェルコードをどうやって実行するかについて説明しましたが、これで全てではありません。シェルコードはカーネルランドで実行されますが、攻撃者が求めるのはユーザーランドでの管理者権限です。どちらでも攻撃者ができることは理論上は同様ですが、実現する方法は異なります。例えばディレクトリ内のファイルリストを得たい場合、特権シェルでは‘dir’と打ち込むだけで良いのに対して、シェルコードでは何百行ものアセンブリコードを書く必要があります。 そのため、エクスプロイトの目標は攻撃者が特権シェルを得ることであり、この実現のためにはさらなる労力が必要です。シェルコードはカーネルランドで実行されるため、まず (特権つきの)ユーザーランド スレッドを作成しそのスレッドでcmd.exeを実行する必要があります。 ここでは2つの問題について考える必要があります。ユーザーランドスレッドを見つける、または作成する方法と、ユーザーランドでシェルコードを実行するためにメモリをユーザーランドに確保する方法です。 前者の、ユーザーランドスレッドを見つける方法が必要になるのは、シェルコードが実行されるコンテキストが通常のプロセスのコンテキストではないためです。 もしシェルコードがプロセスのコンテキストで実行されている場合は、IRET命令を使うだけでユーザーランドに戻ることができます。しかし、今回の場合IRETを実行するとカーネルがフリーズします。 この問題を解決する方法はいくつかありますが、その中で最も一般的で有用な手法はasynchronous procedure call(APC)です。これは非同期イベントを処理するためにWindowsが提供している機構です。 APCを使うことでプログラムは別のプロセスのものであっても指定したスレッドのコンテキストで関数を実行できます。この機構によりシェルコードは容易に、かつ正当に新たなユーザーランドスレッドを作成できます。 APCを登録するとき、新たなユーザーランドスレッドが実行を開始するアドレスを指定する必要があります。しかし、今のところメモリを確保できるのはカーネルランド内のみで、ユーザーランドのスレッドからは勿論アクセスすることができません。ユーザーランドのシェルコードを実行するためには、ユーザーランドから可視であるメモリ領域を用意してシェルコードを保存する必要があります。 したがって、後者のユーザーランドにメモリを確保する方法が問題になります。 この問題を解決する一般的な方法としてはZwAllocateVirtualMemoryで新しいマッピングを作成することが挙げられます。しかしこれはやや冗長で、実はWindows7ではより簡単な方法があります。それはKUSER_SHARED_DATAを使うことです。 KUSER_SHARED_DATAはユーザーランドとカーネルランドの双方でマップされている専用のマッピングに保存されているデータ構造体で、しかも固定のアドレス(それぞれ0x7FFE0000、0xFFFFF78000000000)に配置されています。 ユーザーランドのシェルコードをこのマッピングに保存すれば全て上手くいきます。アドレスが既知であるため、カーネルランドのシェルコードはこのマッピングにユーザーランドのシェルコードをコピーして、APCを問題なく登録することができます。

KUserSharedDataPte.png

図.13: 専用マッピング 0x7FFE0000(usermode) / 0xFFFFF78000000000(kernelmode)に保存されたシェルコード

KUserSharedDataDump.png

図.14: Shellcode本体

このように任意コード実行の達成後にやるべきことは多いものの、どれもほぼストレートに解決できるものです。 これでエクスプロイトの目標が達成となります。

影響を受けるバージョン

この脆弱性はCVE番号CVE-2019-0708が割り当てられており、Microsoftは既にセキュリティパッチKB4499175を2019年5月15日に公開しています。 脆弱性についての詳細や影響を受けるバージョン、緩和策についてはこちらをご覧ください。

緩和策・ワークアラウンド

必要が無ければ、リモートデスクトップサービスを無効にしてください。

サービスを無効にできず、脆弱性に対する更新プログラムをインストールできない場合は、次のワークアラウンドが役立つ場合があります。これらのワークアラウンドは特定の条件における攻撃に対してのみ有効であることに注意してください。

  • ネットワークレベル認証 (NLA) を有効にする
    • ネットワークレベル認証が有効な場合、リモートデスクトップ接続のセッションを確立する前にサーバーマシンへのログインが求められます。そのため、資格情報を持たない攻撃者からの脆弱性利用を防ぐことができます。
  • ネットワーク境界のファイアウォールでTCPポート3389をブロックする
    • RDPが利用するポートをブロックすることで、外部からの直接の攻撃を防ぐことができます。ファイアウォールの内部からの攻撃は防ぐことはできません。