Jump to content
Nytro

Dissecting modern browser exploit: case study of CVE-2018–8174

Recommended Posts

Dissecting modern browser exploit: case study of CVE-2018–8174

When this exploit first emerged in the turn of April and May it spiked my interest, since despite heavy obfuscation, the code structure seemed well organized and the vulnerability exploitation code small enough to make analysis simpler. I downloaded POC from github and decided it would be a good candidate for taking a look at under the hood. At that time two analyses were already published, first from 360 and second from Kaspersky. Both of them helped me understand how it worked, but were not enough to deeply understand every aspect of the exploit. That’s why I’ve decided to analyze it on my own and share my findings.

Preprocessing

First in order to remove integer obfuscation I used regex substitution in python script:

As to obfuscated names, I renamed them progressively during analysis. This analysis is best to be read with source code to which link is at the end.

Use after free

Vulnerability occurs, when object is terminated and custom defined function Class_Terminate() is called. In this function reference to the object being freed is saved in UafArray. From now on UafArray(i) refers to the deleted object.

 
Also notice the last line in Class_Terminate(). When we copy ClassTerminate object to UafArray its reference counter is increased. To balance it out we free it again by assigning other value to FreedObjectArray. Without this, object's memory wouldn't be freed despite calling Class_Terminate on it and next object wouldn't be allocated in its place.
 
0*5pmZOoPIYW039gfZ.png

Creating and deleting new objects is repeated 7 times in a loop, after that a new object of class ReuseClass is created. It's allocated in the same memory that was previously occupied by the 7 ClassTerminate instances. To better understand that, here is a simple WinDbg script that tracks all those allocations:

bp vbscript!VBScriptClass::TerminateClass ".printf \"Class %mu at %x, terminate called\\n\", poi(@ecx + 0x24), @ecx; g";
bp vbscript!VBScriptClass::Release ".printf \"Class %mu at: %x ref counter, release called: %d\\n\", poi(@eax + 0x24), @ecx, poi(@eax + 0x4); g";
bp vbscript!VBScriptClass::Create+0x55 ".printf \"Class %mu created at %x\\n\", poi(@esi + 0x24), @esi; g";

Here is allocation log from UafTrigger function:

Class EmptyClass created at 3a7d90
Class EmptyClass created at 3a7dc8
...
Class ReuseClass created at 22601a0
Class ReuseClass created at 22601d8
Class ReuseClass created at 2260210
...
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 22605c8
...
Class ClassTerminateB created at 2260600
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 1
Class ClassTerminateB at 2260600, terminate called
Class ClassTerminateB at: 70541748 ref counter, release called: 5
Class ClassTerminateB at: 70541748 ref counter, release called: 4
Class ClassTerminateB at: 70541748 ref counter, release called: 3
Class ClassTerminateB at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 2260600

We can immediately see that ReuseClass is indeed allocated in the same memory that was assigned to 7 previous instances of ClassTerminate This is repeated twice. We end up with two objects referenced by UafArrays. None of those references is reflected in object's reference counter. In this log we can also notice that even after Class_Terminate was called there are some object manipulations that change its reference counter. That's why if we didn't balance this counter out in Class_Terminate we would get something like this:

Class ClassTerminateA created at 2240708
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 1
Class ClassTerminateA at 2240708, terminate called
Class ClassTerminateA at: 6c161748 ref counter, release called: 5
Class ClassTerminateA at: 6c161748 ref counter, release called: 4
Class ClassTerminateA at: 6c161748 ref counter, release called: 3
Class ReuseClass created at 2240740

Different allocation addresses. Exploit would fail to create use after free condition.

Type Confusion

Having created those two objects with 7 uncounted references to each, we established read arbitrary memory primitive. There are two similar classes ReuseClass and FakeReuseClass. By replacing first class with second one a type confusion on mem member occurs.

 

In SetProp function ReuseClass.mem is saved and Default Property Get of class ReplacingClass_* is called, result of that call will be placed in ReuseClass.mem.

 
Inside that getter UafArray is emptied by assigning 0 to each element. This causes VBScriptClass::Release to be called on ReuseClass object that is referenced by UafArray. It turns out that at this stage of execution ReuseClass object has reference counter equal to 7, and since we call Release 7 times, this object gets freed. And because those references came from use after free situation they are not accounted for in reference counter. In place of ReuseClass a new object of FakeReuseClass is allocated. Now to get its reference counter equal to 7, as was the case with ReuseClass we assign it 7 times to UafArray. Here is memory layout before and after this operation.
 
