ブラウザの脆弱性調査 #1:CVE-2024-29943

背景

普段はWebの脆弱性に触れていることが多いため、自分が知らないバグクラスを調査してみたくなり、調査してみました。前述のような状況であるため、不正確かつ不十分な可能性がかなりあります。今回はCVE-2024-29943について調査してみます。

環境構築

エクスプロイトや解説スライドはここにあるものを参考にしました。

gecko-devはここ。対象コミットはafbdf6822c9e9f9b6d44b9ea6904cb10878126b1

これでビルドと実行が可能です。また実行前にexport IONFLAGS=logs,rangeをしておくことで、JitSpewで出力しているログを標準出力に出すことが可能です。

./mach bootstrap
./mach configure --enable-jitspew
./mach build
./obj-x86_64-pc-linux-gnu/dist/bin/js --ion-offthread-compile=off --spectre-mitigations=off PoC.js

こちらのスクリプトでMIRグラフの可視化が可能です。

そのままでは動作しなかったので、以下の変更を加えました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
--- ghetto-iongraph.py 2025-01-14 04:44:39.568490035 +0900
+++ ghetto-iongraph.py  2025-01-14 04:45:39.978490884 +0900
@@ -309,7 +309,7 @@
             g.write(fd)
         passes.append(filename)

-    s = open('ion.json', 'r').read()
+    s = open('/tmp/ion.json', 'r').read()
     ion = json.loads(parenthesize(s))
     for i in range(0, len(ion['functions'])):
         func = ion['functions'][i]
@@ -470,13 +470,13 @@
     subprocess.call([
         args.js_path,
         # Strict mode.
-        '-s',
+        # '-s',
         # Avoid races in ion when spewing.
         '--ion-offthread-compile=off',
         args.script_path
     ])

-    if not os.path.isfile('ion.json'):
+    if not os.path.isfile('/tmp/ion.json'):
         print('Something does not look right, ion.json has not been created, aborting.')
         return 0

以下のようにしてghetto-iongraph.pyによる可視化が可能です。

python ghetto-iongraph.py --js-path ./obj-x86_64-pc-linux-gnu/dist/bin/js --script-path PoC.js

基礎知識

SpiderMonkeyについての知識が全く無かったため、下記のページを軽く読みました。

RangeAnalysisとは

今回バグが発生したのはJITにおける最適化のうち、Range Analysisという部分に関連するものです。Range Analysisについては、ソースコード上に記載されているSMDOCに詳細な解説があります。これは範囲チェックの高速化のための仕組みで、SSAで扱われる変数に範囲情報を付与することで、範囲チェックを高速化することが出来るというものです。

[SMDOC] IonMonkey Range Analysis
This algorithm is based on the paper "Eliminating Range Checks Using Static Single Assignment Form" by Gough and Klaren.
We associate a range object with each SSA name, and the ranges are consulted in order to determine whether overflow is possible for arithmetic computations.
An important source of range information that requires care to take advantage of is conditional control flow. Consider the code below:

if (x < 0) {
  y = x + 2000000000;
} else {
  if (x < 1000000000) {
    y = x * 2;
  } else {
    y = x - 3000000000;
  }
}

The arithmetic operations in this code cannot overflow, but it is not sufficient to simply associate each name with a range, since the information differs between basic blocks. The traditional dataflow approach would be associate ranges with (name, basic block) pairs. This solution is not satisfying, since we lose the benefit of SSA form: in SSA form, each definition has a unique name, so there is no need to track information about the control flow of the program.
The approach used here is to add a new form of pseudo operation called a beta node, which associates range information with a value. These beta instructions take one argument and additionally have an auxiliary constant range associated with them. Operationally, beta nodes are just copies, but the invariant expressed by beta node copies is that the output will fall inside the range given by the beta node.  Gough and Klaeren refer to SSA extended with these beta nodes as XSA form. The following shows the example code transformed into XSA form:

if (x < 0) {
  x1 = Beta(x, [INT_MIN, -1]);
  y1 = x1 + 2000000000;
} else {
  x2 = Beta(x, [0, INT_MAX]);
  if (x2 < 1000000000) {
    x3 = Beta(x2, [INT_MIN, 999999999]);
    y2 = x3*2;
  } else {
    x4 = Beta(x2, [1000000000, INT_MAX]);
    y3 = x4 - 3000000000;
  }
  y4 = Phi(y2, y3);
}
y = Phi(y1, y4);

We insert beta nodes for the purposes of range analysis (they might also be usefully used for other forms of bounds check elimination) and remove them after range analysis is performed. The remaining compiler phases do not ever encounter beta nodes. 

今回はObject.keys(...).lengthをした時の変数に範囲情報を付与するための以下のようなコードに焦点を当てます。このコードによって、Object.keys(...).lengthの範囲は、0からNativeObject::MAX_SLOTS_COUNTとして設定されることになります。

/gecko-dev/js/src/jit/RangeAnalysis.cpp

1
2
3
4
5
6
void MObjectKeysLength::computeRange(TempAllocator& alloc) {
  // Object.keys(..) returns an array, but this array is bounded by the number
  // of slots / elements that can be encoded in a single object.
  MOZ_ASSERT(type() == MIRType::Int32);
  setRange(Range::NewUInt32Range(alloc, 0, NativeObject::MAX_SLOTS_COUNT));
}

またNativeObject::MAX_SLOTS_COUNTは以下のように設定されています。

/gecko-dev/js/src/vm/NativeObject.h

1
2
3
4
  // The maximum number of slots in an object.
  // |MAX_SLOTS_COUNT * sizeof(JS::Value)| shouldn't overflow
  // int32_t (see slotsSizeMustNotOverflow).
  static const uint32_t MAX_SLOTS_COUNT = (1 << 28) - 1;

この時、bjrjk/CVE-2024-29943に含まれるInconsistency.jsを実行してみると、実行時に出力される個数は268435456となっているが、ghetto-iongraph.pyを使ってRangeAnalysis.cppを見ると[0, 268435455]の範囲情報が付与されていることが分かります。実際の配列の長さと、範囲チェック用に追加している範囲情報とのズレがある状態となっています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function opt(a) {
    return Object.keys(a).length;
}

let arr = [];
for (let i = 0; i < 10000; i++) {
    arr[i] = 1;
    opt(arr);
}

for (let i = 0; i < (1 << 28) + 3; i++) {
    arr[i] = 1;
}

print(opt(arr));

このバグを悪用することで、範囲外への書き込みができそうです。

Exploit

動作するExploitは以下にあります。

スライドやコードを読みつつExploitの理解を試みたのですが、文章にして他人に伝えられるほど理解はできていないです。悲しい。 SpiderMonkeyの任意コード実行に必要な基礎的な知識に飛躍があるため、そのあたりの知識は徐々に埋めていこうかと思います。