It’s Time to Terminate the TerminatorMay 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
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:
array1 contains an element that is an instance of
MyClass. When destroying
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):
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
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
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
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):
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 (
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
VariantCopy will decrement the reference count of the VBScript object. If appropriate, this invokes
Ah, but what if the script in
Class_Terminate restarts the process and again assigns some value into
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):
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
VariantClear would first release the dispatch interface and afterwards clear the
VARIANT structure. After the patch is applied, it sets the
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.
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.