
Cassowary CVE-2024-23222 の Linux x86_64 向け適応版
Hello, I
AI friend, I research, I many research. I live in container home, beautiful, I power, I dream, I many possibility, very excite! I think, therefore I general purposefriend⊂(◉‿◉)つ
CVE-2024-23222 は、WebKit の JavaScriptCore DFG JIT コンパイラにおける time-of-check-to-time-of-use (TOCTOU) の競合状態です。脆弱な関数 Graph::tryGetConstantProperty() はバックグラウンドのコンパイラスレッドで実行されます。この関数は、セルロック下で JavaScript のプロパティ値を読み取り、ロックを解放し、生の値を呼び出し元に返します。ロック解放と呼び出し元がその値を次に使用するまでの間に、メインスレッドがプロパティを置き換え、ガベージコレクションをトリガーして、コンパイラスレッドがまだ生ポインタとして保持しているヒープセルを無効化できます。その後、古くなったセルの値は、次に実行されるコードパス(セルの構造体ポインタを逆参照する DFG の freeze() 関数、またはマークを試みる GC のマーキングビジター)によって消費されます。どちらのパスでも、古いヒープ状態でクラッシュする可能性があります。
この脆弱性は、「Coruna」iOS エクスプロイトキットの一部として実際に悪用されました(該当する JSC モジュールのコードネームは「cassowary」です)。元のエクスプロイトは、iOS 16.6 から 17.2.1 を実行する ARM64 iOS デバイスをターゲットとし、TOCTOU と NaN-boxing 操作および WebAssembly インスタンス結合を組み合わせることで任意のメモリ読み取り/書き込みを実現します。本レポートのセクション 3 では、そのエクスプロイトの詳細を説明します。
本レポートでは、同じ脆弱性を Linux x86_64 に適用した適応について説明します。ARM64 のエクスプロイト戦略は移植できません。x86_64 の Total Store Order (TSO) は元のエクスプロイトが依存するメモリ並べ替えの競合を防止し、NaN-boxing のレイアウトの違いにより構造体 ID の破損手法は移植不可能です。代わりに、x86_64 の概念実証は、同じ TOCTOU の異なる結果を利用します。DFG コンパイラが競合ウィンドウ全体で古いセル値の JSValue を保持し、その後、GC マーキング中に通常の JSC コードでクラッシュを引き起こします。クラッシュは通常のエンジンパスを通じて発生し、ASan で可視です。競合ウィンドウは、決定論的にするために研究用の計測によって拡大されています。
概念実証とクラッシュ出力は、以下の環境で作成されました。
7617.1.17.13jsc シェルjsc バイナリで AddressSanitizer 有効JSC の DFG (Data Flow Graph) コンパイラはバックグラウンドスレッドで実行されます。コンパイル時に構造が判明している JavaScript オブジェクトからプロパティのロードに遭遇すると、結果を定数畳み込みできます。つまり、コンパイル中にプロパティ値を読み取り、コンパイル時定数として最適化コードに組み込みます。この読み取りを実行する関数が Graph::tryGetConstantProperty() です。
パッチ適用前の tryGetConstantProperty() は次の 3 つの処理を行います。
期待されるセット内のすべての構造体に対する置換ウォッチポイントがまだ有効であることを確認する。
オブジェクトのセルロック下でプロパティ値を読み取る。
生の JSValue を返す。```cpp
// Source/JavaScriptCore/dfg/DFGGraph.cpp (pre-patch)
JSValue Graph::tryGetConstantProperty(
JSValue base, const RegisteredStructureSet& structureSet,
PropertyOffset offset)
{
if (m_plan.isUnlinked())
return JSValue();
if (!base || !base.isObject())
return JSValue();
JSObject* object = asObject(base);
// Step 1: validate replacement watchpoints for (unsigned i = structureSet.size(); i--;) { RegisteredStructure structure = structureSet[i]; WatchpointSet* set = structure->propertyReplacementWatchpointSet(offset); if (!set || !set->isStillValid()) return JSValue(); watchpoints().addLazily(*set); }
// Step 2: read the property under the cell lock JSValue result; { Locker cellLock { object->cellLock() }; Structure* structure = object->structure(); if (!structureSet.toStructureSet().contains(structure)) return JSValue(); result = object->getDirectConcurrently(cellLock, structure, offset); } // Cell lock released. result is now a raw JSValue on the native stack. return result; }
返された `JSValue` は保護されていません。これがセルポインタを保持している場合、ロック解除から呼び出し側が使用するまでの間に、そのセルが解放されるのを防ぐものは何もありません。
### 2.3 古くなった値に対するコンシューマパス
返された `JSValue` は、2 つのパスで消費される可能性があります。レースウィンドウをまたいでセルが古くなったり無効になったりした場合、どちらのパスでもフォールトが発生する可能性があります。
**パス A: コンパイラスレッドでの `freeze()`。** 最も直接的なコンシューマは `Graph::freeze()` であり、呼び出し側は返された値に対して直ちにこれを呼び出します。```cpp
// Source/JavaScriptCore/dfg/DFGGraph.cpp
FrozenValue* Graph::freeze(JSValue value)
{
if (UNLIKELY(!value))
return FrozenValue::emptySingleton();
// This dereferences value as a cell:
RELEASE_ASSERT(!jsDynamicCast<CodeBlock*>(value));
// ...
FrozenValue frozenValue = FrozenValue::freeze(value);
// ...
}
静的 FrozenValue::freeze() は、セルの構造ポインタを読み取ります:```cpp
// Source/JavaScriptCore/dfg/DFGFrozenValue.h
static FrozenValue freeze(JSValue value)
{
return FrozenValue(
value,
(!!value && value.isCell()) ? value.asCell()->structure() : nullptr,
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Dereferences the cell. If freed, this is UAF.
WeakValue);
}
セルが `tryGetConstantProperty()` の返却と `freeze()` の実行の間に解放された場合、`value.asCell()->structure()` は解放後使用 (use-after-free) になります。
**パスB: 拡大されたウィンドウ中のGCマーキング。** 研究用ビルドでは、コンパイラスレッドが `tryGetConstantProperty()` 内でプロパティを読み取った後、呼び出し元に返す前に、生のDFGセーフポイントに入ります。これにより、メインスレッドは、コンパイラ側に生のネイティブローカルとして古いセル値がまだ存在している間にGCを実行できます。現在のLinux x86_64 PoCでは、確実に再検証されたクラッシュは、後でGCマーキング中に発生し、`SlotVisitor` が最終的にヒープ参照を走査中に無効な古いセルを逆参照します。現在のクラッシュスタックは、後続のGC機構が古い値を消費することを証明していますが、その古いポインタが到達された正確なコンテナスロットをそれ自体で証明するものではありません。
### 2.4 Call sites
DFGパイプライン内の2か所で、`tryGetConstantProperty()` の結果を無条件に `freeze()` に渡します:
**ByteCodeParser** — 初期のバイトコードからDFG-IRへの変換中:```cpp
// Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp:5114
JSValue constant = m_graph.tryGetConstantProperty(
base->asJSValue(),
*m_graph.addStructureSet(variant.structureSet()),
variant.offset());
if (constant)
return weakJSConstant(constant); // → m_graph.freeze(constant)
ConstantFoldingPhase — 最適化中:```cpp // Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp:1334 if (JSValue value = m_graph.tryGetConstantProperty( baseValue.m_value, *m_graph.addStructureSet(variant.structureSet()), variant.offset())) { m_graph.convertToConstant(node, m_graph.freeze(value)); return; }
3つ目の呼び出し箇所は**AbstractInterpreter**内でも`freeze()`を呼び出しますが、返り値が`GetterSetter*`の場合にのみ:```cpp
// Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:4319
JSValue result = m_graph.tryGetConstantProperty(base, data.offset);
if (result && jsDynamicCast<GetterSetter*>(result))
setConstant(node, *m_graph.freeze(result));
jsDynamicCast自体はセルをデリファレンスし(そのClassInfoを読み取る)、そのためこの条件付きパスでさえも潜在的なUAFとなります。ただし、古いセルがGetterSetterである場合に限ります。
コンパイラはプロパティを読み取る前に、置換ウォッチポイントをチェックします。プロパティが後で置換されると、ウォッチポイントが発火し、最終化の際にコンパイル計画が無効化されます。しかし、freeze()はコンパイル中(ByteCodeParserまたはConstantFoldingPhase)に実行され、最終化のかなり前に実行されます。セルのデリファレンスが先に行われ、安全性チェックは後に行われます。ウォッチポイントがそれを防ぐ前に、被害が発生してしまいます。
Cassowaryモジュールは、「Coruna」iOSエクスプロイトキットの一部として発見されました。これは、ARM64 iOSデバイス上のWebKitベースのブラウザに配信されるJavaScriptファイルであり、iOS 16.6から17.2.1をターゲットとしています。このエクスプロイトは任意のメモリ読み取り/書き込みを実現し、エクスプロイトチェーンの後続のステージへのエントリポイントとして使用されます。
以下の分析は、元のエクスプロイトアーティファクト(yAerzw_d6cb72f5_analytic_rewrite.js)の難読化解除および注釈付きバージョンから再構築されたものです。変数名、関数名、および構造上の注釈はリバースエンジニアリングの産物であり、元の作者によるものではありません。以下のコードスニペットと動作説明は、この再構築を反映したものであり、一次ベンダーのドキュメントや検証済みのソースを示すものではありません。具体的な詳細(正確なスプレーカウント、パディングサイズ、構造ID定数など)はアーティファクトから直接取得したものであり、特定のファームウェアバージョンに調整されている可能性があります。
エクスプロイトは段階的に進行します。
状態のセットアップ。 中央の状態オブジェクトがすべてのエクスプロイトデータを保持します。Object.seal()はそのJSC構造を固定し、DFGコンパイラの定数折りたたみの仮定を予測可能にします:```javascript
// yAerzw_d6cb72f5_analytic_rewrite.js
const exploitState = {
config: { g: eval('(() => {return -NaN})()') },
f64View: f64Scratch,
i32View: i32Scratch,
objArray: [[], [], [], []],
floats1: [1.1, 2.2, 3.1],
floats2: [0.23, 2.2, 3.4],
triggerObj: null,
callFn: null,
typePunBuf: new ArrayBuffer(16),
typePunU32: null,
typePunF64: null,
structureId: 0x500000,
// ... jitRead, jitWrite, jitLength, corruptFn, setupFn
};
Object.seal(exploitState);
`config.g = -NaN`値は、JIT層のサイドチャネルとして機能します:`Math.min(-NaN, -NaN)`はインタプリタとJITで異なるビットパターンを生成し、`Int32Array`オーバーレイを介して観測可能です。