首页
社区
课程
招聘
[原创]CVE-2019-1208漏洞调试笔记
2019-11-29 22:52 7386

[原创]CVE-2019-1208漏洞调试笔记

2019-11-29 22:52
7386

CVE-2019-1208漏洞调试笔记

目录

前言

这篇文章之前已经在其他地方发过,这里再发一个格式更友好一点的版本,便于查阅。一并修改了之前的几处笔误。由于国家相关征求意见稿的要求,此类分析文章后面就不太方便公开发布了。

 

CVE-2019-1208是趋势科技的@elli0tn0phacker在今年6月发现的一个vbscript漏洞,报告中提到这个漏洞是通过补丁比对发现的,这引起了笔者的兴趣。最近,笔者花了一些时间对该漏洞进行了比较详细的研究。在这篇文章中,笔者将从漏洞成因、修复方案、利用编写三个方面对该漏洞进行介绍。

 

读者将会看到,代码开发者是如何在修复旧漏洞时不经意间引入新漏洞。在这个例子中,引入的还是一个非常严重的远程代码执行漏洞。通过这个例子读者也会发现,有时候通过补丁比对就可以发现新漏洞。

 

该漏洞从20196月更新被引入,到20199月更新被修复,只存活了短短3个月,因此编写这个漏洞的利用并无价值,笔者写这个漏洞的利用只是为了概念验证。

 

尽管微软已经在2019年8月的IE更新中全面禁用了vbscript,但出于安全性考虑,完整利用代码不予公开。

漏洞成因

这是一个vbscriptUAF(Use After Free)漏洞,漏洞成因还要从微软今年6月的补丁说起。

6月补丁分析

微软在20196月的vbscript更新中引入了下面几个函数:

  • SafeArrayAddRef
  • SafeArrayReleaseData
  • SafeArrayReleaseDescriptor

引入SafeArrayAddRef的作用是为SafeArray提供一种类似引用计数的机制。

 

源码中通过使用STLmap将一些对象/数据指针(如pSafeArraypvData)与一个int型的计数器进行绑定。

 

VbsFilterVbsJoin这两个函数中,在调用实际的rtJoinrtFilter前,会调用SafeArrayAddRef对相关指针的引用计数+1。调用完毕后,再调用SafeArrayReleaseDataSafeArrayReleaseDescriptormap中将指针对应的计数-1,并将指针所对应的keymap中删除。

 

开发者应该是用这种方式修复了一些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就会容易多了。

PoC分析

@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对象+0x280x2C处的值进行偏移运算。调试时发现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传入的数组前,SafeArrayLockpSafeArr->cLocks0变为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的报告,相关步骤实现起来非常简单,这里不再重复叙述。

Bypass ASLR

在前面的基础上,就可以泄露一个指针对象以绕过ASLR,这里笔者采用的方法和和CVE-2019-0752一样,泄露一个Scripting.Dictionary对象的虚表指针,具体操作如下:

 

虚函数劫持

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对象所对应BSTRtype设为0x09,使之成为一个对象(VT_DISPATCH)
  4. 调用fake_dict.Exists,使控制流导向WinExec函数,命令行参数在步骤2中已经构造好

这个过程的示例代码如下:

 

利用约束

这个漏洞利用在任意地址写上有一些受限条件,@elli0tn0phacker已在他的报告中提到,这里也不再重复叙述。

 

这里提一个笔者编写利用时遇到的问题,笔者一开始是在windows7 sp1 x86环境下写的利用,代码全部写完后发现计算器无法弹出,一番调试后发现,传入WinExec函数的命令行参数无法得到正常解释,原因也很简单,来看一下某次win7调试时最终传给WinExec的参数:

 

 

出于利用构造的约束条件,命令行参数的前4个字符是由前面伪造的虚表的地址解释而来,这种情况下很容易造成前4个字符里面有多余字符,因此WinExec也就不能按预期执行后续的命令行。笔者一开始想到的将虚表伪造到0x20202020这个地址,这样命令行参数的前4个字符可以被解释为空格,不会影响整个命令行的解释。但该漏洞中对指定地址的连续写是受限的,笔者最终放弃了这个思路。

 

后来笔者将未加修改的exploitwin10环境试了一下,发现计算器可以成功弹出,以下为某次在win10下调试得到的参数及伪造的虚函数表:

 

 

笔者推测win10win7下进程创建相关函数对命令行参数的处理存在一些差异,win10上的容错性更高一点。

代码执行

最终,笔者成功在windows 10 1709 x86系统的20198月全补丁环境上弹出一个计算器:

 

参考资料

《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》


[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2019-11-29 22:55 被银雁冰编辑 ,原因:
收藏
点赞5
打赏
分享
最新回复 (6)
雪    币: 16156
活跃值: (5966)
能力值: ( LV13,RANK:861 )
在线值:
发帖
回帖
粉丝
大帅锅 4 2019-11-30 15:57
2
0
牛逼
雪    币: 3113
活跃值: (2487)
能力值: ( LV8,RANK:147 )
在线值:
发帖
回帖
粉丝
Roger 1 2019-11-30 17:48
3
0
厉害!
雪    币: 12129
活跃值: (15560)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2019-11-30 20:33
4
0
mark,楼主辛苦了
雪    币: 2049
活跃值: (74)
能力值: ( LV3,RANK:31 )
在线值:
发帖
回帖
粉丝
Littlebirds 2019-11-30 23:52
5
0
tql啦
雪    币: 12143
活跃值: (3393)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
zeroghost 1 2019-12-3 18:52
6
0
tql,话说dalao以后就不在看雪发文章了嘛
雪    币: 3842
活跃值: (1830)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
音货得福 1 2019-12-11 11:28
7
0
看起来很棒, 厉害
游客
登录 | 注册 方可回帖
返回