Let’s dive into an exploitation exercise for CVE-2020-1062, a recent Internet Explorer (IE) use-after-free (UAF) vulnerability. By exploiting CVE-2020-1062, an attacker can potentially execute arbitrary code within the sandboxed browser process—though typically, attackers would need to combine this vulnerability with an additional sandbox escape vulnerability in a full attack chain.

After an anonymous contributor who wished to be credited as “Edward Thompson” reported the vulnerability via the iDefense Vulnerability Contributor Program, we passed the information to Microsoft, who fixed the issue in May, 2020.

The back story

CVE-2020-1062 is present in the jscript.dll module, the legacy JavaScript engine bundled in for compatibility. All versions of IE since IE9 up to IE 11 use jscript9.dll by default. However, both jscript9.dll and jscript.dll (older JavaScript engines) are present on Windows systems.

Jscript.dll isn’t loaded by default on recent IE versions but can be loaded into all IE versions from IE9 up to IE 11 by using the IE8 compatibility mode. The following tags in your script will make IE load jscript.dll alongside jscript9.dll to support the IE8 compatibility mode.

<meta http-equiv="x-ua-compatible" content="IE=8" />
<script language="jscript.encode">

Browser Fuzzing,” a presentation from the 2014 Hack in the Box conference, shows how to force jscript.dll to load. It also dives into the interaction between both the scripting engines in IE. It’s important to note that exploits for vulnerabilities in the legacy JavaScript engine have been resurfacing publicly, including three specific jscript.dll bugs: CVE-2018-8653, CVE-2019-1367, and CVE-2020-0674. Microsoft has released out-of-band patches for all.

Understanding the bug

CVE-2020-1062 is a UAF vulnerability within the jscript.dll module. Specifically, the error is within the JSArrayPush function. To understand the bug, it is important to understand at a high level how this function works.

In JavaScript, we can do a push operation on a JSArray object:

[].push.call(1.1, 1);

This operation is internally handled within the older JavaScript engine jscript.dll using the jscript!JsArrayPush function.

To see what happens under the hood within jscript!JsArrayPush, please see a snippet of the decompiled code below.

<<< Start >>>

Figure 1. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

As seen in Figure 1, the JSArrayPush function first tries to get the "length" property from the "this" object at (1), which is an object representation of the first argument that is passed to the push() call in javascript. It then tries to convert the "length" property to int by calling jscript!ConvertToScalar at (2).

Because the function is trying to get the “length” of the double value via the Number Prototype chain at (1), we can explicitly set the Number.prototype.length to an object reference that we control. For instance, we can set it in the following way:

Number.prototype.length = window.ParamToModal.

In (2), the function takes the “length” property object obtained above and converts it into an int by calling jscript!ConvertToScalar() function. This function triggers a “toString” callback on the fetched “length” property object.

Since we control the “length” property returned in (1), we can set our own “toString” callback in the script. Inside this custom “toString” callback, we simply call document.write("") to clear the current document, which will also free the "this" object.

Once the code returns from executing our custom “toString” callback function, the already freed "this" object will be erroneously used, which causes the UAF vulnerability.

The following two images demonstrate the flow of the trigger for this bug.

<<< Start >>>

Figure 2. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 3. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 4. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

To trigger the issue, we utilize two JavaScript files. The first script file is shown in Figure 2, which contains IE8 compatibility code that jscript.dll will handle. The second script file, snippets of which are shown in Figure 3 and Figure 4, will be regular JavaScript code handled by jscript9.dll. When we run the script snippets shown in Figure 3 and Figure 4, the script file represented by Figure 1b will load inside a new dialog window and then call the trigger function (the call being made is seen in 1), which is represented by 2. Inside this trigger function, we override the Number.prototype.length parameter to the window’s ParamtoModal property such that when lol.push.call() happens, the execution goes to 3. We clear the dialog window’s document object by doc.write(“”). After the function at 3 finishes executing, the program execution is supposed to return to 4 and exit, but this will not happen since we cleared the dialog window’s document object and no further script code will run from it.

