CVE-2022-24125 および CVE-2022-24126 向けのドキュメントと概念実証コード
2022年8月25日、Dark Souls IIIの新たなゲームアップデート1.15.1が、オンラインサービスの復旧とともにリリースされました。このアップデートでは、CVE-2022-24125とCVE-2022-24126の両方が修正され、さらにゲームのP2Pネットワーキングに存在する多種多様なその他の潜在的なセキュリティ脆弱性(OOB読み取り/書き込み)も修正されました。さらに、他のプレイヤーのセーブデータを破損させる既知のエクスプロイトもすべて修正されています。オンラインマルチプレイ中によく遭遇する「呪いのナイフ」などの一般的なチートもパッチされています。
このリポジトリには、FROM SOFTWAREのゲームに影響を与える最新のRCEエクスプロイトCVE-2022-24126の概念実証コードとドキュメントが含まれています。理論上は他のゲームでも可能ですが、この研究はDark Souls IIIで行われたため、焦点はそちらに当てられています。現時点では概念実証コードはDark Souls IIIにのみ存在しますが、脆弱性は以下のゲームに存在することが確認されています:
脆弱性のあるコードはSEKIROにも存在します(クレジット:LukeYui)が、トリガーする方法はありません。Demon's Soulsでの存在は確認されていませんが、非常に可能性が高いです。クローズドネットワークテストは影響を受けましたが、リリース版のELDEN RINGは影響を受けていません。 実際、Dark Souls IIIに存在していたネットワーククラッシュ、範囲外読み取り/書き込み、およびピアのゲームデータを変更できるエクスプロイトの膨大なリストは、ELDEN RINGではパッチされています。LukeYui によるこのリストの作成と、迅速に対応したFROM SOFTWAREに称賛を送ります!ELDEN RINGは、ハッカーが引き起こせる被害の範囲に関して、疑いなく最も安全なFROM SOFTWAREのタイトルであることを嬉しく思います。
一般に信じられているのとは反対に、これはピアツーピアネットワーキングのエクスプロイトではありません。マッチメイキングサーバーに関連しており、別のマッチメイキングサーバーの脆弱性(CVE-2022-24125)のため、脆弱性を引き起こすためにマルチプレイヤーアクティビティに参加する必要がないという点で、はるかに深刻です。
サーバー停止前の数ヶ月間、平均同時プレイヤー数が約20,000人であったことを考えると、これは即座に修正が必要な問題であることは明らかでした。特にELDEN RINGにも存在する可能性があったからです。FROM SOFTWAREが、概念実証ビデオと詳細なエクスプロイトドキュメント(このREADMEの大部分はこれに基づいています)を含む私の最初の報告から40日以上経っても行動を起こさなかったため、私はエクスプロイトの存在を無害な方法で公に示し、開発者に対応を促す注意を喚起することを決意しました。そしてそれは功を奏しました。
NRSessionSearchResult マッチメイキングデータの解析中に、スタックバッファとデータサイズフィールドに対する不適切な境界チェックにより、攻撃者は任意のコードを実行できます。スタックオーバーフローにより、ストリームリーダーが内部で使用する DLMemoryInputStream オブジェクトの vftable_ptr の下位2バイトを上書きでき、実行を慎重に選択された隣接コードにリダイレクトできます。DLMemoryInputStream オブジェクトの構造とデータサイズフィールドを巧妙に悪用することで、任意のコードリダイレクトを達成でき、RCX はパケットのアドレスを指すようになります。そこから、異なるオフセットでの仮想呼び出し(パケットバッファに書き込んだ任意のアドレスにジャンプするようになる)を介した一連のコードリダイレクトを使用して、任意のコード実行を達成できます。
配布経路こそが、この特定のRCEを(すでにRCEであること以上に)特に深刻なものにしています。エクスプロイトは、NRSessionSearchResult 情報を含むマッチメイキングプッシュリクエストを介して送信されます。つまり、攻撃者は自分のオンラインセッションに参加する誰でもターゲットにできます。特にDS3の場合:
PushRequestSummonSign)PushRequestAllowBreakInTarget)PushRequestVisit)PushRequestAcceptQuickMatch)これでも十分に悪質ですが、本当の潜在能力は RequestSendMessageToPlayers リクエストによって解放されます:
message RequestSendMessageToPlayers {
repeated uint32 player_ids = 1;
required bytes push_message = 2;
}
ホストはこのリクエストを使用して、侵入者に PushRequestAllowBreakInTarget プッシュメッセージを直接送信し、彼らがスポーン座標を取得してP2Pセッションに参加できるようにします。それだけです。ゲームがこのリクエストを使用するのはこれだけです。
これがいかにひどく安全でないか、強調してもしすぎることはありません。任意のプレイヤーは、実質的にマッチメイキングサーバーになりすますことができます。このリクエストを使用して PushRequestVisit を介してエクスプロイトを送信することにより、攻撃者はプレイヤーIDがわかっていれば、任意のオンラインプレイヤーをリモートでターゲットにできます。また、攻撃者は、それぞれが多数の可能性のあるプレイヤーIDを含む複数のリクエストを送信することで、非常に迅速にオンラインプレイヤーベース全体にエクスプロイトを拡散させることもできます。
RCEがすべてのゲームに完全に移植できるわけではありませんが、攻撃者に任意のコードリダイレクトを与えるエクスプロイトの核となるアイデアは同じです。これが達成できれば、ゲーム固有の仮想呼び出しチェーンまたはROPチェーンを見つけられる可能性が非常に高くなります。この「最初のステップ」では、以下の脆弱性を使用します:
セッション参加情報を含むマッチメイキングプッシュリクエストは、長さで区切られたデータエントリのチェーンで構成されるカスタムバイナリ形式でその情報を格納します。各エントリは以下の形式です:
struct Entry
{
uint32_t type_or_id; // 確実ではないが、おそらくタイプ(固定長=2、可変長=1?)
uint32_t size;
uint8_t data[size];
}
これらのエントリのデータをコピーするゲーム関数は、サイズフィールドを盲目的に信頼するため、範囲外読み取りが発生します。悪意のあるクライアントは、サイズフィールドに 0x7FFFFFFF などの値を設定することでこれを悪用し、メモリ割り当てを失敗させ、被害者のゲームをクラッシュさせることができます。その後、このサイズは DLMemoryInputStream のコンストラクタにも渡され、これがエクスプロイトの重要な部分になります。
NRSessionSearchResult パーサーでのバッファオーバーラン上記のデータ構造内のエントリの1つは、シリアル化された NRSessionSearchResult オブジェクトです。このデータのパーサーは、最初にプロパティのリストを解析します。これらのプロパティは、4バイト整数、8バイト整数、またはnull終端のワイド文字列にすることができます。このプロパティリストの後には、ホストのSteam persona名(null終端ワイド文字列)と、エクスプロイトには重要ではない追加データが続きます。この関数とプロパティリストパーサーの両方は、文字列を読み取るために固定サイズのスタックバッファを使用しますが、どちらの場合もバッファに対する境界チェックは実行されません。以下は、ホスト名をコピーするゲームコードです(Ghidraデコンパイラを使用して生成し、その後クリーンアップしたもの):
size_t idx = 0;
wchar_t wchr = 0;
do {
// DLEndianStreamReaderのvftableインデックス17のread_wchar()関数
wchr = stream_reader->read_wchar();
player_name_buff[idx] = wchr;
idx++;
} while (wchr != 0);
これによりバッファオーバーランエクスプロイトが発生し、攻撃者はスタックを破損させることができます。
任意のコードリダイレクトを達成するために、これと、パーサーを呼び出す関数によってスタック上にインスタンス化される DLMemoryInputStream オブジェクトのメモリレイアウト(ストリームリーダーが内部で使用するもの)を利用します:
struct DLMemoryInputStream {
uintptr_t* vftable_ptr; // オフセット 0
size_t data_size; // オフセット 4 (32bit) / 8 (64bit)
uint8_t* data_buffer; // オフセット 8 (32bit) / 16 (64bit)
// バッファ以降のエントリはエクスプロイトにとって重要ではない
}
data_size フィールド(バグ #1)を制御できるため、それを data_buffer フィールドのスタックメモリアドレスに設定できます。アドレスが一定で大きすぎない場合(DS3はこれらの要件を満たします)、これが成功します。コンパイラはスタックバッファをフレームの先頭に配置するため、攻撃者はバグ #2 を使用して DLMemoryInputStream の vftable_ptr の下位2バイトを上書きできます。そのため、次の文字が DLEndianStreamReader によって読み取られると、内部で DLMemoryInputStream が呼び出され、コードがリダイレクトされます。2バイトで十分な余地があり、DLEndianStreamReader vftable の22番目の関数にジャンプできます。この関数は、その最初のフィールドが指すオブジェクトの6番目の仮想メソッドを呼び出します。64ビットプロセス(すなわちDark Souls III)では、以下の命令が実行されます:
MOV RCX,qword ptr [RCX + 0x8]
MOV RAX,qword ptr [RCX]
JMP qword ptr [RAX + 0x40]
RCX は DLMemoryInputStream オブジェクトへのポインタであるため、最初の命令は、攻撃者がバグ #1 を使用して data_buffer フィールドを指すスタックアドレスに設定した data_size フィールドを RCX に書き込みます。次の2つの命令は、データバッファのオフセット 0x40 に攻撃者が書き込んだメモリアドレスに実行をリダイレクトします。これで任意のコードリダイレクトが達成されました!そこから攻撃者は、バッファが仮想メソッドテーブルとして機能するようになるため、異なるオフセットでの仮想呼び出しに近いコードを選択することで、ペイロードを適切なメモリ領域にコピーして実行するコードリダイレクションチェーンを設定できます。Dark Souls IIIの概念実証では、RCEを達成するためにわずか3つのガジェットしか必要としないセットアップを見つけました:
140e977001422be020140e40f15これらの3つのガジェットの詳細についてはこちらを参照してください。他のゲームでこの仮想呼び出し方式が実現可能でない場合、任意のコードリダイレクトは、より伝統的なROPエクスプロイトをセットアップするためにも使用できます。
概念実証コードを実行するには、まず接続先のサーバーが必要です。公式サーバーはエクスプロイトのために無効化されていますが、ds3os を使用してプライベートサーバーをセットアップできます。ds3os は小売サーバーの動作を可能な限り模倣するように設計されていますが、このエクスプロイトを修正するためのセキュリティパッチがすでにこのプロジェクトに適用されています。ただし、BuildConfig.h で SEND_MESSAGE_TO_PLAYERS_SANITY_CHECKS と NRSSR_SANITY_CHECKS の定数を false に設定して自分でプロジェクトをビルドすることで、テスト環境をセットアップすることもできます。これにより、安全でない小売サーバーの動作が模倣されます。ds3os が提供する手順に従ってゲームを起動し、サーバーに接続します。
これが完了し、ゲームがサーバーに接続されたら、PoCコードをビルドして Injector.exe 実行可能ファイルを起動します。これにより、エクスプロイトコードを含むDLLがDark Souls IIIプロセスに注入されます。このDLLは、FRPGメッセージをサーバーに送信するゲーム関数を使用して、自分のクライアントにエクスプロイトを配信します。
概念実証では、RequestSendMessageToPlayers を使用して送信される PushRequestVisit メッセージを使用することにしました。これは、ターゲットのゲームが受信後すぐに脆弱なデータを解析する(メインメニューでも同様)という意味で、エクスプロイトの最も強力なバージョンです。