先月、Microsoftは、カメラ・デバイスの仮想化と共有に使用されるWindowsカーネル・コンポーネントであるMicrosoft Kernel Streaming Serverの脆弱性にパッチを適用しました。この脆弱性CVE-2023-36802は、ローカル攻撃者がSYSTEMに権限をエスカレーションすることを可能にします。
このブログ記事では、Windowsカーネルの新しい攻撃対象領域を調査し、0デイ脆弱性を発見し、興味深いバグクラスを探索し、安定したエクスプロイトを構築するプロセスについて詳しく説明します。メモリ破壊とオペレーティング・システムの概念についての基本的な理解は役に立ちますが、この記事に従うのにWindowsカーネルの専門的な知識は必要ありません。また、馴染みのないカーネル・ドライバーの初期分析を実行するための基本についても説明し、新しいターゲットを検討するプロセスを簡素化します。
Microsoft Kernel Streaming Server(mskssrv.sys)は、Windows Multimedia FrameworkサービスのコンポーネントFrame Serverです。このサービスはカメラデバイスを仮想化し、デバイスを複数のアプリケーション間で共有できるようにします。
最初にTPMドライバーの脆弱性としてリストされていたCVE-2023-29360に気づいた後、この攻撃対象領域を調べ始めました。バグは実際にはMicrosoft Kernel Streaming Serverにあります。当時私はMS KS Serverについて詳しく知りませんでしたが、このドライバーの名前だけで興味をそそられました。目的や機能については何も知りませんでしたが、カーネル内のストリーミング・サーバーは脆弱性を探す実りある場所だと考えたのです。手探り状態のまま、私は以下の疑問に答えようとしました。
最初の質問に答えるために、ディスアセンブラーでバイナリーを分析することから始めました。私はすぐに前述の脆弱性を特定しました。シンプルでエレガントなロジックのバグでした。この問題は簡単に引き起こされ、完全にエクスプロイトされるように見えたため、mskssrv.sysドライバーの内部の仕組みをよりよく理解するために、迅速な概念実証を開発することにしました。
IBMニュースレター
AI活用のグローバル・トレンドや日本の市場動向を踏まえたDX、生成AIの最新情報を毎月お届けします。登録の際はIBMプライバシー・ステートメントをご覧ください。
ニュースレターは日本語で配信されます。すべてのニュースレターに登録解除リンクがあります。サブスクリプションの管理や解除はこちらから。詳しくはIBMプライバシー・ステートメントをご覧ください。
まず、ユーザー空間アプリケーションからドライバーにアクセスできる必要があります。脆弱な機能はドライバーのDispatchDeviceControlルーチンからアクセス可能であり、ドライバーにIOCTLを発行することでアクセスできます。そのためには、デバイスのパスを使用してCreateFileを呼び出して、ドライバーのデバイスへのハンドルを取得する必要があります。通常、デバイス名/パスを見つけるのは簡単です。ドライバーでIoCreateDeviceへの呼び出しを見つけて、デバイス名を含む3番目のパラメータを調べます。
デバイス名のNULLポインターを使用してIoCreateDeviceを呼び出すmskssrv.sys内の関数
この場合、デバイス名のパラメーターはNULLです。呼び出し関数名からmskssrvがPnPドライバーであることが示唆されており、IoAttachDeviceToDeviceStackへの呼び出しは作成されたデバイスオブジェクトがデバイススタックの一部であることを示しています。実際には、これは、I/O要求がデバイスに送信されるときに複数のドライバーが呼び出されることを意味します。PnPデバイスの場合、デバイスにアクセスするにはデバイスインターフェースパスが必要です。
WinDbgカーネルデバッガーを使用すると、どのデバイスがmskssrvドライバーとデバイススタックに属しているかを確認できます。
上位デバイスと下位デバイスを示す!drvobjおよび!devobjコマンドからの出力
上では、mskssrvのデバイスがswenum.sysに属する下位デバイス・オブジェクトに接続されていることがわかりますドライバーであり、ksthunk.sysに属する上部デバイスが接続されています。
デバイス・マネージャーから、次のターゲット・デバイスのインスタンスIDを見つけることができます:
デバイス・インスタンスIDとインターフェースGUIDを表示するデバイス・マネージャー
これで、コンフィギュレーション・マネージャーやSetupApi関数を使用してデバイス・インターフェースのパスを取得するのに十分な情報が得られました。取得したデバイス・インターフェース・パスを使用して、デバイスへのハンドルを開くことができます。
最後に、mskssrv.sys内でコードの実行をトリガーできるようになりました。デバイスが作成されると、ドライバーのPnPディスパッチ作成関数が呼び出されます。追加コードの実行をトリガーするには、IOCTLを送信してデバイスと対話し、ドライバのディスパッチ・デバイス制御関数で実行することができます。
バイナリ分析を実行する場合は、静的(ディスアセンブラー、デコンパイラー)ツールと動的(デバッガー)ツールを組み合わせて使用することをお勧めします。WinDbgは、ターゲットドライバーのカーネルデバッグに使用できます。いくつかのブレークポイントを設定することで、コード実行が発生することが予想されます(ディスパッチの作成、ディスパッチのデバイス制御など)。
当初、私はいくつかの問題を抱えていました。ドライバーの中に設定したブレークポイントはすべて攻撃されていませんでした。正しいデバイスを開いているか、何か別のことを行っているのではないかという疑問がいくつかありました。後から、ドライバーがアンロードされていたために、ブレークポイントが設定されていないことに気づきました。インターネットで答えを検索しましたが、mskssrvを検索すると、Windowsでデフォルトでロードされアクセスできるにもかかわらず、あまり成果が得られません。私が見つけた数少ない成果の中に、OSRのスレッドがあり、そこでは誰かが同じような問題に遭遇していました。
あとで分かったことですが、PnPフィルター・ドライバーは、しばらく使用されていない場合はアンロードでき、必要に応じてオンデマンドでロードし直すことができます。
私は、デバイスへのハンドルが開かれた後、DeviceIoControlを呼び出す前にブレークポイントを設定し、ドライバーが最近ロードされたことを確認することで、抱えていた問題を解決しました。
mskssrvドライバーはわずか72KBサイズのバイナリであり、次の関数を呼び出すデバイスIO制御コードをサポートしています。
これらのシンボル名から、ドライバーのいくつかの機能、つまりストリームの送信と受信を扱う何かを推測できます。この時点で、私はドライバーの意図する機能をさらに掘り下げました。Windowsのマルチメディア・フレームワークに関するMichael Maltse氏のこのプレゼンテーションを見つけ、ドライバがカメラ・ストリームを共有するプロセス間メカニズムの一部であることがわかりました。
ドライバーはそれほど大きくなく、IOCTLも多くないため、各機能を調べることでドライバーの内部を把握できました。各IOCTL関数は、コンテキスト登録オブジェクトまたはストリーム登録オブジェクトのいずれかで動作します。これらは、対応する「Initialize」IOCTLによって割り当てられ、初期化されます。オブジェクトへのポインタは、Irp->CurrentStackLocation->FileObject->FsContext2に保管されます。FileObjectは、開いているファイルごとに作成されるデバイスファイルオブジェクトを指し、FsContext2はファイルオブジェクトごとのメタデータを保管するためのフィールドです。
ドライバーと直接通信する方法を理解しようとしているときに、このバグを見つけました。まず、ユーザーモードコンポーネント、fsclient.dllとframeserver.dllの分析です。私はもう少しでバグを見逃すところでした。見落された簡単なチェックを開発者がインスタンス化しているものと思っていたからです。PublishRx IOCTL関数を見てみましょう。
FSRendezvousServer::PublishRx デコンパイル・スニペット
ストリームオブジェクトがFsContext2から取得された後、関数FsRendezvousServer:: FindObjectが呼び出され、ポインターがグローバルなFSRendezvousServerによって保管されている 2 つのリストにあるオブジェクトと一致することを確認します。最初、この関数には要求されたオブジェクトタイプを確認する何らかの方法があると思っていました。しかし、ポインタがコンテキストオブジェクトのリストまたはストリームオブジェクトのリストのどちらかにある場合、この関数はTRUEを返します。オブジェクトの型に関する情報はFindObjectに渡されないことに注意してください。つまり、コンテキストオブジェクトはストリームオブジェクトとして渡すことができます。これはオブジェクトタイプの混乱の脆弱性です!この問題は、ストリームオブジェクトを操作するすべての IOCTL 関数で発生します。この脆弱性にパッチを適用するために、マイクロソフトは FSRendezvousServer:: FindObjectをFSRendezvousServer:: FindStreamObjectに置き換えました。このオブジェクトは、まず型フィールドをチェックしてオブジェクトがストリームオブジェクトであることを確認します。
コンテキスト登録オブジェクトはストリーム登録オブジェクト(0x1D8バイト)より小さい(0x78バイト)ため、ストリームオブジェクトのオペレーションは境界外のメモリ上で実行できます。
オブジェクト・タイプの混乱の脆弱性のイラスト
脆弱性プリミティブを活用するには、アクセスされる境界外メモリを制御する機能が必要です。これは、脆弱なオブジェクトと同じメモリ領域に多くのオブジェクトの割り当てをトリガーすることで実現できます。この技術をヒープまたはプールスプレーと言います。脆弱なオブジェクトは、非ページされ、断片化の少ないヒーププールに割り当てられています。アレックス・イオネスクによる古典的なテクニックを使えば、0x30バイトのDATA_QUEUE_ENTRYヘッダー以下のメモリー内容を完全に制御できるバッファーをスプレーすることができます。この手法を使用してスプレーすることで、次の図に示すメモリ・レイアウトを取得できます。
プール・スプレーの選択した方法を使用することで、0xC0-0x10Fおよび0x150-0x19F内のオブジェクト・オフセットのフィールドを制御できます。エクスプロイト・プリミティブを探すために、ストリーム・オブジェクトのIOTL機能にもう一度アクセスしたところ、制御可能なオブジェクト・フィールドにアクセスし、操作できる場所を見つけました。
私は、PublishRx IOCTLに優れた任意書き込みプリミティブを見つけました。このプリミティブは、任意のメモリ所在地に定数を書き込むために使用できます。関数FSStreamReg::PublishRx:のスニペットを見てみましょう
FSStreamReg::PublishRx デコンパイル・スニペット
ストリーム・オブジェクトには、オフセット0x188にあるリスト・ヘッドが含まれており、FSFrameMdlオブジェクトのリストを記述します。上記のデコンパイル・スニペットでは、このリストが反復され、FSFrameMdlオブジェクトのタグ値がアプリケーションから渡されたシステム・バッファーのタグと一致する場合、関数FSFrameMdl::UnmapPages が呼び出されます。
前述のエクスプロイト・プリミティブを使用すると、FSFrameMdllist、そしてpFrameMdlが指定するFsFrameMdlオブジェクトを完全に制御できます。次に UnmapPages を見てみましょう。
FSFrameMdl:UnmapPagesデコンパイル
上記のデコンパイルされた関数の最後の行では、定数2がこの(FSFrameMdlオブジェクト)の制御可能なオフセット値に書き込まれています。この定数書き込みを I/O Ring 手法と組み合わせて使用することで、任意のカーネル読み書きと特権エスカレーションを実現できます。このテクニックの仕組みについて詳しくは、こちらとこちらをご覧ください。
ここでは定数書き込みプリミティブを利用することにしましたが、この機能には別の便利なエクスプロイト・プリミティブも表示されます。MmUnmapLockedPages の呼び出しに対する BaseAddress と MemoryDescriptorList の引数はどちらも制御可能です。これは、任意の仮想アドレスのマッピングをアンマップし、use-after-freeののようなプリミティブを構築するために使用できる。
この時点で、任意のカーネルの読み取り/書き込みを可能にするエクスプロイト・プリミティブがいくつか特定されています。ストリーム・オブジェクトの内容にチェック項目がいくつかあることに気づいたかもしれません。これらのチェックは、目的のコード・パスをトリガーするために合格しなければなりません。ほとんどの場合、プール・スプレーによってオブジェクトの適切な状態を実現することができます。しかし、私はいくつかの困難を引き起こす問題に直面しました。以下は、FSFrameMdllistをループした後のFSStreamReg::PublishRxのコード・スニペットを示しています。
FSStreamReg::PublishRx デコンパイル・スニペット
上記のデコンパイルでは、bPagesUnmapedは、FSFrameMdl::UnmapPagesが呼び出された場合に設定されるブール変数です。設定された場合、ストリーム・オブジェクトのオフセット0x1a8が取得され、null でない場合は、KeSetEvent が呼び出されます。
このオフセットは、プール内のバッファー割り当てを分離するデータ構造であるPOOL_HEADER内のアウト・オブ・バウンド・メモリーおよびポイントに対応します。特に、ProcessBilledフィールドを指します。このフィールドは、割り当てによって「チャージ」されるプロセスの_EPROCESSオブジェクトへのポインタを格納するために使用されます。これは、特定のプロセスに可能なプールの割り当て数を考慮するために使用されます。すべてのプール割り当てがプロセスに対して「チャージ」されるわけではありません。また、POOL_HEADERのProcessBilledフィールドがNULLに設定されないものもあります。さらに、ProcessBilledに保管されているEPROCESSポインタは実際にはランダムなクッキーとXORになっているため、ProcessBilled には有効なポインタが含まれていません。
これには問題があります。NpFrバッファーは呼び出しプロセスに課金され、ProcessBilledが設定されるためです。必要なエクスプロイト・プリミティブをトリガーするとき、bPagesUnmapedがTRUEに設定されます。無効なポインターがKeSetEventに渡されると、システムがクラッシュします。したがって、POOL_HEADERが無償割り当て用であることを確認する必要があります。この時点で、コンテキスト登録( Creg )オブジェクト自体は課金されないことに気づきました。ただし、このオブジェクトでは、FSFrameMdlオフセットにおけるメモリー内容を制御できません。したがって、NpFrとCregの両方のオブジェクトに散布する必要があり、正しく順序付ける必要もあります。
大規模なプール割り当てとは異なり、NtQuerySystemInformation経由で LFH プール割り当てのアドレスを漏らすことはできません。さらに、割り当て順序はランダムです。したがって、脆弱なオブジェクトに隣接するバッファーがエクスプロイト・プリミティブをトリガーし、システムのクラッシュを回避する正しい順序になっているかどうかを知る方法はありません。幸いなことに、この脆弱性を利用して、隣接するバッファーのプール・リークを引き起こすことができます。ConsumeTxのIOTL関数を見てみましょう。
FSRendezvousServer::ConsumeTx デコンパイル・スニペット
上で、関数FSStreamReg::GetStatsが呼び出されます。
FSStreamReg::GetStats デコンパイル
ここでは、脆弱なストリーム・オブジェクトのアウト・オブ・バウンド・メモリーの内容がSystemBufferにコピーされ、呼び出し元のユーザー空間アプリケーションに返されます。このプール情報リーク・プリミティブは、脆弱なオブジェクトに隣接するバッファーの署名チェックを実行するために使用できます。目的のメモリレイアウト内のオブジェクトが見つかるまで、多数の脆弱オブジェクトのスキャンを実行できます。目的のオブジェクトが見つかると、メモリの配置は次のようになります。
CVE-2023-36802 低断片化ヒープ・プール・グルーム・レイアウト
現在、ターゲットの脆弱なオブジェクトをメモリ内の正しい位置に配置しているため、ターゲット・オブジェクトに対する前述のエクスプロイト・プリミティブは、システムをクラッシュさせることなくトリガーできます。
この問題をMSRCに報告した後、脆弱性のエクスプロイテーションが実際に行われていることが発見されました。
このブログ記事で紹介されているエクスプロイテーションの方法は、考えられる多数のアプローチの一部です。現在、攻撃者が実際にどのようにしてエクスプロイテーションを行っているかについては公開されていません。エクスプロイト・コードはこちらからご覧ください。
過去にさかのぼるパッチ分析により、Windows 10の1809ビルドのmskssrv.sysに新しいコードの大部分が追加されていることが判明しました。新しいコードの追加を監視することは、多くの場合、脆弱性の発見に有益です。
この分析から、ありきたりですが古典的な教訓をもう1つ得ることができます。それは、実行されたチェックについて仮定をしてはいけないということです。友人や同僚は、FsContext2を使用した型の混乱は「一般的ではあるが調査中のバグの型」になる可能性があると提案しました。私は、このバグの型、特にプロセス間通信を扱う要因で、より多くのバリアント分析が必要だと考えています。
この脆弱性は、未知の攻撃対象領域との連携を試みているときに発見されました。システムについて「極めてゼロに近い知識」を持つことは、それを打ち破るための新鮮なマインドセットを持つことも意味します。