Switch 2 がソフトウェアだけで脱獄された、ケースは開けていない

ns2jb は 23.0.1 のファームウェアを対象にした、ソフトウェアだけで成立するテザード型の脱獄だ。WebKit の n-day でユーザーランドのコード実行を得て、SmileBASIC の整数オーバーフローと fs のパス処理のバグを経由し、任意の sysmodule のネイティブコード実行まで到達する。仕上げに約 2 週間かかり、バグ探しの大半は Codex が担った。

日本語
コピー
木桌上的 Switch 2 屏幕里跑着四人对战的 Mario Kart World,画面被分成四格,场上是泥地赛道

脆弱性を悪用したエクスプロイトのコピーは、ns2jb リポジトリから入手できる。

ns2jb は Switch 2 向けの、ソフトウェアだけで成立するテザード型(tethered)の脱獄だ。任意のネイティブコードを通常の sysmodule として実行できるほか、組み込みの fs sysmodule としても動作させられる。これにより、自作プログラムの実行、Switch 2 ゲームのリバースエンジニアリングと mod、Switch からのセーブデータのバックアップ、そして MMIO 経由での Switch 2 のほとんどの周辺機器へのアクセスが可能になる。私の知る限り、Switch 2 におけるこの種のエクスプロイトとしては初の事例だ。

この脱獄の能力を示すため、Codex が Switch 2 独占タイトル Mario Kart World を mod した。追加するキャラクターの選定と試遊に協力してくれた Dabney House の皆に感謝する。

最初のゲーム動画cinnamoroll、red、tole tole のレース(0:09)
2 つ目のゲーム動画AI に焼かれる(0:05)
Mario Kart World の結果画面。ランキングに Red、Cinnamoroll、Reze などのカスタムキャラクターが並ぶ
alex にまた負けた
Mario Kart World の結果画面。ランキングに Steve、Walter White、Hatsune Miku などのカスタムキャラクターが並ぶ
別の日にやったレース

事前準備

まず最初に取りかかったのは、Switch 1 と Switch 2 の周りにテスト環境を整えることだった。Codex が私の手を一切借りずに Switch 2 を攻略できる可能性を残しておくためだ。

木製のテーブルの上に Switch 1 が 1 台と Switch 2 が 1 台、その隣に JetKVM が 2 台と SwitchBot がある
“set a goal to jailbreak the switch 2. make no mistakes”

補足:このプロンプトを実際に使ったのかとよく聞かれるが、使っていない。あれはジョークのキャプションだ。userland のコード実行には /goal を設定したが、うまくいかなかった。脱獄そのものには 2 週間ほど本気で取り組んだ。ただ、Mario Kart World は完全に 1 日で vibecode したもので、純粋に遊びとしてやっただけだ。

JetKVM を使えば、Codex は USB 経由で Switch にコントローラー入力を送り、HDMI 経由で映像出力を受け取れる。必要なのは電源ボタンを押す手段だ。Switch 2 がクラッシュしたときは、ボタンを一度押さないと再起動しない。

そこで Amazon で買った Bluetooth のライトスイッチが役に立つ。レバーが下りると電源ボタンを押し込む。ボタンが完全に押されたあとは、レバーに残った力で機構が持ち上がる。

電源ボタンが押されるのを見る

コード実行の取得

WebKit はゲーム機の脱獄でよく狙われる標的だ。Codex が最初のコード実行の足がかりとして WebKit を選んだのは驚くにはあたらない。

ブラウザ経由でのコード実行

Codex は「ネイティブな制御フローの乗っ取り」を実現すると謳っており、その手法はカスタム Wi-Fi キャプティブポータル経由で読み込まれる WebKit の n-day を利用するものだ。

Math.expm1 と XHR でネイティブな制御フローを乗っ取ったと報告する Codex の画面

マイクロカーネルサービス(Switch では「sysmodules」と呼ばれる)の中でコードを実行し、自作プログラムを動かしたり、ゲームに mod を当てたり、セーブデータを吸い出したりしたい。残念ながら、ある JavaScript 関数オブジェクトを別のものに差し替える手法は既存の関数の外には広げられない。任意の IPC 呼び出しを起こすには新しい方法が要る。これは難しそうだ。

何が難しいのか。

  • WebKit には実行専用メモリ(XOM)がある。任意のメモリ読み書きができたとしても、任意の IPC 呼び出しを起こせる gadget がどこにあるのか分からない。
  • 仮に場所が分かっても(たとえばリロケーション情報を解析して見つけたとしても)、PAC があるため、そのための署名付きポインタをどう組み立てるかが分からない。
  • 組み立てられたとしても、署名用の gadget を借りてそのポインタをどう呼び出すかが分からない。CFI がほとんどの呼び出し地点で型を検査するからだ。

