Analysis of Microsoft IE – jscript.dll ‘Array.sort’ Heap Overflow Vulnerability (CVE-2017-11907)

In December 2017, Google Project Zero disclosed a Heap Overflow vulnerability in Jscript.dll. A proof-of-concept (PoC) exploit can be found here. A CVE-2017-11907 has been assigned to this vulnerability. This disclosure was part of a series of vulnerabilities in WPAD/PAC and JScript that Google Project Zero reported in 2017.

An in depth technical write-up can be found in the blog post “aPAColypse now: Exploiting Windows 10 in a Local Network with WPAD/PAC and JScript“.

Per CWE-122, A heap overflow condition is a buffer overflow, where the buffer that can be overwritten is allocated in the heap portion of memory, generally meaning that the buffer was allocated using a routine such as malloc().

This vulnerability was reported for Microsoft IE 11, however, I could reproduce it on IE8 on Windows 7 SP1. We will analyze this vulnerability in this blog post. Following is a PoC published by Google Project Zero:

<meta http-equiv="X-UA-Compatible" content="IE=8"></meta>

<script language="Jscript.Encode">
	var vars = new Array(100);

	var arr = new Array(1000);
	for(var i=1;i<600;i++) arr[i] = i;

	var o = {toString:function() {
  		for(var i=600;i<1000;i++) {
    		arr[i] = 1337;
  		}
	}}

	function go() {
  		arr[0] = o;
  		Array.prototype.sort.call(arr);
	}


	go();

</script>

This script (PoC) performs following actions:

  1. Create a new Array of 100 elements called vars.
  2. Create one more Array of 1000 elements called arr.
  3. Assign values to elements 1 through 599 (Notice that the arr[0] is still undefined).
  4. Create an object using Immediately Invoked Function Expression and store it in variable ‘o’.
  5. When the function go() is called, arr[0] is assigned Object ‘o’.
  6. After that the Array.sort() is called.

Here, the assignment arr[0] = o; is what causes the crash. We will discuss that later in this blog post.

Crash Analysis:

When accessed this page in IE8 with Page Heap enabled and Debugger attached, we see following crash:

As we can see, the crash occurred while executing mov instruction which tried to copy a value in ECX into 4 bytes starting at the address in ESI. The program crashed because ESI contains 0x0bb9b008 which has invalid data. We need to look at an allocation call stack to see if this is Heap Overflow or some other bug:

So, the ESI is pointing to 0x0bb9b008 while User Pointer/Address is 0x0bb964e0 and the object size is 0x4b20. The vulnerable object starts at 0x0bb964e0 and considering the fact that it’s 0x4b20 in size, it should end at 0x0bb9b000 (0x0bb964e0 (UserPTR) + 0x4b20 (Object Size) = 0x0bb9b000).

0:005> ? esi - 0xbb964e0
Evaluate expression: 19240 = 00004b28

However, the mov instruction tried to write past 0x0bb9b000 until 0x0bb9b008 causing an 8 byte overflow. Let’s the dump the contents at 0x0bb964e0 which is start of the object (UserPTR):

Vulnerable Object

As we can see, the vulnerable object still has valid data. However, ESI is pointing to 0x0bb9b008 which is 8 bytes past the end of the object (0x0bb9b000) and that’s what caused IE to crash. This confirms that it’s a Heap Overflow bug.

What’s the root cause of this vulnerability?

If we monitor memory allocations during object creation, we would notice that the object is created when Array.prototype.sort.call(arr) is called. We can confirm that from Allocation Call Stack:

As we can see, the method Array.prototype.sort.call(arr) called jscript!JsArraySort() which in turn called jscript!JsArrayStringHeapSort(). The jscript!JsArrayStringHeapSort() then called msvcrt!malloc() which then called ntdll!RtlAllocateHeap() and then 0x4b20 bytes of memory was allocated at address 0xbb964e0. Also, looking at the stack trace, we can conclude that the crash occurred while getting some values from the vulnerable (Array) object:

0:005> kb
ChildEBP RetAddr Args to Child
08a5cbb4 7182ac4d 0bb44fc0 0bb5fc1c 08a5cc18 jscript!NameTbl::GetValCore+0xca
08a5cbc4 7182acd0 0bb9b000 00000000 0bb9b008 jscript!ArrayObj::GetValAtIndex+0x5a
08a5cc18 71853058 00000259 0bb9b008 3c7f46d4 jscript!ArrayObj::GetVal+0x24
08a5ccfc 71852e34 07669d10 0bb44fc0 000003e8 jscript!JsArrayStringHeapSort+0x1e3
08a5cd6c 7183599a 00000259 000003e8 08a5cf18 jscript!JsArraySort+0x243
08a5cdd4 71835870 08a5cf18 00000000 00000000 jscript!NatFncObj::Call+0x106

According to the author of this vulnerability, “Array.sort is implemented in JsArraySort which, depending if a comparison function was specified or not, calls JsArrayStringHeapSort or JsArrayFunctionHeapSort. These (vulnerable) functions take several arguments, 2 of which are the input array length and the number of elements currently in the input array (this can be smaller than the array length). The vulnerable functions are going to allcoate 2 buffers to store intermediate data. The size of these buffers will be calculated based on num_elements. However, while filling those arrays it is possible that the number of elements is going to increase, which causes a heap overflow.

