It’s Time to Terminate the Terminator

May 15, 2018 | Simon Zuckerbraun

VBScript is a language whose time of usefulness on the Internet has mainly passed, but it’s still supported in Internet Explorer. Recently, we’ve become aware of a rather dangerous feature that has been hiding in plain view in VBScript. That feature is the ability to define a destructor method on a class. Such a destructor method goes by the name Class_Terminate.

In general terms, what is dangerous about Class_Terminate is that it gives malicious scripts the opportunity to perform actions at a time that such actions may be unexpected. At least for the present time, Microsoft’s decision has been to continue supporting Class_Terminate. As I will ultimately show by the end of this blog post, the dangers posed by Class_Terminate are general enough that patching the currently-known attack vectors required a change to the behavior of a fundamental OLE API. Even after this somewhat drastic step, Class_Terminate still poses unmitigated hazards. While recent patches terminated the bugs discussed here, in the future, Microsoft may want to reconsider their decision to continue supporting this feature.

Terminator Bug #1: Exploiting a Missing SAFEARRAY Lock

Let’s have a look at one example of what can go wrong with Class_Terminate. The first indication we had that Class_Terminate is problematic came from a bug report we purchased (ZDI-18-291), in which the proof-of-concept code looked like this:

Figure One

Here, array1 contains an element that is an instance of MyClass. When destroying array1, the Class_Terminate method of MyClass is invoked, which performs an unexpected operation on the array that is being destroyed.

In this case, the root cause is in OLEAUT32!_SafeArrayDestroyData. VBScript arrays are implemented as SAFEARRAY structures as defined by OLEAUT32. While iterating through the array to clear its contents, _SafeArrayDestroyData fails to maintain a lock on the array buffer. This gives the code in Class_Terminate the opportunity to release the array buffer during the iteration, producing a Use-After-Free (UAF) condition. Though the true root cause is in OLEAUT32, without the Class_Terminate feature, there would not be any way for malicious script to take advantage the missing lock.

This bug can be fixed easily enough by implementing the necessary locking during SAFEARRAY destruction. It turns out, however, that the difficulties posed by Class_Terminate run much deeper.

Terminator Bug #2: Violating Expected Ordering of Operations

Though there are exceptions to the rule, single-threaded components generally don’t expect to be called on a re-entrant basis. Instead, they are written with the expectation that the caller won’t be able to invoke any method on the component until the previous method invocation is complete. However, with Class_Terminate, we can easily construct code to violate this assumption. Although it no longer hits after the May 2018 patch release, consider the following (ZDI-CAN-6199):

Figure Two

In this case, the object we’re going to exploit is a Scripting.Dictionary. Unlike the previous case where there was a bug in the SAFEARRAY code, here there is no bug in Scripting.Dictionary. The last line in Figure 2 invokes the destructor of the Scripting.Dictionary. Since one of the values in the dictionary is an instance of MyClass, the terminator is invoked. From within the terminator, the malicious script invokes a method on the dictionary. This produces a crash since the dictionary is in a half-destructed state and is certainly not prepared for a method call.

If the last line were replaced with some other dictionary method that removes the MyClass instance from the dictionary (for example, Call dict.RemoveAll), this would similarly produce an unexpected dictionary method invocation from within Class_Terminate. However, this particular combination does not happen to produce a crash.

Terminator Bug #3: Snatching a Reference During Object Destruction

Here’s a further example of hijinks during Class_Terminate (ZDI-CAN-6198):

Figure Three

This example was also taken out by the May 2018 patch release. As in the previous example, the ReDim statement invokes the destructor of the Scripting.Dictionary. This, in turn, will destroy the MyClass instance. Then, within Class_Terminate, we copy a new reference to the dictionary into variable dict2. As designed, this increments the reference count of the dictionary, since the dictionary shouldn’t be destroyed while there is an outstanding reference in dict2. But, in this case, it’s too late! Incrementing the dictionary’s reference count has no effect because the dictionary’s destructor has already been invoked, and that’s irreversible. So afterwards, we get a beautiful hanging reference in dict2, free and clear. This one is a nice exploitable UAF.

