Exploiting an integer overflow with array spreading (WebKit)
This article is about CVE-2017-2536 / ZDI-17-358, a classic integer
overflow while computing an allocation size, leading to a heap-based buffer overflow. It was introduced in
99ed479
,
which improved the way JavaScriptCore handled ECMAScript 6 spreading operations, and discovered by
saelo in February. The PoC is short enough to fit into a tweet, and we have a fully
working exploit for Safari 10.1, so this is going to be fun!
The Bug
The following code is used when constructing an array through spread operations:
SLOW_PATH_DECL(slow_path_new_array_with_spread)
{
BEGIN();
int numItems = pc[3].u.operand;
ASSERT(numItems >= 0);
const BitVector& bitVector = exec->codeBlock()->unlinkedCodeBlock()->bitVector(pc[4].u.unsignedValue);
JSValue* values = bitwise_cast<JSValue*>(&OP(2));
// [[ 1 ]]
unsigned arraySize = 0;
for (int i = 0; i < numItems; i++) {
if (bitVector.get(i)) {
JSValue value = values[-i];
JSFixedArray* array = jsCast<JSFixedArray*>(value);
arraySize += array->size();
} else
arraySize += 1;
}
JSGlobalObject* globalObject = exec->lexicalGlobalObject();
Structure* structure = globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous);
JSArray* result = JSArray::tryCreateForInitializationPrivate(vm, structure, arraySize);
CHECK_EXCEPTION();
// [[ 2 ]]
unsigned index = 0;
for (int i = 0; i < numItems; i++) {
JSValue value = values[-i];
if (bitVector.get(i)) {
// We are spreading.
JSFixedArray* array = jsCast<JSFixedArray*>(value);
for (unsigned i = 0; i < array->size(); i++) {
RELEASE_ASSERT(array->get(i));
result->initializeIndex(vm, index, array->get(i));
++index;
}
} else {
// We are not spreading.
result->initializeIndex(vm, index, value);
++index;
}
}
RETURN(result);
}
At [[ 1 ]], the function computes the size of the output array, which it
allocates and initializes at [[ 2 ]]. However, the size computation can
overflow, causing a smaller array to be allocated. JSObject::initializeIndex
does not perform any bounds checks as can be seen in the following piece of
code:
/* ... */
case ALL_CONTIGUOUS_INDEXING_TYPES: {
ASSERT(i < butterfly->publicLength());
ASSERT(i < butterfly->vectorLength());
butterfly->contiguous()[i].set(vm, this, v);
break;
}
/* ... */
As such, a heap buffer overflow occurs. The bug can be triggered through the following script:
var a = new Array(0x7fffffff);
var x = [13, 37, ...a, ...a];
A JSArray
of size 0 is allocated, and then 232 elements get copied
into it, which the browser does not like very much.
The patch for this bug simply adds integer overflow checks to all affected tiers (interpreter + JITs).
Exploitation
Even though the PoC code given above uses a single array multiple times,
JavaScriptCore will allocate a JSFixedArray
for every spread operand of the
array literal (in slow_path_spread
). As such, roughly 4 billion JSValue
s will
have to be allocated, taking up 32 GiB in RAM. Luckily, this isn’t much of a problem due
to the page compression performed by the macOS
kernel. It will, however,
take roughly a minute to trigger the bug.
What is left to do now is to perform some heap feng-shui to place something interesting on the heap that we will then overflow into. We use the following heap spray to exploit the bug:
- Allocate 100
JSArray
s of size0x40000
and root them (i.e. keep references). This will trigger GC multiple times and fill up holes in the heap. - Allocate 100
JSArray
s of size0x40000
, where only every second one is rooted. This triggers GC and leaves holes of size0x40000
in the heap. - Allocate a larger
JSArray
and anArrayBuffer
of the same size. These end up directly after the spray from step 2. - Allocate 4 GiB of padding using
JSArray
s. - Trigger the bug by concatenating
JSArray
s with a combined size of 232 + 0x40000 (containing the repeated byte0x41
).
The target buffer will be allocated in the sprayed region from step 2 and the
victim buffers from step 3 will be overwritten. This increases the size of the victim
array to the sprayed value (0x4141414141414141
), so that it overlaps with the
victim ArrayBuffer
.
The final steps immediately yield the fakeobj and addrof primitives
described in section 1.2 of the JavaScriptCore phrack
paper which can
then be used to write code to a JIT page and jump to it.
In our exploit we perform step 5 in a separate web worker, so that we can launch a second stage shellcode immediately after the victim arrays are overwritten. This way we do not need to wait for the full overwrite to finish, and the heap is only left in a broken state for a very short time, so that garbage collection does not crash (which runs concurrently starting from Safari version 10.1). The full exploit can be found on our GitHub.