漏洞基本信息
我们直接查看漏洞修复的commit,也能对漏洞信息略知一二
大体来讲,就是这里的receiver
与lookup_start_object(holder)
存在类型混淆。由于 ic.cc
将根据 lookup_start_object(holder)
更新缓存, lookup_start_object(holder)
应该位于 JSModuleNamespace
中,但在更新后,实际的属性访问操作在 recevier
对象上运行,该对象不在 JSModuleNamespace
中。因此,如果 receiver
和 lookup_start_object(holder)
不同,就会发生类型混淆。
更详细和深入的漏洞分析可以查看这里,本文不做过多阐述:https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md
POC
首先我们创建两个文件1.mjs
和2.mjs
1 | // 1.mjs |
运行2.mjs可以得到如下内容
1 | ┌─[loorain@LAPTOP-Loora1N] - [~/v8/workdir/CVE-2021-38001] - [10609] |
可以看到这里的module
便是JSModuleNamespace
,原作者通过原型链的方式使得module
在ComputeHandler
中作为holder
,从而实现类型混淆,POC如下
1 | import * as module from "1.mjs"; |
代码解析
具体来说上述POC可以分为下面几个部分
导入模块
1 | import * as module from "1.mjs"; |
- 从
"1.mjs"
中导入所有内容,作为module
对象
1 | // 1,mjs |
定义类和方法
1 | class C { |
- 定义了一个类C,其中包括方法
m()
- 在
m()
方法中,return super.y;
试图通过super
访问属性y
创建对象zz
1 | let zz = {aa: 1, bb: 2}; |
- 定义了一个普通对象
zz
,包括属性aa
和bb
函数trigger
1 | function trigger() { |
- 修改原型链:
C_prototype.__proto__ = zz;
将C
类的原型的原型设置为对象zz
C_prototype.__proto__.__proto__ = module
将zz
的原型设置为module
对象
- 创建实例并设置属性:
- 创建了
C
的实例c
,并设置了多个属性x0
到x4
。
- 创建了
- 调用方法
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我们可以得到类似如下结果,关键信息我已划线标出
可以看到其错误的访问了地址,从而导致了crash,并且这个值正是我们在POC中自行设置的内容。
1 | ... |
构造漏洞原语
经过调试,确定漏洞POC只使用c.x0 = 0x42424242 / 2;
的情况下依然能够触发,修改POC如下:
1 | import * as module from "1.mjs"; |
主要区别:
- 去除
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
26DebugPrint: 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 | ... |
这样我们就保证了上一次0x21212121
的部分变成了一个可控地址的指针,也就是这里的array1
,进一步查看输出
因为不确定需要多少长度的
array
才能使其继续报错或者数值可控,所以需要尝试不同的值,但是一般这种间接寻址偏移不会很大,所以工作量并不多。甚至可以用一个非常大的值去全部覆盖,能保证目标一定在长度范围内。
发现依然报错,job
小看一眼array
1 | pwndbg> job 0x02970804a249 |
进一步更改POC设置array2
1 | ... |
查看输出,发现evil
变成了一个PropertyArray对象,即array2对象内存中的存储第二个指针
数据存储结构:
| 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 | for (let i = 0x0; i < 0x10; i++) { |
查看输出,的确验证了我们的猜想,获得了低32位的内容0x20202020/2