In the subsequent sections, we will be focusing our analysis on the default 32-bit version of the IE Tab process. The vulnerability details remain the same even on the 64-bit version of IE Tab process—this can be enabled after turning on Enhanced Protected Mode—and curious readers can adapt the proof-of -concept (PoC) code to work on 64-IE with minimal changes.

Nailing down the crash location

Looking at the jscript!JsArrayPush function, we can see the crash location in detail.

<<< Start >>>

Figure 5. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

The ConvertToScalar call at (1) frees the ‘this’ pointer and then below at (2), the program crashes when trying to use the stale reference that is held by the local stack variable at ebp-0x20. At (3), the stale pointer is dereferenced to derive another pointer at offset 0x88, which is then called indirectly at (4).

Within the debugger, we can investigate which specific object pointer is stale/free by enabling Page Heap on the IE process (iexplore.exe) and setting a breakpoint at any location before the crash and after the object has been allocated. This can be seen in the figures below.

<<< Start >>>

Figure 6

<<< End >>>

<<< Start >>>

Figure 7

<<< End >>>

<<< Start >>>

Figure 8

<<< End >>>

The size of the target object (which gets freed and re-used) we want to control is 0x48 bytes and it is allocated on the C Runtime(CRT) heap.

Call-based primitives: Boon or bane?

On paper, call-based primitives sound nice. Unfortunately, they aren’t always powerful enough.

To begin, they are less useful and capable than arbitrary read-write (RW) primitives. One of their biggest disadvantages is the need to bypass Address Space Layout Randomization (ASLR). This necessitates either: a) an additional ASLR bypass/information leak bug; or b) working around the call-based primitive to try and convert it into an arbitrary RW primitives.

Unfortunately, in this specific vulnerability, the call primitive we obtain happens right after we free the object in a matter of a few instructions. In the context of UAF vulnerabilities, there is simply not a big enough gap between the object free and object reuse to convert this into other primitives all by itself. In the JSArrayPush function flow, if the ConvertToScalar() function returns successfully—and we need it to return successfully to free the object—we end up straight in the basic block below it, and the indirect call is unavoidable. With this in mind, we can rule out b) from our options. This specific call-based primitive is weak in the sense that we have to rely on an additional ASLR bug. To complicate things, there is a Control Flow Guard (CFG) check for this indirect call—another hurdle to cross.

At this point, it is natural to wonder if there is any value in this particular primitive even if we are able to utilize a separate info leak. Therefore, we will focus on trying to answer this particular question, assuming we have a separate information leak: Can we use this restrictive call-based primitive and bypass everything else for stable, remote code execution?

Essentially, assuming the presence of a separate information leak (out of scope for this post), we will try and convert this weak, call-based primitive into a full arbitrary read-write primitive, which is much more powerful on the road to a full PoC exploit.

To state the goal more precisely, we need to find a CFG valid function within jscript.dll or any other dll loaded in IE (assuming that our separate, hypothetical, info-leak bug can leak the base address of the module we want to work with) that will give us a limited write primitive and allow us to modify (say) the length of a javascript Typed Array. We can then convert this into a full arbitrary read-write primitive.

Looking at the indirect call in Figure 5, it seems to take two arguments. We need to find a CFG valid function that takes exactly two arguments (basically has a “ret 0x8” instruction at the end) so the stack isn’t misaligned once it returns. We don’t have any control over the two arguments pushed on the stack: one being a local stack variable and the other g_sym_length. However, since this is a virtual function call, the address of the ‘this’ pointer held by ‘ecx’ would be pointing to the start of our freed object.

The first double word(dword) within the freed object is expected to point to a virtual function table(vftable).

vftable = [this] ; Note that  we use the symbol [ ] as a way to denote dereferencing the memory akin to *(some_variable) in C/C++

At offset 0x88 within the vftable, which we can reallocate and control, is the virtual function we will call:

Call dword[vftable+0x88]

<<< Start >>>

Figure 9. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 10. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