Based on this information we can draw a rough flowchart to visualize this:

So, these Sorting functions allocate two buffers (Arrays) and their size is based on the number of elements that are currently there in the Input array and an overflow occurs while filling these buffers (Arrays) if the number of elements increase. Looking at the PoC, it seems that the statement arr[0] = o; which assigns previously created object ‘o’ to the 0th element of the Array arr causes an overflow while these Sorting functions fill the buffer.

To validate our assumption, let’s look at our Array when object ‘o’ is assigned to arr[0] and then in the next run change the 0th element to String or Integer and see if it still causes a crash. This is how our Array looks like after arr[0] = o;:

Let’s modify our PoC to assign a string “SlidingWindow” to arr[0] , save it as patched_code.html and see if the crash still occurs:

<meta http-equiv="X-UA-Compatible" content="IE=8"></meta>

<script language="Jscript.Encode">
	var vars = new Array(100);

	var arr = new Array(1000);
	for(var i=1;i<600;i++) arr[i] = i;

	var o = {toString:function() {
  		for(var i=600;i<1000;i++) {
    		arr[i] = 1337;
  		}
	}}

	function go() {
  		arr[0] = "SlidingWindow"; //This shouldn't cause an overflow/crash
  		Array.prototype.sort.call(arr);
	}


	go();

</script>

Notice that we changed arr[0] = o; to arr[0] = “SlidingWindow”;. Let’s attach IE to debugger and access patched_code.html:

As we can see, IE did not crash this time but is our string SlidingWindow there in memory?

Yes, the string SlidingWindow is in memory, yet IE did not crash. That’s because the JsArrayStringHeapSort() function created a buffer (Array) based on the number of elements currently in our Array arr and since arr[0] did not have an Object this time but just a string SlidingWindow, the number of elements did not increase while filling the buffer so an overflow never occurred.

Exploitability:

To exploit a Heap Overflow vulnerability, we usually need to follow these steps:

  1. Control contents of appropriate CPU register:

We need to be able to modify the PoC such that we can point one of the registers to the address in our Heap Spray during crash. In this scenario, it could be Array element assignment like arr[i] = i or arr[i] = 1337 but we will discuss this shortly.

2. Create a memory layout:

We need to create a memory layout in such a way that when vulnerable object is allocated it takes the place of our choosing. For this to happen, we need to allocate several objects (Arrays of BSTR objects for example) in memory which would be adjacent to each other, free some of these objects to create holes and then trigger the vulnerability so that when the vulnerable object is allocated, it takes the place of our (intentionally) free-ed object (i.e. fills the hole).

So, the layout would look something like this: Vulnerable Object + Our Object1 + Our Object2 (with vtable pointer). One thing to note here is that one of the objects (Our Object2 in this example) from our allocations should have a vtable pointer. We will discuss the reason for that shortly.

3. Trigger allocation of vulnerable object and overflow to cause Memory Leak:

Once we have an appropriate memory layout setup discussed earlier, we then need to trigger allocation of vulnerable object and then heap overflow which would write past vulnerable object (beyond 0x4b20 bytes in this case since the object size is 0x4b20 bytes) so that it would corrupt the header (length field) of the object we allocated which is right next to the vulnerable object.

4. ASLR Bypass:

a. Leak/Retrieve vtable pointer: The next step is to find that object from our allocations from step 2 whose header field was corrupted. We allocated several objects from step 1, so how do you know which one was corrupted? For that, we simply need to traverse through each of these objects and check the header (length field). Since we already know the size of the object we allocated in step 1, we just need to check if the value in header field is different than the expected size. If yes, then that’s the object with corrupt header field.

Now that we know the corrupted object, we need to read past this object until we encounter a vtable pointer of the object which is next to the corrupted object. This object is also from the allocations that we made in step 2.

b. Calculate DLL/Image Base Address: Now that we know the vtable pointer, we can utilize it to calculate the base address of a loaded DLL/Image or module, Jscript.dll in this case. This way we bypass/defeat ASLR.

5. Perform Heap Spray (DEP Bypass + Shellcode):

We now need to spray the heap with a ROP chain + Shellcode. This ROP chain will help us bypass Data Execution Prevention (DEP) so that our Shellcode can execute. Since we have a base address of loaded module/DLL (Jscript.dll in this case), we can build our ROP chain based on gadgets from Jscript.dll. The only thing is that we can’t use hard coded address but Base Address + Offset.

We can use Mona.py to build a ROP chain using following command:

mona rop -m jscript.dll -rva

6. Trigger the overflow again to get EIP control:

Now that we have our ROP chain + Shellcode in memory, we need to point EIP into our Heap Spray so that it can start executing our ROP chain and then Shellcode. For this, we need to trigger heap overflow again such that EIP lands in our Heap Spray.

