作者:css_hacker

原文链接:https://www.anquanke.com/post/id/176493

背景阐述

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

文章目标

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

动手实战

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

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

img

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

背景知识

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

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

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

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

我们挨个解决上述问题。

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

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

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

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

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

img

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

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

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

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

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

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

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

img

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

Exp 部分开始

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

1 – 越界写 to 越界读写

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

这里继续修改 poc:

img

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

img

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

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

调整poc 如下:

img

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

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

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

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

接下来需要 fake 一个 DataView 来完成任意地址读写,怎么样才能稳定的 fake 一个DataView呢?需要再次数据喷射吗, 还是有其他技巧?想要 fake 任意对象,首先需要知道该对象的元数据,需要 fake 的 TypedArray 元数据怎么获得?

补充一些背景知识

以下为 TypedArray 的元数据信息,+0x38 处存放着视图的实际数据。

已经获得的越界读写只能访问数组后面的内存,如果 TypedArray 元数据被分配在越界读写数组的前面怎么办?需要数据喷射吗?原理可行,但是这里采用 fake Array 的方式来完成,这样更加简单、稳定。 fake Array 的方式需要点背景知识,这里来补充下。

Array 的背景知识

img 先回顾一下第一篇文章中介绍的 Array 的元数据, 常用的域包括 left 、length、size、 next Segment 几个。

img

Array 头部的 next segment 信息存储的是下一个 segment 的头部,其余的域属于当前的 segment 。为什么Chakra 不把 segment 放在一起,而是用指针的方式链接起来呢?因为 Chakra 在管理数组存储的时候,需要管理一种特殊的数组:Sparse 数组。即以下这种数组使用方式:

原始的数组空间不足以在索引 0x100000 处存储数据,所以需要 new 一块新的内存, 然后这块数据的相关信息保存在 next segment 位置。

Fake Array

Array 的背景知识可以解决 fake Array 的问题,进而解决TypedArray 元数据怎么获得的问题。既然我们知道 next segment 保存的是下一个Array的信息, 如果我们利用越界写把它指向 DataView 的元数据,那么不就可以读取TypedArray 的元数据了吗,任意地址读写不就达到了吗?说干就干,我们实现以下逻辑:

img

fake TypedArray 前后对比: 我们可以观察到,fake TypedArray 以后,windbg 已经将它识别为 TypedArray,标志着 fake TypedArray 的成功。

img

大致的逻辑是: 越界写将 int_arr 的next segment 指向 DataView, 然后用 int_arr 来读取 TypedArray 的元数据。读取的 TypedArray 元数据信息保存到另一个 TypedArray(dv) 的数据部分。这片新的内存即成为一个 fake 的 TypedArray,这里称为:dv_rw。由于 dv_rw 的元数据中包含视图数据的地址信息,而 dv 对 dv_rw 的元数据完全可控,也就完成了任意地址读写的目标,详细的逻辑请参考示例代码。

任意地址读写 to RCE

代码执行的前提条件是:我们对当前模块足够熟悉,知道 Chakra 中可执行代码位于哪里,以便我们获取到需要的 gadget 来完成代码执行。目标分解,需要以下三步骤即可代码执行:

1. Chakra 的基地址。

2. 解析 PE 获取 code 段信息, 获取gadges。

3. 修改虚表指针,指向 gadgets。

1. Chakra 的基地址

leak 模块基址的思路很简单:通过 leak_obj_addr 泄漏任意一个 obj 的 vtable,然后将 vtable 进行 0x10000 对齐后,每次减去 0x10000 去匹配 PE 文件的 Dos header 中的 Magic data。

img

2. 解析 PE 获取 code 段信息, 获取gadges

关于 PE 结构的解析,可以用第三方软件辅助我们解析(比如:CFF explorer), 也可以MS参考官方文档: https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format。先理清大致概念后,再动手写解析的代码。 获取 gadget 的逻辑,可以参考以下笔者的示例代码:

3. 修改虚表指针,指向 gadgets

寻找 int3 的地址,将 obj 的虚表重定向到该地址,执行 int3 一般厂商即认可代码执行的有效性。

img

但是我们还是希望通过努力,在 pc 上弹出一个计算器,这样更直观,也更加接近 pwn2own 的赛制要求。

弹出计算器

通过 rop 的方式,弹出计算器,首先需要控制 stack 指针(rsp)。rsp 的获得大致有两种途径:(1)修改 rsp 为可控的值(2)通过任意地址读写泄漏 rsp。 这里为了简单,我们采用第一种方式。至于第二种方式,有兴趣的小伙伴可以参考 pwnjs 项目(https://github.com/theori-io/pwnjs)。后续如果时间允许,我也将第二种方式详细的实现放在博客上。 通过修改虚表指针的方式,我们可以控制至少一个寄存器,这里的可控寄存器是 rax 。rax 是 fake 的虚表,虚表的第一项为 int3 的地址。

img

stack pivot

理想的 gadget 是一些 rsp,rax 的直接交互, 如: xchg rsp, rax 或者 mov rsp,rax 或者 push rax; pop rsp 之类,但是这里我们并不能直接获得这类 gadget 。通过编写小工具,很容易定位到一些有用的同等效力的gadget, 如:

栈迁移后,我们就可以着手准备 rop 链和 shellcode 了。这部分的整体逻辑示意图如下:

img

通过调用 VirtualProtect 将地址属性修改为可执行,然后执行 shellcode。

补充一些背景知识

x64 calling convention (https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=vs-2019), 参数依次存放在 rcx, rex, r8, r9 和栈上。 VirtualProtect 的函数原型如下:

该 API 需要4个参数,依次对应 lpAddress <—> rcx, dwSize <—> rdx, flNewProtect <—> r8, lpflOldProtect <—> r9。

我们理想的 gadget 当然就是: pop rcx; pop rdx; pop r8; pop r9; ret; 同样的,实际上并没有这类 gadget,我们选择一些同等效力的替代 gadget :pop rcx; ret 和 pop rdx; ret 和 pop r8x; ret; 由于 r9 没有类似的 gadget,我选择另外一个gadget:

准备 VirtualProtect 参数的 rop 链,的示例代码如下:

上面的 rop 执行后,寄存器的值如下:

img

VirtualProtect rop 链调用之前的内存属性为:读写:

img

VirtualProtect rop 链调用之后的内存属性为:读写+执行:

img

表明 rop 链调用 VirtualProtect 已经成功,剩下的就只有实现 shellcode部分了。

最终效果如下:

img

Last but not least

还记得当初的目标吗? ”零基础在浏览器中稳定的弹出第一个计算器“。 在 Chakra 的漏洞利用中,我们只需要解决ASLR 和 DEP的问题。在 Edge 中,将面临 CFG、 Sandbox 、CIG 、ACG 等挑战。如何将 exp 稳定的移植到 Edge 中?怎样处理CFG?…

Stay tuned !