前言
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::Decrement函数时,find方法找不到对应的key,函数直接返回0。这个返回结果被SafeArrayReleaseData/SafeArrayReleaseDescriptor理解为对应的指针的引用计数为0,从而将对应的SafeArray对象和数据销毁。
具体地,开发者借助RefCountMap类实现了一个“伪引用计数机制”,通过一个map将所关心的SafeArray指针与一个int型计数器绑定起来,计数值只有0、1、不存在,三种情况。
相关操作函数的声明如下:
![86e16b640861f5c7cd437dd3d7f7ea4c.png](https://img-blog.csdnimg.cn/img_convert/86e16b640861f5c7cd437dd3d7f7ea4c.png)
RefCountMap::Decrement函数的伪代码如下:
![9fdeeaeba86ff22652840d1ab5497e5c.png](https://img-blog.csdnimg.cn/img_convert/9fdeeaeba86ff22652840d1ab5497e5c.png)
了解了这些知识后,回过头去理解@elli0tn0phacker报告中的Figure 5就会容易多了。
PoC分析
@elli0tn0phacker给出的poc大致如下:
![3dd22b218947c39991da2e6dd6cdfd6c.png](https://img-blog.csdnimg.cn/img_convert/3dd22b218947c39991da2e6dd6cdfd6c.png)
由于漏洞的存在,我们知道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对象的成员变量中存储着解释所需的许多信息,比较重要的几个如下:
![049417dfd9c7bd007c32aaa85ffaf6e9.png](https://img-blog.csdnimg.cn/img_convert/049417dfd9c7bd007c32aaa85ffaf6e9.png)
借助调试插件,我们可以得到 PoC代码编译后的P-Code:
![9718acedac9d0c4b70acf89bae6a89cd.png](https://img-blog.csdnimg.cn/img_convert/9718acedac9d0c4b70acf89bae6a89cd.png)
以下是上述用到的部分指令对应的字节码(全部指令请参考Boris的插件源码):
![6534f9bf80039014cb520e0e1d3d51d9.png](https://img-blog.csdnimg.cn/img_convert/6534f9bf80039014cb520e0e1d3d51d9.png)
从P-Code中可以看出, arr(0) = 1这句对应的指令索引的是本地变量栈(OP_CallLclSt, 0x25),Call Join(arr)这句对应的指令索引的也是本地变量栈(OP_LocalAdr, 0x19),从两个指令名称中我们可以猜测arr被存储在本地变量栈上。
在IDA Pro中对vbscript!CScriptRuntime::RunNoEH进行逆向,我们来看一下上述两个指令解释分支的汇编代码:
![137270b3836b2c94a85f359021c249bc.png](https://img-blog.csdnimg.cn/img_convert/137270b3836b2c94a85f359021c249bc.png)
![4933694851c9b68c2af7b0f730f20702.png](https://img-blog.csdnimg.cn/img_convert/4933694851c9b68c2af7b0f730f20702.png)
上述两个分支都调用了CScriptRuntime::PvarLocal方法,再来看一下CScriptRuntime::PvarLocal方法的实现:
![64cadac60031a7bae10148bf280a6aa4.png](https://img-blog.csdnimg.cn/img_convert/64cadac60031a7bae10148bf280a6aa4.png)
可以看到CScriptRuntime::PvarLocal接收一个索引,并且基于CScriptRuntime对象+0x28或0x2C处的值进行偏移运算。调试时发现PoC两处对arr的操作索引均为1,所以存储arr的地址为:
poi(pCScriptRuntime + 0x28) - 0x10*1
上述分析验证了上面对于指令作用的猜想,PoC中每次使用arr变量时,都会传入对应的索引去本地变量栈中进行访问。
明白了arr的存取原理后,我们可以清晰地在调试器中观察arr的变化过程,从而理解整个UAF的过程。
笔者在开启页堆后对PoC进行了调试。我们先将断点下到OP_LocalAdr指令的解释分支,可以看到Join(arr)执行时访问到的arr,命中断点时ebx即为CScriptRuntime,调试时arr从本地变量栈(ebx+0x28)进行索引,读者请留意下图中蓝色高亮的指针,ReDim语句执行后它会发生变化。
![c945d4cd5de7dda4068b1857f155c2b2.png](https://img-blog.csdnimg.cn/img_convert/c945d4cd5de7dda4068b1857f155c2b2.png)
我们对上图中高亮数据(SafeArray指针)所在的内存下一个写入断点,观察这个位置上数据的几次变化过程。
第一次是在ReDim(OP_ArrNamReDim)执行时,对之前arr的清理阶段(OP_ArrNamReDim指令的解释流程在后面“修复方案”一节中会进一步说明。):
![c2d895ec9b4f0a1f6f9ec71ef2667ff6.png](https://img-blog.csdnimg.cn/img_convert/c2d895ec9b4f0a1f6f9ec71ef2667ff6.png)
第二次是在OP_ArrNamReDim执行时,将新创建的arr复制到本地变量栈的对应内存处,可以看到蓝色高亮处的指针已经发生变化,此时的SafeArray已经变为刚刚创建的二维数组。
![3ae481ee75dc865f10d07b659c00d133.png](https://img-blog.csdnimg.cn/img_convert/3ae481ee75dc865f10d07b659c00d133.png)
最后,我们将断点下到OP_CallLclSt的解释分支,目的是断在arr(0) = 1这句对arr的访问过程,由于“漏洞成因”所描述的设计上的问题,此时本地变量栈上的arr已经被释放:
![50dfb671551db36ede9515b5912103f0.png](https://img-blog.csdnimg.cn/img_convert/50dfb671551db36ede9515b5912103f0.png)
追踪到的释放栈回溯如下图,读者可以看到,这个不当的释放正是由于SafeArrayReleaseDescriptor传入了未在map注册的指针所导致。
![b1576b049e96b0414e82340fa0106328.png](https://img-blog.csdnimg.cn/img_convert/b1576b049e96b0414e82340fa0106328.png)
通过以上调试,读者应该可以清晰感受到整个Use After Free过程。
修复方案
清楚漏洞成因后,我们来看一下微软在9月更新中是如何修复该漏洞的。笔者用Bidiff工具比对了8月更新和9月更新两个vbscript.dll,发现在rtJoin(rtFilter均类似,下面只以rtJoin进行说明)函数中,在对数组内的元素进行操作前后,加了一对SafeArrayLock/SafeArrayUnlock函数:
![dfe101772384a4020b61494cb303a6fe.png](https://img-blog.csdnimg.cn/img_convert/dfe101772384a4020b61494cb303a6fe.png)
微软采用对SafeArray加锁的方式来修补这个由之前的补丁引入的问题。SafeArrayLock会令pSafeArr->cLocks的值+1。这样,当在安装9月补丁后再次打开PoC。由于前面的+1操作,就可以令ReDim指令无法得到正常执行,我们来看一下具体的逻辑。
这里再引述一下上面提到的P-Code,可以看到ReDim arr(1, 1)这句语句对应的P-Code如下:
![aff20512fb715358d038faf958d4c84d.png](https://img-blog.csdnimg.cn/img_convert/aff20512fb715358d038faf958d4c84d.png)
笔者在调试器中跟了一遍OP_ArrNamReDim指令(0x0A) 的执行逻辑,发现有如下几个关键点:
![0f799406356722cb309ed17ff03d1edc.png](https://img-blog.csdnimg.cn/img_convert/0f799406356722cb309ed17ff03d1edc.png)
有意思的是,调试前笔者以为这里的ReDim最终会调用oleaut32!SafeArrayRedim函数,结果并没有。
结合上述逻辑,当补丁中在操作Join传入的数组前,SafeArrayLock令pSafeArr->cLocks从0变为1,从而在执行ReDim arr(1, 1)对应的指令时,无法通过3.1.1这一步,新数组无法被创建,Join函数执行完后本地变量栈中的数组指针不会得到更新,之前的UAF问题也就无从谈起了。Filter函数的修复方案同上。
以下为上述过程中涉及到的函数调用及说明:
![edd7a94e649ca4856fe5313980893dc1.png](https://img-blog.csdnimg.cn/img_convert/edd7a94e649ca4856fe5313980893dc1.png)
这个修复方案和CVE-2016-0189的修复方案思路一致。
利用编写
@elli0tn0phacker在他的报告中已经给出了这个漏洞的exploit编写思路,但没有公布完整代码。作为概念验证,笔者亲手编写了对应的exploit,以下对部分细节进行说明。
伪造超长数组
通过触发漏洞,可以得到一块大小为0x30的空闲内存。借助堆的特性,如果在Join函数执行完后立即申请一些字符串长度为(0x30 - 4)的BSTR对象,就可以实现对被释放内存的占位。减4是因为BSTR的字符串前面还有4字节的长度域,会一并申请。
实践证明这里的操作还是比较简单的,并不需要过多的堆风水技巧,下面是一个可以成功占位的代码示例:
![87935097630716721b2e9a9d3203a595.png](https://img-blog.csdnimg.cn/img_convert/87935097630716721b2e9a9d3203a595.png)
占位后,因为笔者已经在字符串中构造了假的超长数组,当下次访问arr时,成功占位的字符串会被解释为SafeArray结构体,从而得到一个基地址为0,元素个数为0x7fffffff,元素大小为1的超长数组。
任意地址读取
这部分,以及如何构造一块可读写内存的步骤请参考@elli0tn0phacker的报告,相关步骤实现起来非常简单,这里不再重复叙述。
Bypass ASLR
在前面的基础上,就可以泄露一个指针对象以绕过ASLR,这里笔者采用的方法和和CVE-2019-0752一样,泄露一个Scripting.Dictionary对象的虚表指针,具体操作如下:
![cd985ed5eba789ca4b2c9a17ab1f58d7.png](https://img-blog.csdnimg.cn/img_convert/cd985ed5eba789ca4b2c9a17ab1f58d7.png)
虚函数劫持
若PoC要在windows 10上执行,必须要绕过CFG。笔者最终采用了@elli0tn0phacker在他报告中提到的方法,即对CVE-2019-0752的利用方式稍作改动:
1.借助BSTR复制并伪造一个假的Dictionary虚表(fake_vtable),并改写Dictionary.Exists函数指针为kernel32!WinExec,由于kernel32!WinExec是系统自带函数,因此可以绕过CFG检测
2.借助BSTR复制并伪造一个假的Dictionary对象(fake_dict),将虚表替换为上述的假虚表,将WinExec的命令行参数写入虚表指针后4字节开始的地址
3.将假的Dictionary对象所对应BSTR的type设为0x09,使之成为一个对象(VT_DISPATCH)
4.调用fake_dict.Exists,使控制流导向WinExec函数,命令行参数在步骤2中已经构造好
这个过程的示例代码如下:
![79857066504b5feb6f2775701f2b8c86.png](https://img-blog.csdnimg.cn/img_convert/79857066504b5feb6f2775701f2b8c86.png)
利用约束
这个漏洞利用在任意地址写上有一些受限条件,@elli0tn0phacker已在他的报告中提到,这里也不再重复叙述。
这里提一个笔者编写利用时遇到的问题,笔者一开始是在windows7 sp1 x86环境下写的利用,代码全部写完后发现计算器无法弹出,一番调试后发现,传入WinExec函数的命令行参数无法得到正常解释,原因也很简单,来看一下某次win7调试时最终传给WinExec的参数:
![2d998e7e6156f62498e58f9a1999feec.png](https://img-blog.csdnimg.cn/img_convert/2d998e7e6156f62498e58f9a1999feec.png)
出于利用构造的约束条件,命令行参数的前4个字符是由前面伪造的虚表的地址解释而来,这种情况下很容易造成前4个字符里面有多余字符,因此WinExec也就不能按预期执行后续的命令行。笔者一开始想到的将虚表伪造到0x20202020这个地址,这样命令行参数的前4个字符可以被解释为空格,不会影响整个命令行的解释。但该漏洞中对指定地址的连续写是受限的,笔者最终放弃了这个思路。
后来笔者将未加修改的exploit在win10环境试了一下,发现计算器可以成功弹出,以下为某次在win10下调试得到的参数及伪造的虚函数表:
![471f6a1be9dbbe522d4acbc1493e068c.png](https://img-blog.csdnimg.cn/img_convert/471f6a1be9dbbe522d4acbc1493e068c.png)
笔者推测win10和win7下进程创建相关函数对命令行参数的处理存在一些差异,win10上的容错性更高一点。
代码执行
最终,笔者成功在windows 10 1709 x86系统的2019年8月全补丁环境上弹出一个计算器:
![2d7fe61677dcb7130d6da86a1fdf9146.png](https://img-blog.csdnimg.cn/img_convert/2d7fe61677dcb7130d6da86a1fdf9146.png)
参考资料
《Delving deep into VBScript》
《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》
声明
本文仅限技术研究与讨论,严禁用于非法用途。由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,安恒信息以及文章作者不为此承担任何责任。安恒信息拥有对此文章的修改和解释权,未经允许不得修改、增减内容,不得以任何方式将其用于商业目的。
团队简介
猎影威胁分析团队是安恒安全研究院一只专注于攻防技术研究的团队,团队由一只擅长攻防技术研究、APT分析、二进制研究的年轻队伍组成。欢迎有志于安全检测、二进制研究和攻防技术研究的小伙伴加入我们团队。
招聘二进制安全研究员
职位描述:
1、在日常可疑文件分析的基础,进行数据挖掘,寻找APT攻击事件;
2、分析客户反馈的可疑文件,编写分析报告,提供解决方案等;
3、负责热门安全事件、最新漏洞的分析,编写分析报告或poc代码等
4、研究新的检测方法,维护和完善APT检测等产品策略
5、协助内部威胁分析平台建设等
职位要求:
1、熟悉windows、Linux上调试手段,能够熟练使用常用逆向分析工具(IDA、WinDbg、OD等);
2、熟悉C/C++、汇编语言,至少熟悉一门脚本编程语言,能快速完成POC代码编写;
3、熟悉病毒、木马通信原理和常用技术以及常见加密算法等;
4、熟悉安全漏洞原理,有独立文档漏洞分析能力;
5、至少1年以上逆向分析、安全研究相关工作经验,能力优先不受工作年限限制;
6、具备大数据挖掘能力,对数据极度敏感,能够快速对数据进行关联分析;
7、思路清晰,善于主动思考,有创新、能独立分析和解决问题,具有良好的沟通能力和团队合作精神;
8、有漏洞分析、病毒木马分析、Web攻防、威胁情报挖掘、反APT攻击、机器学习相关、IOT、ICS等工作经验的优先。
往期精选
围观
安全牛Matrix:安恒信息WAF技术创新、规模及影响力第一
热文
魔芋行动(Operation Ninikachu )针对韩国某大型集团的定向攻击
热文
成功登陆科创板,安恒信息开启2.0时代