时间:2016-12-07 08:27:46 作者: 点击:次
在刚刚发布的微软11月份安全更新中,比较幸运刷了两个Edge的脚本引擎漏洞,对微软的脚本引擎漏洞有一定的见解,所以打算写篇文章与大家分享一下。由于这两个漏洞修复时间比较短,所以选择一个比较旧的9月份的漏洞跟大家分享。
如果大家有什么问题,欢迎通过老实敦厚的大宝微博与我交流。

漏洞简介
在9月份微软安全更新中,修复了一个chakra脚本引擎的类型混淆漏洞(CVE-2016-3377)。经过分析,该漏洞影响win10 x64平台的edge浏览器,攻击者经过精心构造的网页,可以在受害者机器上达到远程代码攻击的效果。(x64平台的利用实在比x86的难太多:( )
0×0漏洞分析
由于chakra脚本引擎已经开源,所以可以从github上得到此次漏洞修复的代码如下(in JavaArray.cppMapHelper()):

漏洞的修复很简单,就是把原来的DirectSetItemAt函数变成SetItem,接下来重点分析这两个函数的区别。
以下是两者对应的源码:

DirectSetIteamAt并不是一个虚函数,默认调用对象是JavaArray类型,但是里面并没有对typeId进行任何判断。

如图,SetItem是一个虚函数,会根据调用的对象分别调用不同的SetItem实现(JavaNativeIntArray是JavaArray的子类)。
而且在内部会对typeid进行判断。在漏洞代码中,假如newArr并不是JavaArray对象,而是其子类的对象,就会引发越界读的漏洞。根据源码分析与对比,最终构造的测试PoC如下:

由于此漏洞需要利用ES6的标准实现JS的类的继承,所以只会影响Edge比较新的版本,Edge的旧版本和IE11并不影响。
通过Proxy类构造畸形的y,当在MapHelper遍历y的prototype时候,会进入如下代码:
调用y类的constructor函数,也就是fake函数,用于创建newObj对象,也就是Array.prototype.map()函数即将返回的对象。
在JS中,即使fake是一个类,但实际上它是一个函数,包括其他类Array等也是一个函数,当调用new fake()的时候实际上是进入了class fake中的[Symbol.species]函数中,换句话说,[Symbol.species]就是类fake的构造函数。
但是有一个概念需要区分,因为fake是函数,所以它的constructor就是Function,这与[Symbol.species]是不同的一个概念。
在构造函数中,返回的对象是n[5],这是一个JavaNativeIntArray类,因此newObj指针指向的是一个JavaNativeIntArray类,并不是JavaArray类。再往下,避过其他if,最后进入如下代码:

newArr也就是上文提到的newObj,查看这个对象的typeId:


可以看到,JavaArray的typeId应该是0x1c,但是这里是0x1d。
0×1 Out Of Bound Write
在64位的edge中,JavaArray的每个element占用的内存大小是0×8字节,因为要保存双精度浮点数以及对象地址等信息,但是在JavaNativeIntArray中每个element占用的内存大小是0×4字节,如下图所示,调用DirectSetItemAt之前:

因此每次调用JavaArray的DirectSetItemAt会占用JavaNativeIntArray两个element的位置,调用一次DirectSetItemAt之后:

在mapHelper遍历的过程中,即使length没有发生越界,最终也必然会导致越界写的行为发生,因此此漏洞仅仅影响64位的edge浏览器。但是单单的越界写是不足够的,还需要满足两个条件最佳:
1. 可以控制越界写越多少界,例如我想越8字节时就8字节,16字节时就16字节。
解决方法:根据此漏洞的特性,通过控制y和n[5]数组的长度可以控制越界写的长度,当然还需要考虑内存对齐的细节。
2. 可以越过中间某些数据不写。例如有时候我们只想修改后面数组的长度,但是在长度之前有某些重要的字段,如果修改了就会导致edge的crash。
解决方法:首先查看漏洞附近的代码:

假如可以令这个HasItem返回false,就可以跳过中间我们不想覆盖的字段,方法也很简单,在数组中间某些index位置设置成null,如图:

至此,任意越界写已经实现,下一步就是通过越界写修改相邻IntArray的长度。
0×2 制造一个big_array
首先需要了解IntArray在内存中的数据结构:

图中框着的地方分别是length,segment,segment的size,和segment的length,只要把length,segment_size和segment_length覆盖了,就达到目的,而且中间重要的字段,例如虚函数地址等要跳过,不然会导致crash。
而具体要覆盖的值可以通过双精度浮点型指定要覆盖的值,覆盖以后如图:

具体JS代码如下:

然后检验是否修改成功,并且保存这个数组的索引:

0×3 制造big_DataView
第一步通过heap feng shui把某个dataview放进big_array的后面:

然后以0×1034作为特征值,查找这个dataview的内存位置,然后修改对应的length:

检验是否成功,并且保存这个dataview的索引:

0×4 任意内存读写
在查找dataview的bytelength特征值的同时,保存dataview的buffer_address的地址的位置,保存下这个索引:

Dataview的内存结构,分别是bytelength和buffer_address

任意内存读:

把需要读的地址写入dataview的buffer_address,再读取这个dataview偏移0×0地方的数据。
同理任意内存写:

0×5 获取任意对象地址
最后一步,就是获取任意对象的地址,我的代码如下:

因为得到的big_array是n[6],所以把需要读取的obj放入n[7][0],再通过越界读获取n[7]的segment的地址,再通过任意地址读,读取segment地址+0×18和0x1c的数据,即可得到这个对象的地址。

0×6 后续
至此,该漏洞的详细原因分析和远程代码执行所需要的任意地址读写和获取任意对象地址已经全部成功,剩下的部分(bypass cfg/dep)有兴趣的同学可以尝试实现(tips:利用JIT in WARP Shader,文章针对x86,在x64上会有比较大难度)。

(当然还需要一个逃逸沙箱的漏洞结合,不过这是后话。)文章中也提到在x64上利用的可能性:

在x86上会存在很好利用的gadgets用于构造ROP:

但是在x64平台想要编译出比较好用的gadgets JIT code需要一点耐心去写webGL代码,而且在x64中参数是通过寄存器传递,这样对栈中的数据的控制难度就会加大。
因此可以选择使用push rcx//pop rsp//retn这样的指令以控制rsp,然后通过精心构造的webGL代码可以构造出需要的gadgets。
深度技术 Windows10 22H2 64位 专业版 V2
深度技术 Windows10 32位 专业版 V2026.0
【Win10 纯净版】秋叶系统32位下载 v2026
系统之家 Win10 32位专业版(免激活)v2026.0
系统之家 Win10 64位专业版(免激活)v2026.0
番茄花园Windows 10 专业版32位下载 v202
【Win10 纯净版】秋叶系统64位下载 v2026
【Win10 装机版】秋叶系统32位下载 v2026
【Win10 装机版】秋叶系统64位下载 v2026
番茄花园Windows 10 专业版64位下载 v202
雨林木风 Windows10 22H2 64位 V2026.02(
雨林木风 Windows10 32位 官方专业版 V20
萝卜家园 Windows10 64位 优化精简版 V20
萝卜家园 Windows10 32位 优化精简版 V20
番茄花园Windows 10 专业版64位下载 v202
番茄花园Windows 10 专业版32位下载 v202
系统之家 Win10 64位专业版(免激活)v2026.0
系统之家 Win10 32位专业版(免激活)v2026.0
深度技术 Windows10 22H2 64位 专业版 V2
深度技术 Windows10 32位 专业版 V2026.0