We want to search the program’s address space for write-based gadgets, but not ones that write to a memory inside the freed object itself. We want to look for writes that happen to a memory location that is referenced via a dword within our freed object. Essentially, we want writes to a memory location of the form [[this+ some_offset1] +some_offset2] (i.e., a write into a location which is obtained via at least a double dereference from the ‘this’ pointer to the freed object). Figure 9 and Figure 10 highlight this in some detail. Note that some_offset1 has the restriction that it should be less than 0x48 bytes. This is because the freed object is of size 0x48. If we reallocate the freed object, we can control 0x48 bytes fully. If some_offset1 is greater than 0x48 bytes, we could still control it in certain cases where some offset1 value is large and we can heap groom a controlled object within that space. However, we would ideally want to avoid this as it can complicate our heap spray and grooming strategies, which can lead to stability issues for our PoC exploit.

There isn’t any restriction on some_offset2 because we can fully control the value of the [this+some_offset1] and thus, ensure that the calculation [this+ some_offset1] +some_offset2 essentially stays within a location we can control.

Let us see what the ideal function “gadget” will have to be:

  1. Function's Relative Virtual Address (RVA) is listed in the CFG table as a valid indirect call location (CFG valid function).
  2. Function calling convention is thiscall and takes two arguments (and has a “retn 0x8” instruction at the end of the function).
  3. Within the function, there is a write operation to a memory location that is referenced as a double/triple/quadruple pointer either directly or indirectly via the 'this' pointer (ecx/rcx)—essentially any writes of the form *(*(this+ index) ), etc.
  4. Any other function that satisfies only c) above but is called from a function that satisfies a) and b).

In c), we are placing a restriction that the function needs to write to a memory location that we can control. Since we can’t control the two arguments passed, this memory location needs to be derived via an arithmetic calculation on the ‘this’ pointer, which is our freed object that we can reallocate and control. This reference can be a double pointer reference or even deeper.

With d), we are saying either the function itself can satisfy all the conditions or any of the callee functions inside can also work.

We want to hunt functions with the above criteria in all of the loaded modules within the IE process. Strategically, we will start our search with the three modules we are sure will be loaded into the IE container process we are running in—mshtml.dll, jscript.dll and jscript9.dll. We start with these three modules because, as mentioned earlier, we are assuming we have leaked the base address of one of the modules within IE via another IE vulnerability. And it is quite likely that a separate information leak in IE would provide us with a base address of either the HTML or Javascript engines.

Parsing the PE headers and the CFG table of each binary will tell us at how many CFG valid functions are present.

Number of CFG valid functions:

Jscript9.dll  7928
Mshtml.dll    37029
Jscript.dll      883

There are more than 45,000 functions. Attempting to manually hunt for functions that satisfy the above criteria would be extremely time-consuming and painful.

Thankfully, we have a number of binary analysis and automation tools that we can put to work here. We will consider one of them, Binary Ninja (BN), which has a powerful Python-based API and Intermediate Languages (ILs) to help with this task.

Binary Ninja analysis   

Binary Ninja has a rich, public API and there are a number of blog posts that describe using the API to help with various tasks, including some helpful articles on the Trail of Bits website that show the power of the API and the tool in general. 

Let’s see how we can utilize Binary Ninja to achieve each of our four conditions:

  1. FunctionRVA is listed in the CFG table as a valid indirect call location (CFG valid function).

Binary Ninja parses the PE binary’s CFG table and stores the address information in a data variable that can be obtained via the BinaryView.

<<< Start >>>

Figure 11. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

As of May 2020, Binary Ninja has an issue where it doesn’t parse the Load Configuration Directory Table for certain PE files. To deal with this case (specifically, jscript9.dll’s CFG table isn’t parsed by Binary Ninja), we can easily write a few lines of script to parse the Load Configuration Directory Table from the PE32 Optional header, obtain the GuardCFFunction table, and parse all the RVAs of the respective functions marked as valid indirect call targets.

  1. Function calling convention is thiscall and takes two arguments (has a “retn 0x8” instruction at the end of the function).

