CVE-2016-0189 vbs脚本引擎损坏漏洞分析

Editor 2018-06-19 18:06

漏洞背景


CVE-2016-0189是一个vbscript脚本引擎损坏漏洞,最初作为一个0day被用在针对韩国的APT攻击中,并在2016年3月10号于MS16-051中被修复。3个月后,国外安全人员通过补丁比对分析了漏洞成因,并将利用代码上传到Github。从此0189便被广泛纳入各种挂马,在今年的CVE-2018-8174出现之前,CVE-2016-0189一直是较新版本IE的挂马首选。


由于之前在调试这个漏洞时发现参考资料很少,特别是国内只看到一篇文章讨论这个漏洞,于是决定把调试过程写一下,不足之处请见谅。


漏洞成因


在vbs解析引擎中,vbscript!AccessArray函数用来访问数组成员。例如访问一个二维数组A的成员A(1,1)时,vbs解析引擎就会调用这个函数,根据传入的索引计算待访问的地址。


2015年Hacking Team的泄漏中有两个通过重载valueof函数来触发的flash 0dayCVE-2015-5119/CVE-2015-5122。这两个漏洞的思路是当赋值方是个对象,而被赋值方出于某种原因要求接收一个数值时,会调用该对象的valueof方法进行转换,而valueof方法是可以被重载的,这样就可以在重载的valueof函数中进行一些自定义的操作,例如释放对象,改变数组大小等。


在vbscript!AccessArray函数内,当访问语句为如下形式时,就有可能进入上面的情景,而事实确实如此。


A(js_obj,1)


具体的逻辑如下图所示,cp_var_index->vt代表索引变量的类型,当索引类型为VT_I2和VT_I4时,直接返回该对象的值,而其他情况下会调用vbscript!rtVariantChangeTypeEx函数,并在里面调用oleaut32!VariantChangeTypeEx函数,随后会调用对象的valueof方法。



我们来看一下VT_I2和VT_I4代表什么:

The value of aVT_I2type property MUST be a2-byte signed integer. It MUST be formatted inlittle-endianbyte order.

The value of aVT_I4type property MUST be a4-byte signed integer. It MUST be formatted inlittle-endianbyte order.

当传入索引是有符号短整型和有符号长整型型时,就直接使用其值,而当索引为其他类型时(例如当传入对象为js对象时,变量类型为VT_DISPATCH),就调用vbscript!rtVariantChangeTypeEx函数将对象值转化为要求的类型,最终通过调用valueof方法返回该值。


In vbscript!AccessArray

call vbscript!rtVariantChangeTypeEx

call oleaut32!VariantChangeTypeEx

...

call valueof


如果我们在重载的valueof内改变数组的大小,当返回上层继续访问数组元素时,就会产生问题。poc中的思路是:先定义一个比较大的二维数组A(1, 2000),然后通过访问A(js_obj, 2)去调用重载的js_obj.valueof()方法将一维索引转化为合理的数组下标。在js_obj.valueof()内将数组缩小为A(1, 1),然后迅速用UAF进行占位,并在valueof方法的最后返回1,作为转换后的索引。转换完成后访问A(1, 2) 。然而这时候的A(1, 2)已经变成了占位后的内存。攻击者多次利用这一特性来操控内存,分别实现泄漏一个类对象地址,任意地址读,和任意地址写。在此基础上找到vb的安全选项开关,利用一个单精度浮点数(vbSingle)的类型值(4)去覆盖原来的安全开关属性值(0x0E->0x?4),从而开启上帝模式。


vbscript.dll内判断对象安全性的函数是COleScript::InSafeMode,汇编代码如下图所示。可以看到test指令对 dword ptr [ecx+174] 的值与0x0B(00001011)进行与运算,若结果为0,即认为不处于SafeMode,放行对象执行。



调试环境

windows 7 sp1 x86 无补丁+vbscript.dll/oleaut32.dll 5.8.7601.17514+windbg 6.11 x86


POC分析与调试

poc的主入口为exploit函数,原代码的注释写的很清晰,可以看到exploit函数分为5个步骤:



步骤1:泄漏VBScriptClass对象地址


getAddr函数的逻辑又可以分为如几步:

初始化一个ArrayWrapper类实例,此过程调用重载的Class_Initialize方法初始化一个二维数组A(1, 2000)

在访问A数组时针对一维索引传入一个js对象,导致调用重载的valueof方法

在重载的valueof方法中,调用triggerBug函数

