RPC ランタイムにおける 3 つのリモートコード実行脆弱性を探る
エグゼクティブサマリー
Akamai のリサーチャーである Ben Barnea が、Microsoft Windows RPC ランタイムの重要な脆弱性を 3 つ発見しました。これらは、 CVE-2023-24869、 CVE-2023-24908、 CVE-2023-23405に指定され、ベーススコアはいずれも 8.1 です。
この脆弱性はリモートコード実行につながる可能性があります。RPC ランタイムライブラリはすべての RPC サーバーにロードされ、Windows サービスで一般的に使用されています。そのため、あらゆる Windows バージョン(デスクトップおよびサーバー)が影響を受けます。
この脆弱性は、RPC ランタイムで使用される 3 つのデータ構造における整数オーバーフローです。
これらの脆弱性は、「責任ある開示」に従って Microsoft に報告され、 2023 年 3 月の Patch Tuesday で修正されました。
概要
MS-RPC は Windows ネットワークで頻繁に使用されるプロトコルであり、多くのサービスやアプリケーションがこのプロトコルに依存しています。そのため、MS-RPC の脆弱性は甚大な影響を及ぼす可能性があります。Akamai Security Intelligence Group はこの 1 年間、MS-RPC の調査に取り組んできました。私たちは脆弱性を見つけて悪用し、調査ツールを構築して、このプロトコル内の文書化されていない部分を文書にまとめました。
これまで投稿したブログでは、サービスの脆弱性に焦点を当てていましたが、今回は MS-RPC の"エンジン"である RPC ランタイムの脆弱性を検証します。これらの脆弱性は、 2022 年 5 月に発見された脆弱性と類似しています。
整数オーバーフローのパターン
3 つの新たな脆弱性には共通点があります。これらは、以下に示す 3 つのデータ構造への挿入による整数オーバーフローがもたらす脆弱性です。
SIMPLE_DICT(値のみを保存するディクショナリ)
SIMPLE_DICT2(キーと値を両方保存するディクショナリ)
Queue(キュー)
これらのデータ構造はいずれも、満杯になると拡張される動的配列を使用して実装されています。この拡張では、現在の配列に割り当てられているサイズの 2 倍のメモリが割り当てられます。この割り当てによって整数オーバーフローが発生しやすくなります。
図 1 は、RPC ランタイムから逆コンパイルしたコードです。SIMPLE_DICT 構造への挿入プロセスと、整数オーバーフローをトリガーする可能性のある脆弱なコード行(強調表示)が示されています。
脆弱性を探る
脆弱性をトリガーするためには、その根本原因を理解し、脆弱な関数へのフローが存在するかどうかや、トリガーにかかる時間を把握する必要があります。
簡潔にするために、3 つの脆弱性の 1 つである Queue(キュー)データ構造の脆弱性について説明します。他の 2 つの整数オーバーフローも性質が似ているので、以下のセクションの分析はこれらにも当てはまります。
整数オーバーフローについて
キューは、単純な FIFO(先入れ先だし)のデータ構造です。RPC ランタイムのキューは、キューエントリの配列、現在の容量、キューの最後のアイテムの位置を含む構造体を使用して実装されます。
新しいエントリがキューに追加されると(使用可能なスロットがある場合)、すべてのアイテムが前にずれて、新しいアイテムが配列の先頭に追加されます。そして、キューの最後のアイテムの位置がインクリメントされます。
キューからの取り出しが発生すると、最後のアイテムが取り出され、最後のアイテムの位置がデクリメントされます(図 2)。
前述のとおり、この脆弱性は新しいエントリの挿入で発生します。動的配列が満杯状態の場合、コードは次の処理を実行します。
次のサイズの新しい配列を割り当てます。
CurrentCapacity*2*sizeof(QueueEntry)古いアイテムを新しい配列にコピーします
古いアイテムの配列を解放します
容量を 2 倍にします
32 ビットシステムの場合、新しい配列サイズの計算でオーバーフローが発生します。
キューに 0x10000000(!)のアイテムを入力します。
拡張が生じます。新しい割り当てサイズが計算され、0x10000000 * 16 になります。 オーバーフローとなり、新しい割り当てサイズは 0 になります。
長さゼロの配列が割り当てられます。
コードにより、古いアイテムの配列が新しい小さな配列にコピーされます。これにより、ワイルドコピー(大きなリニアコピー)が作成されます。
64 ビットシステムでは、大きな割り当てがあると、失敗になるので、この脆弱性を悪用することはできません。コードは境界外の書き込みをトリガーせず、正常に終了します。64 ビットシステムは、この問題に対しては脆弱ではありませんが、 他の整数オーバーフローに対しては脆弱です (SIMPLE_DICT と SIMPLE_DICT2)。
コードフロー
RPC 接続は、OSF_SCONNECTION クラスを使用して表現されます。各接続で複数のクライアントコール(OSF_SCALL)を処理できますが、その接続において 指定時間に実行が許可されるコールは 1 つだけ であり、他のコールはキューに入れられます。
そのために、キューを使用する興味深い関数、OSF_SCONNECTION::MaybeQueueThisCall が使用されています。 この関数は、接続に到達した新しいコールのディスパッチの一部として呼び出されます。この場合、キューは別のコールが処理されている間、着信したコールを"保留する"ために使用されます。
したがって、キューへの挿入はユーザーが制御できますが(クライアントコールを順番に送信することによって)、この関数は要件として、その接続で現在、コールが処理中であることを必要とします。つまり、キューに入れるには、完了に時間がかかるコールが必要になるのです。コールが処理されている間に、ディスパッチキューを埋める新しいコールを複数個送信します。
では、完了に最も時間のかかるのはどのような関数コールでしょうか。
最適な候補は、無限ループを発生させることができる関数です。
次に最適なオプションは認証強制の脆弱性です。サーバーが私たちに接続するので、応答時間を制御できます。
最後の手段は、複雑な論理を持つ複雑な関数や、大量のデータを処理するため完了に時間がかかる関数です。
私たちは、 独自の認証強制の脆弱性を使用することにしました。
トリガーにかかる時間
キューへの挿入に必要なものや、使用可能な方法はわかりましたが、ここで 1 つ重要な疑問が浮上します。それは現実に実行可能なのかという問題です。
私たちには、整数オーバーフローを発生させる変数に対する最低限の制御力しかありません。refcount(参照カウント)と同様に一度に 1 つしかインクリメントできないのです。この種の整数オーバーフローは、完全に制御できる 2 つの変数の加算や乗算によるものや、追加サイズをある程度制御できる場合(パケットサイズなど)よりも少し扱いにくくなります。
先ほど述べたとおり、0x10000000(~268M)のアイテムを割り当てる必要があります。これは大変です。
私のマシンでこの脆弱性をトリガーしてみると、キューに入れられるコールは 1 秒あたり約 15~20 個程度でした。 つまり、平均的なマシンでトリガーする場合、約 155 日かかることになります。 私たちは、キューに挿入される 1 秒あたりのコール数はもっと多いと想定していました。RPC ランタイムがこんなに遅いのはなぜでしょうか。マルチスレッドではないのでしょうか。
同じ接続に対して複数のスレッドが同時に異なるコールを処理し、キューに入れると私たちは想定していました。しかし、リバースエンジニアリングの結果、実際のフローは少し異なることがわかりました。
MS-RPC パケットの処理
このコードは、コールがディスパッチされる直前に、必要に応じて新しいスレッドをスピンし、OSF_SCONNECTION::TransAsyncReceive を呼び出します。TransAsyncReceive は、同じ接続に対する要求を受信しようとし、その後、新しいスレッドに要求を送信します(CO_SubmitRead を呼び出します)。
もう一方のスレッドは TppWorkerThread からの要求を拾い、最終的に ProcessReceiveComplete になり、MaybeQueueThisCall を呼び出して、SCALL をディスパッチキューに入れます。その後、伝播してこの接続の新しい要求の受信が試行されます。
結局、複数のスレッドが稼働していたとしても、実際にその接続に使用されているのは 1 つのスレッドだけです。つまり、複数のスレッドからコールを同時にキューに追加することはできません。
パケットの「残り」
私たちは、1 秒あたりのコール数を増やして、この脆弱性のトリガーにかかる時間を最小限に抑える方法を探しました。そして、受信コードのリバースエンジニアリングをしている時に、 パケットの長さがパケット内の実際の RPC 要求よりも大きい場合、RPC ランタイムが残りを保存することに気が付きました。その後の新しい要求のチェック時には、ソケットをすぐに使用することはできません。まず、パケットの「残り」があるかどうかをチェックし、残りがあれば、その残りから新しい要求を処理します。
これにより、送信するパケット数を大幅に少なくし、各パケットに最大数の要求を入れ込むことが可能になりました。このとおりに実行してみたところ、キューに追加される 1 秒あたりのコール数はそれほど変わらず、あまり効果はないようでした。
まとめ
これらの脆弱性が悪用される可能性は低いと思われましたが、私たちは、MS-RPC に関する昨年の調査で明らかになった重要な脆弱性のリストにこれらを追加しました。悪用しにくい脆弱性であっても、有能な(そして忍耐強い)攻撃者は悪用する可能性があることを忘れないでください。
MS-RPC は数十年前からありますが、いまだに発見されていない脆弱性が潜んでいます。
この調査がきっかけとなって他のリサーチャーの方々にも MS-RPC とそのアタックサーフェスを調べていただければ幸いです。問題に迅速に対応し、修正してくださった Microsoft に感謝いたします。
Akamai の GitHub リポジトリ では、役立つツールやテクニックが多数公開されています。