We can check function.calling_convention to see if a particular function is following the thiscall convention, but in our experience of looking at PE files within Binary Ninja, it doesn’t always analyze the convention correctly. Here again, we can quickly write a few lines to achieve a basic, limited thiscall checking function that should work for our purposes.

<<< Start >>>

Figure 12. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

The idea is to start looking at each instruction inside a function and check if the register ecx is read without it being initialized first. If this happens, it’s clear the function expects the ‘this’ pointer in ecx. Note that this is a very small, hacked-up version of a thiscall calling convention analyzer and there are many MLIL operations that we aren’t checking against here to make a definitive decision. However, based on our understanding of the target code layout, it is an acceptable risk of ignoring certain corner cases/imperfect hits with our code.

The second part of this condition is that the function should clean the stack upon return for two arguments. Essentially, we are looking for a “ret 0x8” instruction at the end, which is readily available by checking function.stack_adjustment.value.

<<< Start >>>

Figure 13. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

  1. Within the function, there is a write operation to a memory location that is referenced as a double/triple/quadruple pointer either directly or indirectly via the 'this' pointer (ecx/rcx)
  2. Any other function that satisfies only c) above but is called from a function that satisfies a) and b).

These two steps are the meat of the analysis and Binary Ninja is especially useful in tackling these with some scripting.

Let’s look at an example function within Binary Ninja through the lens of the MLIL SSA (Single Static Assignment).

<<< Start >>>

Figure 14. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

The mem#0 operations in the figure above are SSA memory uses and essentially, encapsulate all operations within the function that operate on memory. These are readily available from the python API as well.

<<< Start >>>

Figure 15. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

For each memory use, we will check if the operation involves one of the three operations below and analyze for potential instructions that are useful.

A high-level flow of the logic to analyze each function follows:

Switch (operation of each memory use):

  1. MLIL_SET_VAR_SSA
    1. Is the memory being read coming from ‘ecx’ (this)? Track it as store1?
    2. Is the memory being read coming from another register that was also pointing to the ‘this’ pointer? If so, track it in
    3. Is the memory being read coming from another register that was pointing to a register from store2? Track it in store3.
    4. store4 logic similar to the above.
    5. store5 logic similar to the above.
  2. MLIL_CALL_SSA
    1. If it’s an indirect call or an imported function, ignore. We won’t resolve this statically here. (Indirect calls can be resolved with some analysis but are out of scope for this post.)
    2. If one of the arguments passed to this function via the stack or via ‘ecx as ‘this’ is from store1/store2/store/3/store4/etc. tracked above, then mark this callee and go to Step 1 and analyze the function. Note that we can terminate based on the recursion depth. A recursion depth of 4, for instance, will analyze callee functions four levels deep. funciton1àfunciton2-àfunction3-àfunction4
  3. MLIL_STORE_SSA
    1. If the value that is being written to is from within the tracked store1/store2/store/3/store4/… we found a potentially useful write!

The full python script to do this is here.

The script can either be run against each module or in a headless mode to target multiple modules within the IE process. Note that the headless mode for Binary Ninja currently is enabled only with a commercial license of Binary Ninja.

The following are snippets of the output when we run our Binary Ninja script against the three modules:

<<< Start >>>

Figure 16: MSHTML
Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 17: Jscript.dll
Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 18: Jscript9.dll
Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

Jscript.dll didn’t have any meaningful hits, but MSHTML had roughly 50 hits and jscript9.dll had about 80 hits. We have reduced our search space from 45k odd functions to ~20 potential functions and we can breathe a bit easier knowing that these functions already follow our four conditions and are good candidates for converting our call primitive to an arbitrary read-write primitive. Now we can manually go through each function and make sure that there aren’t additional hurdles, such as hard-to-reach code paths because of additional uncontrolled constraints. Also, as a reminder, we want to make sure that the some_offset1 value from the ‘this’ pointer for the write primitives for each case is less than or equal to 0x48.

After skimming through the hits manually, one particular function within mshtml.dll stands out.