triggerBug函数中调用ArrayWrapper.Resize方法将数组缩小为A(1, 1),这将导致多余部分的内存被释放

利用精心构造的数据(y数组)迅速占用刚刚被释放的内存

在重载的valueof方法最后返回1,返回后继续访问A(1, 2),从而将s对象写入可控的内存

遍历y数组,通过对比VarType找出s对象(s是一个空class实例)并返回其地址

VBScriptClass继承了NameTbl,其头部是一个NameTbl结构体,NameTbl结构体偏移0x08处的值是一个指向NameList结构体的指针,NameList结构体偏移0x2C处是一个CDISPIDTable结构体,而CDISPIDTable结构体偏移0x08处的值是一个指针数组,每个数组元素都指向一个VAR结构体,代表一个类成员在类对象中的实体,整个关系如下所示:

先来看一下resize后的aw.A:


//VBScriptClass

0:005> dd0200ac80l30/4

0200ac80 6d061748000000020200ace002009420

0200ac90 00000e70000000000200adbc00000000

0200aca0 000000000039be4c000000000055fcf8

//classname

0:005> du0039be4c

0039be4c "ArrayWrapper"

//NameList

0:005> dd0200ace0

0200ace0 0200adb8000000c80000010000000100

0200acf0 000040000200adbc0200ae700200ad70

0200ad00 0000000f000000030000004000000003

0200ad10 000000140200ad180200adbc0200ae10

0200ad20 0200ae50000001350000013f00000000

0200ad30 0200acf40200ad080000004700000000

0200ad40 00000140000001410000004300000000

0200ad50 0000013500000141000000000200ad1c

//CDISPIDTable

0:005> dd0200ace0+2c

0200ad0c 00000003000000140200ad180200adbc

0200ad1c 0200ae100200ae50000001350000013f

0200ad2c 000000000200acf40200ad0800000047

0200ad3c 00000000000001400000014100000043

0200ad4c 00000000000001350000014100000000

0200ad5c 0200ad1c0200ad380000004477fb323f

0200ad6c 08011193000000000200ae500200ae10

0200ad7c 00000000000000000000000000000000

//slots_start

0:005> dd0200ad18l8

0200ad18 0200adbc0200ae100200ae5000000135

0200ad28 0000013f000000000200acf40200ad08

//slot前两个成员为aw显式声明的两个成员函数, 第三个成员为aw.A

0:005> dd0200ae50l4

0200ae50 0000600c000000000200ae5c0036c1e8

//可以看到此时aw为一个二维数组,大小为(1+1,1+1)

0:005> dd0036c1e8l8

0036c1e8 088000020000001000000000034d8678

0036c1f8 00000002000000000000000200000000

//aw.A.pvData

0:005> dd034d8678

034d8678 00000000000000000000000000000000

034d8688 00000000000000000000000000000000

034d8698 00000000000000000000000000000000

034d86a8 00000000000000000000000000000000

034d86b8 7d3b38dd0807e4ee0000bb8041414141

034d86c8 00000009000000000055fcf800000000//被写入的s对象

034d86d8 00440044004400440044004400440044

034d86e8 00440044004400440044004400440044

//aw.A.pvData, 以byte查看

0:005> db034d8678

034d8678 0000000000000000-0000000000000000 ................

034d8688 0000000000000000-0000000000000000 ................

034d8698 0000000000000000-0000000000000000 ................

034d86a8 0000000000000000-0000000000000000 ................

034d86b8 dd383b7dee e40708-80bb000041414141 .8;}........AAAA

034d86c8 0900000000000000-f8 fc550000000000 ..........U.....

034d86d8 4400440044004400-4400440044004400 D.D.D.D.D.D.D.D.

034d86e8 4400440044004400-4400440044004400 D.D.D.D.D.D.D.D.

再来看一下占位成功后的y数组情况:


//可以看到y是一个一维数组,成员数量为(32+1)

0:005> dd00370bf0l6

00370bf0 089200010000001000000000003d2928

00370c00 0000002100000000

//y.pvData

0:005> dd003d2928

003d2928 6d060008e0d3a79f034d86c46d065482

003d2938 6d060008e0d3a79f034e425c6d065482

003d2948 6d060008e0d3a79f034efdf46d065482

003d2958 6d060008e0d3a79f034fb98c6d065482

003d2968 6d060008e0d3a79f035075246d065482

003d2978 6d060008e0d3a79f035130bc6d065482

003d2988 6d060008e0d3a79f0351ec546d065482

