银雁冰@猎影实验室
推荐语:作为曾经的微软主力JS引擎,Chakra在其短暂的生命周期中为我们留下了许多经典漏洞,本文我们一起来看一个Chakra引擎的JIT漏洞。
前言
虽然微软已经在新版Edge浏览器中弃用Chakra引擎,但作为曾经的微软主力JS引擎,Chakra在其短暂的生命周期中为我们留下了许多经典JS漏洞,这些案例是学习脚本引擎漏洞非常不错的资料。本文我们一起来看一个Chakra引擎的JIT类型混淆漏洞。
漏洞复现
Bruno Keith在《Attacking Edge through theJavaScript compiler》这篇paper中给出了该漏洞的PoC。为便于观察,我们对其稍加改动,调试所用Poc如下。
漏洞调试的第一步有两个关键要素:能复现问题的样本和能复现问题的环境,上面已经有了样本,接下来需要搭建能复现漏洞的环境。
首先在ChakraCore项目中搜索当前漏洞的CVE号:
随后定位到该漏洞补丁对应的commit:
接着选取一个补丁之前的版本,这里最终选取的是d1dc14版本(Bruno Keith指出的版本)。许多文章中一般只会提及所需ChakraCore版本对应的sha1,如果遇到这种情况,可以直接替换下面链接中的sha1进行下载:
下载完成后,需要编译生成可供调试的二进制文件,ChakraCore原生支持VS编译,这里采用VS2015编译,编译选项为x64+release。编译完成后,在如下相对路径会生成相关二进制文件(如需生成IR等中间代码,请编译为Debug版本):
Build\VcBuild\bin\x64_release
我们主要关注ch.exe(用于加载ChakraCore.dll)和ChakraCore.dll(漏洞模块)。
现在已经有了复现问题的环境,将ch.exe设为启动项目,并在”属性->调试->命令行参数”配置PoC文件全路径,启动调试,可以观察到如下异常:
为了进一步定位问题,接下来我们采用Windbg Preview的TTD(Time Travel Debugging)进行调试,TTD调试的一个好处是可以在不用重启调试器的情况下进行反向回溯。
安装完Windbg Preview后,首先在界面上设置一下源码路径和符号路径:
接着用管理员权限启动调试器,进行TTD文件路径等基本设置,点击Record开始调试,此过程中如果出现弹窗,请不要点cancel,否则TTD调试将终止。
脚本执行完毕后,在调试器中观察到和VS中相同的崩溃点:
定位成因
从上一小节最后一张截图中我们可以看到崩溃时rax被当做指针来寻址,出问题的rax取自rdi,看一下rdi里面存了什么:
似乎一个DynamicObject对象的auxSlots指针被覆写为了一个非预期值,而且这个非预期值来PoC。
接下来借助TTD回溯auxSlots对应的内存地址是在前面何时被改写的:
可以看到前一次改写这个地址处的数据是在JIT代码中,紧接着往前回溯一条指令,我们来看一下改写前此处是什么:
而0x2337、2这两个整数此时仍Inline存储在DynamicObject中。
此时回头看一下PoC,注意到数字1、2、3、4、5、0x1337、0x2337都是PoC里面设置的属性值。
为什么上述DynamicObject对象的5个属性会变成这种两头存储的状态?
为了回答这个问题,首先要理解DynamicObject对象的属性存储方式,关于这部分更多的细节可以参考《AttackingEdge through the JavaScript compiler》。这里仅借助ChakraCore源码中对auxSlots的注释进行说明:
以上注释对应的源码文件:
lib\Runtime\Types\DynamicObject.h
可以看到此时auxSlots对应的情况似乎是(#2),接下来我们再次往前回溯,看再前面一次修改auxSlots对应地址处的操作在什么时候?
观察一下堆栈,可以看到这次改写auxSlots是由OP_SetProperty操作码引发的:
继续利用刚才的内存写入断点,再往前回溯一次,查看相关对象的内容:
可以看到此时对应的auxSlots的情况对应上面注释中的(#3)。
现在我们来正向推演一下上述几次auxSlots的变化情况,如下图所示:
到这里已经比较明显了,在执行PoC中的下面这句JS代码时,有几处关键操作:
opt({a:1, b: 2, c: 3, d: 4});
1).对象o对应的DynamicObject的属性存储方式存在一次布局转变,在转变前后布局从(#3)变为(#2);
2).在下一次访问auxSlots时,正常逻辑应该是以(#2)的方式去解引用对应地址的auxSlots指针,然后将0x1337这个值写入auxSlots地址处的第一个8字节。然而JIT代码显然没有意识到这一点,还在以(#3)的形式进行访问,从而意外改写了auxSlots指针(并且这个指针在PoC中可控);
3).在最后一次访问auxSlots时,代码逻辑以(#2)的方式访问auxSlots,并试图往auxSlots指针内写入一个值,从而导致崩溃。
以上仅以调试器视角审视了漏洞成因,漏洞的实际触发逻辑中与ChakraCore的JIT执行流程、ObjectHeaderInlined、符号更新机制、属性赋值、类型检查都存在关系,关于这方面的更多细节可以参考《AttackingEdge through the JavaScript compiler》。
利用编写
这个漏洞给了我们非常好的一个任意地址写入能力,Bruno Keith已经在《Attacking Edge through theJavaScript compiler》介绍了这个漏洞的利用思路,并在网上公开了相关利用代码。接下来我们尝试在ChakraCore模拟器内复现一下这个漏洞的利用过程,并对其中的一些要点进行说明。
我们的目的是要借助该漏洞实现任意地址读写原语(当然也可以实现任意对象地址泄露和任意地址对象伪造两个原语),继而实现RCE。
如何将一个任意auxSlots指针写入能力转换为任意地址读写?Bruno的思路是先借助上述 2)将一个布局为(#1)的DynamicObject(简称obj)对象写入auxSlots,然后借助3)将obj对象的auxSlots域改写为一个ArrayBuffer对象。为什么可以实现这一步?从上面的调试结果我们可以看到崩溃时尝试将一个可控值写入异常auxSlots指针的偏移+0x10处,如果这个异常auxSlots被改写为一个属性非Inline的DynamicObject对象(ArrayBuffer继承自这一对象),那么偏移+0x10处即为auxSlots。
经过上述操作,接下来obj对象属性的修改就直接作用到了ArrayBuffer对象。我们就可以改写ArrayBuffer对象内的一些成员,比如它的buffer和bufferLength:
但此时我们仍无法将上述ArrayBuffer的buffer转换为完全可控的地址,聪明的Bruno提出可以将buffer设置为第二个ArrayBuffer对象,这样,通过操作第一个ArrayBuffer的buffer(借助一些Typed Array即可实现),我们可以完全控制第二个ArrayBuffer的元数据,包括buffer和bufferLength。这样:
a). 我们可以通过obj设置第一个ArrayBuffer的buffer;
b). 借助a),我们可以将第一个ArrayBuffer的buffer设置为第二个ArrayBuffer,并且可以修改第一个ArrayBuffer的bufferLength为合适的值;
c). 借助Typed Array,我们可以将第一个ArrayBuffer的buffer(即使它已经被伪造)的偏移+0x38处设为任意地址,特别地,与b)结合,我们可以将第二个ArrayBuffer的buffer设置为任意地址;
d). 借助Typed Array,我们可以读/写第二个ArrayBuffer的buffer(即使它已经被伪造),与c)结合,我们可以构造出任意地址读写原语。
上述对象关系如下:
通过上述c)的思路可以读取第二个ArrayBuffer偏移+0x00处的ArrayBuffer虚表指针,继而得到ChakraCore.dll的基地址,从而绕过了ASLR。
有了任意地址读写原语后,接下来的操作就比较常规了,在真实的Edge环境上,由于CFG和ACG这两个缓解措施的存在,我们需要覆盖栈上的返回地址绕过CFG,并借助纯ROP操作绕过ACG:
ⅰ). 找到一个栈上的返回地址并覆盖之,使控制流导向我们控制的ROP gadget;
ⅱ). 伪造一个函数栈,通过ROP gadget将RSP寄存器的值切换至伪造栈;
对于ⅰ),已经有不少前辈提过定位JS函数栈的方法,比如Ivan Fratric、Henry Li,Minkyo Seo等,pwn.js里面也有源码。
这里我们采用寻找ChakraCore!ThreadContext::globalListFirst全局变量这一方式,这个地址存储着一个8字节的ThreadContext对象,ThreadContext对象的偏移0xc8处为stackLimitForCurrentThread,借助它即可以定位到JS函数栈。调试器中的相关结构体如下(不同版本ChakraCore中相关结构体偏移不一样,这里以本文调试版本为例):
需要指出的是,在ch.exe加载ChakraCore.dll的模拟器内,我们可以很容易通过上述过程定位到JS栈地址,但在对实际环境中的Chakra.dll进行试验时,这种方式不一定适用。真实环境中往往需要借助Herry Li在《How to find the vulnerability to bypass the Control Flow Guard》内提到的方法,并且由于微软给的符号里面无法使用dt命令获取完整的结构体信息,需要通过逆向去定位具体成员的偏移,难度比模拟器大。
找到栈地址后,在调试器里面选一个比较适合的返回地址(本次选择了ChakraCore!JsRun),计算出相对偏移后,即可在代码中通过搜索栈上数据进行定位,将其覆写为第一个ROP gadget的地址,并将紧邻的8字节覆写为伪造栈的地址,其余gadget在伪造栈中进行构造。ROP方面,第一个gadget如下:
// 5C5B 5D C3
pop rsp
pop rbx
pop rbp
ret
伪造栈上的几个gadgets如下:
// 59 C3
pop rcx
ret
// 5A C3
pop rdx
ret
// 5C C3
pop rsp
ret
伪造栈上的执行逻辑为用WinExec弹出一个计算器。
我们来观察一下Stack Pivot前的寄存器信息:
经过第一个gadgets的执行,rsp寄存器被切换到伪造栈。来看最后一个gadget执行后的情况:
最终,我们在ChakraCore.dll引擎模拟器上借助覆盖栈上的返回地址,并且通过完全ROP的方式弹出一个计算器:
补丁分析
这个漏洞的补丁可以在ChakraCore的官方commit中找到,可以看到补丁代码中在关键地方设置了类型检查,并调整了一些符号更新的相关逻辑,以确保之前发生混淆的过程不会再发生。有兴趣的读者可以自行进行源码调试。
参考资料
- 《Attacking Edge through the JavaScript compiler》By Bruno Keith
- 《A tale of Chakra bugs through the years》By Bruno Keith
- 《Using the JIT vulnerability to Pwning Microsoft Edge》 By Herry Li
- 《How to find the vulnerability to bypass the ControlFlow Guard》 By Herry Li
- 《CVE-2019-0539 Root Cause Analysis》By Rom Cyncynatus andShlomi Levin
- 《CVE-2019-0539 Exploitation》By Rom Cyncynatus andShlomi Levin
- 《Microsoft Edge Chakra JIT Engine Attack Surface》By Elliot Cao
- 《Microsoft Edge: Chakra:incorrect JIT optimization withTypedArray setter CVE-2017-8548》By Minkyo Seo
- 《From Zero To Zero Day》By Jonathan Jacobi
- 《1-Day Browser & Kernel Exploitation》By @theori-io
发表评论
您还未登录,请先登录。
登录