It’s been a while since a pure JavaScript vulnerability was widely used by exploit kits. The last few years mostly gave us IE Use-After-Free vulnerabilities. When those were dealt with by Microsoft’s IsolatedHeap and MemoryProtection mechanisms, introduced in the middle of 2014, the stage was clear for Flash to take over.
Now, as Flash is marching towards its imminent death, Silverlight has been dying for a long time, and Java applets must be signed and played only after the user is prompted, we can expect some new trend to arrive on the scene.
CVE-2015-2419 (Jscript9 Memory Corruption), the subject of our paper, was patched 5 months ago, but is still used across most Exploit Kits. However, no satisfying analysis, regarding either its root cause or how to successfully exploit it, has been published. We think an analysis of this kind is needed: First, to see a pure JS exploit for IE 11, despite the heap corruption mitigations in IE mentioned above. Second, unless Microsoft comes up with similar protections for IE’s JS engine, this might be the kickoff for similar exploits as soon as Flash disappears.
We started with a working exploit taken from Rig EK (MD5: 4497f09502758ae82f122b21b17d3644. It looks exactly the same as in Angler EK). It’s heavily obfuscated and tricky, which makes the job of understanding the vulnerability, and the exploit directly from the exploit code, very complicated.
Let’s dig in to the process of analyzing this exploit:
Root cause analysis
First, we verified that the exploit reaches an expected call to kernel32!VirtualProtect to change the shellcode protection to executable. At that stage, we could also see that the stack was pivoted to a heap address. We then executed iexplore.exe once again with page heap enabled, to make the exploit crash IE and locate the specific instruction of the crash.
This is what we got:
Let’s look at the code before the crash, to understand where edx comes from:
Or shortly, edx <- [this+0x34]-0x8.
This is what the rest of the function looks like:
If you look carefully, the purpose of the above code is to free an item in a doubly linked list:
- Set the next item’s ‘prev’ pointer to point to the previous item.
- Set the previous item’s ‘next’ pointer to point to the next item.
- Free the current item’s memory.
In this list, each item looks like this:
Pointer to previous item |
Pointer to next item |
Pointer to this+0xc |
To confirm that it’s a double-free vulnerability, we’ll take a look at the call stack at the moment of the crash:
There is an attempt to free the address for the second time using the object’s Dispose method, triggered this time by the Garbage Collector.
As we have page heap enabled, we can see how this address was de-allocated the first time:
The object was de-allocated the first time during the execution of JSON.stringify().
As we already know from previous publications, the vulnerability is triggered when we stringify a deeply nested JS object. How deeply is it nested? It’s at least 5 hierarchies. That means we should see it somewhere in the code.
As we can see above, JSON::StringifyObject calls ThreadContext::ReleaseTemporaryGuestAllocator, which is the function that actually triggers the call to msvcrt!free.
Let’s take a look:
The value at this+0x3c4 is compared to 5. If it’s greater or equal, the execution will jump to the code that actually frees the object for the first time (otherwise, it increments it). Note that this piece of code is similar to the Dispose method of TempArenaAllocatorWrapper object we already saw above.
However, the redundant code is not the one causing the trouble, as it seems to be doing exactly what the Dispose method does.
The root cause of the bug is that, even after freeing the memory, the pointer still holds a reference to the freed address. Both issues are fixed in the patched version of jscript9.dll:
We can see that, before reaching the code that writes two DWORDs and then frees the memory, the code first verifies that the pointer at this+34 is not null:
mov edx,dword ptr [esi+34h]
test edx,edx
After freeing that memory, it is set to null. Now we can be sure that the Garbage Collector won’t try to free it again:
and dword ptr [esi+34h],0
We’ll spare the reader the excitement of seeing the fixed ThreadContext::ReleaseTemporaryGuestAllocator code, and just say that now it calls the Dispose method instead of independently executing the similar code.
Let’s see what we have so far:
- 4 nested lists (with a total of 5 hierarchies) in a JS object are enough to cause uncontrolled freed memory when stringifying the object.
- The root cause lies in not nulling the pointer to the memory after it is freed.
- There is no code execution derived directly from triggering the bug. We only saw two write operations right before freeing the memory for the second time.
Now let’s see how it’s leveraged to code execution.
2 DWORDS and a well sprayed heap
As we briefly mentioned above, before freeing the memory, the Dispose method writes 2 DWORDS from the soon-to-be-freed buffer.
Let’s simplify those 6 assembly lines using the following notation:
- [edx+4] <- [edx]. The first DWORD is copied to the address pointed to by the second DWORD.
- [[edx]+4] <- [edx+4]. The second DWORD is copied to the address pointed to by the first DWORD + 4.
We have to occupy the first freed allocation with data before the second one is freed. This is so the first 2 DWORDs are valid pointers, as those addresses in memory will be overwritten when we trigger the bug. This means we must have our heap sprayed to occupy a predictable address, so that overwriting the data in the predictable address will somehow help us to execute code.
Generally speaking, in our exploit, the heap is sprayed with Uint32Array (jscript9!Js::TypedArray<unsigned int,0>) objects. However, just allocating many instances are not enough. For having them allocated consecutively in the memory, they are the elements of an Array (jscript9!Js::JavascriptArray). To make sure we occupy a specific address every time, we allocate many such arrays, also as elements of a larger array.
In terms of code, it looks like the following:
We use the window.setInterval method to have some time gaps (100ms) between allocations of each 400 Array elements. When we reach 4096, we clear the interval and call the function that continues the exploitation process.
So we have:
- One ArrayBuffer of 4 bytes.
- An array of 4096.
- Each element of the array, is an array of 15352.
- The first 85 (of each 15352) are Uint32Array objects, constructed using the above mentioned ArrayBuffer (thus making them of size 1).
The code above is actually a precise heap Spray method that guarantees we’ll have a Uint32Array in 0x128ef000.
Let’s see what these objects look like in memory:
We can use the “mona” plugin (written by corelanc0d3r) to better understand this object:
A key observation at this stage is that the offsets 0x1c and 0x20 of the latter object correspond to its length and start of data.
As we’ve seen in a lot of Flash exploits, one of the common methods of exploiting a UAF or a heap-based buffer overflow is to overwrite the length field of an array-like object to include the entire virtual memory address space. Once we have an object with full read/write access to the virtual memory space of our process, the stage is set for code execution.
Let’s see how this is done in our exploit.
The catcher in the heap
First, we check the size of the freed buffer, and how the exploit successfully catches it before triggering the garbage collector to free it again.
We set a breakpoint on the first freed buffer, within ThreadContext::ReleaseTemporaryGuestAllocator.
We expect this heap to be managed by LFH. mona says it is:
Our first soon-to-be-freed buffer resides at 0x07cd4018. The same breakpoint is hit twice more with the following values for edx: 0x07cd4000, 0x07cd3fe8.
We now expect 3 allocations with the size of 0x0c catching those addresses:
eax, which is the return value of malloc, equals the most recently freed buffer, 0x07cd3fe8.
Now we have all 3 freed buffers caught by new allocations. To see the size and the origin of the allocation, we glance at the stack:
The buffers are now the data of ArrayBuffer objects created with the size of 0xc.
To make sure that the freed buffers are caught by the new allocations, the exploit allocates many ArrayBuffer objects in size 0xc – just in case other buffers with the same size have also been freed besides the 3 above.
If this is true, a breakpoint set on the “crashing” instruction in TempArenaAllocatorWrapper::Dispose will be hit 3 times, with edx pointing each time to one of the addresses we mentioned.
This means that after the few next instructions, the Uint32Array that resides in 128ef000 will be corrupted as follows:
[128ef020] <- 128ef018
[128ef01c] <- 128ef020
In other words, its length is now 0x128ef020 and its data starts in 128ef018!
As you recall, [128ef018+4] is where the array length is. As the array’s first element is now 0x128ef018, setting a new value to Array[1] lets us change the array size to include the entire user-space.
Overwriting both fields lets us corrupt them once again, but this time, with whatever values we want!
This is what happens few steps later:
Looking at the previous instruction, we understand that edi points to the beginning of the array, esi is the index of the array element being modified, and eax is the new assigned value.
Now our corrupted Uint32Array starts at 0x10000 and has a length of 0x7fffffff – that means full read/write memory access.
Although the exploit code is heavily obfuscated, we can point to the code responsible for gaining the full read/write access:
As a part of the function Il1Iza(), we can see a definition of a long JSON string (soon to be parsed to JS objects):
n = ‘{“ll”:”length”,”l”:”charCodeAt”,”I”:”fromCharCode”,”Il”:”floor” … “lllII”:2147483647, … }}’;
A quick glance at this JSON string is enough to understand that it’s a key part of both the exploitation and the obfuscation (but don’t get confused: this is not the JSON which triggers the bug!)
As seen above, the variable lllII will get the value 2147483647 which equals to 0x7fffffff. If we search for references to this variable in the code, we find:
Just as we expected: as the pointer to the beginning of the typed array’s data was overwritten with 128ef018, writing to its second cell will overwrite the length field (at the address 128ef01c).
A quick track also reveals that this[“scope”].Ma is 0x10000, as expected.
Having full read/write memory access is more than enough to gain code execution. Still, let’s see how it is implemented in our case.
From read/write to execute
As IE has full ASLR, we should start seeing some info leaks.
To have an idea as to what we can expect, we can break on the kernel32!VirtualProtect call that changes the shellcode protection to PAGE_EXECUTE_READWRITE, and see what the ROP chain looks like:
After 2 gadgets, we can see the stack layout that will change the protection of the shellcode in order to execute it.
Let’s verify that this is indeed the shellcode:
The code above calls 02fac201, so let’s see what lies there:
We see a short generic shellcode (from the original exploit) which calls the real shellcode (a break instruction we inserted instead of the original shellcode that downloads a malware).
The gadgets we saw above are:
We still need to execute a stack pivot gadget (such as xchg eax,esp) somewhere to actually trigger this ROP chain.
If we look further in the pivoted stack layout, we can see the following gadget in offset 0x90:
This is as we expected (remember this 0x90 offset as it has a key role in triggering the ROP chain). The gadgets used in the exploit are mentioned explicitly (as text) in the JSON we described earlier. Trying to find some clue to these gadgets in the exploit code, we searched for their offset from jscript9 base address in the code. Unsurprisingly, we found them in the JSON:
“IIllII”:[122908,122236,125484,2461125,208055,1572649,249826,271042,98055,62564,162095,163090,340146,172265,163058,170761,258290,166489,245298,172955,82542]
“IlIIII”:[150104,149432,152680,3202586,214836,3204663,361185,285227,103426,599295,365261,226292,410596,180980,226276,179716,320389,175621,307381,792144,183476]
Each one is a list of offsets of length 21. We assume that each of the lists contains the offsets of one gadget in various versions of jscript9. The offsets that fit our version were found third from the end of each list.
We get the feeling that the exploit will read the data in these offsets from the memory to construct the ROP chain according to the current version being exploited. So we’ll break on access to one of the gadgets, just to see which function is actually reading the data from the memory. Then we can set a regular breakpoint on the specific instruction to see all the info leaks.
We saw the following:
We’ll spare you a list of all leaks, as it’s a long one. However, after analyzing it carefully and combining it with the write operations that were carried out, we see the following activity being executed in the JavaScript code:
- Overwrite 128e0020 with the value 0x1BD81BD (which is 0xDEC0DE * 2 + 1). As JavascriptArray must be able to contain both numbers and objects, numbers are represented by an odd number calculated as above. As pointers to objects can’t be odd (heap allocations are aligned – 8 byte in x86 and 16 byte in x64), it enables jscript9 to easily identify the type of an array item.
- Find the Array in which the first element is 0xDEC0DE, assign it a new JavascriptArray, and read the vftable pointer of the object. Calculate a bitwise AND on this address and the mask 0xffff0000 results in jscript9 base address.
- Parse jscript9 header to get the RVA of the import table, find the kernel32 entry within it, and then parse the import address table to find kernel32!VirtualProtect address.
- Read all the offsets in the 2 lists mentioned above to find the gadgets. The stack pivot gadget is supposed to be “lIIIlI”:[148,195] (taken from the JSON, equals 0x94, 0xc3) and the first gadget is “lIIlIl”:[137,65,12,195] (same, equals 0x89, 0x41, 0x0c, 0xc3).
Now that we have (almost) all the information we need, we can finally construct the ROP chain. For that, the exploit again uses the array with the first cell in 0x128e0020, and allocates a new Uint32Array using an ArrayBuffer object with the size of 0x5000.
This is the ArrayBuffer object:
The Uint32Array mentioned above, that will be used to construct the ROP chain, is created for 0x128e0020 to point to. In that way, we’ll be able to leak the address of the ROP chain.
Before actually writing the ROP chain, the shellcode has to be created. To do so, it uses the same trick again and allocates a new Uint8Array pointed to by 0x128e0020. It then writes the shellcode caller we saw above, followed by the actual malicious shellcode.
This is our shellcode caller. Now we really have all the information needed for our ROP chain.
After constructing the full ROP chain with the new Uint32Array allocated earlier, now it has to be triggered.
As you recall, a new JavascriptArray object was allocated in the previous section of infoleaks to calculate the base address of jscript9. As we have the address of that object, we can also overwrite it with whatever we want, using our corrupted Uint32Array. The exploit overwrites the vftable pointer of this array object with the address of the top of the ROP chain.
We can see a JavascriptArray object lies in 0x04828180, and it will be overwritten with eax=13d1b000.
Remember, a stack pivot gadget was written in offset 0x90 from the beginning of the ROP chain. With our vftable pointer to point at the beginning of the ROP chain, let’s see which method is in offset 0x90 in the real vftable of JavascriptArray:
The SetItem method! That means that an attempt to change any of the items in this JavascriptArray will actually trigger the stack pivot gadget. For the ROP chain to trigger correctly, we should expect eax to point to the top of the fake vftable, which in our case is also the top of the ROP chain. Indeed, right before JavascriptOperators::OP_SetElementI calls the stack pivot gadget, eax points to the ROP chain:
The ROP chain & shellcode caller
Other than returning to kernel32!VirtualProtect and jumping to the shellcode, the ROP chain first has the following gadget:
At this point, the stack is already pivoted. Therefore, eax contains the previous esp value that points to the actual stack of the thread. ecx points at the array object on which the SetItem method was invoked. So this gadget actually saves the original esp value prior to triggering the exploit within the object’s header, to later restore it and continue IE’s execution without crashing. The restoration is done at the end of the calling shellcode:
It also restores esi and ebp. The return instruction is the same as that of JavascriptArray::SetItem:
Once again, this is to have IE continue normal execution.
Conclusions
This IE JavaScript exploit was interesting for a few reasons:
On one hand, it was carefully designed to detect if the current version of jscript9 is exploitable. If it’s not, execution is stopped to avoid a crash. Moreover, for the same goal of the ROP chain and the shellcode triggering successfully, it reserves all critical registers and restores them for the execution to continue normally without affecting the user experience.
On the other hand, it does not take any steps to evade exploit mitigation tools. Tools such as EMET or Check Point’s CuckooSploit can easily detect the pivoted stack, the return to VirtualProtect with PAGE_EXECUTE_READWRITE, and other detections.
As we mentioned in our preface, one of the interesting issues is not the effectiveness or wide distribution of this specific exploit, but the potential comeback of a pure JavaScript exploit once Flash and Silverlight fade away.
We have constructed our own POC exploit for the vulnerability, that reduces the amount of original JavaScript code by 80%, and is much more readable and explanatory.
The Check Point IPS blade provides protections against this threat:
Microsoft Internet Explorer Jscript9 Memory Corruption (MS15-065: CVE-2015-2419)