Ptls6::CLsDnodeText::DestroyCore

<<< Start >>>

Figure 19. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

According to the script output in the above Figure 19, Ptls6::CLsDnodeText::DestroyCore is a gold mine for us. There is a potentially useful write within the function itself and one of its callees Ptls6::LsDestroyInternalsInTxtobj has two more useful-looking write primitives. Let’s take a look at the MLIL of the DestroyCore function:

<<< Start >>>

Figure 20. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

At line 37 and 38, we write into edi_1#2 and ecx_2#5, both of which are indirectly derived from a value from ebx#1 +0x3c at line 18. We control ebx#1 because its value is the “EntryValue:ecx”, which means ebx#1 holds the ‘this’ pointer to the function.

Essentially, we have the ability to write an arbitrary (dword) at an arbitrary location and both the source and destination dwords are derived from a value read from an offset of 0x3c from our freed_object. This particular location is perfect as a gadget for us because reading from the offset 0x3c to the ‘this’ pointer is well within our freed object and we can control the ‘this’ pointer by reallocating a fake object here.

But before we can reach this code, there is call to Ptls6::LsDestroyInternalsInTxtobj, as shown in the figure above. We are lucky because the script output told us that this particular callee also has two useful write primitives as seen below.

<<< Start >>>

Figure 21. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

Let’s look at the two locations the script points out above in the callee.

Ptls6::LsDestroyInternalsInTxtobj

<<< Start >>>

Figure 22. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

The write primitives above are such that the destination edx_3#9 is obtained by a calculation on the ‘edx’ value passed to this function and the source eax_5#2 is controlled by a calculation on the ‘ecx value passed to this function. Referring to the screenshot of the function Ptls6::CLsDnodeText::DestroyCore in Figure 20, we see that we control both the ‘ecx’ and ‘edx’ values passed to this function. The arguments to Ptls6::LsDestroyInternalsInTxtobj as seen in Figure 22, specifically the ‘ecx’ and ‘edx’ values passed to it, are values derived from within our freed object and thus, fully controllable.

‘edx’ = [this+0x3c] or [freed_object+0x3c]
‘ecx’ = [[[this+0x3c]+4]] or [[[freed_object+0x3c]+4]]

With this information, we can set up a fake object that we will use to reallocate in place of the freed object that utilizes the write primitive. There are some other actions to take within  Ptls6::LsDestroyInternalsInTxtobj. To reach the basic block, which has our write primitive, we must navigate the flow of the function encountering other function calls in between that might not be fully controllable. You can see the high-level function graph in the figure provided below.

<<< Start >>>

Figure 23. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

The red arrow points to the block where our write primitive sits. The yellow arrows point to blocks that have additional function calls and checks that we want to skip for a clean exit.

Luckily, as we see in the two figures below, most of the code within each basic block is operating directly on esi#1, which is derived from ‘edx’ passed to this function and which, in turn, comes directly from our freed/fake object. We can thus control the flow within the function and skip unnecessary function calls like Ptls6::LsDestroyArrayInBlob.

<<< Start >>>

Figure 24. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 25. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

Taking note of all the conditions that values within our fake object must meet, we can finally move on to take control of the freed object.

Taking control of the free object

After checking in the debugger, we realize that the freed object is a jscript!NameTbl object of size 0x48. And we see that the object is allocated on the CRT heap.

We can utilize the fact that typed arrays (specifically the array views storing metadata about the array) are allocated on the javascript custom heap, however, the ArrayBuffer itself is allocated on the CRT heap. We can use the following function and pass it the size of our freed object (0x48) such that we will cause the freed NameTbl object to be reallocated with our fully controlled ArrayBuffer object.

<<< Start >>>

Figure 26. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

An interesting point here is that even though the freed object is an object allocated via the jscript.dll engine, we are using Uint32/Typed Arrays (part of jscript9.dll) in the above code. This should not matter as both the allocations (NameTbl objects and ArrayBuffers of jscript9 typed arrays) are on the standard CRT heap. The ArrayBuffer object above will be reallocated on top of the freed NameTbl object. Let’s call it ‘fake_object1.’