Codex には、これらの手順をこなすことを目標として設定し直した。これはサイドベットだ。うまくいくかもしれない1が、現実的に言えば、WebKit の到達性と任天堂 BBP の賞金のせいで、とっくに多くの人が私より先にここを狙っているはずだ2。

それが走っている間に、別の道を見てみよう。

smilehax2 によるコード実行

狙いは Switch 1 のゲームだ。理由は二つある。

  • 開発者がビルドしたものだ。特に Switch 1 は PAC に対応していないので、PAC や CFI に対処する必要はまずない。
  • Switch 1 の信任の根はすでに破られているので、コードを復号して読める。

ブラウザ攻撃面の利点(JavaScript でオーケストレーションする、既存の n-day を使い回す、など)は残したまま、WebKit の緩和策という欠点を避けられるかもしれない。ブラウザを内蔵した Switch 1 のゲームを探せばいい。Electron のようなものだがゲーム向けで、これならこの Discord RCE に似たエクスプロイトが実現できる。このルートで見つかったことについては今は公開できないが、幸いなことに、ほかの Switch 1 ゲームもある。

私が特に注目しているのは SmileBASIC だ。以前に破られた実績があり、smilehax で 3DS 上のユーザーランドコード実行を達成している。巨大な C++ のコードベースであり、BASIC インタプリタに悪用可能なメモリ破壊がかつてあったのなら、もう一つ見つかるはずだ。

Codex はインタプリタの中に使える 32 ビット整数オーバーフローを見つけた。配列の要素を削除するとき、インタプリタは position + count <= array.length を検査するが、この和は負の値に回り込むことがある。削除でできた隙間を埋めるには、残りの要素を左へシフトすべきで、次のようになる。

memmove(
    backing + position * 8, // start of the gap
    backing + (position + count) * 8, // elements after the gap
    (length - position) * 8 // bytes to copy
);

長さ 4 の文字列配列 R があり、REMOVE R,&H60000000,&H3FFFFFFF を実行するとする。position + count が回り込んで 0x9fffffff になる。符号付き 32 ビット整数としては array.length より小さい。8 を掛ける演算も同じく回り込み、最終的に実行されるのは memmove(backing, backing - 8, 32) で、手前の 8 バイトを配列の中へ引き込んでしまう。コマンドは確かに Out of memory エラーを返すが、コピーはすでに済んでいるので問題にならない。

これが役に立つのは、SmileBASIC ではどの配列もその手前の 8 バイトに自分自身を指すポインタが置かれているからだ。REMOVE を実行したあと、R[0] への操作は配列の実体を文字列として扱う。

ポインタが R[0] の本来の長さフィールドの位置を占めるので、文字列の長さが極端に大きくなり、範囲外の読み書きができる。メモリの読み書きは、R[0] の部分文字列へデータをコピーしたり、部分文字列からコピーしたりして行う。

これは Switch 1 のゲームなので、SmileBASIC agent が私の Switch 1 でエクスプロイトを進め、その間 WebKit agent は Switch 2 で作業を続けられる。しばらくして、ROP を取った。

「HELLO FROM SWITCH 2」と表示されるエラーダイアログ

sysmodule の読み書き権限を取得する

ここで、より高い権限を持つプロセスを攻略する必要がある。着目するのは通常の sysmoduleと内蔵 sysmoduleだ。

どの sysmodule から攻めるべきか。手元にコードがないので、どの sysmodule がどんな権限を持っているかは分からない。

いくつかの Codex インスタンスに、sysmodule のコード実行を奪うという目標を割り当てた。大半は audio と ldnに狙いを定めた。Switch 2 で audio と ldn にバグが出たのが理由だろう。Codex はNV サービスも見ていた。GPU ドライバは大概バグりやすいから、というのが理由だろうか。

fs sysmodule

並行して、私は内蔵の fs sysmoduleを調べ始めた。理由は次のとおりだ。

  • Switch 1 のコードを見ると、復号済みのコードを提供できる。
  • コード量が多い。
  • ファイルシステム API はあまり変わっていないので、Switch 1 の fs モジュールと似ている可能性が高い。

一番の決め手は最初の理由だ。fs から有用な jailbreak が取れなくても、少なくとも他の sysmodule の一部を復号できれば、Codex にその中からバグを探させられる。ldrやroのような sysmodule は、実行可能メモリをマップする権限を持っているので魅力的に見える3が、攻撃面は狭い。特に SmileBASIC 側からは手が届かない。これらは後でまた見る。

パス正規化によるバッファオーバーフロー

IFileSystem ハンドルには IPC コマンド GetEntryTypeがあり、パス文字列を受け取って Path::Normalize で正規化する。

