导语:2018年8月15日,趋势科技披露了他们发现的一起浏览器漏洞攻击事件。在检测到的攻击流量中,攻击者使用了cve-2018-8373这个漏洞来攻击IE浏览器,访问存在该漏洞的页面可能导致远程代码执行。
一、cve-2018-8373漏洞的初步分析
2018年8月15日,趋势科技披露了他们发现的一起浏览器漏洞攻击事件。在检测到的攻击流量中,攻击者使用了cve-2018-8373这个漏洞来攻击IE浏览器,访问存在该漏洞的页面可能导致远程代码执行。有经验的研究员应该了解,在大部分的EK工具包中,针对IE浏览器的漏洞经常被用来做网页挂马,国外通常称为“Drive-By-Download”攻击。加上该漏洞使用的混淆方式和之前披露的cve-2018-8174类似,漏洞利用代码估计是同一批人所编写的。而趋势科技在报告中提到他们通过启发式分析检测到了该流量,目前来说主流的浏览器漏洞检测方式分为静态和动态两类,静态检测是匹配其触发漏洞以及漏洞利用的关键代码特征,动态检测则是检测浏览器的敏感行为,当然也有利用机器学习对恶意网页进行分类、检测的技术,不再详述。接下来先简单分析下cve-2018-8373。
由于没有拿到攻击样本,只能依据简单的poc来进行分析、复现。poc的代码如下:
接下来我们有两种调试的方式,第一种是利用windbg加载IE浏览器,并通过开启页堆来跟踪漏洞的触发点。第二种是将其中的vbscript代码提取出来,写入到vbs脚本文件中,利用wscript.exe程序加载vbs脚本,再进行分析。由于该demo并没有使用到document相关的api,加上使用wscript.exe调试起来相对简单点,所以这里使用第二种方法进行调试,接下来通过调试来看下poc中对应的二进制程序的代码实现。在win环境下,由vbscript.dll对vbs脚本进行解释执行,通过ida打开vbscript.dll并且加载符号,可以看到一些意义性较强的函数,大致能根据这些函数名称来定位具体的vbs脚本中的代码。
在poc中,将MyClass通过new进行创建并赋值给指定变量cls,该操作首先会触发类的创建以及初始化,创建类的函数由vbscript!VBScriptClass::Create函数完成,在初始化类的各项属性后,将该class封装为一个variant变量,class的创建如下:
最后会将该class封装成一个variant类型的变量
在微软的官网以及windows kits的头文件中均可以查到variant types的定义,部分如下:
针对不同类型的variant变量解释如下:
创建类成功后则会调用vbscript!VBScriptClass::InitializeClass函数对class的内容进行初始化 :
在vbscript!VAR::IsFunction函数中获取class指针
随后调用class的虚函数vbscript!CScriptEntryPoint::Call 进行初始化,最终的调用栈如下:
调试过该vbs漏洞的研究人员应该了解,vbscript!CScriptRunTime::RunNoEH负责对编译后的vbs代码进行解释执行。这里执行类的初始化操作,主要包含了array数组的定义以及Class_Initialize函数的执行。vbscript中创建数组的函数为vbscript!MakeArray,如下:
其中参数a1代表传入的数组的维度个数,由于poc中定义的数组维度为空,因此a1为0,因此直接返回0,array数组并没有构造成功。
随后在函数Initialize_Class中,利用Redim函数将array数组重新定义为一个数组长度为3的一维数组。但由于上次创建数组失败,其实这里并没有如同趋势科技中的报告所说,调用vbscript!RedimPreserveArray函数来对数组进行重新分配,而是再次调用了vbscript!MakeArray这个函数创建了一个数组。在调试过程中分别对vbscript!MakeArray以及vbscript!VBScriptClass::InitializeClass下断点,会发现先命中MakeArray后命中InitializeClass,紧接着再次命中MakeArray。
vbscript中的数组类型为tagSafeArray,结构如下:
内存中的结构如下:
看接下来的poc:
该段代码执行时会先后调用vbscript!AccessArray、vbscript!AssignVar函数来获取cls.array(2)的具体地址。在AccessArray函数会对cls数组进行检测、访问并获取cls第二个数组的地址,随后赋值给第二个参数,并在vbscript!AssignVar函数作为参数传入,在AssignVar函数中会将该地址赋值给esi,并在函数退出前将数据复制到该地址的16个字节。漏洞的触发点就在这里,函数在开始就确认了需要进行赋值操作的数据地址,但是在获取源操作数(这里指cls类)时,发生了对被赋值地址的释放操作,并且也没有对该地址进行检查,最终导致了释放后重用。在获取源操作数时,会将cls类的variant变量传入并进行检测,当检测到源操作数类型为0x9时,会调用vbscript!VAR::InvokeByDispID函数来获取cls的对应值。在一般情况下,是无法将一个类的地址信息传递给一个数组的,vbscript并不支持这么做,会报如下错误:
但是由于MyClass定义了Public Default Property Get P,因此实际上会将P的值作为cls的返回值返回给目标,类似的代码在cve-2018-8174中同样被使用,因此在考虑检测漏洞触发代码时可以将它作为关键的检测因素。
在vbscript!assignvar函数中对cls访问时会调用到如下函数,并将P的值返回给栈上的第4个参数。
接下来会调用下面的代码:
该段代码会触发vbscript!RedimPreserveArray函数来对array数组进行重新赋值,代码如下:
主要的实现在oleaut32!SafeArrayRedim函数中。当申请的数组的长度超过原有的数组长度,会申请新的内存来覆盖原有的safearray中的pvdata指针,并修改safearray的rgsabound区。整个调用堆栈如下:
随后通过vbscript!AssignVar函数获取P的数据,并返回给指定参数,可以看出其调用堆栈是一致的。
当从InvokeByDispID返回到AssignVar时,会将P的值拷贝到原来的cls.array(2)地址处。
但此时该处的地址已经被释放,
这就造成了一个uaf漏洞。该漏洞的利用方式是可以向被释放的内存写入指定数据,即对应数组pvdata+数组偏移+0x8的位置,刚好该释放的堆块的大小为0x30,而一个二维的tagSafeArray格式的数组的大小同样为0x30,趋势科技的分析报告中的样本通过构造大量的二维数组以占用被释放的内存,而本身二维数组的长度是由两个维度长度的乘积决定的,而该通过该漏洞则可以伪造二维数组中第一维度的长度为任意值。这样有了一个伪造的超长二维数组,由于堆块头部的存在,导致后续申请的二维数组存在8字节的错位,导致可以修改伪造的二维数组的头4个字节,这样相当于可以控制一个variant变量的类型以及具体数据,随后将伪造的长度为0x7ffffff的一维tagSafeArray数组通过字符串的形式写入,并更改其变量类型为VT_ARRAY|VT_VARIANT,可以伪造一个首地址为0,长度为0x7ffffff的一维数组,通过该数组实现全局读写,详细的利用方式可以参考看雪论坛上的两篇文章。
二、漏洞触发流程的思考以及漏洞检测
思考本次漏洞触发的核心流程,关键就在于vbscript中array数组的创建、赋值、重构,本次的漏洞触发中所利用到的关键函数如下图:
这里不禁一个疑问,为何需要调用两次makearray来创建数组,直接在定义数组时创建一个定长数组不就省下了一部分漏洞触发的代码吗?带着这样的疑问,重新修改了poc,并删除了Initialize_Class函数的内容,如下:
结果返回一个运行时错误:
为了了解该错误的原因,对array数组的创建进行了跟踪调试,在poc中,数组的名称为array,在调试过程中发现通过vbscript!VbscriptClass::CreateVar函数创建了名为array的变量:
随后调用vbscript!MakeArray创建了array的tagsafearray结构以及对应的数组空间,并将结构的地址写入到array变量+0xc的偏移处,代码如下:
最终的结果如下图:
在调用vbscript!RedimPreserveArray过程中,会对传入的数组类型进行检测,具体代码如下:
当检测到array数组的feature属性&0x10的结果为1时,反回0x8002000D错误。可以看到通过Dim array(2)定义的tagsafearray结构的feature属性值为0x892,查看微软对feature的解释如下:
0x892的值为0x880|0x12得来,具体代码如下:
当数组成功创建后,会将其feature属性值同0x12进行逻辑或,结果就会创建一个静态数组,而静态数组是不可以被重新修改其数组大小的,该类型的数组也就不存在释放后重用漏洞。而通过Redim array()创建的是一个动态的tagSafeArray结构体数组,初始化的结果就是array变量+0xc处的数据为0,第一次调用Redim函数的时候,会先从vbscript!vbscriptclass::createvar创建的array变量+8的地方获取到array数组地址,经过判断后array数组的地址为0,最终调用了vbscript!MakeArray函数来创建一个新的数组,具体的代码逻辑如下:
这里同样思考另外一个问题,在漏洞触发过程的第二步仅仅是对array数组的重新创建,是否必须要利用到Initialize_Class该函数来执行,答案当然是非必需的,这里仅需要让array在被vbscript!RedimPreserveArray函数调用前具有长度为3的数组即可,因此可以构造如下的利用demo,也可以实现相同的效果。
这样在考虑对该漏洞进行静态检测时,在不考虑样本被混淆、加密的情况下,首先应该是对Public Default Property Get以及Redim、Redim Preserve等关键字进行检测,至于Initialize_Class关键字则不是必要检测字段,如果做动态检测,则可以对如vbscript!MakeArray、vbscript!RedimPreserveArray、vbscript!AssignVar等函数hook,对数组的创建、重新分配、赋值等操作做检测。当然这里仅仅是对漏洞触发的部分代码进行检测,其实针对漏洞利用的代码如heap spary、rop、RW对象等进行检测会更加有效。