Background
In May 2018, the Advanced Threat Response Team of 360 Core Security Division detected an APT attack exploiting a zero-day vulnerability and captured the world’s first malicious office sample that uses a browser zero-day vulnerability.
The sample exploited a use-after-free vulnerability in the VBScript engine which was fixed by Microsoft as CVE-2018-8174 in May 2018 security update, the detailed analysis of the bug could be found at:
After analyzing the patch for CVE-2018-8174 carefully, we realized that the fix was not so complete and there still exists similar problems which could be leveraged to achieve reliable remote code execution in VBScript engine. We reported the issues we found to Microsoft immediately and Microsoft addressed them with a new fix (CVE-2018-8242) in July 2018 security update.
In this blog, we will introduce the details of the new exploitable issues with CVE-2018-8174 patch we found. We will also discuss how we exploit the new bug we found and get reliable remote code execution in an upcoming blog (part II).
The Original ITW zero-day
Before starting to introduce the new bug, let’s have a quick look at the old bug first. The PoC for the original bug looks like this:
Here is how the poc works:
1. It defines a VBScript class named |cla1|, the class has a |Class_Terminate| function, which will be called each time when an instance of |cla1| is being destructed.
2. It defines a vbscript SafeArray |arr| and set the first element of the array to be an instance of |cla1|.
3. It calls “erase” on the array, which will first clear all the elements in the array and then release the whole SafeArray including its’ internal buffer.
4. Because the first element of the array is an instance of |cla1|, when it gets cleared, the | Class_Terminate| callback will be called.
5. Inside the callback, we grab a reference to the element again by executing: Set o = arr(0)
6. After returned from the callback, arr(0) will be freed.
7. Now we have a reference to an already freed object, which gives us a use-after-free bug.
The Fix in May 2018 Security Patch
Microsoft’s fix for this bug was quite simple:
In oleaut32!VariantClear, if it is clearing a VT_DISPATCH (Object) variant, it will first set the type of the variant to VT_EMPTY before trigger the |Class_Terminate| callback, as shown below:
And the pseudo code of the patch looks like this:
if (variant is a dispatch object) {
variant.vt = VT_EMPTY;
Call VBScriptClass::Release which will call the class Terminate function.
}
Now with this patch, the original PoC won’t work anymore, because when you try to grab a reference to the element |arr(0)| in the callback function with the following statement in the poc:
Set o = arr(0)
You will get an empty variant because the variant’s type has already been set to VT_EMPTY, so the use after free will not happen.
Is the Patch Perfect?
Let’s consider the root cause of this vulnerability:
When we erase a SafeArray, it clears the elements in the array one by one. The problem is that we can still access the SafeArray when the elements are being cleared, by using the |Class_Terminate| callback.
Here “Access” a SafeArray means that we can:
1.Read elements from the array (which is leveraged in the original zero-day poc)
2.Write elements to the array.
3.Modify the internal data of the array, including free the internal buffer of the array.
Now let’s get back to the patch: the patch prevents us from reading the element which is being cleared in the callback. It is easy to notice that this patch only (partially) prevents the first pattern but cannot address the other 2 patterns.
Let’s see whether we can find some new issues using that 2 patterns, after the CVE-2018-8174 patch.
Show Me New zero-days
New Issue 1 – SafeArray Double Free
Let’s try pattern 3, to see what will happen if we try to delete the internal buffer of the SafeArray inside the |Class_Termiante| callback, with the new poc:
Here we slightly modified the original poc, tried to erase the array again inside the |Class_Terminate| callback, and to our surprise the new poc crashed immediately:
(14c8.258): Break instruction exception – code 80000003 (!!! second chance !!!)
eax=00000000 ebx=00000000 ecx=000014c8 edx=00000000 esi=65789d80 edi=00000000
eip=6578cd6f esp=0727ca14 ebp=0727ca38 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
verifier!VerifierStopMessage+0x27f:
6578cd6f 837df800 cmp dword ptr [ebp-8],0 ss:0023:0727ca30=00000000
0:006> k
ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
0727ca38 6578ab0c verifier!VerifierStopMessage+0x27f
0727ca9c 6578794d verifier!VerifierDisableFaultInjectionExclusionRange+0x414c
0727cb00 65787aa5 verifier!VerifierDisableFaultInjectionExclusionRange+0xf8d
0727cb24 65787d30 verifier!VerifierDisableFaultInjectionExclusionRange+0x10e5
0727cb40 65789e10 verifier!VerifierDisableFaultInjectionExclusionRange+0x1370
0727cb5c 779616d9 verifier!VerifierDisableFaultInjectionExclusionRange+0x3450
0727cbbc 778b8c0a ntdll!RtlpNtSetValueKey+0x3089
0727ccc0 778b64b8 ntdll!RtlFreeHeap+0x2eaa
0727cd10 7553d83b ntdll!RtlFreeHeap+0x758
0727cd24 753cc819 combase!CoCreateFreeThreadedMarshaler+0x1362b
0727cd40 753cc8a4 OLEAUT32!_SafeArrayFreeData+0x34
0727cd4c 753ccb52 OLEAUT32!_SafeArrayReleaseDescriptor+0x38
0727cd5c 5e02f348 OLEAUT32!SafeArrayReleaseDescriptor+0x12
0727cd68 5e034c1e vbscript!AutoVariantRef::ArrayPinnableData::Unpin+0x21
0727cd6c 5e034c12 vbscript!VbsErase+0xee
0727cda8 5dfec837 vbscript!VbsErase+0xe2
You can see that the verifier detected a critical error when calling RtlFreeHeap to free a heap block, which usually indicates a potential heap corruption. Notice that the problem is triggered with |VbsErase| in the callstack.
Let’s check the heap block being freed which triggered the verifier warning:
address 041b4fd8 found in
_DPH_HEAP_ROOT @ 701000
in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
40b3f08: 41b4000 2000
65789e42 verifier!VerifierDisableFaultInjectionExclusionRange+0x00003482
779616d9 ntdll!RtlpNtSetValueKey+0x00003089
778b8c0a ntdll!RtlFreeHeap+0x00002eaa
778b64b8 ntdll!RtlFreeHeap+0x00000758
7553d83b combase!CoCreateFreeThreadedMarshaler+0x0001362b
753cc819 OLEAUT32!_SafeArrayFreeData+0x00000034
753cc8a4 OLEAUT32!_SafeArrayReleaseDescriptor+0x00000038
753ccb52 OLEAUT32!SafeArrayReleaseDescriptor+0x00000012
753741f7 OLEAUT32!SafeArrayDestroyDescriptor+0x00000087
5e034be6 vbscript!VbsErase+0x000000b6
You can see that the heap block passed to |RtlFreeHeap| had already been freed earlier, in another call to VbsErase.
Now it’s quite clear that it is a double free vulnerability. In our poc, when the first call to VbsErase tries to free the memory of the SafeArray descriptor, it has already been freed unexpectedly by the second call to VbsErase in our |Class_Terminator| callback.
The double free vulnerability itself is quite simple, however it requires some technique to exploit it. I will have some further discuss on this vulnerability including some detailed analysis and how to write a poc exploit for it, in a next blog. You can try to write a poc exploit by yourself if you are interested in it.
New Issue 2: Object Use After Free Caused by Reference Count Overflow
Now we have a new double free vulnerability. But We didn’t want to just stop here and got to have a rest, our brain storm continued.
So how about the pattern 2? Can we make some trouble by writing elements to the SafeArray inside the |Class_Terminate| callback?
The answer is yes. The problem is not so obvious: we just write some elements to a SafeArray whose memory is going to be freed, we are not writing out of the bounds of the array; we are not writing to already freed memory. Seems no big deal?
However, consider the following situation:
1. We have a SafeArray with 2 elements.
2. We call erase on the SafeArray.
3. The elements in the SafeArray will be cleared one by one, it will first clear arr(0), and then clear arr(1)
4. When clearing arr(1), we can trigger a |Class_Terminate| callback. And inside the callback, we store an object to arr(0) like this: Set arr(0) = SomeObject
. In VBScript, store an object into a SafeArray will increase the reference count of that object, so we get SomeObject.ref_cnt += 1. The reference count of the object will be decremented by 1 when we clear the object from the array, in normal case.
5. Now after arr(1) is cleared, all elements in the SafeArray have been cleared. It will not go back to clear arr(0) because arr(0) has already been cleared before clearing arr(1). Next it will directly free the memory of the internal SafeArray buffer.
6. So SomeObject.ref_cnt will not get decremented. It will cause a reference count leak on the object.
7. The reference count of a VBScript object is 32-bits. By triggering the ref count leak for multiple times, we can finally overflow the 32-bits ref count and the ref count will become very small. Then we can free the object by clearing a few references to it, while we still have many references pointing to that object. This means with this vulnerability, we can create a dangling pointer to an already freed vbscript object and access the freed object at any time we want, which is quite easy to exploit.
The full poc is shown below:
The above PoC took ~10 minutes to crash on my test machine. It crashes when trying to access an already-freed object in oleaut32!VariantClear:(58.1154): Access violation – code c0000005 (!!! second chance !!!)
eax=00000000 ebx=071cd010 ecx=0e9d2fc0 edx=04000000 esi=00000000 edi=00000009
eip=7537bbce esp=071ccfc0 ebp=071ccfe0 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
OLEAUT32!VariantClear+0xfe:
7537bbce 8b01 mov eax,dword ptr [ecx] ds:0023:0e9d2fc0=????????
address 0e9d2fc0 found in
_DPH_HEAP_ROOT @ 3fb1000
in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
dab3fa4: e9d2000 2000
65789e42 verifier!VerifierDisableFaultInjectionExclusionRange+0x00003482
779616d9 ntdll!RtlpNtSetValueKey+0x00003089
778b8c0a ntdll!RtlFreeHeap+0x00002eaa
778b64b8 ntdll!RtlFreeHeap+0x00000758
752d77c5 msvcrt!free+0x00000065
5f430619 vbscript!VBScriptClass::`scalar deleting destructor’+0x00000019
5f476fca vbscript!VBScriptClass::CheckDelete+0x00000028
7537bbb9 OLEAUT32!VariantClear+0x000000e9
60db19e2 IEShims!IEShims_SetRedirectRegistryForThread+0x00005472
5f43224e vbscript!AssignVar+0x0000013e
The Fix in July Security Patch
We reported the 2 new vulnerabilities to Microsoft and they addressed them in a single CVE (CVE-2018-8242) in July security update.
Quick analysis shows that this time they fixed the new issues like this:
Now for VBScript operations which will free a SafeArray’s internal buffer (erase, redim), before clearing elements in the array, it will first set the internal SafeArray descriptor to null. After this fix, you will not be able to access (read/write/free) the SafeArray inside the |Class_Terminate| again because the descriptor has already been cleared when the callback is invoked.
This patch perfectly fixed the new issues we reported.
BTW, it is not a security issue but, have you noticed that the above patch introduced a resource (memory) leak problem? Try to figure it out 🙂
Some thoughts
This case is an interesting adventure for me. Analyzing the poc/patch – thinking – finding a new way to crash – pop a calc, each step brings me a lot of fun.
What I learned (again) from this case is that security patch analysis is very important for a bug hunter. I have seen many cases that a security patch failed to address all the possible paths of a vulnerability and we can still find some way to trigger the vulnerability after the patch. And I have also seen cases that a security patch can expose new attack surfaces or introduced new bugs directly into the software. And I think maybe both developers and security researchers should spend more time on security patches.