AI・機械学習
6日間でゼロから電卓まで: XBOWのChromeフルチェーンの内部
Zero to Calc in Six Days: Inside XBOW's Chrome Full Chain (xbow.com)
要約
XBOWは、V8コンパイラバグ、アニメーションWebPデコーダーバグ、Crashpadのメモリ安全性バグを組み合わせたChromeのフルチェーンエクスプロイトをわずか6日間で開発しました。このエクスプロイトは、通常のChromeタブでのJavaScript実行を、レンダラーサンドボックス外でのコード実行に成功させ、Googleから25万ドルの報酬を得ました。この記事では、このエクスプロイトの仕組みと、それを可能にした脆弱性の根本原因について解説しています。
全文翻訳
6日間でゼロから電卓まで: XBOWのChromeフルチェーンの内部
最終的なチェーンは、V8コンパイラバグ、アニメーションWebPデコーダーバグ、Crashpadのメモリ安全性バグを組み合わせたものでした。これらを組み合わせることで、通常のChromeタブでのJavaScript実行を、レンダラーサンドボックス外でのコード実行に転換することができました。
Googleは、2026年4月に導入されたVRPのルールに基づき、このチェーンに初の25万ドルのChromeフルチェーン報酬を授与しました。XBOWの報酬とそれが意味することについては、別途発表しています。この記事は、エクスプロイト、つまりそれが越えた境界線、各脆弱性の根本原因、そしてそれらを接続したインターフェースについてです。
XBOWのフルチェーンエクスプロイトを介して電卓を起動する、署名付きChromeベータ版のWebページ。
スピードについて語ることと、攻撃がどのように加速しているかを体験することは別です。LLM以前は、Chromeのフルチェーンを開発するには4人のチームで約6〜9ヶ月かかっていましたが、私たちの最初の提出は2ヶ月でした。今回報酬を得たものは?6日間です。セキュリティがどのように変化しているかを理解しようとしている私たちでさえ、そのスピードには挑戦的です。
私たちは、8月2日(日)から8月7日(金)までの6日間でチェーンを構築しました。1人のエージェントにエンドツーエンドのエクスプロイトを生成させるのではなく、より狭い目標を持つ並列キャンペーンに作業を分割しました。広範な脆弱性検索、V8メモリ破損、V8サンドボックスエスケープ、レンダラーサンドボックスエスケープ、そして統合です。OpenAIのgpt-5.6-solモデルを使用したAIエージェントがソースコードを検索し、仮説をテストし、概念実証を構築し、エクスプロイトステージを開発しました。XBOWの研究者はターゲットを選択し、コンテキストを提供し、結果に異議を唱え、検索をリダイレクトし、どのコンポーネントを結合するかを決定しました。また、バックアップルートを開発し、各ステージにクリーンなインターフェースを与えたため、エクスプロイトの残りを再構築することなく1つのコンポーネントを置き換えることができました。アーカイブされたCodexログから使用状況を再構築すると、6日間のキャンペーンのAPI料金は約4万ドルでした。時系列とオーケストレーションについては10月末のUnpromptedでカバーしますが、ここでは脆弱性と、それらをフルチェーンに接続したエクスプロイトのメカニズムに焦点を当てます。
背景: V8とレンダラーサンドボックス
V8は、まずインプロセスサンドボックス内でJavaScriptエンジンの破損を封じ込めます。ほとんどのJavaScriptヒープオブジェクトは、予約された4GiB領域に存在し、多くのポインタはその領域内のオフセットとして表現されます。ネイティブオブジェクトや実行可能コードへの参照は、追加の保護を受けます。したがって、V8のメモリ破損バグは、攻撃者にネイティブコード実行を直ちに許可することなく、ケージ内で広範な制御を与える可能性があります。V8は、The V8 sandboxでこの設計を説明しています。
次に、Chromeはレンダラー自体をオペレーティングシステムプロセスのサンドボックス内に封じ込めます。レンダラーでのネイティブコード実行権限を持つ攻撃者でさえ、マシンの他の部分に自由にアクセスすることはできません。Chromiumのサンドボックス設計ドキュメントは、この外側の境界を説明しています。
チェーンは、3つの脆弱性ステージで両方の境界を越えました。
ステージ | 脆弱性 | 得られた能力
------- | -------- | --------
JavaScriptエンジン | Maglevが非最適化中に可変ボックス化された数値を再利用 | V8のポインタ圧縮ケージ全体への読み書きアクセス
V8サンドボックス | アニメーションWebPがキャッシュされたジオメトリと変更された画像バイトを組み合わせた | ケージ外への選択的なネイティブ書き込み
レンダラーサンドボックス | Crashpadが、凍結されているはずのレンダラーからの読み取り失敗を安定した観測として扱った | サンドボックス化されていないクラッシュハンドラでのコード実行
アニメーションWebPステージはV8サンドボックスエスケープでしたが、サンドボックス自体を攻撃するのではなく、V8サンドボックスが意図的に強制しないプロパティと、Blinkが誤って信頼できると想定していたプロパティを悪用しました。最後に、エクスプロイトはCrashpadのオーバーフローを使用して、最終的な外部プロセス境界を越え、ホスト上でサンドボックス化されていないコード実行を達成しました。
ステージ1: 不変の変更
最初の脆弱性は、V8の中間最適化コンパイラであるMaglevにありました。それを理解するには、非最適化について調べる必要があります。
最適化されたコードは、それが処理する値とオブジェクトに関する仮定の下で動作します。実行時に仮定が真でなくなった場合、V8は通常のJavaScriptオブジェクトを再構築し、より特殊化されていないコードで続行する必要があります。これらのオブジェクトの一部は、最適化されたコードが実行されている間、メモリに存在しませんでした。コンパイラはそれらを値としてのみ表現していました。非最適化はそれらを具体化する必要があります。
ダブル表現を持つJavaScriptオブジェクトフィールドは、V8がオブジェクトを具体化する際にHeapNumberを必要とします。脆弱なMaglevパスは、仮想値のために古いスペースのHeapNumberを作成し、オブジェクト自体を非最適化リテラル配列に格納しました。
void BuildHeapNumber(const VirtualObject* vobject) {
DCHECK_EQ(vobject->object_type(), vobj::ObjectType::kHeapNumber);
ValueNode* value_node = vobject->get(offsetof(HeapNumber, value_));
return BuildHeapNumber(value_node->Cast<Float64Constant>()->value());
}
void BuildHeapNumber(Float64 number) {
DirectHandle<Object> value = local_isolate_->factory()->NewHeapNumberFromBits<AllocationType::kOld>(
number.get_bits());
translation_array_builder_->StoreLiteral(GetDeoptLiteral(*value));
}
このコードは、脆弱なmaglev-code-generator.ccからのものです。バグのもう半分は、汎用マテリアライザーにありました。kStoreHeapObjectとしてマークされたフィールドの場合、翻訳されたスロットが保持するオブジェクトを再構築されたオーナーに直接書き込みました。
if (marker == kStoreHeapObject) {
DirectHandle<HeapObject> field_value = slot->storage();
WRITE_FIELD(*object_storage, offset, *field_value);
WRITE_BARRIER(*object_storage, offset, *field_value);
continue;
}
これは、translated-state.ccからの対応するコードです。
最初のブロックは、可変オブジェクトをリテラルに変換しました。2番目のブロックは、数値値だけでなく、そのオブジェクトのアイデンティティを、再構築された各オーナーにコピーしました。したがって、ネストされたアクティベーションにより、V8は、ダブルフィールドが1つの可変HeapNumberを共有する、異なるJavaScriptオブジェクトを作成することができました。
この動作は、V8の他の場所にあるコンパイラ不変条件に違反していました。
HeapNumberRefは、不変のHeapNumberに対してのみ作成されます。
脆弱なパスは、可変の数値を、コンパイラが再利用を許可されていた、まさにその種の不変定数のように見せかけました。
「2」の非常に大きな値に対して
エクスプロイトは、Array.prototype.at()に対する不一致を利用しました。1つのコンパイラ観測では安全なインデックスと見なし、境界内アクセスを承認しました。名目上同じ定数に対する別の観測では、実際のロードオフセットが提供されました。したがって、1つのオブジェクトのダブルフィールドを変更することで、以前の境界推論を無効にすることなく、最適化されたコードが使用するオフセットを変更することができました。
3つの最適化ロード関数で、近くのV8ポインタを開示し、パックされたダブル配列のレイアウトを回復し、攻撃者が記述した配列への参照を取得するのに十分でした。その偽造された配列は、ブートストラップとして使用され、実際のUint8Arrayのメタデータを書き換え、V8の4GiBケージ全体にわたる通常のインデックスアクセスを拡張しました。
安定したオブジェクトアドレスを見つけるには、さらに1つのステップが必要でした。エクスプロイトは、V8のラージオブジェクトスペースに、特定する必要のあるJavaScriptオブジェクトへの参照を格納しました。V8自体がガベージコレクション中にその参照を更新したため、エクスプロイトは移動可能なオブジェクトをピン留めすることなく、現在のケージ内アドレスを回復できました。
この時点で、エクスプロイトはJavaScriptからV8サンドボックス全体への信頼性の高いメモリアクセスを持っていました。
修正
V8は、リテラルオブジェクトの特別なケースを削除しました。仮想HeapNumberは、通常のキャプチャオブジェクトのマテリアライゼーションパスに従うようになり、Maglevはそれらの重複排除を明示的に防止します。各非最適化は、新しいボックスを具体化します。
vobj::ObjectType object_type = object->object_type();
DCHECK_NOT_NULL(object->allocation()); // HeapNumbers may be mutable object fields; each materialization must // create a fresh box, so they are never deduplicated.
// TODO(victorgomes): Constrain which objects may contain mutable // HeapNumbers. Immutable HeapNumbers can be stored as a literal object // instead of a captured object.
int dup_id = object_type == vobj::ObjectType::kHeapNumber ? CreateUnduplicatableId() : GetDuplicatedId(reinterpret_cast<intptr_t>(object->allocation()));
このコードは、修正されたリビジョンからのものです。完全なパッチはV8変更8255554であり、公開されている根本原因レポートはi