首页
论坛
专栏
课程

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

2018-6-9 15:31 3980

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

2018-6-9 15:31
3980

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


 

目录

漏洞背景

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 0day CVE-2015-5119 / CVE-2015-5122。这两个漏洞的思路是当赋值方是个对象,而被赋值方出于某种原因要求接收一个数值时,会调用该对象的valueof方法进行转换,而valueof方法是可以被重载的,这样就可以在重载的valueof函数中进行一些自定义的操作,例如释放对象,改变数组大小等。

 

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

A(js_obj,1)

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

 


我们来看一下 VT_I2VT_I4 代表什么:

  • The value of a VT_I2 type property MUST be a 2-byte signed integer. It MUST be formatted in little-endian byte order.

  • The value of a VT_I4 type property MUST be a 4-byte signed integer. It MUST be formatted in little-endian byte 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函数的逻辑又可以分为如几步:

  1. 初始化一个ArrayWrapper类实例,此过程调用重载的Class_Initialize方法初始化一个二维数组A(1, 2000)
  2. 在访问A数组时针对一维索引传入一个js对象,导致调用重载的valueof方法
  3. 在重载的valueof方法中,调用triggerBug函数
  4. triggerBug函数中调用ArrayWrapper.Resize方法将数组缩小为A(1, 1),这将导致多余部分的内存被释放
  5. 利用精心构造的数据(y数组)迅速占用刚刚被释放的内存
  6. 在重载的valueof方法最后返回1,返回后继续访问A(1, 2),从而将s对象写入可控的内存
  7. 遍历y数组,通过对比VarType找出s对象(s是一个空class实例)并返回其地址

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

 

 

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

// VBScriptClass
0:005> dd 0200ac80 l30/4
0200ac80  6d061748 00000002 0200ace0 02009420
0200ac90  00000e70 00000000 0200adbc 00000000
0200aca0  00000000 0039be4c 00000000 0055fcf8

// class name
0:005> du 0039be4c
0039be4c  "ArrayWrapper"

// NameList
0:005> dd 0200ace0
0200ace0  0200adb8 000000c8 00000100 00000100
0200acf0  00004000 0200adbc 0200ae70 0200ad70
0200ad00  0000000f 00000003 00000040 00000003
0200ad10  00000014 0200ad18 0200adbc 0200ae10
0200ad20  0200ae50 00000135 0000013f 00000000
0200ad30  0200acf4 0200ad08 00000047 00000000
0200ad40  00000140 00000141 00000043 00000000
0200ad50  00000135 00000141 00000000 0200ad1c

// CDISPIDTable
0:005> dd 0200ace0+2c
0200ad0c  00000003 00000014 0200ad18 0200adbc
0200ad1c  0200ae10 0200ae50 00000135 0000013f
0200ad2c  00000000 0200acf4 0200ad08 00000047
0200ad3c  00000000 00000140 00000141 00000043
0200ad4c  00000000 00000135 00000141 00000000
0200ad5c  0200ad1c 0200ad38 00000044 77fb323f
0200ad6c  08011193 00000000 0200ae50 0200ae10
0200ad7c  00000000 00000000 00000000 00000000

// slots_start
0:005> dd 0200ad18 l8
0200ad18  0200adbc 0200ae10 0200ae50 00000135
0200ad28  0000013f 00000000 0200acf4 0200ad08

// slot前两个成员为aw显式声明的两个成员函数, 第三个成员为aw.A
0:005> dd 0200ae50 l4
0200ae50  0000600c 00000000 0200ae5c 0036c1e8

// 可以看到此时aw为一个二维数组,大小为(1+1, 1+1)
0:005> dd 0036c1e8 l8
0036c1e8  08800002 00000010 00000000 034d8678
0036c1f8  00000002 00000000 00000002 00000000

