未来を形作るテクノロジーの深掘り記事。

Switch 2脱獄:セキュアブートを破った攻撃チェーン

Switch 2脱獄は、BASICインタプリタの整数オーバーフロー、fsのパス処理バグ、ローダーのレース条件を連鎖させた。その教訓を解説。

遠隔操作ハードウェアに接続された携帯ゲーム機と、作業台上で電源ボタンを押すロボットアーム型レバー。
最近のコンソール解析環境は、KVM-over-IPと、クラッシュのたびに電源ボタンを物理的に押すBluetoothレバーを組み合わせています。

出荷直後のコンソールには、ハードニングされたマイクロカーネル、すべてのリターンアドレスに対するポインタ認証、実行専用メモリ、制御フロー整合性(CFI)、そして署名付きのブートチェーンが備わっています。それから2週間後、一人の愛好家がファイルシステムサービス、つまり本体で最も信頼されたプロセスとして任意のネイティブコードを動かしていました。これが初の一般公開Switch 2脱獄の話で、セキュアシステムを作るエンジニアなら誰もが少し落ち着かなくなるはずです。緩和策が破られたからではありません。ほとんどの緩和策は設計通りに機能していました。問題は、攻撃者がそれらを正面から突破する必要がなかったことです。論理バグを一つずつ使って、迂回していったのです。

私は長年、インフラのセキュリティを防御側の立場で担当してきましたが、ここで見られるパターンは、本番環境のインシデントレビューで望む以上に何度も目にしてきたものです。個々には大したことのないバグが連鎖し、それぞれが次の段階に届くだけの権限をわずかずつ得ていくというパターンです。最初の足がかりはSwitch 2本体ではありませんでした。Switch 1向けゲーム、SmileBASICインタプリタのメモリ破壊バグで、それも後方互換モードで動いていたものです。そこから連鎖はファイルシステムサービスへ登り、ローダーのレース条件を突き、最終的にどのsysmoduleのIDでもネイティブコードを実行できる状態に至りました。どのリンクも検討に値します。サーバーサイドのシステムでもチームがよく犯すミスに対応しているからです。

ブラウザの扉が閉じていて、ゲームの扉が開いていた理由

WebKitはコンソール脱獄の定番の最初の標的で、研究者もキャプティブポータルと既知の脆弱性(n-day)を使って足がかりを得ました。しかしすぐに行き止まりになりました。Switch 2のブラウザスタックには、非常にうまく組み合わさる3つの緩和策が施されています。実行専用メモリ(XOM)があるため、任意の読み書きができても有用なコードガジェットを見つけられません。ポインタ認証(PAC)があるため、署名オラクルなしにコードポインタを偽造できません。そしてCFIがあるため、正しく署名されたポインタでも、ほとんどの呼び出し箇所で型チェックを通過できません。どれか一つを破るだけでも研究プロジェクトになります。3つすべてを連鎖させ、しかもJavaScriptのサンドボックスから破るのは、ほとんどキャリアを懸けた仕事です。

そこで攻撃は、本体で最も防御の弱いテナント、つまりサードパーティ製のSwitch 1ゲームに方向転換しました。その判断は冷静で正しいものでした。ゲームのバイナリはNintendoのセキュリティチームではなくスタジオが作るため、緩和策フルセットでコンパイルされている可能性がずっと低いのです。さらにSwitch 1にはそもそもPACがなく、その互換環境で動くものはその脆弱性を引き継ぎます。加えてSwitch 1のルートオブトラストは数年前に破られているため、ゲームコードは復号してオフラインで解析できます。3DSで既に攻撃されていた巨大なC++製BASICインタプリタ、SmileBASICは、明らかに柔らかい標的でした。

この話はコンソールの枠を超えて当てはまる教訓があります。あなたのセキュリティ態勢は、実行を許容している中で最も硬化されていないコードのセキュリティ態勢に等しい。後方互換性は、自ら選んだのではなく引き継いだ攻撃対象領域です。Xboxのセキュリティモデルは2000年代に苦い経験からこれを学びました。未硬化のタイトルにおけるセーブデータの脆弱性が、はるかに堅牢なカーネルをこじ開ける楔になったのです。この件は初代Xboxハックがハードウェアセキュリティモデルについて教えることで取り上げました。20年経った今も、同じ手口は通用します。

