
Exploiting a patched vulnerability in JavaScriptCore
This is an exploit for a WebKit vulnerability that was originally discovered by Fluoroacetate during the pwn2own competition in Vancouver. While I did not discover this bug, I wrote this exploit to practice my exploit development skills. The original writeup for this exploit is here from Zero Day Initiative . While this write up is very good and it was instrumental in helping me understand the vulnerability, it is from the point of view of someone verifying the vulnerability. I found that some key details are missing when trying to engineer this exploit from scratch and I hope to fill in some of the gaps that the ZDI write up missed and gain practical skills on how to engineer a complicated exploit from scratch.
These steps serve as an outline to get arbitrary code execution within JavaScriptCore (JSC), the JavaScript engine for WebKit
The vulnerability that will be exploited is an integer overflow that occurs in the code produced by the DFG just in time (JIT) compiler for WebKit. This specifically occurs in the compileNewArrayWithSpread function. This function will be called when code using JavaScript spread syntax to create a new array is JITed by DFG.

Inside the JITed code, first it will compute the size of the array. It does this by adding the length of each argument passed to the array constructor. As it computes the size for each addition it checks for an overflow of the size. After this it will call the compileAllocateNewArray function passing the length that was computed in this function.

The compileAllocateNewArray will then pass the length that was computed before to emitAllocateButterfly.

The emitAllocateButterfly will then left shift the size 3 bits which is equivalent to multiplying it by 8. However, there is no check for an overflow and thus a number such as 0x20000001 can overflow to 0x8
This c program illustrates this vulnerability:


We can use this vulnerability to trick the JavaScript engine into thinking we've allocated an array with size 0x20000001 but actually only have allocated enough space for 1 JSValue (8 bytes). This will result in an out-of-bounds (OOB) read and write (R/W) primitive that can then be leveraged to achieve arbitrary R/W and eventually remote code execution (RCE).
Identify the vulnerability
In order to confirm that we have an OOB read we will try to trigger this vulnerability on an address sanitizer (ASAN) build of JSC.
To do this from the WebKit directory we can run the commands:
Tools/Scripts/set-webkit-configuration --asan
Tools/Scripts/build-jsc --jsc--only --debug
This will build a debug build of JSC with ASAN enabled allow us to verify whether of not we have successfully triggered the vulnerability.
Here is the first iteration of exploit.js
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)
When running this I get the following error:
Program terminated with signal SIGKILL, Killed. The program no longer exists.
My guess was that too much memory was being consumed when trying to allocate such a large array. In order to confirm this, I added a breakpoint to the JITed code by adding a call to m_jit.breakpoint() inside of the compileNewArrayWithSpread which adds an int3 instruction to the JITed code.
After adding the breakpoint I found that it wasn't hit and then I decided to test a length of 0x20001. I then realized that the code wasn't even being compiled so I added more iterations to activate the DFG compiler
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)
Using this code we were able to hit the breakpoint without a SIGKILL! As is usually the case, fixing one issue brings out another and we got a SIGABORT instead... Using the gdb bt command we can see that operationNewArrayWithSize was called which called create.
It seems strange that our JITed code would be calling operationNewArrayWithSize and it must be that the JITed code had to take a slow path to the JavaScript engine for some reason.

We can see in the compileAllocateNewArrayWithSize that there is indeed a bailout to operationNewArrayWithSize. We then need to find out why exactly we are bailing out to the slow case.
We can see that in compileNewArrayWithSpread that shouldConvertLargeSizeToArrayStorage is set to false and that slow path won't be in the compiled code.
Therefore it makes sense that the slow path is being hit somewhere within emitAllocateJSObject

emitAllocateJSObject calls emitAllocateJSCell which in turn calls emitAllocate.