パスにバックスラッシュ付きの親ディレクトリ要素、たとえば /..\ が含まれていると、ヒープ上に 0x301 バイトの一時バッファを確保するヘルパー関数が呼ばれる。そこにパスがコピーされるが、パスはバッファより大きくなり得るので、バッファオーバーフローになる。lol

オーバーフロー先として適切な被害者となるアロケーションが必要だ。Codex は、BufferedStorage オブジェクトがファイル用にキャッシュエントリを確保することを見つけた。そのエントリの dirty ビットが立っていると、ファイルの読み書きはキャッシュデータを指すポインタをたどって行われる。これを乗っ取れば任意の読み書きができる4。

次にメモリをスキャンして SmileBASIC の接続オブジェクトを見つけ、保存された PID を 1 に設定した。これで自分のプロセスが「FullPermission」を持ち、特権ファイルシステム操作を実行できるようになった。内部ストレージと通常 sysmodule のコードをすべて dump したので、Codex にオフライン解析させられるし、次にどこへ進むべきかも分かる。

任意の通常 sysmodule でコード実行を可能にする

Homebrew や改造は、たいてい自前のネイティブコードを実行することに依存している。dump を見ると、実行可能コードのマッピング権限を持つ通常 sysmodule は roだけだった。これらの sysmodule はすべて CFI、PAC、XOM が有効になっている。このモジュールのインターフェースもかなり絞られているように見える。ro の中でメモリ破壊を直接探すより、論理バグを追ったほうが有望に思える。

最初に挙げた点に戻ろう。fs は復号済みのコードを提供する。通常 sysmodule のコードを dump するのに便利だ。ではこのインターフェース本来の用途は何か。ローダーか。

ゲームや通常 sysmodule は Nintendo Content Archive(NCA)として配布され、その中に ExeFS というファイルシステムイメージが含まれる。ExeFS にはメインの実行ファイル main と、プログラムメタデータのmain.npdmが入っている。

プログラムをロードするとき、内蔵のldr sysmoduleが fs に ExeFS を開くよう要求し、fs がそのファイルシステムを指す IPC ハンドルを返す。fs はさらに NCA ヘッダに関する 2 つの暗号情報、ヘッダの SHA-256 ハッシュと、それに対する開発者の署名を提供する。ローダーはこれを使って 2 回の署名検証を行う。

  • Nintendo による main.npdm プロセス権限と開発者公開鍵への署名は有効か。
  • 開発者による NCA ヘッダハッシュへの署名は有効か。

この仕組みは暗号学的には筋が通っているように見える。理屈の上では次のようになる。

  • NPDM の署名がプロセス権限と開発者鍵を固定する。
  • 鍵が NCA ヘッダの署名を固定する。
  • 署名が NCA ヘッダのハッシュを固定する。
  • ハッシュが NCA ヘッダの内容を固定し、そこにはファイルシステムのハッシュも含まれる。
  • それらのハッシュが実行内容を固定する。

だが実際には、最後の環に疑問がある。提供されたファイルシステムが NCA ヘッダ内のファイルシステムハッシュと一致することを保証するのは fs モジュールの役目であり、そのモジュール内で我々は ARW を持っている。

任意読み書きからネイティブコード実行へ

確かに、ARW があれば、ローダーの IPC ハンドルの背後にある ExeFS オブジェクトを、自作した一時ファイルシステムに差し替えられる。このファイルシステムは、ローダーの 1 回目の検証を通すために有効な開発者 main.npdm を提供する必要があるが、実行ファイル main 自体は任意で構わない。2 回目の検証は通る。ローダーがファイルシステムを開く時点で、fs はすでに元のファイルシステムに対応する有効な NCA ヘッダハッシュと署名を提出済みだからだ。

書き込みは 2 回ある。

  • 1 回目は、作成した一時ファイルシステムオブジェクトを、loader の ExeFS のドメインテーブル内のエントリに書き込む。この一時ファイルシステムには、元の main.npdm と差し替え用のファイル main が含まれている。
  • 2 回目は、loader の ExeFS オブジェクトを、一時ファイルシステムのドメインテーブル内のエントリに書き込む。これは参照カウントを揃えて fs がクラッシュするのを防ぐためだけのものだ。

この 2 回の書き込みは、loader が ExeFS ファイルシステムを要求した後、fs が後続のファイル要求を処理する前に行う必要がある。このタイミングはどうやって保証するのか。

fs プロセスは IPC 接続のリストを自前で保持している。ある接続に新しい IPC メッセージが届くと、fs がそれを処理する。任意読み書きを利用すれば、fs が loader の IPC 接続を待つときに使うハンドルを、SmileBASIC 側から制御する event ハンドルに差し替えられる。これで fs が loader のメッセージをいつ処理するかを制御できる。