One thing to note here is that we need to make ESP point into our Heap Spray at the right time. Because if ESP is still pointing to the Stack and when EIP encounters a RET instruction for the first time (while executing ROP gadgets), the execution will return to the Stack and not our Heap Spray. So, we will no longer be controlling the execution flow.

Challenges

Now one of the challenges here is controlling the contents of appropriate CPU register during crash (Step 1). In this case since the crash occurs while executing mov dword ptr [esi],ecx, we must be able to control what value goes inside ECX because that’s what being copied into an address referenced by ESI. To be able to do this, we need to figure out what part of the PoC code we need to modify that will allow us to control the value/address that goes in ECX.

I noticed that the value in ECX is always same after every crash, which is C0C00003. I tried playing around with the values of arr[i] = i as well as arr[i] = 1337 thinking that one of these might have been translated into C0C00003. However, I noticed that changing these values never affected ECX and it always contained C0C00003.

Another challenge is about creating a memory layout (Step 2). One of the hurdles is more or less similar to what I faced while analyzing CVE-2017-11793, a Use-After-Free (UAF) vulnerability which was also part of aPAColypse now disclosure. The Jscript allocations do not use Default Process Heap (00790000) but a different one, 00990000. These are the heap available:

0:005> !heap
Index Address Name Debugging options enabled
1: 00790000
2: 00990000
3: 00ae0000
4:
<!– snip –>

Let’s look at !heap -p -all output:

As we can see, our vulnerable object 0x0bb964e0 was allocated from 00990000 and not the Default Process Heap 00790000. So, if we want to create a memory layout where this vulnerable object would end up landing (Step 2), we need to create an object (from Jscript.dll) that would cause memory allocations from Heap 00990000 instead of Default Process Heap.

Like last time, I’m not familiar with Jscript allocations yet but mshtml only. However, we can try to learn a few things about Jscript Objects from Google Project Zero’s aPAColypse now blog post.

Jscript Vars and Strings

On a 64 bit machine, JScript VAR is a 24 byte structure which represents a Javascript variable. Based on the explanation from aforementioned blog post, in most cases, we can visualize Jscript variable memory layout as follows which is sufficient to follow the exploit:

This is how a JScript VAR would look like in memory when stored a Double Precision Number (Floating Point 64):

A Jscript String is also a Jscript VAR whose VAR type is 8. This is how a Jscript String looks like:

The String VAR points directly into the character array. Which means, when we access the String VAR we get a pointer to the start of the character array. So, if we decrement this pointer by 4, we get the String length. One more thing to note here is that the BSTR allocations are handled by OLEAut32.dll and allocations are made on a separate heap which is different heap than used by other Jscript objects. I still haven’t gone through it yet but if you’re interested in knowing more about this, you can go through this Heap Feng Shui in JavaScript paper.

That’s how it would look like on a 64 bit machine. Since we’re dealing with IE in 32-bit mode, the var size etc would be less than what’s mentioned above. For example, instead of a Double Precision Number, we would be dealing with a Single Precision Number (a floating point number on a 32 bit machine).

Overwriting Temporary/Vulnerable Buffer to Control the Contents (Step 1)

Now that we have some (theoretical) understanding of Jscript VAR and Strings, let’s understand our vulnerable buffer which is a temporary buffer/array that JsArrayStringHeapSort() creates. This is the buffer we overflow into. The size of this array depends on number of elements currently in the Input array and each element of this array is 48 bytes in size (on a 64 bit machine). Following is the structure of this array:

As mentioned in the JsArrayStringHeapSort() flow chart earlier, it retrieves each element of the array and check if that element is defined. If yes, it then performs following:

  1. Store the original array element at offset 16. This is a JScript VAR.
  2. Convert the original array element to a JString String and store a pointer to this Jscript String at offset 0.
  3. Store the index of current element in array at offset 8.
  4. Store either 0 or 1 at offset 40. This depends on the VAR type of the original array element.

As researchers of this vulnerability mentioned, there’s nothing much in this temporary buffer that we can control directly. However, if a member or element of the input array is a double precision number, it’s value is stored at offset 24. This number is something that we can control directly. For this, we need to create a number with double representation and then leverage our overflow to overwrite a pointer somewhere after the end of the buffer with a pointer to the memory we directly control (which is usually a heap spray). The details of how this can be accomplished using a NameList JScript object which has a pointer to a hashtable are available in Stage 2: Overflow section of Google Project Zero’s aPAColypse now blog post.

Alright, this is a lot of theory! When time permits, I will try to figure out how to cause JScript VAR or String allocations, verify their memory layout as well as layout of vulnerable/temporary buffer and then see how we can control this buffer to control the contents of CPU register (Step 1). Once that is done, rest of things like creating a memory layout, creating a memory leak to get Base address of JScript.dll, ALSR + DEP bypass and EIP control should be pretty straight forward.

Thanks for your time and I would love to have your feedback.

References

https://googleprojectzero.blogspot.com/2017/12/apacolypse-now-exploiting-windows-10-in_18.html

https://www.blackhat.com/presentations/bh-usa-07/Sotirov/Whitepaper/bh-usa-07-sotirov-WP.pdf

Leave a comment