Within the fake_object1, instead of the value 0x41414141, we want to include a pointer to another controlled object fake_object2. This fake_object2 will achieve the following:

  1. JsArrayPush will treat the first dword within fake_object2 as a vftable pointer and call a function at an offset 0x88 from the start of the vftable.
    1. Also, within JsArrayPush, right after our call primitive, there is another indirect call via a pointer that is at an offset of 0x80 from the vftable. We will simply make a call to MSHTML!CFlipAheadTravelEntry::AddRecoveryEntry, which is a CFG valid function with a single “retn 0x8” instruction. This will ensure we don’t crash at the second indirect call within JsArrayPush.
  2. We will make this call(at offset 0x88 from the fake vftable) be resolved to the CFG valid location MSHTML!Ptls6::CLsDnodeText::DestroyCore.
  3. MSHTML!Ptls6::CLsDnodeText::DestroyCore will take our fake_object2 (passed as the ‘this’ pointer in ecx) and write to a controlled location. We looked at this write primitive in the previous section via static binary analysis.
  4. The above write to a controlled location will be used to write to the length of a JavaScript LargeHeapBlock ArrayData.
  5. The above step will give us a full arbitrary RW primitive.
Using the heap spray technique

A natural question that arises is how do we get the address of the above fake_object2 that our fake object 1 (which will be allocated on top of the freed NameTbl object) should point to? The hint is within d), where our goal is to corrupt the length of a LargeHeapBlock ArrayData. This is a technique from the explib2 exploitation library.

On IE-32 bit, we can also easily use the heap spray technique used in explib2.

Essentially, we can spray the heap such that at address 0x1a1b13000, we have the ArrayData of a LargeHeapBlock. Starting at address 0x1a1b3020 is the array data that we fully control. The idea is explained in this paper and blog post and the reader is highly encouraged to refer to those to understand how and why this heap-spraying technique works.

With this in mind, let’s look at how our fake object layout is going to be in memory. The following is how our fake_object1(size 0x48) will look in memory after we reallocate the freed NameTbl object:

<<< Start >>>

Figure 27. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 28. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

Here is how our heap spray lands with the fake object2 as illustrated in Figure 29 and Figure 30.

<<< Start >>>

Figure 29. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 30. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

Arbitrary RW to CFG bypass to EIP control

We corrupted the length of the ArrayData of a JavaScriptNativeIntArray to gain full arbitrary RW capabilities. Refer to the explib2 library to see the helper functions that we have at our disposal, including read32, write32, readString, writeString, memcpy, getModuleBase, etc.

The bulk of the exploit technique in explib2 is implemented in the Explib2.go() function. It uses the ActiveX “SafeMode” bypass technique to get code execution without any ROP and shellcode. Unfortunately, this technique has since been mitigated against and is not useful for us.

Let’s look at other options. One of the publicly known techniques against Microsoft’s CFG implementation is corrupting the return address of a stack frame—a known design weakness of CFG. For those interested, this post serves details using this technique, which involves using RtlCaptureContext to leak the stack pointer.

Microsoft initially launched Return Flow Guard (RFG), which was a software implementation of Intel’s stack protection in hardware. This was later dropped, and Microsoft is instead waiting for the upcoming, hardware-based protection with Intel CET (Intel’s hardware Control Flow Enforcement Technology).

On Windows 10, RtlCaptureContext function is still not added in the CFG suppressed calls list, which means, barring additional hurdles, we can still use the same technique to leak the stack pointer and corrupt the return address on the stack.

For the purposes of this post, we will use a simple type confusion technique (similar to what we did with converting our initial call primitive to an arbitrary RW one). The idea is to hijack a virtual function call within an object to call WinExec.

The object we chose to corrupt with our full RW primitive is a JavaScript TypedArray, which is also used (although differently) in the theori and improsec posts.

The following line of code in JavaScript will cause a TypedArray to be allocated in memory.