003d2998 6d060008e0d3a79f0352a7ec6d065482

//y(0), 以byte查看

0:005> db034d86c4

034d86c4 4141414109000000-00000000f8 fc5500 AAAA..........U.

034d86d4 0000000044004400-4400440044004400 ....D.D.D.D.D.D.

034d86e4 4400440044004400-4400440044004400 D.D.D.D.D.D.D.D.

034d86f4 4400440044004400-4400440044004400 D.D.D.D.D.D.D.D.

034d8704 4400440044004400-4400440044004400 D.D.D.D.D.D.D.D.

034d8714 4400440044004400-4400440044004400 D.D.D.D.D.D.D.D.

034d8724 4400440044004400-4400440044004400 D.D.D.D.D.D.D.D.

034d8734 4400440044004400-4400440044004400 D.D.D.D.D.D.D.D.

下图分别以aw.A视角(红色框区域)和y视角(黄色框区域)看s对象,不难发现有4字节的错位:


理解上图之后,我们就可以根据条件查找包含s对象的y数组成员,调试时恒为y(0)。poc里面申请32次内存是确保释放后的内存一定被y中的某个成员再次使用,进而从32个成员里面找出包含s对象的那个成员。


For i=0To31

' Mid(y(i),3,1) 即取出上面日志中的0900, 与s对象的类型vbObject进行比对, 若相等则进入if, 调试时i恒为0, 说明释放后的内存被y(0)立即占用

' Mid(y(i),3+4,2) 即取出上图中的 f8 fc5500, strToInt将其转化为0055fcf8, 即s对象的地址

If Asc(Mid(y(i),3,1))=VarType(s) Then

addr=strToInt(Mid(y(i),3+4,2))

End If

y(i)=Null

Next



0:005> dd0055fcf8l30/4

0055fcf8 6d061748000000230000000002009420

0055fd08 00000e70000000000000000000000000

0055fd18 00000000003715240200ac8000000000

0:005> ln6d061748

(6d061748)   vbscript!VBScriptClass::`vftable'   |  (6d06c518)   vbscript!__pfnDefaultDliNotifyHook2

Exact matches:

vbscript!VBScriptClass::`vftable'=<notypeinformation>

//可以看到泄漏的s对象正是Dummy类的实例

0:005> du00371524

00371524 "Dummy"


泄漏了s对象的地址后,我们通过相关数据结构去获取vbscript!SafetyOption的内存地址,并将其改写为0x00或0x04。查找过程如下:

下面对此进行说明。

步骤2:读取CSession对象指针

poc中读取CSession对象指针的代码如下所示:

由前面的分析已知addr是一个VBScriptClass实例指针,我们可以看到代码把addr+8的地址传入leakMem函数,并通过Mid(mem, 3, 2)将CSession对象指针获取出来并转换为16进制。

一个疑问

为什么上述代码不写成如下形式?


mem=leakMem(arg1, addr+&hc)

csession=strToInt(Mid(mem,1,2))

为了回答这个问题,我们先来看一下leakMem函数的实现:

leakMem的基本逻辑是在占位内存中构造一个字符串地址(Data High)为待读取地址的字符串对象,然后再次利用漏洞触发UAF,从而使aw.A(1, 2)处改写为一个VT_BSTR对象,随后读取该对象,从而使addr处的数据被当做定长字符串读出,最后在读取的字符串中定位CSession指针对应的部分并转化为对应的32位地址。

图片出处

我们来回顾一下BSTR对象的结构:

图片出处

关键点在于字符串前面的4字节,这4字节是一个长度域,指定了后面待读取的unicode字符串长度。现在再来看一下前面问题的那个问题。

这是本次调试中getAddr函数返回的VBScriptClass实例:


0:005> dd0055fcf8l30/4

0055fcf8 6d06174800000023

0055fd08 00000e70000000000000000000000000

0055fd18 00000000003715240200ac8000000000

原poc中传入的是addr+8,那么可以构造出如下的BSTR结构:


//长度

0:005> dd0055fcf84l1

0055fcfc 00000023

//数据

0:005> db0055fcf8+8l23*2

0055fd00 0000000020940002-700e000000000000 .... ...p.......

0055fd10 0000000000000000-0000000024153700 ............$.7.

0055fd20 80ac000200000000-3f32fb7786110008 ........?2.w....

0055fd30 0000000074ab0002-0000000000000000 ....t...........

0055fd40 000000000000