0*WBfrAPkWQ0eSSGcc.png
 
0*5nnyMj8ed51GH3uP.png

After this is done the getter function will return a value that will be assigned to the old ReuseClass::mem variable. As can be seen on memory dumps, old value was placed 0xC bytes before the new one. Objects were specially crafted to cause this situation, for example by selecting proper length for function names. Now value written to ReuseClass::mem will overwrite FakeReuseClass::mem header, causing type confusion situation.

 
0*ZNTR_smAzAtr8e51.png
 
Last line assigned string FakeArrayString to objectImitatingArray.mem. Header now has value of VT_BSTR
Q=CDbl("174088534690791e-324") ' db 0, 0, 0, 0, 0Ch, 20h, 0, 0

This value overwrote objectImitatingArray.mem type to VT_ARRAY | VT_VARIANT and now pointer to string will be interpreted as pointer to SAFEARRAY structure.

Arbitrary memory read

The result is that we end up with two objects of FakeReuseClass. One of them has a mem member array that is addressing whole user-space (0x00000000 - 0x7fffffff) and the other has a member of type VT_I4 (4 byte integer) with pointer to an empty 16 byte string. Using the second object, a pointer to string is leaked:

some_memory=resueObjectB_int.mem

It will be later used as an address in memory that is writable. Next step is to leak any address inside vbscript.dll. Here a very neat trick is used.

 
First we define that on error, script should just continue regular execution. Then there is an attempt to assign EmptySub to a variable. This is not possible in VBS but still a value is pushed on the stack before error is generated. Next instruction should assign null to a variable, which it does, by simply changing type of last value from the stack to VT_NULL. Now emptySub_addr_placeholder holds pointer to the function but with type set to VT_NULL.
 
Then this value is written to our writable memory, its type is changed to VT_I4 and it is read back as integer. If we check the content of this value it turns out to be a pointer to CScriptEntryPoint and first member is vftable pointing inside vbscript.dll
 
0*wYUKXNfAKfXzldKD.png

To read a value from arbitrary address, in this case pointer returned from LeakVBAddr, the following functions are used:

 
Read is acheived by first writing address+4 to a writable memory, then type is changed to VT_BSTR. Now address+4 is treated as a pointer to BSTR. If we call LenB on address+4 it will return value pointed to by address. Why? Because of how BSTR is defined, unicode value is preceded by its length, and that length is returned by LenB.
 
0*CZn48k5akdPsDBls.png

Now when address inside vbscript.dll was leaked, and having established arbitrary memory read it is a matter of properly traversing PE header to obtains all needed addresses.

 
Details of doing that won’t be explained here. This article explains PE file in great details.

Triggering code execution

Final code execution is achived in two steps. First a chain of two calls is built, but it’s not a ROP chain. NtContinue is provided with CONTEXT structure that sets EIP to VirtualProtect address, and ESP to structure containing VirtualProtect's parameters.

 
First address of shellcode is obtained using previously described technique of changing variable type to VT_I4 and reading the pointer. Next a structure for VirtualProtect is built, that contains all necessary parameters, like shellcode address, size and RWX protections. It also has space that will be used by stack operations inside VirtualProtect. After that a CONTEXT structure is built, with EIP set to VirtualProtect and ESP to its parameters. This structure also has as first value a pointer to NtContinue address repeated 4 times. Final step before starting this chain is to save the structure as string in memory.
This function is then used to start the chain. First it changes type of the saved structure to 0x4D and then sets its value to 0, this causes VAR::Clear to be called.
 
0*l87ck6ZwLr8_i7Dn.png

And a dynamic view from debugger

 
0*ABe2awAsraxR1pWD.png

Although it might seem complicated this chain of execution is very simple. Just two steps. Invoke NtContinue with CONTEXT structure pointing to VirtualProtect. Then VirtualProtect will disable DEP on memory page that contains the shellcode and after that it will return into the shellcode.

Conclusion

CVE-2018–8174 is a good example of chaining few use after free and type confusion conditions to achieve code execution in very clever way. It’s a great example to learn from and understand inner workings of such exploits.

Useful links

Commented exploit code

Kaspersky’s root cause analysis

360’s analysis

Another Kaspersky’s anlysis

CVE-2014–6332 analysis by Trend Micro

 

Sursa: https://medium.com/@florek/dissecting-modern-browser-exploit-case-study-of-cve-2018-8174-1a6046729890

  • Upvote 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...