var typed_array = new Uint8Array(0x100);

Let look at how the Uint8Array object looks in memory via a debugger.

<<< Start >>>

Figure 31. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

<<< Start >>>

Figure 32. Copyright © 2020 Accenture. All rights reserved.

 

The Js::TypedArray<unsigned char,0>::Subarray function is definitely useful for us since it takes two arguments, as seen above, that are user-controlled and can be invoked directly from JavaScript like so:

typed_array.subarray(arg1, arg2);

Our idea here is to overwrite the vftable pointer of a TypedArray object (since the vftable itself is in read-only memory and we can’t directly write to it). We will craft a fake vftable object, which holds the same contents as the original vftable except at offset 0x188, and we will replace the pointer to the Subarray function with a call to Kernel32!WinExec (which is still marked as a CFG valid call).

Since WinExec also takes two arguments, hijacking the call to Js::TypedArray<unsigned char,0>::Subarray with WinExec will not make the stack misaligned on return.

Let’s see how the indirect call to Js::TypedArray<unsigned char,0>::Subarray is made in the debugger.

With the following statement,
typed_array.subarray(arg1, arg2);

the jscript9 engine eventually reaches the following path to make the indirect call.

<<< Start >>>

Figure 33. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

As we can see in Figure 33, it pushes our two arguments to the stack and then makes the indirect call to the virtual function within the vftable at offset 0x188, which we can hijack.

We ideally want to make a WinExec call like the following:
WinExec(buffer,0x5)

Here, the buffer argument points to a string with our command (e.g., “c:\windows\notepad.exe”) and 0x5 is the uCmdShow parameter passed to WinExec, which represents SW_SHOW.

Astute readers will have noticed a slight hiccup here. Just above the code where we reach the indirect call, the function calculates if the value of argument2 is greater than argument1. If this check fails, it will copy argument 1 into argument 2 and then make the call.

0x5 or really any valid uCmdShow value is less than the value of a pointer address that holds our command string. Therefore, the second parameter will also contain the string pointer when the indirect call happens. Luckily, the WinExec call still works, but we have limited control over the second argument in this case. We work around this by executing the following command via WinExec so we indirectly can run arbitrary programs with multiple arguments.

"powershell.exe -Command \"Start-Process notepad\""

Leaking the address for WinExec

The explib2 library readily provides us with a leakAddress() function that will let us leak the address of the TypedArray we created earlier.

var typed_array = new Uint8Array(0x100);

To leak the import table of jscript9, we read the first dword within the vftable above, which is the address of jscript9!HostDispatch::AddToPrototype. Starting from this address, we start scanning for any JavaScript function that uses a kernel32 function from the import table.

One such function, as shown below, is JsEnumerateHeap. We look for this function by searching for a hash that we create from the first 20 bytes that are unique.

<<< Start >>>

Figure 34. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

The import table location from which the above TlsGetValue call is resolved is shown below.

<<< Start >>>

Figure 35. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

We read the address of kernel32!TLsgetValueStub and again start searching for kernel32!Winexec. For this, we scan for the hash of the first 8 bytes of WinExec and if it matches, we skip 8 bytes (to skip the stack cookie storing instruction) and hash another 40 bytes. This way, we can uniquely identify WinExec in memory.

<<< Start >>>

Figure 36. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

Now that we have the WinExec address leaked without relying on hardcoded offsets, we will get back to finalizing the storage for our fake vftable object.

A good candidate for storing our fake vftable object is the heap-spray buffer. Since each buffer is of 0x1000 bytes, we have enough space to put our fake object and our command string to WinExec.

We will first use the memcpy() function within explib2 to copy the original TypedArray vftable contents into our own heap buffer. This is useful to resolve any other calls that happen via the vftable before we reach jscript9!Js::TypedArrayBase::CommonSubarray.

Next we overwrite the fake vftable object at offset 0x188 with a pointer to kernel32!WinExec.
and use the writeString() function within explib2 to write our command string
 "powershell.exe -Command \"Start-Process notepad\"" to our heap buffer.

