CVE-2018-8373 VBScript engine vulnerability analysis

前言

CVE-2018-8373是trendmicro(趋势科技)研究人员于2018年7月发现的VBScript引擎的UAF漏洞。它是同年2018-8174的兄弟版本漏洞。

漏洞原理

漏洞POC如下所示:

1618801914502

POC定义了MyClass类,包含一个array成员变量,两个成员函数,Class_Initialize以及Default Property Get P。前者是不建议使用的方法,现已被新的过程替代。对象初始化时,会被自动唤醒。POC中,Class_initialize是重载的。当VBScriptClass::InitializeClass时,重载此函数。

默认属性不需要特殊说明即可开源访问,POC中的Default Property Get函数会重载MyClass的默认属性。当调用被用来访问cls时,会处理重载的函数。

漏洞触发流程简化为下面三步:

1.cls=New MyClass

初始化MyClass时会重载Class_Initialize(通过vbscript!CScriptEntryPoint::Call派发进入到对应的编码者实现的Class_Initialize方法中)。在此函数中,ReDim array(2)会调用Vbscript!RedimPreservearray来修改数组,元素数为3,也就是修改array数组的长度:

1
2
3
Private Sub Class_Initialize
ReDim array(2)
End Sub

1618823784863

2.cls.array(2)**

由于VB引擎扫描代码从左至右,首先执行cls.array(2)操作。

调用函数vbscript!AccessArray判断对应array(2)是否可访问,首先拿到array内存对象。

1618824214055

中间经过一顿判断,esi此时为array内存对象地址。

1618824772305

1618824931648

分别取出cbElements(数组元素的大小)成员、rgsabound.cElements(元素个数)成员,pvData成员(数组缓冲区起始地址),【ebp+c】拿出目标数组成员下标,我们要访问的是array(2),因此此时下标为2。计算方式为数组成员下标cbElements+pvData = 2\0x10+0x08342fd0 =08342ff0 ,08342ff0 被拷贝到求值栈。

3. cls.array(2)=cls

接着执行等号右边的部分,对cls访问时将执行重载的Get P函数。

1
2
3
Public Default Property Get P
ReDim Preserve array(1)
End Property

通过ReDim修改array数组的长度,Preserve属性表示对数组中已赋值的那些数据不清空 。修改array数组内存的操作,由Vbscript!RedimPreserveArray函数实现。

1618826030792

对array类型一顿判断后,最终调用SafeArrayRedim函数,原型如下:

1
HRESULT __stdcall SafeArrayRedim(SAFEARRAY *psa, SAFEARRAYBOUND *psaboundNew)

1618826259753

第一参数为array对象,第二参数为新array对象数组元素个数。

调用完成后,array对象的pvData被修改为新的数组缓冲区,同时元素个数也变为2。同时,原先的pvData缓冲区被释放。

1618826579284

1
2
3
4
5
6
7
8
9
10
11
0:005> !heap -p -a 07bd2fe0  //NEW
address 07bd2fe0 found in
_DPH_HEAP_ROOT @ 51000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
7a6316c: 7bd2fe0 20 - 7bd2000

0:005> !heap -p -a 08342fd0 //Old
address 08342fd0 found in
_DPH_HEAP_ROOT @ 51000
in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
7c63410: 8342000 2000

继续执行,调用函数AssignVar完成赋值,如下所示。

1618826953582

在赋值之前,存在VAR::clear()函数调用,释放之前的引用,即释放Old array的array(2),由于之前已被释放,第二次访问则产生崩溃。

1618826897201

小结,cls.array(2) = cls语句,在赋值前保存array(2)地址到求值栈,而后Class的Default Property Get P内redim操作导致array对象数组内存被重新分配,而求值栈的地址被释放。最终赋值时二次重用导致崩溃。

漏洞利用

多维数组补充知识。

小例子如下所示

1
2
Dim array2(0,1,2,3,4)
IsEmpty(array2)

1618988017886

可以看到在SAFEARRAY结构中从右至左分别为第一维,第二维等等,至少在内存布局中tagSAFEARRAYBOUND结构是这样体现的。

漏洞利用步骤同样分为三步:

  • 利用漏洞修改二位指针数组长度为0x0FFFFFFF
  • 实现任意地址读写
  • 构造CONTEXT,执行shellcode
修改数组长度

首先,定义两个数组,分别记为array和array2,array就是原理poc中的array数组,array2数组为二维数组,其中每个元素的值都为3。

1618988676848

由于SAFEARRAY结构在堆中申请内存大小为0x30,根据调试来看,其在结构前有额外的0x10字节大小内存。我们首先在类初始化重载函数Class_Initialize函数中构造array(2)也即为其分配0x30的pvData数据缓冲区大小。

一样的,在重载函数Default Property Get函数释放array.pvData,将array2设置为新的array.pvData。

1618989496108

上面代码执行时,首先将array数组大小修改为100001,接着利用赋值语句循环申请大量array2结构的二维数组,即也要申请大量的SAFEARRAY结构体,其中有一个会占用已经被释放的原先array.pvData内存。

执行array(2)=cls后,Default Property Get函数返回值0x0fffffff将被写入SAFEARRAY.rgsabound[1].cElements成员,表示当前二维数组的第一为大小为0x0fffffff,也即我们修改了此数组的长度。