最初の扉を開いた整数オーバーフロー

SmileBASICのバグは、境界チェックにおける教科書的な整数オーバーフローです。セキュアコードのレビューチェックリストが必ず捕まえろと書いてあるタイプのものであり、それでも出荷され続けてしまうタイプでもあります。インタプリタの配列削除コマンドは、引数を次のように検証していました:

// intent: ensure the removed range is inside the array
if (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の配列に対してREMOVE R, &H60000000, &H3FFFFFFFを呼び出すと、position + countが0x9FFFFFFFにラップアラウンドします。これは符号付き32ビット値としては大きな負の数になり、配列の長さより「小さい」と判定されます。8による乗算もラップし、結果としてmemmove(backing, backing - 8, 32)が実行されてしまいます。コマンドは後でメモリ不足エラーを表示しますが、コピーはすでに行われています。そしてさらに巧妙なのは、SmileBASICの配列の直前8バイトには配列自体へのポインタが入っていることです。削除後、R[0]は長さフィールドが実際にそのポインタである文字列として解釈されます。その結果、読み書きできる途方もなく長い文字列ができあがります。インタプリタ自身のメモリレイアウトが、任意の読み書きをご丁寧にプレゼントしてくれたのです。

もしあなたがC言語やC++で仕事をしているなら、そのチェックを少し見つめてみてください。position + count <= lengthは、システムコードで最もよく使われる境界チェックの慣用句ですが、合計がオーバーフローする場合には間違っています。正しい形はposition <= length && count <= length - position、またはチェック付き演算です。私は前の職場のパケットパーサーや、あるオープンソースのストレージエンジンで、この種のバグの亜種を見つけたことがあります。これがコードレビューを通ってしまうのは、一見すると明らかに正しく見えるからです。これは仕様と実装のギャップの縮図です。仕様には「範囲を検証せよ」と書かれていても、実装は範囲の隣接部分を検証していて、その差が悪用可能になるのです。仕様と実装のギャップこそバグが潜む場所であるという記事も書きました。コンソールのインタプリタは、ファジングツールではなく意欲的な若者たちによってそれが見つけられる場所なのです。

パス処理のオーバーフローでファイルシステムサービスへ権限昇格する

ゲームプロセス内のユーザー空間での読み書きは、それだけでは何の役にも立ちません。Switchのマイクロカーネル設計では、すべてのサービスが独立したプロセスで、それぞれ独自のIPCの表面を持つためです。価値のある標的はfs、つまり組み込みのファイルシステムsysmoduleでした。その理由は実に再帰的です。fsは復号済みのコードをローダーに提供するコンポーネントだからです。fsを掌握すれば、他のすべてのsysmoduleのコードをオフライン解析用にダンプできます。要塞の兵站係が、設計図をそのまま手渡してくれるようなものです。

バグはパスの正規化処理におけるヒープオーバーフローでした。GetEntryTypeのIPCコマンドはパス引数を正規化しますが、バックスラッシュを伴う親ディレクトリ要素、/..\を含むパスは、固定の0x301バイトのバッファを確保するヘルパーに流れ込み、十分な長さチェックなしにパスをそこへコピーします。十分に長いパスは、隣接するヒープオブジェクトへバッファをオーバーフローさせます。研究者がこれを発見したときのコメントは、「lol」という一語だけだったそうです。

その「lol」は痛いところを突いています。パスの正規化は、暗号処理が受けるようなセキュリティの精査を決して受けない、地味な配管コードの典型だからです。攻撃者は隣接するキャッシュオブジェクトを破壊し、そのダーティビットによってファイルI/Oを攻撃者が制御するポインタ経由にリダイレクトさせることで、オーバーフローを完全な任意読み書きに変えました。これは見事なヒープグルーミングのプリミティブです。その後、メモリスキャンによって、エクスプロイトは自身のプロセスに対応するfsのテーブル内の接続オブジェクトを見つけ、保存されたプロセスIDを1に書き換えました。これによってゲームプロセスは、ファイルシステムに対する「FullPermission」を得ました。何が起きたかに注目してください。暗号チェックは一つも破られていません。権限モデルはメモリ上のデータによって強制されていて、攻撃者はそのメモリに書き込めたのです。

多層の要塞の断面等角図。小さな人物が狭い通用路を通って施錠されたゲートを迂回している様子。
攻撃チェーンの各段階は、誰も警備していなかった通路を通ることで、硬化された扉を迂回しました。

鍵に一切触れずに、飛行中のファイルシステムを入れ替えて署名チェックを突破する

チェーンで最も巧妙なリンクは、鍵に触れずにコード署名を回避するものです。プログラムローダー(ldr)は、何かを実行する前に二つの署名を検証します。プロセスの権限メタデータに対するNintendoの署名と開発者鍵、そして、コンテンツアーカイブのヘッダーハッシュに対する開発者の署名です。理論上、そのハッシュは実行ファイルのすべてのバイトを推移的に固定します。しかし実際にはギャップがあります。提供するファイルシステムが、すでに報告したハッシュと一致していることを保証するのはfsの仕事であり、攻撃者はfsの内部で任意の読み書きができたのです。

そこでエクスプロイトは、ローダーが実行ファイルのファイルシステムを要求し、fsがすでに正当なハッシュと署名を渡し終えるまで待ちます。その後、ローダーのIPCハンドルの背後にあるファイルシステムオブジェクトを、正規の署名付きメタデータファイルは返すものの、任意の差し替え実行ファイルを提供する細工されたものに入れ替えます。署名チェックは通ります。攻撃者が変更する必要のなかったバイトに対するチェックだったからです。タイミングは、fsがローダーの接続で待機しているハンドルを、ゲームプロセスから攻撃者が制御できるイベントに書き換えることで制御されます。レース条件が、決定的な割り込みに変わるのです。

暗号のチェーンは、ヘッダーハッシュから実行ファイルの内容に至るまで、紙の上では健全でした。ただ一つ、「ファイルを提供しているプロセスは正直である」という実行時の前提としてしか存在していなかったリンクが、破られたのです。

これは外套を羽織った TOCTOU(検査時と使用時の不一致)バグで、この解説の中で最も応用の利く教訓です。コンポーネントAがデータを検証し、コンポーネントBがその後、可変な参照を通じてそれに基づいて動くなら、そこには入れ替えの窓が存在します。以前、内部のデプロイシステムをレビューしたことがあります。コンテナイメージのダイジェストは検証していたのに、ロールアウト時にはレジストリからタグ指定でプルしていました。同じバグが別の帽子をかぶっていたのです。どちらの場合も修正は同じです。使用時点で検証するか、消費者には、第三者が正直であり続けることを信頼しなければならないハンドルではなく、不変で検証済みのオブジェクトを渡すことです。すべての仲介者が侵害されている前提で設計することはパラノイアではありません。攻撃者がすでに部分的に書き込めるマシンでは、それは単に正確なモデリングです。これは壊滅的な事態に耐えるシステムのための防御的エンジニアリングの中心的な主張であり、このエクスプロイトはその格好の事例研究です。

カーネルハンドルを借りて内側から fs にパッチを当てる

最後の手口は小さく、そして見事です。fsプロセス自身のコードにパッチを当てるには、そのためのカーネルハンドルが必要でした。エクスプロイトは、メモリ内の4バイトのハンドルフィールドをマジックバリュー0xffff8001で上書きすることでそれを得ました。カーネルはこの値を「現在のプロセスのハンドル」として解釈します。ゲームプロセスは無害なAPIを呼び出し、自分自身のプロセスハンドルを受け取り、それをコードローディングのsysmoduleの権限で動く不正なプロセスに渡しました。そのプロセスは正規のカーネル呼び出しを使ってfsのコードをマップしてパッチを当てました。個々の操作はすべて認可されていました。その組み合わせが、デバイス上で最も信頼されたプロセスの完全な侵害になったのです。

これは聞き覚えがあるかもしれません。一度見始めると、混乱した代理人(confused deputy)問題はあちこちにあります。書き込み可能な設定を持つ無害なバイナリを許可するsudoルール、より強力なサービスに自分自身を渡せるクラウドのIAMロール、CIジョブが渡されたものすべてに署名するビルドシステムなどです。このパターンの権限昇格側面は、Unixの権限昇格の歴史で扱いました。仕組みは変わっても、バグの形は変わりません。ハンドル、ケイパビリティ、トークンは、それを格納しているメモリの整合性と同じ強さしか持ちません。

セキュアブートの設計者が繰り返し間違えること

全体を引いて眺めると、チェーン全体に一貫したパターンがあります。それは私がレビューしてきた深刻な企業侵害のほとんどの背後にあるのと同じパターンです。検証済みのコアは持ちこたえたのに、その周囲はそうではなかった。Nintendoのブートロム、署名方式、カーネルの緩和策はどれも問題ありませんでした。破られたのは、インタプリタの境界チェック、パス正規化のバッファ、書き込み可能なメモリに格納された権限フィールド、二つの信頼されたサービス間のTOCTOUの窓、そしてユーザー空間から書き込み可能な構造体からカーネルポインタを受け取るハンドルテーブルでした。どれも特殊なものではありません。どれも、個別に見ればそれなりに合理的に見えるため、レビューを通過してしまう類のものです。

  • 地味なコードを監査せよ。パスの正規化、長さチェック、ハンドルテーブルこそ、攻撃チェーンが実際に始まる場所であり、暗号ではありません。レビューの時間は、アルゴリズムの高度さではなく、攻撃者が到達できる範囲に比例して配分してください。
  • すべての緩和策の欠落を攻撃ロードマップとして扱え。システム上のあるプロセスにPAC/CFIがない場合(互換レイヤー、サードパーティ製バイナリ、レガシーインタプリタなど)、それを侵入口と想定し、そこから何に到達できるかをモデル化してください。
  • 位置による信頼を排除せよ。データを検証した後に可変なハンドルを下流に渡すコンポーネントは、入れ替えの窓を作り出しています。不変で自己検証可能なオブジェクトを渡すか、使用時に再検証してください。
  • 攻撃者が書き込めるメモリは、どこであれすべてのメモリに書き込めると仮定せよ。攻撃者が到達可能なメモリに格納された権限フラグ、PIDフィールド、ハンドル値は、強制のための仕組みではなく、攻撃対象領域の一部です。

もう一つの居心地の悪い教訓はツールです。このエクスプロイトは、KVM-over-IPデバイスを通じてハードウェアへの物理アクセスを与えられたAIエージェントによって開発されました。映像の入力、USB経由のコントローラー入力、そしてクラッシュのたびに電源ボタンを押すBluetoothレバーのガジェットがありました。複数のエージェントのインスタンスが、二週間にわたって昼夜を問わず、異なる攻撃対象領域を並行して調べていました。AIを活用したセキュリティ研究をどう評価するにせよ、クローズドなハードウェアにおける脆弱性発見の経済性は変わってしまいました。以前は根気強い専門家が必要だったバグも、疲れを知らない三人のアシスタントを監督できる根気強い専門家が必要になっただけです。「このsysmoduleをリバースエンジニアリングする人なんていない」という前提で脅威モデルを組んでいたなら、その前提は今月で期限切れです。

チェーンこそが成果物

脱獄の記事を、Nintendoの失敗のリストとして読みたくなるのは自然です。しかし、それを鏡として読んでみてください。私がこれまで防御を手伝ってきたすべてのシステムには、SmileBASICに相当するもの(誰も硬化しなかったレガシーコンポーネント)、パスバッファに相当するもの(想定を超えて成長した配管コード)、そしてローダーのレースに相当するもの(可変な仲介者を信頼していた検証)がありました。攻撃者の強みは、どの一つのステップでの賢さでもありません。攻撃者はチェーンで考えるのに対して、防御側はコンポーネントで考えるという点にあるのです。

ですから、今週のチームへの宿題は「誰かが私たちの署名を偽造できるか」ではありません。最も信頼された内部サービスを選び、そのサービスと通信する、最も硬化されていないプロセスで攻撃者がすでに任意の書き込みを持っていると仮定して、チェーンを前へたどってみることです。最初の権限昇格はどこに着地するか。その身元で何に到達できるか。やがて自分たちのfs sysmoduleも見つかるでしょう。KVM、Bluetoothレバー、そして二週間の忍耐を持った誰かより、先に見つけるのはあなたであるべきです。