Finally, we invoke the hijacked call as follows that will give us code execution by running notepad.

target_arr.subarray(target_arr_buffer, 0x5); //target_arr_buffer points to lpCmdLine for WinExec

For cleanup and ensuring process continuation, the only thing we need to take care of is the original vftable pointer, which we had overwritten in the TypedArray object. We can simply save the vftable pointer before overwriting and restore it after our call to subarray. This will ensure that our process continuation is smooth and that we don’t encounter any crashes after return.  Note that we get code execution inside the LOW integrity sandboxed process.

For a detailed explanation of the implementation, please refer to the source code (with comments) of the exploit

Exception handler chain within JsArrayPush

We have a nifty little bonus trick. Luckily, the vulnerable piece of code within JsArrayPush is wrapped under a try/catch block. We can see this with the ‘exchain’ command inside the debugger after putting a breakpoint on the function of interest.

<<< Start >>>

Figure 37. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

The exception handling trickles down to the last level exception handler, which we can register by wrapping the trigger function in a try/catch try catch as shown below.

<<< Start >>>

Figure 38. Copyright © 2020 Accenture. All rights reserved.

<<< End >>>

This will let us try the exploit multiple times in case the reallocation of the freed object fails or if the heap spray fails. The exception handler will let us continue access violation faults, such as accessing freed memory or read only memory.

Conclusion: An interesting case study with a happy ending

For us, this was an interesting study of a UAF vulnerability in IE11, where the exploitability was not immediately evident. While this specific vulnerability gave us a very constrained and limited call-based primitive, we were able to demonstrate that if we separately obtain a single information leak—in this case, it was assumed to be the base address of MSHTML.dll—we can bypass all other mitigations and gain reliable code execution using just this one vulnerability. It would be fair to say that the biggest challenge in exploiting this vulnerability to gain code execution is bypassing ASLR to leak the base address of a module.

Memory-corruption bugs often provide powerful exploitation primitives, but each primitive needs to be carefully studied to determine its practical exploitability. To that end, binary analysis tools and automation are immensely valuable to bug hunters and exploit writers alike.

iDefense is always on the lookout for zero-day vulnerability submissions from contributors across the world. Please join our Vulnerability Contributor Program (VCP) – one of the oldest third-party, vendor-agnostic bug-bounty programs – and send us your submissions.

To learn more about vulnerabilities and exploits, including advance notifications, please visit the iDefense services overview page and follow us on twitter @iDefense.

 

Accenture Security

Accenture Security is a leading provider of end-to-end cybersecurity services, including advanced cyber defense, applied cybersecurity solutions and managed security operations. We bring security innovation, coupled with global scale and a worldwide delivery capability through our network of Advanced Technology and Intelligent Operations centers. Helped by our team of highly skilled professionals, we enable clients to innovate safely, build cyber resilience and grow with confidence.  Follow us @AccentureSecure on Twitter or visit us at www.accenture.com/security.

Accenture, the Accenture logo, and other trademarks, service marks, and designs are registered or unregistered trademarks of Accenture and its subsidiaries in the United States and in foreign countries. All trademarks are properties of their respective owners. All materials are intended for the original recipient only. The reproduction and distribution of this material is forbidden without express written permission from Accenture. The opinions, statements, and assessments in this report are solely those of the individual author(s) and do not constitute legal advice, nor do they necessarily reflect the views of Accenture, its subsidiaries, or affiliates. Given the inherent nature of threat intelligence, the content contained in this report is based on information gathered and understood at the time of its creation. It is subject to change. Accenture provides the information on an “as-is” basis without representation or warranty and accepts no liability for any action or failure to act taken in response to the information contained or referenced in this report

Copyright © 2020 Accenture. All rights reserved. Accenture, its logo, and High Performance Delivered are trademarks.

 

Rohit Mothe

SECURITY INNOVATION PRINCIPAL

Subscribe to Accenture's Cyber Defense Blog Subscribe to Accenture's Cyber Defense Blog