// aw.A.pvData
0:005> dd 034d8678
034d8678  00000000 00000000 00000000 00000000
034d8688  00000000 00000000 00000000 00000000
034d8698  00000000 00000000 00000000 00000000
034d86a8  00000000 00000000 00000000 00000000
034d86b8  7d3b38dd 0807e4ee 0000bb80 41414141
034d86c8  00000009 00000000 0055fcf8 00000000 // 被写入的s对象
034d86d8  00440044 00440044 00440044 00440044
034d86e8  00440044 00440044 00440044 00440044

// aw.A.pvData, 以byte查看
0:005> db 034d8678
034d8678  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
034d8688  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
034d8698  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
034d86a8  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
034d86b8  dd 38 3b 7d ee e4 07 08-80 bb 00 00 41 41 41 41  .8;}........AAAA
034d86c8  09 00 00 00 00 00 00 00-f8 fc 55 00 00 00 00 00  ..........U.....
034d86d8  44 00 44 00 44 00 44 00-44 00 44 00 44 00 44 00  D.D.D.D.D.D.D.D.
034d86e8  44 00 44 00 44 00 44 00-44 00 44 00 44 00 44 00  D.D.D.D.D.D.D.D.

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

// 可以看到y是一个一维数组,成员数量为(32+1)
0:005> dd 00370bf0 l6
00370bf0  08920001 00000010 00000000 003d2928
00370c00  00000021 00000000

// y.pvData
0:005> dd 003d2928
003d2928  6d060008 e0d3a79f 034d86c4 6d065482
003d2938  6d060008 e0d3a79f 034e425c 6d065482
003d2948  6d060008 e0d3a79f 034efdf4 6d065482
003d2958  6d060008 e0d3a79f 034fb98c 6d065482
003d2968  6d060008 e0d3a79f 03507524 6d065482
003d2978  6d060008 e0d3a79f 035130bc 6d065482
003d2988  6d060008 e0d3a79f 0351ec54 6d065482
003d2998  6d060008 e0d3a79f 0352a7ec 6d065482

// y(0), 以byte查看
0:005> db 034d86c4
034d86c4  41 41 41 41 09 00 00 00-00 00 00 00 f8 fc 55 00  AAAA..........U.
034d86d4  00 00 00 00 44 00 44 00-44 00 44 00 44 00 44 00  ....D.D.D.D.D.D.
034d86e4  44 00 44 00 44 00 44 00-44 00 44 00 44 00 44 00  D.D.D.D.D.D.D.D.
034d86f4  44 00 44 00 44 00 44 00-44 00 44 00 44 00 44 00  D.D.D.D.D.D.D.D.
034d8704  44 00 44 00 44 00 44 00-44 00 44 00 44 00 44 00  D.D.D.D.D.D.D.D.
034d8714  44 00 44 00 44 00 44 00-44 00 44 00 44 00 44 00  D.D.D.D.D.D.D.D.
034d8724  44 00 44 00 44 00 44 00-44 00 44 00 44 00 44 00  D.D.D.D.D.D.D.D.
034d8734  44 00 44 00 44 00 44 00-44 00 44 00 44 00 44 00  D.D.D.D.D.D.D.D.

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

 


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

For i = 0 To 31
  ' Mid(y(i), 3, 1) 即取出上面日志中的 09 00, 与s对象的类型vbObject进行比对, 若相等则进入if, 调试时i恒为0, 说明释放后的内存被y(0)立即占用
  ' Mid(y(i), 3 + 4, 2) 即取出上图中的 f8 fc 55 00, 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> dd 0055fcf8 l30/4
0055fcf8  6d061748 00000023 00000000 02009420
0055fd08  00000e70 00000000 00000000 00000000
0055fd18  00000000 00371524 0200ac80 00000000

0:005> ln 6d061748
(6d061748)   vbscript!VBScriptClass::`vftable'   |  (6d06c518)   vbscript!__pfnDefaultDliNotifyHook2
Exact matches:
    vbscript!VBScriptClass::`vftable' = <no type information>

