コールド・ハード・キャッシュ - キャッシュの悪用による RPC インターフェース・セキュリティのバイパス
エグゼクティブサマリー
Akamai の研究者たちは、Microsoft Windows RPC サービスに 2 つの重要な脆弱性を発見し、 CVE-2022-38034 および CVE-2022-38045 に指定され、ベーススコアはそれぞれ 4.3 と 8.8 です。
これらの脆弱性は、MS-RPC セキュリティコールバックのキャッシング経由でのバイパスを許す設計上の欠陥を利用するものです。
私たちは、パッチが適用されていない Windows 10 および Windows 11 マシンに脆弱性が存在することを確認しました。
これらの脆弱性は、Microsoft に確実に開示され、10 月の Patch Tuesday で修正されました。
脆弱性検出プロセスには、Akamai の研究者が開発した自動化ツールと方法論を活用しています。
Akamai では、脆弱性の概念実証(POC)と調査で使用したツールを公開しています。公開先: RPC ツールキットリポジトリ
概要
MS-RPC は、Windows オペレーティングシステムの基盤の 1 つです。1990 年代にリリースされたこの機能は、以来システムの多くの部分に根付いています。サービスマネージャーといえば RPC、LSASS も RPC、COM も RPC です。ドメインコントローラーに対するドメイン操作にさえ、RPC を使用するものがあります。MS-RPC がいかに一般的になっているかを考えると、それは厳密に調査され、文書化され、研究されていそうなものです。
しかし、実際はそうではありません。RPC の使用に関する Microsoft のドキュメントは非常に良いものですが、使用法以上のことはあまり書かれていません。また、RPC を調査する研究者によって書かれたものも少なく、特にそのセキュリティについては資料がありません。これはおそらく、(Microsoft が加担していることは明らかだが MS-RPC に限らず一般に)RPC が非常に複雑で、研究や理解が困難であるという事実に起因していると思われます。
しかし、常にこういった課題に取り組んでいる私たちは、とにかく MS-RPC の深海に飛び込んでみることにしました。これは興味深い研究テーマあるからだけでなく、セキュリティ上の意義からもです。現在でも、RPC に頼った一般的な攻撃手法が多くあります(例えば T1021.003 は MS-COM、 T1053.005 は MS-TSCH、 T1543.003 は MS-SCMRを経由して発生しています)。MS-RPC にはセキュリティメカニズムが組み込まれていますが、もしそれに脆弱性が存在して、バイパスや悪用の可能性があったり、公開された RPC サービスが悪用されることにより、望ましくない方法でマシンに影響を与えるとすればどうでしょうか。
実際、キャッシングを通じて 1 つのセキュリティメカニズムをバイパスする方法を見つけることができました。この調査では、多数の条件を必要とせずに(後で詳しく説明します)、リモートサーバー上の権限をエスカレーションするために悪用される可能性のあるサービスがいくつか見つかりました。現時点では、悪用の可能性がある 2 つの実例に関する情報を共有できます。すなわち、 WksSvc および srvsvcです。発見したその他の脆弱性に関しては、開示プロセスが完了した後に更新情報を公開します。
このブログ記事では、RPC サーバーのセキュリティ・コールバック・メカニズム、キャッシングによるバイパス方法、および私たちが Windows サービスに脆弱性の可能性があることを報告するために行った 研究の自動化手法 に焦点を当てます。この自動化ツールおよびその未加工の出力も、当社の GitHub リポジトリの RPC ツールキットで公開しています。当社のリポジトリには、私たちが信頼する他の研究者による有用な参考文献や成果物へのリンクも含まれています。
セキュリティコールバック
脆弱性そのものについて説明する前に、MS-RPC が実装する最も基本的なセキュリティメカニズムの 1 つであるセキュリティコールバックについて少し説明しておくことが必要でしょう。セキュリティコールバックを使用すると、RPC サーバー開発者は RPC インターフェースへのアクセスを制限できます。これにより、ユーザーは独自のロジックを適用して、特定のユーザーにアクセスを許可したり、認証または転送タイプを強制したり、特定のオプションへのアクセスを禁止したりできます(サーバーが公開する機能は、opnum と呼ばれる操作番号で表されます)。
このコールバックは、クライアントがサーバー上で公開関数を呼び出すたびに RPC ランタイムによって起動されます。
この調査では、リモートクライアントとサーバーのやり取りに焦点を当てました。RPC ランタイムサーバー側のコードの実装は、ALPC エンドポイントと、名前付きパイプなどのリモートエンドポイントで異なるため、これについて説明します。
キャッシング
RPC ランタイムは、パフォーマンスと使用率を向上させるために、セキュリティコールバックの結果のキャッシングを実装します。基本的には、毎回セキュリティコールバックを呼び出す前に、ランタイムがキャッシュされたエントリーを使用しようとすることを意味します。実装について詳しく見てみましょう。
RPC_INTERFACE::DoSyncSecurityCallback は、セキュリティコールバックを呼び出す前に、キャッシュエントリーが存在するかどうかをまず確認します。これは、OSF_SCALL::FindOrCreateCacheEntry の呼び出しによって行います。
OSF_SCALL::FindOrCreateCacheEntry は次の操作を実行します。
SCALL(クライアントの呼び出しを表すオブジェクト)からクライアントのセキュリティコンテキストを取得する。
クライアントのセキュリティコンテキストからキャッシング辞書を取得する。
インターフェースポインターを辞書のキーとして使用する。その値がキャッシュエントリーとなる。
キャッシュエントリーが存在しない場合は、エントリーを作成する。
キャッシュエントリーには、インターフェースのプロシージャ数、ビットマップ、インターフェースの世代という 3 つの重要なフィールドがあります。
RPC サーバーのライフタイム中に、インターフェースを変更できます。たとえば、サーバーが既存のインターフェースで RpcServerRegisterIf3 を呼び出した場合などです。さらに RPC_INTERFACE::UpdateRpcInterfaceInformation が呼び出され、インターフェースが更新されて、インターフェースの世代が増加します。これにより、キャッシュエントリーが古いインターフェースからのものである可能性があるため、キャッシングでキャッシュを「リセット」する必要があるということが認識されます。
キャッシングメカニズムは、インターフェースベース(デフォルトのふるまい)とコールベースの 2 つのモードで動作します。
インターフェースベースのキャッシング
このモードでは、キャッシングはインターフェースベースで動作します。つまり、キャッシングの観点からは、呼び出しが同じインターフェース上で起こる限りは、2 つの異なる関数の呼び出しの間に違いはありません。
セキュリティコールバックを呼び出す代わりにキャッシュエントリーを使用できるかどうかを確認するために、RPC ランタイムは、キャッシュエントリーに保存されているインターフェースの世代を実際のインターフェースの世代と比較します。 キャッシュエントリーの初期化ではインターフェース世代がゼロになるため、初めて比較が実行されるときにはインターフェース世代は異なり、セキュリティコールバックが呼び出されます。コールバックが正常に返されると、RPC ランタイムはキャッシュエントリーのインターフェース世代を更新します(すなわち、成功したキャッシュエントリーとして「マーク」され、これを使えばセキュリティコールバックを再度呼び出すことなくインターフェースにアクセスできることになります)。次にクライアントが同じインターフェース上の関数を呼び出すと、このキャッシュエントリーが使用されます。
コールベースのキャッシング
このモードは、RPC インターフェースが RPC_IF_SEC_CACHE_PER_PROC フラグ付きで登録されている場合に使用されます。このモードでは、セキュリティコールバックがどのプロシージャにアクセスを許可したかを記録するビットマップに基づいてキャッシングが行われます。したがって、クライアントが Foo 関数を呼び出し、セキュリティコールバックが正常に返された場合には、Foo に対するキャッシュエントリーができます。クライアントが Bar を呼び出すと、セキュリティコールバックが再び呼び出されます。
キャッシング要件
では、キャッシングが機能するために必要なものは何でしょうか。まず、用語を明確にしておく必要があります。MS-RPC は、バインディングハンドルを使用したクライアントとサーバー間の論理接続を表します。クライアントとサーバーは、指定された関数を使用してバインディングデータを操作できます。
バインディングは認証できます。これは、サーバーが( RpcServerRegisterAuthInfo を呼び出して)認証情報を登録したときに発生し、クライアントがバインディングに認証情報を設定します。これにより、サーバーはクライアントのアイデンティティーに関する情報を取得できます。この認証プロセスの出力は、クライアント用に作成されたセキュリティ・コンテキスト・オブジェクトです。
キャッシングメカニズム全体が、このセキュリティコンテキストに基づいています。つまり、バインディングが認証されていない場合、そのクライアントにはセキュリティコンテキストが作成されていないため、キャッシングは有効になりません。キャッシングが有効になるには、 サーバーとクライアントの両方で、認証情報を登録および設定する必要があります。
しかし、サーバーが認証情報を登録しなかった場合はどうなるでしょうか。キャッシングを有効にできるでしょうか。ここで多重化について説明します。
多重化
Windows 10 バージョン 1703 まで、サービスは他のサービスと同じ svchost プロセスを共有できました。RPC ランタイムオブジェクトの一部がすべてのインターフェース間で共有されるため、このふるまいは MS-RPC のセキュリティを阻害します。たとえば、エンドポイント(TCP ポート 7777 など)を登録する場合、このエンドポイントを使用して、同じプロセスで実行されているすべてのインターフェースにアクセスできます。したがって、ローカルでのみアクセスされることが期待される他のサービスにもリモートでアクセスできるようになります。これについては、Microsoft のこちらのページでも解説されています。
エンドポイントが多重化されているという事実はすでにある程度知られており、文書化されていますが、もう 1 つの同様のふるまいである SSPI 多重化について説明したいと思います。 認証情報の登録の一環で、サーバーは使用する認証サービスを指定する必要があります。この認証サービスとは セキュリティ・サポート・プロバイダ(SSP)で、クライアントから受信した認証情報を処理するパッケージです。 ほとんどの場合、これは NTLM SSP、Kerberos SSP、Microsoft Negotiate SSP のいずれかで、Kerberos と NTLM の間で最適なオプションが選択されます。
RPC ランタイムは、認証情報をグローバルに保存します。つまり、2 つの RPC サーバーが同じプロセスを共有し、そのうちの 1 つが認証情報を登録すると、もう 1 つのサーバーにも認証情報が与えられます。クライアントは、どちらのサーバーにアクセスするときにもそのバインディングを認証できるようになりました。セキュリティの観点から見ると、認証情報を登録されなかったサーバーが、クライアントがバインディングを認証したりキャッシングが実行されたりすることを期待していないのにもかかわらず、これらを強制的に使用することができます。
CVE-2022-38045 - srvsvc
RPC セキュリティコールバックとキャッシング動作に関する新しい知識が得られたので、この機構を実際に悪用できるかどうかを確認しました。以前に off-by-one 脆弱性を確認したことのある srvsvc に立ち戻りました。
srvsvc は MS-SRVS インターフェースを公開しています。Server サービス(LanmanServer とも呼ばれています)は、SMB 共有の管理を担当する Windows サービスです。共有されるのは、Common Internet File System(CIFS)サーバーによってネットワーク経由でアクセス可能になるリソース(ファイル、プリンター、ディレクトリツリーなど)です。基本的に、ユーザーはネットワーク共有によって、さまざまな日常タスクにネットワーク上の他のデバイスを使用できるようになります。
srvsvc のセキュリティコールバックを見てみると、関数にはすでに発見したものとは別の脆弱性が存在する可能性があることに気づきました。セキュリティコールバックのロジックを見てみましょう。
上図のとおり、srvsvc のセキュリティコールバックには次のロジックが含まれています。
リモートクライアントが 64~73 の範囲(両端を含む)の関数にアクセスしようとした場合 - アクセスを拒否
クラスターアカウントではないリモートクライアントが 58~63 の範囲(両端を含む)の関数にアクセスしようとした場合 - アクセスを拒否
つまり、リモートクライアントは、インターフェースのこれらの特定関数にアクセスできないようになっているのです。この範囲チェックは、制限された関数が機密であり、これらを呼び出すのは予期された(ローカル)プロセスのみであることを示唆しています。
このチェックでは、これらの関数へのリモートアクセスを防止しようとしますが、リモートの攻撃者はキャッシングを悪用してこのチェックを回避できます。リモートの攻撃者はまず、この範囲にない機能(リモートで利用できる関数)を起動します。セキュリティコールバック関数は RPC_S_OK を返すため、RPC ランタイムは結果を成功したものとしてキャッシュします。インターフェースは RPC_IF_SEC_CACHE_PER_PROC フラグ付きで登録されていないため、キャッシングはインターフェースベースになります。その結果、次に攻撃者が 任意の 関数を同じインターフェース上で呼び出すと、キャッシュエントリーが使用され、アクセスが許可されます。これは、攻撃者が本来アクセスできるべきでないローカル関数を呼び出すことができるようになっており、セキュリティコールバックがまったく呼び出されないということを意味します。
srvsvc は認証情報を登録しないため、通常の状況ではクライアントはバインディングを認証できず、キャッシングは有効になりません。確認されたのは、サーバーマシンの RAM 容量が 3.5 GB 未満 の場合、srvsvc は他のサービスと同じ svchost プロセスを共有するということです。 「AD Harvest Sites and Subnets Service」および「Remote Desktop Configuration Service」の各サービスは認証情報を登録するため、srvsvc がキャッシュ攻撃に対して脆弱になります。
この特定のシナリオでは、攻撃者は 58~74 の opnum を使用して制限された関数にアクセスできます。攻撃者がこれらの関数で実行できることの 1 つは、 リモートマシンの認証を強制することです。
宝探しへ
セキュリティコールバックのキャッシングメカニズムを悪用すると脆弱性が実際に生じる可能性があることがわかったので、キャッシュ攻撃に対して脆弱な可能性がある他のインターフェースを探してみることにしました。 しかし、すべてのインターフェースを手動で検索するのは時間がかかり根気のいる作業なので、これを自動化する方法が必要でした。
RPC インターフェースを探す際には、現在実行されているプロセスを使う方法とファイルシステムから探す方法の 2 つがあります。
実行中のプロセスでは、メモリにすでにロードされている RPC サーバーを確認できます。これは、(例えば Impacket の rpcmap または rpcdump を使用して)リモートサーバー上でリモートエンドポイントマッパーを照会することで行うこともでき、(RpcView や RpcEnum のようなツールを使って)ローカルで行うこともできます。ただし、この方法には次のような問題があります。 現在ロードされていないインターフェースはまったく検出されず、クライアントインターフェースも登録されていないため確認できません。
別の方法として、Windows ファイルシステムをスクレイプし、内部でコンパイルされた RPC インターフェースを検索することもできます。インターフェースごとに RpcServerRegisterIf に渡された引数を分析することで、登録情報を解析します。これは RpcEnum で実行されるのと同様の方法ですが、メモリの代わりにファイルシステムをスクレイプします。
この調査では、必ずしもメモリにロードされていないインターフェースを含める目的で、ファイルシステムを使う方式を選択しました。 このプロセスを自動化するためにさまざまなスクリプトとツールを作成しました。これらは RPC ツールキットリポジトリで入手可能です。
キャッシングが有効になっているインターフェースを検索するために、RPC インターフェース自体を解析する必要はありません。必要な情報はすべて、RPC サーバー登録コールから抽出できます。登録関数は、RPC インターフェース構造体、登録フラグ、セキュリティコールバックポインターを受け入れます。さらに、RPC インターフェース構造を解析すると、インターフェースで公開される関数や、RPC サーバーとクライアントのどちらで使用されるかなどの有用な情報が得られます。私たちの興味は主に、(脆弱性が存在する可能性がある)RPC サーバーにありますが、RPC クライアントからも情報収集に利用できるようなサーバーの呼び出しに関する有用な知見が得られます。
RPC サーバーインターフェース構造は ドキュメント化されており、フィールドを推測する必要はありません。また、サイズフィールドと転送構文は一定です(実際には DCE NDR と NDR64 の 2 つの転送構文がありますが、DCE NDR 以外は見たことがありません)。
(Yara や正規表現を使用して)これら 2 つの定数を探して、すべての RPC インターフェース構造体を見つけることは簡単です。見つかれば、 インタープリター情報フィールド を用いてサーバーがどの関数を実装しているかを確認できます。
しかし、インターフェースのセキュリティコールバック(存在する場合)とキャッシュされているかどうかに関する情報はまだ不足しています。このためには、信頼できる逆アセンブラを頼る必要があります。まっとうな逆アセンブラならどれも相互参照機能があるので、RPC サーバーの全てのインターフェース登録関数呼び出しを見つけるのは簡単です。そこからは、関数呼び出しの引数を解析して、(スクレイピングされた RPC サーバーデータと相互参照できるように)インターフェース構造体アドレス、セキュリティ・コールバック・アドレス(存在する場合)、RPC インターフェースフラグを抽出するだけです。
作成したスクレイピングスクリプトは、まさにこのように動作します。これらは、 RPC ツールキットに Windows Server 2012 および Server 2022 からの 出力 とともに公開しています。
CVE か対象外か
これらの方法論や理論は魅力的ですが、実際に結果を出すことはできるでしょうか。
答えは イエスです。 セキュリティコールバックとキャッシングの両方を使用するインターフェースの数は 120 を超えており、その多くは文書化されていません。これだけでは、セキュリティコールバックがキャッシングの影響を受けることはほとんどないため、パニックを起こす理由はありません。通常、セキュリティコールバックによって行われるチェックは、トランスポート・プロトコル・シーケンス(TCP など)や認証レベルなど、キャッシュできない値に対して実行されます。これらで変化があれば、新しい接続を確立する必要があり、いずれにしても新しいセキュリティコンテキストが必要になるので、キャッシュがリセットされ、すべての起こりうるキャッシングのバイパスが無効になります。
この調査アプローチでは、いくつかの脆弱性が見つかりました。他のものはまだ開示プロセス中のため、現時点ではそのうちの 1 つしか議論することができません。
WksSvc
CVE-2022-38034 CVSS スコア: 4.3
WksSvc は MS-WKST インターフェースを公開しています。このサービスは、ドメインメンバーシップ、コンピューター名、および SMB プリンターサーバーと同様の SMB ネットワークリダイレクターへの接続の管理を担当します。このインターフェースのセキュリティコールバックを見てみると、いくつかの関数が他の関数とは異なる形で処理されていることがわかります。
opnum が 8~11 の間の関数もローカルクライアントから呼び出されるようにチェックされており、リモートの呼び出しは許可されていません。しかし、キャッシングがあるため、最初に別のリモートで許可されている関数を呼び出した後で制限された関数を呼び出した場合はどうなるでしょうか。
ご推察のとおり、最初の呼び出しの結果がキャッシュされるので、ローカルに制限された関数をリモートで呼び出すことができます。ここで問題になるのは、これらの関数は、ローカルクライアントのみに制限されることを保証する必要があるほど重要なのかということです。
公開された関数は NetrUseAdd、 NetrUseGetInfo、 NetrUseDel、 NetrUseEnumです。なじみがあるとすれば、それは netapi32.dll からアクセスできるからでしょう(例えば NetUseAdd を参照)。
これは良いことです。この攻撃で何ができるかを知る手がかりとなります。つまり、net use と同様に、リモートサーバーを任意のネットワーク共有フォルダーに接続して、任意の論理ドライブ文字にマッピングすることもできるということです(おそらく偶然ではないでしょう)。
これにより、次の 2 つの攻撃シナリオが生まれます。
1.共有フォルダーで認証を要求します。そうすることで、NTLM リレー攻撃のために別のサーバーにリレーしたり、トークンを保存してパスワードをオフラインで攻撃することができます。
2.あるいは、既存のファイルサーバーをマスカレードして(つまり、新しいサーバーを装って)、興味深いファイルや有用なファイルを配置することもできます。これらのファイルは自分の管理下にあるため、必要に応じてそれらを武器にして、うまくいけばターゲットユーザーに感染させることができます。
これら 2 つのシナリオと、ローカルに制限された関数をリモートで呼び出しできることは、Microsoft がこの脆弱性に 4.3 のスコアを付け EoP と分類するのには十分でした。
しかし、これはまだ終わりではありません。解決すべきいくつかの注意点があります。
セキュリティコンテキスト
WksSvc の下にある RPC サーバーは、自動で認証登録を行いません。サービスが単独で実行されている場合、クライアント側の認証は実行できません( RPC_S_UNKNOWN_AUTHN_SERVICE のエラーが発生します)。そのため、 SSPI 多重化も悪用するためには、サービスが他のサービスと共に実行されている必要があります。 これにより影響を受ける Windows のバージョンが制限されます 。影響を受けるのは、 Windows 10 バージョン 1703 より前のバージョンか、3.5 GB 未満の RAM で実行されている新しいバージョンとなります。
ログオンセッション
ネットワークにマップされたフォルダーに内在するもう 1 つの問題は、フォルダーがそれを作成したユーザーの ログオンセッション に制限されていることです。セキュリティのバインドとキャッシングを取得するためには、まずログオンする必要があるので、常にターゲットマシン上の既存の(対話型の)セッションとは異なるログオンセッションを作成します。 意図や目的にかかわらず、我々が発見した脆弱性は悪影響を及ぼさないということを意味します。作成したネットワークマッピングは、短期間のログオンセッションの下にあり、通常のユーザーがマシンにログインしたときに作成したものではないため、表示されません。
これを克服するためには、 NetrUseAdd のコードをもう少し深く掘り下げる必要がありました。結果として、 NetrUseAdd に渡すとグローバル名前空間にマッピングを作成するように指示するフラグが見つかりました。これは、すべてのユーザーに影響を与えます。これらのフラグは、一般に参照可能なヘッダーファイル LMUse.h にも含まれています。
フラグを設定すると、コードはグローバルマッピングを正常に作成し、対話型セッションに影響を与えて悪用の試みが完成します。
まとめ
MS-RPC は大規模で複雑なプロトコルです。また、Windows のコア機能の一部でもあります。開発者が RPC サーバーのセキュリティを確保するために使用できるセキュリティ機能を備えていますが、セキュリティの問題を生じうる脆弱性が存在しているため、まさにセキュリティ研究者にとって興味深いトピックです。
しかし、このトピックについては表立った研究は多くは行われていません。このブログ記事では、MS-RPC(セキュリティコールバック)の大規模なセキュリティメカニズムを取り上げ、コールバック結果のキャッシングの形でバイパスを発見しました。また、脆弱性のある RPC サーバーを見つけるための調査方法についても詳しく説明し、脆弱性を記述した調査結果をいくつか示しました。
私たちはこの投稿と、これに付属する RPC ツールキットリポジトリが、RPC サーバーとセキュリティメカニズムの調査に役立つことを願っています。