投稿者:Philippe Laulheret
ClipSP(clipsp.sys)は、Windows 10 および 11 でクライアントライセンスやシステムポリシーを実装するために使われる Windows のドライバです。
Cisco Talos の研究者により、署名のバイパス、特権昇格、サンドボックスエスケープなど、clipsp.sys に関係がある以下の 8 件の脆弱性が発見されました。
- TALOS-2024-1964 (CVE-2024-38184)
- TALOS-2024-1965 (CVE-2024-38185)
- TALOS-2024-1966 (CVE-2024-38186)
- TALOS-2024-1968 (CVE-2024-38062)
- TALOS-2024-1969 (CVE-2024-38187)
- TALOS-2024-1970 (CVE-2024-38062)
- TALOS-2024-1971 (CVE-2024-38062)
- TALOS-2024-1988 (CVE-2024-38062)
HITCON と Hexacon でも、この研究プロジェクトについて発表しました。本記事の最後に、Haxacon でのプレゼンテーションの録画を掲載しています。
ClipSp とは
ClipSp は、Microsoft Windows 10 および 11 に搭載されているファーストパーティドライバです。クライアント ライセンス プラットフォーム(CLiP)の主要コンポーネントの 1 つであり、ライセンス機能とシステムポリシーの実装を担っています。このドライバの情報はあまりありません。ほとんどの Microsoft ドライバと DLL についてはデバッグシンボルが一般公開されていますが、ClipSp のデバッグシンボルは、Microsoft 社のシンボルサーバーから削除されています。デバッグシンボルは、関数名をはじめとする関連するデバッグ情報を提供するものです。これを活用すれば、セキュリティ研究者はバイナリの多くの関数の背後にある意図を推測できます。デバッグシンボルがないということは、そうした推測もできないということです。また、驚くべきことに、このドライバは難読化されています。Microsoft 社のバイナリではめったにないことで、リバースエンジニアリングがいっそう難しくなると考えられます。公開されている研究は少なく、その多くは Talos の調査より前に行われたものか、Talos の報告を受けてリリースされたものかのどちらかです。後者の研究では、ClipSp の古いバージョンのシンボルも公開されています。このドライバを研究したい人にとって有効な足掛かりとなるはずです。このソフトウェアの特に興味深い点は、Windows アプリケーションのライセンスを Windows アプリストアから取得する機能と、Windows 自体のアクティベーションサービスに関連する機能の実装を担っていることです。
難読化解除
ClipSP ドライバは、Microsoft 社独自の難読化ツール Warbird で難読化されています。幸い、参考になる過去の研究があり、今回の研究のニーズにも応用できます。ドライバの難読化解除には、バイナリ エミュレーション フレームワークの Qilingを活用することにします。ドライバの難読化されたセクションの難読化解除を行う部分をエミュレートし、実行可能なメモリ領域をダンプして、Talos で使用しているリバースエンジニアリング ツールにインポートします。
通常の動作では、難読化に関係する部分は以下のようになっています。
見ての通り、復号関数が異なるパラメータで 2 回呼び出された後、難読化が解除された実際の関数が呼び出され、最後に、当該セクションを再難読化する関数が 2 回呼び出されます。
Ida Python を使えば、復号関数(実際には 2 つの別個の関数が存在)への参照をすべて追跡できます。また、RCX と RDX レジスタが割り当てられている関数呼び出しの前の命令を調べれば、引数を復元できます。呼び出し規約によると、これら 2 つのレジスタは関数の第 1 引数と第 2 引数です。Talos が手を加えた Qiling スクリプトにこの情報を入力すれば、復号関数をエミュレートして難読化解除されたバイナリ全体をダンプできます。ドライバの難読化解除が済んだら、リバースエンジニアリングを開始し、Windows がドライバと通信している方法とさまざまなビジネスロジックの要素について理解し、脆弱性を検出できるようになります。
ドライバの通信
通常、ドライバは、ユーザーランドからアクセスできるデバイスを登録するか、他のドライバが使用するための関数をエクスポートします。ClipSp の場合はドライバの動作がやや異なり、「ClipSpInitialize」関数をエクスポートします。この関数は ClipSp によって設定されるコールバック関数の配列へのポインタを取得します。呼び出し側のドライバは、このポインタを使用して ClipSp 関数を呼び出します。System32 フォルダ全体で「ClipSpInitialize」を grep 検索した結果、ClipSp を使用する可能性が最も高いのは「ntoskrnl.exe」であることがわかりました。次に可能性が高いのは、ClipSp 関数の一部を使用するいくつかのファイルシステムドライバです。以降は、「ntoskrnl」が ClipSp とどのようなやり取りを行うかについて重点的に取り上げます。
Windows カーネル内の ClipSp 関数への相互参照を分析すると、これらの関数とやり取りするためには、SystemPolicy クラスで「NtQuerySystemInformation」を呼び出す必要があることがわかります。CLiP エコシステムのその他のバイナリは、こうしたシステムコールを発行するとともに、非公開の API と他のソフトウェアを切り離すためにリモートプロシージャコール(RPC)インターフェイスを提供します。しかし、「NtQuerySystemIformation」エンドポイントとの直接のやり取りを妨げるものはありません。これは、意図された RPC クライアントライブラリによって実施される追加チェックの一部をバイパスする便利な方法になります。
構造体の難読化
正規のバイナリが SystemPolicy クラスとどのようにやり取りするかを見てみると、残念なことに以下のようになっています(wlidsvc!DeviceLicenseFunctions::SignHashWithDeviceKey の例)。
これは、API に渡されるデータをカプセル化する別の難読化レイヤです。ここでの考え方は、バイナリ変換のネットワーク(Feistel 暗号とも呼ばれる)を使用し、コード内のさまざまな操作をインラインで実行してデータを暗号化するというものです(上図参照)。API 呼び出しの一部は、使用された操作のリストを提供します。このリストを基に、カーネルが適切なパラメータを使ってそれらの操作を直接呼び出し、データを復号します。つまり、暗号化コードと関連するパラメータの両方をそのまま取得し、この API を呼び出す際に再利用すれば、簡単に難読化に対処できるということになります。逆コンパイラの出力を Visual Studio にコピーアンドペーストするのは少し面倒ですが、通常はそれでうまくいきます。システムコールから戻る前に、結果として得られるデータは同様の方法で難読化されます。先ほども述べたとおり、動作している実装からデータを取得するのが最も簡単な対処方法です。全体として、データの形式は以下のようになります。
内部ペイロード(左)は、実行する必要があるコマンド番号を含む size-value エントリの配列です。次がカーネルからの応答を暗号化するために使用される Warbird のマテリアルです。そして最後はコマンド固有のデータですが、どの ClipSp 関数が呼び出されるかによって異なります。
このデータは、提供された配列のエントリ数を概ね指定する構造体にカプセル化され、その後、全体が暗号化されます。図の一番右の列に残っている Warbird のデータは、提供されたデータの復号方法をカーネルに指示するためのものです。
利用できる可能性があると推測されるコマンドは、以下のとおりです。
大半のコマンドが ClipSp を呼び出しますが、いくつか(特に 100 番未満)は Windows カーネルのみで処理されている可能性もあります。
サンドボックスについての考慮事項
Microsoft 社は、LPAC(Less Privileged Application Container)サンドボックスというツールを提供しています。これは、低い権限のコンテキスト内でコードの一部を実行できるかどうかをテストできるものです。コンセプト実証でこれを使用すれば、ClipSp の API が LPAC のコンテキストから実際にアクセス可能であることを確認できます。特に興味深いことに、これらのアプリケーションコンテナは通常、パーサーやブラウザのレンダリングプロセスなど、リスクの高いターゲットのサンドボックス化に使用されています。そのため、特権昇格の脆弱性が見つかれば、サンドボックスエスケープも可能になるでしょう。
ライセンスの処理
リバースエンジニアリングの過程で、ClipSp が処理するライセンスファイルが非常に興味深いことが判明しました。ライセンスファイルは通常、UWP アプリケーション(アプリストアで提供されるものと、Notepad などデフォルトでインストールされているもの)とやり取りする際に、Microsoft 社から暗黙のうちに取得されます。また、Windows のアクティベーション、ハードウェアのバインディング、さまざまなアプリケーションの暗号化マテリアルの提供などの目的にも使用されます。
当初、ライセンスファイルは、「SpUpdateLicense」コマンドでインストールされる不透明なデータの BLOB に見えます。これは、上記のプロセスに従って、コマンド「_id = 100」で呼び出せます。既存のライセンスは、Windows レジストリの以下の場所に保存されています。
HKLN\SYSTEM\CurrentControlSet\Control\{7746D80F-97E0-4E26-9543-26B41FC22F79}
このレジストリキーにアクセスできるのは、SYSTEM ユーザーのみです。管理者権限のプロンプトから以下のコマンドを実行すると、SYSTEM ユーザーとして regedit を開けます。
PsExec64.exe -s -i regedit
これらのライセンスの形式はほとんど文書化されていませんが、どうやって解析されているかを見ると非常に参考になります。ライセンスは tag-length-value(TLV)形式となっており、許可されているタグのリストが ClipSp 内にハードコードされているタプル形式の配列(tag, internal_index)に含まれています。解析時に、それぞれの有効な TLV エントリへのポインタが、internal_index で示される場所の配列に格納されます。
署名のバイパス(TALOS-2024-1964)
ライセンスはさまざまな署名認証局によって署名され、公開キーは ClipSp にハードコードされています。認証コードは以下のとおりです。
「entry_of_type_24」の値は、ライセンスの解析時に保存されるポインタであり、その署名を参照します。「entry_of_type_24」と「License_data」では、ライセンス BLOB の最初から署名までのバイト数をカウントするために使われるポインタ演算に違いがあります。
解析に関係する部分は以下のようになっています。
エントリのタグに関連付けられた内部インデックスが 24 になると、処理ループは一時的に終了します。署名へのポインタが保存され、さらにデータが残っていれば、ライセンス処理が再開されます。
なお、このアプローチには欠陥があります。ライセンスの署名の後にデータがある場合、解析は引き続き行われますが、署名との照合は行われません。そのため攻撃者は、適切な署名認証局によって署名されたライセンスさえ入手できれば、実質的にあらゆるライセンスの署名チェックをバイパスできます。
境界外読み取りの脆弱性(TALOS-2024-1965、TALOS-2024-1968、TALOS-2024-1969、TALOS-2024-1970、TALOS-2024-1971、TALOS-2024-1988)
ライセンス構造体と TLV データへのポインタの配列が使用されている場所を相互参照すると、特定のエントリの長さやサイズまたはそれに関連するデータを返すラッパー関数が多数見つかります。ほとんどの場合、これらの関数はセキュリティ問題が起きないように実装されていますが、一部のエントリでは、ライセンス BLOB で提供されるデータのサイズを仮定している(検証していない)ため、境界外読み取りの脆弱性が発生する可能性があります。以下のスクリーンショットは、こうした脆弱性の例です。
これらの 2 つの関数は、DeviceID フィールドのサイズまたはその内容を取得します。ただし、データが 11 行目に到達するようにフォーマットされている場合(つまり、提供されたライセンスのタイプ 5 のエントリがない場合)、エントリ 18 のデータフィールドが、サイズと値の両方を提供するために使用されます。このとき、ポインタを逆参照しますが、十分なデータが提供されたかどうかはチェックされません。たとえば有効なライセンス BLOB の最後に、データフィールドの長さが 1 バイトしかない DeviceID エントリ(タイプ 18)を追加した場合、「get_DeviceIDSize」関数は 2 バイトのデータを想定しているため、境界外から 1 バイトを読み取ることになります。さらに、「get_DeviceID」を呼び出す関数はすべて、ライセンスファイルの末尾に 1 バイトが追加されたポインタを受け取ることになります。このように「get_DeviceIDSize」関数の誤った情報を基に動作した結果、さらなる OOB(境界外)読み取りの問題が発生する可能性があります。
境界外読み取りが境界外書き込みに(TALOS-2024-1966)
DeviceIdSize フィールドの境界外読み取りが発生する可能性がある上記のケースについて詳しく見てみましょう。このケースでは、メモリ内で隣接関係にあるデータが意味のある方法で変更された場合、DeviceID オブジェクトの予想サイズがライフタイムを通じて変更される可能性があるという、非常に興味深い状況が生まれます。ライセンス BLOB に続くデータの最初のバイトも、DeviceID のサイズを定義する(未署名の短い)値の先頭バイトとして読み取られることになります。ClipSp でこれら 2 つの関数がどのように使用されているかを見てみましょう。ハードウェアライセンスのインストールに関係する部分は以下のようになっています。
「get_DeviceIDSize」関数が複数回呼び出されています。1 回はメモリ割り当てルーチンに size フィールドを提供し、もう 1 回は「memcpy」のパラメータとして使用されます。2 回の呼び出しの間に size フィールドが変更されると、境界外書き込みの脆弱性を引き起こす可能性があります。
こうした脆弱性のエクスプロイトは容易ではありません。悪意のあるライセンス BLOB の直後に意味のあるデータが配置されるように PagedPool ヒープを形成しつつ、2 回の取得処理の競合状態に勝つ必要があるからです。
まとめ
以上で見てきたように、コードを難読化することにより、簡単にエクスプロイトできる脆弱性や、ちょっとしたメモリ破損、単純なロジックバグが見つかりにくくなります。ClipSp の場合、問題はさらに深刻です。この攻撃ベクトルによりサンドボックスエスケープが引き起こされかねず、侵害されたユーザーに重大な被害が生じる可能性があるからです。
今回の研究は、たとえ最初は Feistel 関数という難問に取り組まなくてはならないとしても、普段あまり行われない分析を試みることの重要性を、セキュリティ研究者に改めて思い起こさせるものでした。また、難読化をプロジェクトに活用することを決めたソフトウェアエンジニアやプロジェクトマネージャにとっては、難読化するというアプローチが、ちょっとしたバグを早期に発見する通常のバグ発見プロセスの妨げになる可能性があるということを改めて肝に銘じるきっかけとなるはずです。
本稿は 2024 年 11 月 25 日にTalos Group のブログに投稿された「Finding vulnerabilities in ClipSp, the driver at the core of Windows’ Client License Platform」の抄訳です。