这篇文章之前已经在其他地方发过,这里再发一个格式更友好一点的版本,便于查阅。一并修改了之前的几处笔误。由于国家相关征求意见稿的要求,此类分析文章后面就不太方便公开发布了。
CVE-2019-1208
是趋势科技的@elli0tn0phacker
在今年6
月发现的一个vbscript
漏洞,报告中提到这个漏洞是通过补丁比对发现的,这引起了笔者的兴趣。最近,笔者花了一些时间对该漏洞进行了比较详细的研究。在这篇文章中,笔者将从漏洞成因、修复方案、利用编写三个方面对该漏洞进行介绍。
读者将会看到,代码开发者是如何在修复旧漏洞时不经意间引入新漏洞。在这个例子中,引入的还是一个非常严重的远程代码执行漏洞。通过这个例子读者也会发现,有时候通过补丁比对就可以发现新漏洞。
该漏洞从2019
年6
月更新被引入,到2019
年9
月更新被修复,只存活了短短3
个月,因此编写这个漏洞的利用并无价值,笔者写这个漏洞的利用只是为了概念验证。
尽管微软已经在2019年8
月的IE
更新中全面禁用了vbscript
,但出于安全性考虑,完整利用代码不予公开。
这是一个vbscript
的UAF
(Use After Free
)漏洞,漏洞成因还要从微软今年6
月的补丁说起。
微软在2019
年6
月的vbscript
更新中引入了下面几个函数:
SafeArrayAddRef
SafeArrayReleaseData
SafeArrayReleaseDescriptor
引入SafeArrayAddRef
的作用是为SafeArray
提供一种类似引用计数的机制。
源码中通过使用STL
的 map
将一些对象/数据指针(如pSafeArray
和pvData
)与一个int
型的计数器进行绑定。
在VbsFilter
和VbsJoin
这两个函数中,在调用实际的rtJoin
和rtFilter
前,会调用SafeArrayAddRef
对相关指针的引用计数+1
。调用完毕后,再调用SafeArrayReleaseData
和SafeArrayReleaseDescriptor
在map
中将指针对应的计数-1
,并将指针所对应的key
从map
中删除。
开发者应该是用这种方式修复了一些UAF
问题。但修复方案中没有考虑到当Join/Filter
传入的数组中有类对象时,在Public Default Property Get
这一潜在的回调中可以对数组进行操作(比如ReDim
)。这样,当调用完 rtJoin/rtFilter
后返回VbsJoin/VbsFilter
时,对应的pSafeArray/pvData
指针已被更新,原先的设计是将之前已在map
中“注册”的指针传入后续的SafeArrayReleaseData/SafeArrayReleaseDescriptor
进行引用计数减操作,但现在传入SafeArrayReleaseData/SafeArrayReleaseDescriptor
的指针均不在map
中(因为被重新创建了)。这导致在调用RefCountMap<void *>::Decrement
函数时,find
方法找不到对应的key
,函数直接返回0
。这个返回结果被SafeArrayReleaseData/SafeArrayReleaseDescriptor
理解为对应的指针的引用计数为0
,从而将对应的SafeArray
对象和数据销毁。
具体地,开发者借助RefCountMap
类实现了一个“伪引用计数机制”,通过一个map<tagSAFEARRAY *,int>
将所关心的SafeArray
指针与一个int
型计数器绑定起来,计数值只有0、1、不存在
,三种情况。
相关操作函数的声明如下:
RefCountMap<void *>::Decrement
函数的伪代码如下:
了解了这些知识后,回过头去理解@elli0tn0phacker
报告中的Figure 5
就会容易多了。
@elli0tn0phacker
给出的poc
大致如下:
由于漏洞的存在,我们知道arr(0) = 1
语句执行前arr
已被释放,而且从代码中可以看到arr
是在回调中被ReDim
的。那么arr
到底存在哪里?为什么arr(0) = 1
索引的是ReDim
后被释放的SafeArray
,而不是Redim
前的SafeArray
?
这就涉及到 vbscript
虚拟机的相关知识。
卡巴斯基实验室的Boris Larin
曾写过一篇关于vbscript
虚拟机的文章,并且开源了相关的调试插件。
在文章中,作者对vbscript
虚拟机进行了比较细致的介绍。vbscript
的所有代码都会先被编译为P-Code
,随后通过CScriptRuntime::RunNoEH
对所有P-Code
进行解释执行,CScriptRuntime
对象的成员变量中存储着解释所需的许多信息,比较重要的几个如下:
借助调试插件,我们可以得到 PoC
代码编译后的P-Code
:
以下是上述用到的部分指令对应的字节码(全部指令请参考Boris
的插件源码):
从P-Code
中可以看出, arr(0) = 1
这句对应的指令索引的是本地变量栈(OP_CallLclSt, 0x2E)
,Call Join(arr)
这句对应的指令索引的也是本地变量栈(OP_LocalAdr, 0x19
),从两个指令名称中我们可以猜测arr
被存储在本地变量栈上。
在IDA Pro
中对vbscript!CScriptRuntime::RunNoEH
进行逆向,我们来看一下上述两个指令解释分支的汇编代码:
上述两个分支都调用了CScriptRuntime::PvarLocal
方法,再来看一下CScriptRuntime::PvarLocal
方法的实现:
可以看到CScriptRuntime::PvarLocal
接收一个索引,并且基于CScriptRuntime
对象+0x28
或0x2C
处的值进行偏移运算。调试时发现PoC
两处对arr
的操作索引均为1
,所以存储arr
的地址为:
上述分析验证了上面对于指令作用的猜想,PoC
中每次使用arr
变量时,都会传入对应的索引去本地变量栈中进行访问。
明白了arr
的存取原理后,我们可以清晰地在调试器中观察arr
的变化过程,从而理解整个UAF
的过程。
笔者在开启页堆后对PoC
进行了调试。我们先将断点下到OP_LocalAdr
指令的解释分支,可以看到Join(arr)
执行时访问到的arr
,命中断点时ebx
即为CScriptRuntime
,调试时arr
从本地变量栈(ebx+0x28
)进行索引,读者请留意下图中蓝色高亮的指针,ReDim
语句执行后它会发生变化。
我们对上图中高亮数据(SafeArray指针
)所在的内存下一个写入断点,观察这个位置上数据的几次变化过程。
第一次是在ReDim
(OP_ArrNamReDim
)执行时,对之前arr
的清理阶段(OP_ArrNamReDim
指令的解释流程在后面“修复方案”一节中会进一步说明。
第二次是在OP_ArrNamReDim
执行时,将新创建的arr
复制到本地变量栈的对应内存处,可以看到蓝色高亮处的指针已经发生变化,此时的SafeArray
已经变为刚刚创建的二维数组。
最后,我们将断点下到OP_CallLclSt
的解释分支,目的是断在arr(0) = 1
这句对arr
的访问过程,由于“漏洞成因”所描述的设计上的问题,此时本地变量栈上的arr
已经被释放:
追踪到的释放栈回溯如下图,读者可以看到,这个不当的释放正是由于SafeArrayReleaseDescriptor
传入了未在map
注册的指针所导致。
通过以上调试,读者应该可以清晰感受到整个Use After Free
过程。
清楚漏洞成因后,我们来看一下微软在9
月更新中是如何修复该漏洞的。笔者用Bindiff
工具比对了8
月更新和9
月更新两个vbscript.dll
,发现在rtJoin
(rtFilter
均类似,下面只以rtJoin
进行说明)函数中,在对数组内的元素进行操作前后,加了一对SafeArrayLock/SafeArrayUnlock
函数:
微软采用对SafeArray
加锁的方式来修补这个由之前的补丁引入的问题。SafeArrayLock
会令pSafeArr->cLocks
的值+1
。这样,当在安装9
月补丁后再次打开PoC
。由于前面的+1
操作,就可以令ReDim
指令无法得到正常执行,我们来看一下具体的逻辑。
这里再引述一下上面提到的P-Code
,可以看到ReDim arr(1, 1)
这句语句对应的P-Code
如下:
笔者在调试器中跟了一遍OP_ArrNamReDim
指令(0x0A
) 的执行逻辑,发现有如下几个关键点:
有意思的是,调试前笔者以为这里的ReDim
最终会调用oleaut32!SafeArrayRedim
函数,结果并没有。
结合上述逻辑,当补丁中在操作Join
传入的数组前,SafeArrayLock
令pSafeArr->cLocks
从0
变为1
,从而在执行ReDim arr(1, 1)
对应的指令时,无法通过3.1.1
这一步,新数组无法被创建,Join
函数执行完后本地变量栈中的数组指针不会得到更新,之前的UAF
问题也就无从谈起了。Filter
函数的修复方案同上。
以下为上述过程中涉及到的函数调用及说明:
这个修复方案和CVE-2016-0189
的修复方案思路一致。
@elli0tn0phacker
在他的报告中已经给出了这个漏洞的exploit
编写思路,但没有公布完整代码。作为概念验证,笔者亲手编写了对应的exploit
,以下对部分细节进行说明。
通过触发漏洞,可以得到一块大小为0x30
的空闲内存。借助堆的特性,如果在Join
函数执行完后立即申请一些字符串长度为(0x30 - 4
)的BSTR
对象,就可以实现对被释放内存的占位。减4
是因为BSTR
的字符串前面还有4
字节的长度域,会一并申请。
实践证明这里的操作还是比较简单的,并不需要过多的堆风水技巧,下面是一个可以成功占位的代码示例:
占位后,因为笔者已经在字符串中构造了假的超长数组,当下次访问arr
时,成功占位的字符串会被解释为SafeArray
结构体,从而得到一个基地址为0
,元素个数为0x7fffffff
,元素大小为1
的超长数组。
这部分,以及如何构造一块可读写内存的步骤请参考@elli0tn0phacker
的报告,相关步骤实现起来非常简单,这里不再重复叙述。
在前面的基础上,就可以泄露一个指针对象以绕过ASLR
,这里笔者采用的方法和和CVE-2019-0752
一样,泄露一个Scripting.Dictionary
对象的虚表指针,具体操作如下:
若PoC
要在windows 10
上执行,必须要绕过CFG
。笔者最终采用了@elli0tn0phacker
在他报告中提到的方法,即对CVE-2019-0752
的利用方式稍作改动:
借助BSTR
复制并伪造一个假的Dictionary
虚表(fake_vtable
),并改写Dictionary.Exists
函数指针为kernel32!WinExec
,由于kernel32!WinExec
是系统自带函数,因此可以绕过CFG
检测
借助BSTR
复制并伪造一个假的Dictionary
对象(fake_dict
),将虚表替换为上述的假虚表,将WinExec
的命令行参数写入虚表指针后4
字节开始的地址
Dictionary
对象所对应BSTR
的type
设为0x09
,使之成为一个对象(VT_DISPATCH
)fake_dict.Exists
,使控制流导向WinExec
函数,命令行参数在步骤2
中已经构造好这个过程的示例代码如下:
这个漏洞利用在任意地址写上有一些受限条件,@elli0tn0phacker
已在他的报告中提到,这里也不再重复叙述。
这里提一个笔者编写利用时遇到的问题,笔者一开始是在windows7 sp1 x86
环境下写的利用,代码全部写完后发现计算器无法弹出,一番调试后发现,传入WinExec
函数的命令行参数无法得到正常解释,原因也很简单,来看一下某次win7
调试时最终传给WinExec
的参数:
出于利用构造的约束条件,命令行参数的前4
个字符是由前面伪造的虚表的地址解释而来,这种情况下很容易造成前4
个字符里面有多余字符,因此WinExec
也就不能按预期执行后续的命令行。笔者一开始想到的将虚表伪造到0x20202020
这个地址,这样命令行参数的前4
个字符可以被解释为空格,不会影响整个命令行的解释。但该漏洞中对指定地址的连续写是受限的,笔者最终放弃了这个思路。
后来笔者将未加修改的exploit
在win10
环境试了一下,发现计算器可以成功弹出,以下为某次在win10
下调试得到的参数及伪造的虚函数表:
笔者推测win10
和win7
下进程创建相关函数对命令行参数的处理存在一些差异,win10
上的容错性更高一点。
最终,笔者成功在windows 10 1709 x86
系统的2019
年8
月全补丁环境上弹出一个计算器:
《From BinDiff to Zero-Day: A Proof of Concept Exploiting CVE-2019-1208 in Internet Explorer》
《RCE WITHOUT NATIVE CODE: EXPLOITATION OF A WRITE-WHAT-WHERE IN INTERNET EXPLORER》
[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com
最后于 12小时前 被银雁冰编辑 ,原因: