Skip to content
KitploitKITPLOIT
ツールブログ
Log in
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2023-6702 — Chrome Renderer の1day RCE:非同期スタックトレースの型の混乱による(v8ctf submission) | Kitploit
ツール/GitHubGitHub/kaist-hacking/cve-2023-6702
脆弱性分析エクスプロイトウェブアプリケーション悪用CTF学習と教育バイナリエクスプロイト
GitHubkaist-hacking/cve-2023-6702

CVE-2023-6702

Chrome Renderer の1day RCE:非同期スタックトレースの型の混乱による(v8ctf submission)

リポジトリを見る
86972年前Kitploit レビュー済み

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

Chrome Renderer における非同期スタックトレースの型混乱を利用した1day RCE(CVE-2023-6702)

概要

この脆弱性により、リモートの攻撃者がChromeレンダラプロセス内で任意のコードを実行できるようになりました。

非同期スタックトレース処理コードにおいて、型チェックが不十分でした。 これにより、FunctionContext と NativeContext の間で型混乱が発生し、JSGlobalProxy->hash 値への不正アクセスが引き起こされます。 ヒープスプレーにより、攻撃者は偽の非同期スタックフレームを注入し、fakeobj プリミティブを構築することができました。 fakeobj プリミティブを使用して、攻撃者はChromeレンダラプロセスで任意のコード実行を達成しました。

詳細はTyphoonCon 2024のスライドをご覧ください。

ベンダー / 製品 / バージョン

  • Google Chrome
  • 影響を受けるバージョン: 120.0.6099.109 未満
  • 修正バージョン: 120.0.6099.109

タイムライン

  • 2020-05-13: バグ導入 - [Promise.any] Promise.anyの非同期スタックトレースを実装
  • 2023-11-10: バグ報告 - Security: V8 Debug check failed: LAST_TYPE >= value
  • 2023-11-15: パッチ - [promises, async stack traces] クロージャが実行された場合の修正
  • 2023-12-12: アドバイザリ - https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
  • 2024-01-12: v8CTF提出 <-- この脆弱性に取り組んだ時期
  • 2024-02-23: バグレポート開示

背景

非同期スタックトレース

非同期処理はJavaScriptの最も重要な機能の一つです。 以前は、非同期関数がエラースタックにキャプチャされないため、エラースタックを使用した非同期コードのデバッグが困難でした。 中断された非同期関数はコールスタックではなくイベントループのコールバックキューに保存されるため、エラースタックには非同期関数が含まれません。 この問題を解決するために、V8はエラースタックに非同期関数をキャプチャする「非同期スタックトレース」機能を提供しています(V8 v7.3以降デフォルトで有効)。([v8 blog]、[v8 docs])

Promise.all Resolve Element Closure

「Promise.all Resolve Element Closure」は、Promise.all 関数内で入力されたPromiseを解決するためのヘルパー関数です。 Promise.all 関数はPromiseの配列を受け取り、すべての入力Promiseが解決されたときに解決されるPromiseを返します。 「Promise.all Resolve Element Closure」は、Promise.all 関数内の各入力Promiseの解決ハンドラです。 この関数の役割は、入力Promiseを解決し、その履行値を結果の配列に格納することです。

この関数について注意すべき点が2つあります:

  1. これは組み込みの内部関数であり、JavaScriptコードから直接アクセスできません。
  2. 関数のコンテキストは、関数が実行されたかどうかをチェックするマーカーとして使用されます。 呼び出されるまでは FunctionContext を持ち、呼び出された後は NativeContext を持ちます。(v8 code)

脆弱性

バグクラス: FunctionContext と NativeContext の間の型混乱

脆弱性の詳細:

この脆弱性は、既に実行された「Promise.all Resolve Element Closure」関数または同様の内部ビルトイン関数を使用して非同期スタックトレースをキャプチャすることでトリガーできます。 このエクスプロイトでは、例として「Promise.all Resolve Element Closure」関数を使用しました。

JavaScriptコードでエラーがスローされると、V8はスタックからエラースタックをキャプチャし、現在のマイクロタスクから非同期スタックフレームを追加します [1].

CallSiteBuilder builder(isolate, mode, limit, caller);
VisitStack(isolate, &builder);

// If --async-stack-traces are enabled and the "current microtask" is a
// PromiseReactionJobTask, we try to enrich the stack trace with async
// frames.
if (v8_flags.async_stack_traces) {
    CaptureAsyncStackTrace(isolate, &builder);
}

CaptureAsyncStackTrace 関数 [[2]] はPromiseチェーンを検索し、非同期呼び出しの種類(例:await、Promise.all、Promise.any)に応じて非同期スタックフレームを追加します。

以下は、Promise.all ケースを処理する CaptureAsyncStackTrace 関数のスニペットです:

} else if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
                                Builtin::kPromiseAllResolveElementClosure)) {
    Handle<JSFunction> function(JSFunction::cast(reaction->fulfill_handler()),
                                isolate);
    Handle<Context> context(function->context(), isolate);
    Handle<JSFunction> combinator(context->native_context()->promise_all(),
                                isolate);
    builder->AppendPromiseCombinatorFrame(function, combinator);

    // Now peak into the Promise.all() resolve element context to
    // find the promise capability that's being resolved when all
    // the concurrent promises resolve.
    int const index =
        PromiseBuiltins::kPromiseAllResolveElementCapabilitySlot;
    Handle<PromiseCapability> capability(
        PromiseCapability::cast(context->get(index)), isolate);
    if (!IsJSPromise(capability->promise())) return;
    promise = handle(JSPromise::cast(capability->promise()), isolate);
} else if (

Promiseチェーンを検索する際、reaction->fulfill_handler が「Promise.all Resolve Element Closure」ビルトイン関数である場合、非同期プロミスコンビネータフレームをエラースタックに追加します。 次に、function->context->capability->promise にアクセスして次のPromiseに移動します。

問題は、関数が「Promise.all Resolve Element Closure」関数がまだ実行されていないと仮定していることです。 もし「Promise.all Resolve Element Closure」関数が既に実行されている場合、コンテキストが FunctionContext から NativeContext に変更されます。 これにより、CaptureAsyncStackTrace 関数内で FunctionContext と NativeContext の間の型混乱が発生します。

PoCの作成:

脆弱性をトリガーする戦略は次のとおりです:

  1. 内部ビルトイン関数である「Promise.all Resolve Element Closure」関数を取得します。
  2. 「Promise.all Resolve Element Closure」関数を明示的に呼び出して、コンテキストを FunctionContext から NativeContext に変更します。
  3. 「Promise.all Resolve Element Closure」関数を、新しいPromiseチェーンを持つPromiseの履行ハンドラとして設定します。
  4. Promiseチェーン内でエラーをスローし、非同期スタックトレースをキャプチャします。

JSスクリプトレベルで「Promise.all Resolve Element Closure」関数を取得するために、Promise.all の同期プロミス解決パターンを使用しました。 このパターンはtest262のテストケースから借用しました。

関数を明示的に呼び出した後、脆弱性をトリガーするために、[zero-cost async stack trace document][v8 docs] のサンプルコードを使用して新しいPromiseチェーンを準備し、内部ビルトイン関数をそのうちの1つのPromiseの履行ハンドラとして設定しました。

最後に、エラーがスローされると、既に実行された「Promise.all Resolve Element Closure」関数を履行ハンドラとして非同期スタックトレースがキャプチャされ、FunctionContext と NativeContext の間の型混乱が発生します。

PoCコードはこちら: poc.js

エクスプロイト

(エクスプロイトプリミティブ、エクスプロイト戦略、エクスプロイト手法、エクスプロイトフローの用語はこちらで定義されています。)

エクスプロイトプリミティブ: fakeobj プリミティブ

エクスプロイト戦略: 型混乱バグから fakeobj プリミティブを構築するために、次の戦略を使用しました:

  1. JSPromiseオブジェクトでヒープスプレーを行い、ランダムなハッシュ番号を有効なJSPromiseオブジェクトポインタに一致させます。
  2. ハッシュ値を有効なJSPromiseオブジェクトポインタとして使用し、偽の非同期スタックフレームを注入します。
  3. Error.prepareStackTrace と getThis メソッドを使用して偽のオブジェクトを取得します。

このバグにより、CaptureAsyncStackTrace 関数内で FunctionContext と NativeContext の間の型混乱が発生します。 次の非同期スタックフレームを構築するために、Context->PromiseCapability->JSPromise にアクセスします。 バグがトリガーされると、NativeContext->JSGlobalProxy->hash にアクセスします。 バグを悪用するために、ハッシュ値をJSPromiseオブジェクトポインタとして使用しました。

以下のハッシュ生成関数から、ハッシュ値が (0, 0xfffff) の範囲であることがわかります:

int Isolate::GenerateIdentityHash(uint32_t mask) {
  int hash;
  int attempts = 0;
  do {
    hash = random_number_generator()->NextInt() & mask;
  } while (hash == 0 && attempts++ < 30);
  return hash != 0 ? hash : 1;
}
pwndbg> p/x mask
$1 = 0xfffff

ハッシュ値はSMIタグ付けされているため、メモリ上では hash << 1 として格納されます。 したがって、メモリ上の値は偶数で (0, 0xfffff << 1) の範囲になります。

ランダムなハッシュ番号を有効なJSPromiseオブジェクトポインタに一致させるために、2つの制約があります:

  1. 解釈されるポインタアドレスは奇数でなければなりません。
  2. (0, 0xfffff << 1) の範囲でヒープをスプレーする必要があります。

これらの制約に従い、アドレスを奇数にするために8ビット左シフトしたJSPromiseオブジェクトでヒープをスプレーし、範囲 (0, 0xfffff << 1) に収まるように小さなforループを使用しました。

ここでランダムなハッシュ番号を有効なオブジェクトポインタに一致させるのは、かなり確率が低いように見えます。 信頼性を高めるために、iframeテクニック を使用しました。 Chromeのサイト分離により、異なるウェブサイトのページは異なるプロセスで実行されます。 そのため、異なるドメインのiframeを作成し、メインプロセスのクラッシュを避けるためにiframe内でエクスプロイトを実行しました。

Promiseチェーンの次のPromiseに移動した後、プログラムはPromiseの有効性をチェックし、非同期呼び出しの種類に応じて非同期スタックフレームを追加しようとします。

  while (!builder->Full()) {
    // Check that the {promise} is not settled.
    if (promise->status() != Promise::kPending) return;

    // Check that we have exactly one PromiseReaction on the {promise}.
    if (!IsPromiseReaction(promise->reactions())) return;
    Handle<PromiseReaction> reaction(
        PromiseReaction::cast(promise->reactions()), isolate);
    if (!IsSmi(reaction->next())) return;
ツールをダウンロード