Analyzing CVE-2018-8653 with REVEN: Use-after-Free in Internet Explorer Scripting Engine
by Luc
Categories: Technical -
Tags: Use After Free - UaF - Reverse Engineering - Garbage Collector - Memory Management - CVE - Demo - REVEN -
Discover Timeless Analysis Live.
In this post we will have a look at the proof of concept for CVE-2018-8653 that comes from a very interesting blog post from Philippe Laulheret et al. at MacAfee Labs. To summarize, the vulnerability exploits various seemingly innocent behaviors in Internet Explorer’s scripting engine (jscript.dll) to trigger a use-after-free condition. More specifically, under certain conditions, we have access to a this
pointer in a callback that is not tracked by the garbage collector, and we can delete the associated object, leaving this
dangling.
When the garbage collector is in a certain state, we can then replace the object pointed by this
by a
different one, allowing for type confusions.
To get a better understanding of the vulnerability, we recommend reading the
McAfee blog post
that explains the root cause of the vulnerability in details.
In today’s article, we will use the vulnerability as background to observe the memory management mechanisms in Internet Explorer’s javascript engine, and how it leads to the object being replaced in the context of the vulnerability. To do so, we will use our analysis tool, REVEN, that allows recording live VMs, and then produce execution traces containing all the instructions, CPU state, memory state, of the recorded VM, that we can query both from a GUI and from a Python API.
To perform the analysis of the CVE, we started by using the Page Heap
WinDbg tool on a PoC of the
vulnerability that led to a crash, in order to get a first look at the problem.
Then, we recorded the PoC described in the McAfee article, that displays an “alert” windows containing the “1337” number as a
string, replacing an empty Regex
object, which demonstrates the type confusion. We recorded about 10 seconds of the live VM in REVEN, for a total of more than 3.6 billions recorded instructions.
Preliminary analysis
Page Heap yielded the following result:
Notice the calls to jscript!GcBlock::`scalar deleting destructor` that are followed by RtlFreeHeap: these confirm that we are facing a Use-After-Free, as described in the McAfee article.
Now that we got a first hint about what happened, we can have a look with REVEN at where the crash occurred with PageHeap enabled, at jscript!ConvertToObject+0x29, and analyze what happened to the pointed to area of memory:
Clearly we’re facing an array of VARs as defined in the PoC through the string creation:
[VarType] | [Value] | [Unused] |
[0000000000000003] | [0000000000000539] | [00000000000000000] |
You can find an explanation about how the VAR objects work in the following blog post from Google Project Zero (Search for the “Understanding JScript VARs and Strings” headline).
Here is the part of the PoC responsible for creating this string object:
The displayed value 0x539 is hexadecimal for 1337 which indicates that we’re facing the replaced object. As described in the McAfee article, the string created in the javascript code mimics int
objects and one of these int
s is displayed by the alert function.
We will analyze the different steps that lead to the replacement of the object and analyze what the object is, as it will help understand the whole process.
As we will see, the object that will be replaced is stored in the Garbage Collector cache itself.
The Garbage Collector cache (that appears to be named GcBlock) is a small cache, which holds and distributes VAR objects when GcBlock::PvarAlloc is called. To our knowledge, this cache isn’t documented, yet it is quite important for this vulnerability as it helps understanding the memory layout and what is needed to manipulate and replace it.
Allocation/Free of the object
With the Memory History feature from REVEN, we can navigate through each and every read and write that occurred on the memory area representing the object inside the GcBlock
. In combination with the backtrace, we can actually go back until we reach the allocation of the object, performed by GcAlloc::PvarAlloc, and get a good hint of what happened.
Here are some of the functions that are called:
- GcAlloc::PvarAlloc
- GcBlockFactory::PblkAlloc
- GcAlloc::SetMark
- GcAlloc::ReclaimGarbage
- GcContext::CollectCore
- NameTbl::ScavengeCore
The call to GcAlloc::PvarAlloc is actually the allocation of the original object, the one which this
still holds a reference to (a RegExp, from the javascript code available in appendix 1). Notice the class of some of these functions: GcAlloc. The object is allocated from the GcBlock (Garbage Collector cache).
Even before this RegExp allocation, we can see the allocation of the cache block itself, by the function GcBlockFactory::PblkAlloc. This fact allows us to quickly find its size when it calls the “new” function: 0x970. We will see later that this piece of information is important.
If we recall the result of the PageHeap in WinDbg, we can assume that this cache is actually the one being replaced.
GcBlock Allocation/Free, tracking with Python API
In this section we will analyze a bit how allocations and deallocations are handled by the Garbage Collector cache, and show how we can track all of them with REVEN. As a matter of fact, being able to track these allocations is convenient for exploitation as we are not facing the common allocation from ntdll functions. Hence, the replacement of the full object entirely depends on the behaviour of this cache.
The Variable Type of a JScript VAR object is a short identifier that describes, -as one can guess-, the type of the object. For example, 3 for integer, 5 for double, 8 for string etc. The first thing to see is how the VariableType of an object is modified when the Garbage Collector is triggered or when an allocation is requested. With the Memory History, we can see that the Variable Type also defines the state of the object. As such, if this Variable Type is 0 then the object is considered freed. The garbage collector can also mark the Variable Type with the mask 0x800. This mechanism is used as part of its algorithm of garbage collection. The global idea of this cache is that when all of the objects in the GcBlock are free, then the whole block is freed.
We developed a REVEN API script that tracks the state of the GcBlock during the scenario. You can now try a Jupyter notebook version of this script online by clicking here. The notebook is also available on github.
Here is the output when applied to the Proof-Of-Concept scenario:
When all of the objects are freed (VariableType equals to 0), the GcBlock itself is freed and ready to be allocated again by the PoC.
Now we will analyze the re-allocation of this block with the forged string.
Replacement object creation
Usually when dealing with Use-After-Free, one important part is determining how to reallocate the area that has been freed so we can control what is pointed by the dangling pointer. In this case, we need to allocate a new area in such a way that the allocator returns the same chunk. The requested area size plays a crucial part in the allocator’s decision of which block to reuse, so our new object must have (almost) the same size as the one that has been freed
In the previous part we showed that the GcBlock is 0x970 bytes. In this part we will simply explain why the size of the string in the proof of concept is 0x230 and how we could have found this size ourselves.
We saw previously what function is responsible for building this new string object: it is NameList::FCreateVval:
With a quick analysis in our favorite disassembler, we can see that this function calls NoRelAlloc::PvAlloc. It seems like a good candidate to determine the size of the allocated area. NoRelAlloc::PvAlloc takes an int as only parameter computed with the following instruction:
From REVEN, we see that rax value is 0x230, which is exactly the size of the string provided in the javascript PoC:
If we continue the execution analysis, with REVEN or statically, we see that the size is manipulated a bit more:
It is multiplied by 2 and 8 is added afterwards, probably for some header space.
Next a new memory area of this size (0x94c) is requested to ntdll. The returned pointer from this allocation is exactly the same pointer as the previously freed GcBlock. The fact that 0x94c is close to 0x970 induced the allocator to serve the same block size.
We can now say that the size of the memory area for the string is computed this way:
Finding the string size is just applying this trivial expression:
In the case of the GcBlock with mem_size = 0x970, we get string_size = 0x239.
The original exploit author chose 0x230, which is perfectly fine as it will return the same block, and 0x10 bytes aligned. Maybe an interesting point would be to determine precisely the size of the returned block as it is returned in this case by the Low Fragmentation Heap and has a fixed size. This isn’t the object of this post but the answer can be found by analyzing the RtlpAllocateHeapInternal functions.
First conclusion
To sum-up what this proof of concept is doing, we have shown that it comes from a dangling pointer that is still accessible, even though the target is freed. But, where usually the replacement occurs for the object itself, in this case the PoC had to free the whole cache so that the replacement object (a string) is filling the whole cache block itself and not the single object. Here is a (simplified) diagram describing what happened:
Now a question remains, what would have happened if we replaced only the one object in the cache? Is is still a Use-After-Free? We recorded this case to analyze it and the answer is yes, it is still a Use-After-Free, but it doesn’t seem to be exploitable as is.
Replacing the object in the cache
Whilst trying to minimize the original proof of concept, we encountered this case where we simply replaced the original object by another one, and displayed it with alert. We only used an array of 16 initial objects (400 for the original PoC):
And only 32 objects for the overlapping array (0x1000 for the original PoC):
We didn’t need the other object array either.
The result of this light PoC is the alert displaying the object of the overlapArray variable, which is definitely different from the original RegExp. As explained in the original article, the vulnerability is induced by the “this” pointer, which still holds a reference to the original object even though it has been deleted. In this test-case, we just reallocated a new object, of which the VAR object will be allocated in the same memory area as the original object by the Garbage Collector cache. Hence, when calling “alert” on this, the VAR displayed is the one replaced.
Technically, one can also use the dangling pointer without replacing the freed object, but actually, in case of an invalid object, Javascript just displays the biggest default object which is the Object Window.
We recorded the replacement behavior as it is interesting to see what is happening exactly. Using once again the script we developed earlier, we can see the state of the GcBlock, which is never freed at any moment:
As one can see, allocations and deallocations are pretty messy compared to the first trace.
This is due to the fact that, contrary to the first trace,
we did not set the GcBlock in a clean state by performing the dummy object allocations/deallocations step.
Here is a (simplified) diagram of what happened in this case:
This behavior still qualifies as a use-after-free, but as stated before, there isn’t much we can do about it using the Garbage Collector cache.
Conclusion
The original post describes the vulnerability using mainly static analysis and explained the root cause really well. In today’s article we have shown how REVEN can be used complementarily to static analysis, for example to examine mechanisms such as the Garbage Collector memory layout, that are not trivial to observe statically. We have shown that understanding these mechanisms can be quite important when trying to write a reliable exploit, and how in particular REVEN’s Python API allows us to build models of these mechanisms.
Appendix
Appendix 1: Original PoC
Appendix 2: Light PoC, using the Garbage Collector cache
Discover Timeless Analysis Live.