1618990862207

任意地址读写

在上一步中,我们有一个修改了长度的二维数组,导致使用这个数组可以进行越界访问。首先,我们需要找到这个二维数组在array数组的哪个位置,也就是需要找到此元素下标,我们记为index_vuln。

思路就是遍历array数组,找到一个array2的一维元素个数为0x0FFFFFFF,使用UBound函数则即最大上标为0x0FFFFFFE,如下所示,即可定位到此元素下标。

1618991809663

找到这个array2后,我们即可使用array(index_vuln)(a,b)来索引这个数组,其中a∈[0,0x0FFFFFFE],b∈[0,1],也就是说我们可以越界访问array(index_vuln)(a,b).pvData指向的一大片内存。

接下来,需要找到可用于类型混淆的数组元素。

由于在大量连续申请array2数组时,导致array2.pvData内存的连续,每个array2.pvData中间间隔8字节的堆头数据。我们可以利用这一点,通过越界读写,错位赋值来改变VARIANT结构的vt字段,最终构造一个基地址为0,元素大小为1,元素个数为0x7FFFFFFF的超级一维数组;在使用同样方法泄露出用来读写的地址,从而实现任意地址读写。(银师傅的总结过于精炼,直接使用)

那么,先要找到array(index_vuln)(a,b)中的某个元素,它的前一个元素由于堆分配问题,导致向后错位了8字节,存储的VARIANT数据与前面的短整形值为3不一样了,到了当前元素时,变为长整型值为2。我们将当前元素表示为array(index_vuln)(index_A,0),那么前一个元素表示为array(index_vuln)(index_A-1,0),也就是说,从当前元素开始,存储的都是长整型值为2的元素,接着,在经过一次堆分配,又变为正常的短整型值为3。同样的,我们需要另一个元素,它来和array(index_vuln)(index_A,0)实现类型混淆,可预见的最合适的便是从array(index_vuln)(index_A,0)向前的8字节,它是错位后的array2数组的第一个元素。

那么先找到index_A,利用VB的VarType函数,判断VARIANT的vt类型,通过分别定位类型为2及3定位到index_A,逻辑如下:

1619080149803

拿到index_A,如何定位index_B,思路是通过将array(index_vuln)(index_A,0)设置为一段字符串,如”AAAA”,那么将有一个array数组的元素array2的Data变为低两字节8,通过遍历array(i)(0,0)=8,即可拿到index_B。

1619080169946

以上,我们拿到了错位8字节的两个VARIANT结构,可用来进行类型混淆。

1619074181747

借助假的字符串数据类型混淆后可构造一个超大一维数组。

首先令array(index_vul)(index_A,0)处存储我们的伪造字符串,接着array(index_vul)(index_A+2,0)存储我们的伪造数组的字符串。使用array(index_B)(0,0)和array(index_B)(2,0)改变两处的类型,使array(index_vul)(index_A,0)变为长整型(vt=3),array(index_vul)(index_A+2,0)变为数组类型(vt=200c)。如下所示

1619075664181

1619080218155

rw_primit第一行语句执行完后,可以看到array(index_vul)(index_A+2,0)被覆盖为我们的fakearray字符串。

1619076907750

执行完rw_primit后

1619077108133

拿到了一个泄露块内存util_mem,一个可以实现32位内存读写的超大一维数组array(index_vul)(index_A+2,0)。

任意地址读

使用上面的两个工具进行封装即可。

1619080249290

和8174类似的,使用LenB函数特性,写入要读的地址+4,将其类型改为字符串类型8,即可拿到目标地址的值,读完后再将其类型改回长整型3。

任意地址写

通过array(index_vul)(index_A+2,0)访问内存块util_mem,设置类型为长整型3,在设置util_mem+8为要写的data值。

1619080316187

shellcode执行

采用和8174差不多的思路,即可实现利用。

1619080386765

1619080656138

总结

8373是我完整分析的第二个有关脚本引擎相关漏洞,总的来说,对基础语法不熟悉称为阅读poc和理解漏洞原理和利用思路的重大阻碍,幸好调试过程可以帮助我更好地理解代码执行流程,以及引擎内部处理流程。

后续需要继续加强此类基础语法的学习,另外总结漏洞的关联性,利用手法等等。

感谢前辈们的工作,得以很快的学习并理解,瑞思拜!

参考链接

[1]UAF Bug Affects Internet Explorer, Runs Shellcode

https://www.trendmicro.com/en_us/research/18/h/use-after-free-uaf-vulnerability-cve-2018-8373-in-vbscript-engine-affects-internet-explorer-to-run-shellcode.html

[2]CVE-2018-8373:VBScript引擎UAF漏洞

https://xz.aliyun.com/t/2588

[3]记一次CVE-2018-8373利用构造过程

https://bbs.pediy.com/thread-246327.htm

[4]利用CVE-2018-8373 0day漏洞的攻击与Darkhotel团伙相关的分析

https://ti.qianxin.com/blog/articles/analyzing-attack-of-cve-2018-8373-and-darkhotel/

[5]CVE-2018-8174:从UAF到任意地址读写

https://saturn35.com/2019/08/12/20190812-1/