// 可以看到泄漏的s对象正是Dummy类的实例
0:005> du 00371524
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> dd 0055fcf8 l30/4
0055fcf8  6d061748 00000023
0055fd08  00000e70 00000000 00000000 00000000
0055fd18  00000000 00371524 0200ac80 00000000

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

// 长度
0:005> dd 0055fcf8+8-4 l1
0055fcfc  00000023

// 数据
0:005> db 0055fcf8+8 l23*2
0055fd00  00 00 00 00 20 94 00 02-70 0e 00 00 00 00 00 00  .... ...p.......
0055fd10  00 00 00 00 00 00 00 00-00 00 00 00 24 15 37 00  ............$.7.
0055fd20  80 ac 00 02 00 00 00 00-3f 32 fb 77 86 11 00 08  ........?2.w....
0055fd30  00 00 00 00 74 ab 00 02-00 00 00 00 00 00 00 00  ....t...........
0055fd40  00 00 00 00 00 00

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

 

如果代码这样写:

mem = leakMem(arg1, addr + &hc)
csession = strToInt(Mid(mem, 1, 2))

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

// 长度为0, 无法读出数据
0:005> dd 0055fcf8+c-4 l1
0055fd00  00000000

0:005> db 0055fcf8+c
0055fd04  20 94 00 02 70 0e 00 00-00 00 00 00 00 00 00 00   ...p...........
0055fd14  00 00 00 00 00 00 00 00-24 15 37 00 80 ac 00 02  ........$.7.....
0055fd24  00 00 00 00 3f 32 fb 77-86 11 00 08 00 00 00 00  ....?2.w........
0055fd34  74 ab 00 02 00 00 00 00-00 00 00 00 00 00 00 00  t...............
0055fd44  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0055fd54  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0055fd64  00 00 00 00 00 00 00 00-00 00 00 00 35 32 fa 7c  ............52.|
0055fd74  88 11 00 00 60 af 00 02-f8 81 00 02 2b 00 00 00  ....`.......+...

此时构造的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> dd 03218184+4 l4
03218188  0000400c 00000000 0036f914 00440044

// 覆盖前的SafetyOption
0:005> dd 0036f914 l1
0036f914  0000000e

// 覆盖后的SafetyOption,SafetyOption & 0x0B失效(0x024f0004 & 0x0B = 0)
0:005> dd 0036f914 l1
0036f914  024f0004

步骤5

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

 

漏洞检测

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

  1. 首先hook oleaut32!VariantChangeTypeEx函数

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

0:013> dt ole32!tagSAFEARRAY
    +0x000 cDims : Uint2B
    +0x002 fFeatures : Uint2B
    +0x004 cbElements : Uint4B
    +0x008 cLocks : Uint4B
    +0x00c pvData : Ptr32 Void
    +0x010 rgsabound : [1] tagSAFEARRAYBOUND

0:013> dt ole32!tagSAFEARRAYBOUND
    +0x000 cElements : Uint4B
    +0x004 lLbound : Int4B

// 调用VariantChangeTypeEx前的aw.A
0041e7a0  08800002 00000010 00000000 02ff0f58
0041e7b0  000007d1 00000000 00000002 00000000

// 调用VariantChangeTypeEx后的aw.A
0041e7a0  08800002 00000010 00000000 02ff0f58
0041e7b0  00000002 00000000 00000002 00000000

// rgsabound[0].cElements大小变化如下:
7d1(2000+1) -> 2(1+1)

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

 

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

致谢

特别感谢 Hu JiangXu XilinYang 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



[招聘]欢迎市场人员加入看雪学院团队!

最后于 2019-3-26 13:08 被银雁冰编辑 ,原因:
上传的附件:
最新回复 (3)
vlinkstone 2018-6-9 16:35
2
0
support~~~感谢冰神分享
roysue 3 2018-6-10 18:32
3
0
其实看雪论坛现在发md非常方便,可以尝试一下。
byClark 2018-8-16 16:46
4
0
方便告知一下这是哪一家的安全软件吗
游客
登录 | 注册 方可回帖
返回