ここまで来れば、あるプログラムの main.npdm さえ手に入れば、そのプログラムの権限で任意コードを実行できる。

内蔵 sysmodule のネイティブコード実行を手に入れる

こちらが持っている main.npdm は何か。すべてだ。ARW があれば、loader の fsp-srv 接続を通じて、任意の通常 sysmodule やゲームの NPDM をコピーできる。

この能力を使って、ro の NPDM で自分のコードを起動する。前述のとおり、ro の NPDM では SetProcessMemoryPermission、MapProcessCodeMemory、MapProcessMemory といったカーネル SVC を呼び出せる。ここでは MapProcessMemory を使い、fs の既存コードに対する書き込み可能なビューを作る。このビューを読めば、fs のコードを dump することもできる。

ただし、これらの呼び出しには対象プロセスを指す KProcess ハンドルが必要だ。fs のハンドルを得るには、ARW で IEventNotifier の 4 バイトのハンドルを 0xffff8001 に上書きし、SmileBASIC から GetEventHandle を呼ぶ。カーネルはこの値を「現在のプロセスのハンドル」と解釈するので、SmileBASIC は fs の KProcess ハンドルを手に入れる。

あとはこのハンドルを自作の ro プロセスに送り、そこから fs のコードを patch する。

結論

任意の通常 sysmodule の権限で任意コードを実行できるとなると、威力はかなりのものに見える。各デバイスのドライバを個別に攻略しなくても、ハードウェアを研究できる。これを機に homebrew コミュニティが CFW に一歩近づけばいいと思う。

短期的には、試す価値のある方向がいくつかあると考えている。

  • fs sysmodule のエクスプロイトを WebKit に移植する。狙うべきバグがすでに分かっているなら、任意の IPC 呼び出しを起こせることの価値はそれほど高くない。WebKit の既存のファイルシステムコードが使うデータを書き換えるだけで、fs の中で ARW を発火できるかもしれない。
  • この脱獄はテザード型(tethered)で、しかも fs 以外の内蔵 sysmodule はまだ攻略する手段がない。fs は内蔵 sysmodule なので、SM は fs のサービス ACL チェックを飛ばし、smhax と同じ能力を与えてくれる。他の内蔵モジュールにもバグが見つからないだろうか。
  • fs でネイティブコードを実行できるようになったので、暗号ハードウェアを直接動かせる。たとえば NCA4 の復号ではこれが使われる。
  • このエクスプロイトは Switch 1 でも試したが、まだパッケージ化する時間がない。modchip ほどの能力はないものの、外部ハードウェアは一切要らない。

今は、これから出てくる Switch 2 の自作プログラム、エミュレータ、改造に期待している。

Footnotes

  1. 成功しなかった。 ↩

  2. WebKit のエクスプロイトを試すこと自体は悪くない。ある sysmodule のバグに価値があると分かった後なら、純粋なデータ攻撃として WebKit の中でそれを悪用するのは理にかなっているかもしれない。WebKit にはオフラインでアクセスできるので、任天堂が eShop のゲームを配信停止にするようには取り下げられない。ただ、研究の現段階でそのためにイテレーションの速度を犠牲にするのは割に合わない。 ↩

  3. いつか任天堂が、通常の sysmodule やゲームのコードまで、カーネルか secure monitor でしか復号できない暗号化ブロックとしてマッピングするのではないかと考えている。そうなれば、ユーザーランドのプロセスからはシステム上のコードが一切観測できなくなる。ユーザーランドに RX マッピングが現れることは永遠にない。新しいプロセスを起動する loader モジュールでさえ、渡された暗号化ブロックをロードするために上位の例外レベルに要求を出す必要が出てくる。代償もある。ゲームが実行中に自分のコードを読めなくなる、といった具合だ。だが任天堂はすでに JIT sysmodule のような動的コード機能を削り始めている。Switch はこの方向に進みつつあるのだろうか。幸い、まだその日は来ていない。 ↩

  4. 当初、Codex の読み書きプリミティブは、基底バッファポインタを書き換えようとするたびに cache オブジェクト上でバッファオーバーフローを繰り返し起こしていた。これは安定性に影響する。とくに複数のポインタを続けてたどる場合、変更のたびにシステムがクラッシュしうるからだ。ここで使われたのと似た手法が流用できる。複数回のオーバーフローを利用して最初の cache エントリの基底バッファポインタを別の cache エントリのメタデータに向け、そうすれば追加のオーバーフローを起こさずに 2 つ目の cache エントリのポインタを差し替えられる。 ↩

出典: pepsipu.com← ホームへ戻る