一个补丁引发的RCE: 对CVE-2019-1208的深入剖析
前言
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
?
详细地,开发者借助RefCountMap类实现了一个“伪引用计数机制”,通过一个map
?
相关操作函数的声明如下:
RefCountMap
相识了这些知识后,回过头去明确@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, 0x25),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的地址为:
poi(pCScriptRuntime + 0x28) - 0x10*1? ?
上述剖析验证了上面临于指令作用的意料,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月更新中是怎样修复该误差的。笔者用Bidiff工具比对了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的陈诉,相关步骤实现起来很是简朴,这里不再重复叙述。
?
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工具所对应BSTR的type设为0x09,使之成为一个工具(VT_DISPATCH)
4.挪用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月全补丁情形上弹出一个盘算器:
?
参考资料
《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》