这样可以成功读取包含Csession地址的字符串。


如果代码这样写:


mem=leakMem(arg1, addr+&hc)

csession=strToInt(Mid(mem,1,2))


但当传入addr+c时,构造的BSTR如下:


//长度为0, 无法读出数据

0:005> dd0055fcf84l1

0055fd00 00000000

0:005> db0055fcf8+c

0055fd04 20940002700e0000-0000000000000000  ...p...........

0055fd14 0000000000000000-2415370080ac0002 ........$.7.....

0055fd24 000000003f32fb77-8611000800000000 ....?2.w........

0055fd34 74ab000200000000-0000000000000000 t...............

0055fd44 0000000000000000-0000000000000000 ................

0055fd54 0000000000000000-0000000000000000 ................

0055fd64 0000000000000000-000000003532fa7c ............52.|

0055fd74 8811000060af0002-f88100022b000000 ....`.......+...

此时构造的BSTR的长度域为0,数据无法正常读出。


步骤3:读取COleScript对象指针


poc中读取COleScript对象指针的代码如下,原理和上一步完全相同,此处不再过多分析:


步骤4:覆写vbscript!SafetyOption


在IE8中,vbscript!SafetyOption位于COleScript对象的+0x174处。poc代码中通过调用overwrite函数去覆写这一值,如下:



代码中伪造一个type=0x400C间接寻址对象,接着触发漏洞,随后将一个Csng(单精度浮点)对象的写入COleScript+0x16C开始的16个字节处,COleScript+0x174处正好写入Csng的type值4,从而开启上帝模式。


//type=0x400c对应间接寻址

0:005> dd03218184+4l4

03218188 0000400c000000000036f91400440044

//覆盖前的SafetyOption

0:005> dd0036f914l1

0036f914 0000000e

//覆盖后的SafetyOption,SafetyOption &0x0B失效(0x024f0004&0x0B=0)

0:005> dd0036f914l1

0036f914 024f0004


步骤5

开启上帝模式后,就可以弹出cmd窗口了。


漏洞检测

下面通过逆向某安全软件来看一下对CVE-2016-0189的动态检测方案。

首先hook oleaut32!VariantChangeTypeEx函数

可以看到代码中对CVE-2016-0189的检测逻辑为:在调用oleaut32!VariantChangeTypeEx函数前后检查rgsabound[0].cElements所对应的第二维度的大小,若调用后的大小小于调用前的大小,则视为检出。


0:013> dt ole32!tagSAFEARRAY

+0x000cDims : Uint2B

+0x002fFeatures : Uint2B

+0x004cbElements : Uint4B

+0x008cLocks : Uint4B

+0x00cpvData : Ptr32 Void

+0x010rgsabound : [1] tagSAFEARRAYBOUND

0:013> dt ole32!tagSAFEARRAYBOUND

+0x000cElements : Uint4B

+0x004lLbound : Int4B

//调用VariantChangeTypeEx前的aw.A

0041e7a0 08800002000000100000000002ff0f58

0041e7b0 000007d1000000000000000200000000

//调用VariantChangeTypeEx后的aw.A

0041e7a0 08800002000000100000000002ff0f58

0041e7b0 00000002000000000000000200000000

//rgsabound[0].cElements大小变化如下:

7d1(2000+1)->2(1+1)


这里有一个疑问,MSDN对多维数组的rgsabound域解释.aspx)如下:



但调试时发现A(1, 2000)的rgsabound实际使用顺序和文档描述相反,看检测逻辑里面判断的也是rgsabound[0]->cElements。我们以实际调试结果为主。


致谢

特别感谢Hu Jiang,Xu Xilin,Yang Kang在调试过程中的指导


参考链接

《CVE-2016-0189》https://theori.io/research/cve-2016-0189

《theori-io/cve-2016-0189》https://github.com/theori-io/cve-2016-0189

《Nebula漏洞利用包CVE-2016-0189漏洞利用分析》http://www.freebuf.com/sectool/131766.html

《WinDbg 漏洞分析调试(三)之 CVE-2014-6332》https://paper.seebug.org/240/

《Write Once, Pwn Anywhere》https://www.blackhat.com/docs/us-14/materials/us-14-Yu-Write-Once-Pwn-Anywhere.pdf



原文出自:[原创]CVE-2016-0189 vbs脚本引擎损坏漏洞分析

本文由看雪论坛 银雁冰 原创

转载请注明来自看雪社区



最新评论 (0)
登录后即可评论
返回