Terminator Bug #4: VARIANT Double-Clear

I feel that this one is the pièce de résistance of Class_Terminate vulnerabilities.

Suppose there’s a component that has a read/write property of type VARIANT. A typical C++ implementation looks like this (exclusive of initialization and shutdown):

Figure Four

The field m_prop1 is of type VARIANT. Whenever it holds a pointer to an object (VT_DISPATCH), it should maintain an elevated reference count on the object. When m_prop1 is cleared or new data is copied into it, it decrements the reference count. All this is taken care of automatically by the variant manipulation APIs (VariantCopy, VariantClear, etc.)

Consider what happens when script uses this component and first places a VBScript class instance into Prop1, then later places some different value into Prop1. Before clearing the original value in m_prop1, VariantCopy will decrement the reference count of the VBScript object. If appropriate, this invokes Class_Terminate.

Ah, but what if the script in Class_Terminate restarts the process and again assigns some value into Prop1? Then VariantCopy is re-entered, and as before, it decrements the reference count of the VBScript object. But this is wrong, because the reference count has now been decremented twice for a single outstanding pointer! The result is an unbalanced reference count. Down the line, this produces a UAF when the VBScript class instance is released prematurely.

The following proof-of-concept is a concrete example. Though there is nothing exceptional about the code in Figure 4 (and we have verified that MSHTML contains code that can be abused in this way), it nevertheless turns out that the very simplest way to illustrate the problem is with a VBScript array. Somewhat like the code in Figure 4, a VBScript array allows you to get and set VARIANT data (ZDI-CAN-6197):

Figure Five

Kaspersky reports finding an exploit in the wild making use of code similar to what is shown here in Figure 5; however, in the information reported publicly up to this point, there has been some confusion about the true mechanism of this vulnerability. The Kaspersky article also mentions that Qihoo reported an equivalent bug to Microsoft, and gave it the name “Double Kill”. Notably, that name is a hint to the correct explanation as detailed above.

Microsoft Response as of May 2018

Microsoft fixed the first bug shown above (ZDI-18-291) in the April 2018 update, and the remainder in the May 2018 update. The changes can be summarized as follows:

• April 2018: Added locking around destructive operations on VBScript arrays.
• May 2018: Modified VBScript behavior: An attempt to read the value of a VBScript variable (or array element) during an assignment of that variable now yields the variable’s new value as opposed to the old value. The same applies to other destructive operations (ReDim, Erase).
• May 2018: Modified the behavior of VariantClear. Originally, when clearing a VT_DISPATCH, VariantClear would first release the dispatch interface and afterwards clear the VARIANT structure. After the patch is applied, it sets the VARIANT to VT_EMPTY immediately, prior to calling Release. (Note that VariantCopy makes use of VariantClear, so it inherits this behavior change as well.)

I find it remarkable that Microsoft took the risky step of modifying the behavior of an API so deeply entrenched and fundamental as VariantClear. As detailed above, however, this was a necessity for remediating vulnerabilities involving Class_Terminate. The mere fact that Microsoft was willing to risk breakage to all the millions of lines of COM code that are out there after all these years indicates the seriousness of the problem. While getting these issues fixed is great, it’s unlikely that we’ve seen the last of Class_Terminate bugs. I’ll be watching this area closely, and should additional patches be released, they could definitely be detailed in a future blog.

Conclusion

We’ve explored some of the ways that Class_Terminate can violate security assumptions, and we’ve even seen that remediating the problem has already necessitated a behavioral change to the fundamental API VariantClear. Though we can’t say much more on the subject at the present, we have become aware that even the latest patches do not remove all risks associated with Class_Terminate. Our recommendation to Microsoft is to end support for Class_Terminate in its current form, perhaps by making its invocation asynchronous.

You can find me on Twitter at @HexKitchen, and follow the team for the latest in exploit techniques and security patches.