Ten months old tweetable bug leads to RCE - phoenhex team
漏洞背景
今天我们讨论的是ChakraCore中的一个漏洞,该漏洞可导致远程代码执行(RCE)。这个漏洞从我发现之日起几乎存在了十个月之久。之所以没有报告,是因为Chakra很长时间没有新版本发布,因此这个漏洞从未作为Edge浏览器的一部分发布。
前置知识
ChakraCore中的JS对象
在ChakraCore中,对象的默认存储模式使用指向连续内存缓冲区的指针来保存属性值,并使用称为Type的对象来描述给定属性名的属性值存储位置。
JSObject的布局如下:
- vfptr: 虚函数表指针
- type: 保存Type指针
- auxSlots: 指向保存对象属性的缓冲区指针
- objectArray: 如果对象有索引属性,则指向JSArray
ChakraCore中的数组
数组使用3种存储类型以实现优化:
- NativeIntArray: 整数以未装箱形式存储在4字节中
- NativeFloatArray: 数字以未装箱形式存储在8字节中
- JavascriptArray: 数字以装箱形式存储,对象指针直接存储
JIT背景
ChakraCore有一个JIT编译器,具有两层优化:
- SimpleJit
- FullJit
FullJit层执行所有优化,并使用一种直接的算法来处理被优化函数的控制流图(CFG),包括:
- 对图进行向后传递
- 向前传递
- 另一个向后传递(称为DeadStore传递)
在这些传递过程中,会收集每个基本块的数据,以跟踪各种符号的使用信息,这些符号代表JS变量,也可以代表内部字段和指针。
漏洞分析
该漏洞于2018年9月在commit 8c5332b8eb5663e4ec2636d81175ccf7a0820ff2中引入。这个commit尝试优化一个称为AdjustObjType的指令,并引入了一个新指令AdjustObjTypeReloadAuxSlotPtr。
考虑以下代码片段:
|
|
JIT必须在[[ 1 ]]处生成AdjustObjType指令以正确扩展后备缓冲区。这个优化尝试使用向上暴露的使用信息来决定是生成AdjustObjType还是AdjustObjTypeReloadAuxSlotPtr。
问题在于,当没有后续代码会导致auxSlots被使用时,auxSlots指针不会被设置为向上暴露,因此优化会生成AdjustObjType指令。但实际上auxSlots指针会被重新加载,这导致在某些条件下会出现以下情况:
- auxSlots指针"存活"并被加载到寄存器中
- 在写入新属性前执行AdjustObjType
- auxSlots指针没有被重新加载
- 使用之前已满的auxSlots指针写入属性
最终导致在原始auxSlots缓冲区之后发生8字节越界写入,经过一些处理后,这足以实现高度可靠的读/写原语。
漏洞利用
目标设定
利用这个漏洞时,我的目标是实现两个众所周知的原始功能:
- addrof: 允许泄漏任何JavaScript对象的内部地址
- fakeobj: 允许在任意内存地址获取JavaScript对象的句柄
限制条件
我们有几个限制需要考虑:
- 无法控制越界写入的偏移量,它总是auxSlots缓冲区之后的第一个QWORD
- 不能写入任意值,因为我们赋值的是JSValue
寻找合适的覆盖目标
为了将这个8字节越界写入转化为更强大的原语,我最终选择了覆盖数组段。Chakra使用基于段的实现来处理稀疏数组,避免内存膨胀。
段在内存中的布局如下:
- uint32_t left: 段的最左索引
- uint32_t length: 该段中设置的最高索引
- uint32_t size: 段实际能存储的元素数量/容量
- segment* next: 指向下一个段的指针(如果有)
如果我们写入0x4000越界,最终会得到一个left == 0x4000且length == 0x10000的段,这使我们能够更自由地读取段外的数据。
Chakra堆风水
Chakra中的大多数对象都是通过称为Recycler的分配器分配的。这是一个基于桶的分配器,其中内存范围被保留给特定大小的桶。这意味着最终在同一个桶中的大小相同的对象有很大可能被相邻放置。
通过控制对象在传递前设置的属性数量,我们可以控制auxSlots分配在哪个桶中。
构建addrof原语
通过使损坏的段在内存中直接跟随包含对象指针的数组,我们可以从段中越界读取这些指针值,并将它们作为原始数字返回到JavaScript中。
构建fakeobj原语
我们做同样的事情但方向相反:损坏JavascriptArray的段,并在其后放置NativeFloatArray的段。然后我们可以在浮点数组中伪造指针值,并通过从对象数组段越界读取来获取指针句柄。
获取任意读写原语
从这一点开始,实现读写原语的步骤相当直接。我们将伪造一个Uint32Array,以便可以控制其缓冲区指针。
为了在Chakra中伪造类型化数组,我们需要知道它的虚表指针,因为在开始为其赋值时会使用它。我们的第一步是泄漏一个虚表指针,并使用静态偏移计算我们想要的虚表指针。
绕过第一次修复
该漏洞最初被修复,使得仅分配常规属性不再允许我们触发漏洞。然而,可以定义一个具有特殊处理的访问器来触发完全相同的情况。
结论
在这篇博客文章中,我们看到有限的原始功能足以完全破坏一个进程。希望您喜欢这篇博文。谢谢!