算是分析的第一个漏洞,参考了网上的很多分析,从漏洞触发往前进行逆向分析。动态调试使用 Win xp pro SP3
什么是 UAF? Use-After-Free ,即指针向一块内存,但是经过某些方式(或代码逻辑错误)使程序认为该内存是正在使用的(实际上已被释放),再次利用该内存时会发生错误。
采用网上的 POC:
// 这是代码的主要部分,具体 EXP 需要到 exploit-db 上去找(使用 Python )
<html> <head> <script> var obj, event_obj; function ev1(evt) { event_obj = document.createEventObject(evt); document.getElementById("sp1").innerHTML = ""; window.setInterval(ev2, 1); } function ev2() { var data, tmp; data = ""; tmp = unescape("%u0a0a%u0a0a"); for (var i = 0 ; i < 4 ; i++) data += tmp; for (i = 0 ; i < obj.length ; i++ ) { obj[i].data = data; } event_obj.srcElement; } obj = new Array(); event_obj = null; for (var i = 0; i < 200 ; i++ ) obj[i] = document.createElement("COMMENT"); </script> </head> <body> <span id="sp1"> <img src="aurora.gif" onload="ev1(event)"> </span> </body> </html>
1.首先需要了解这些函数作用,以及一些 HTML 的基础知识。不必太过执着于知识的细节,有一个大致的了解即可。以下相关的动态调试的截图是在一次或多次调试中出来的,地址不具有一致性。
2.简述一下这些代码的运行流程
- 先看非 </script> 部分的 html 代码,标签为 sp1,因 html 有很好的容错性,不存在 aurora.gif 也不会报错。页面载入后会触发 ev1() 函数,将事件 event 作为事件传入(一个事件拥有多个属性)。
- ev1 函数调用 createEventObject 函数为 event 事件创建事件对象,会存储一个指针指向 event 事件。接着调用 getElementById("sp1").innerHTML = "",获取 sp1 标签下的元素,并将 sp1 下的 html 元素清空。调用 window.setInterval 函数,每隔一毫秒执行一次 ev2 函数。
- ev2 函数的核心部分是 event_obj.srcElement 函数,不断的通过事件对象访问事件中的元素。而前面的部分是为了将内存空间尽可能多的填充为 0a0a0a0a ,以此获得执行自己代码的机会。此部分是漏洞利用的关键部分,对漏洞分析来说不必过分关注。
3.从漏洞触发入手,了解相关数据结构即漏洞原理(信息搜集阶段)。首先,可以将漏洞利用代码(exp)开始的几字节改为 \xCC ,获得介入的机会。漏洞暂停后使用 kb 命令,查看调用栈。
此处注意一点,因为 JS 是解释执行的,不像 C/C++ 等代码按照流程执行代码,因此顺着调用栈找回去会花费很长的时间去定位解释执行的步骤(解释执行是在一个循环里,每一次的循环和前一个循环的关联不大),不值得。因此,需要找到 GetDocPtr 函数触发漏洞的地方并进行分析。
4.使用 IDA 从 Window 符号服务器拉取符号,通过函数名称找到相应的函数(了解 C++ 虚函数的基础知识)。
此处特别注意函数名称以及变量前的名称。此函数通过传入 CElement 类的指针返回一个 CDoc 类型的指针。ecx 是 CElement 类的虚指针,此处可以看到漏洞触发是在第一个 mov 的地方(若 ecx 地址处存着大量 0a0a0a0a,则 call 会跳转到 0a0a0a0a 地址处,若此处恰好被很多 90909090 填充,则会 nop 一直到自己的代码处,实现漏洞利用)。
可以看到此处 ecx 已经为 0a0a0a0b ,原因如下:
以上是使用条件断点:bp 7e3af652 ".if(poi(poi(poi(poi(ebx+c)+18))) = 0a0a0a0a){} .else{g}" 断在函数 GS_IDispatchp 中,单步执行到触发漏洞函数的地方。
查看函数可知,this 指针是 CElement 类,因此目前可以看到漏洞的发生与 CElement 的释放有关。 在内存中查看:
8c0 处是 10h,后面刚好跟了 10h 的数据,因此可以认为这里刚好被字符串填充了,而前面提到利用漏洞是对 obj.data 进行填充。通过前面提供的条件断点(可以通过 IDA 找到传递关系链,从而往前定位),向前定位。
可以看到 GetParam 函数传入了 CEventObj 的 this 指针和 EventParam ** ,细读这个函数的代码,可以发现,GetParam 函数将 CEventObj this 指针偏移 18h 处的值放入 EventParam *(局部变量) 中,此处即可以得到 CEventObj +18h 存储着 EventParam。
静态分析,发现 GetParam 填入的值与此有关联,因此查看一下:
如果耐心调试,不断的运行并在断点处查看 01c7ee80 处的地址,几次后会出现:
那么这个漏洞的原因已经显示的差不多了,即 EventParam 被释放后, 代码没有检测到,而且还直接通过该指针访问。但这样的分析是不完整的。但还是有几个疑问
①CEeventObj 虚函数表指针后面两个四字节是什么?IE 通过什么知道何时释放内存?
②这个 POC 从开始到真正触发走了一个怎样的流程?
5.从 POC 一开始进行分析。在 IDA 或者 使用 Windbg 的 x 命令,搜索 createEventObject
对此下断点并进行结合静态分析进行调试,断住后的调用栈如下图所示:
它们都有参数是 01c60db0 ,经过查看是 CDocument 类型的虚指针,接着进行第一个函数调用 CDocument::Doc ,应该是初始化 Doc 类的,不必太多关注,步过即可。运行到 GetParam 函数,此函数与之前的流程不同:
此处的执行没有任何跳转,而在漏洞出发点附近的是经历了第一个跳转。
接着一直执行到 CEventObj::Create ,该函数中会分配内存,并调用 CEventObj 的构造函数:
在该构造函数中,会调用基类的构造函数 CBase,注意此处 MemAlloc 返回的地址 + 4 处的值:
接着将 CEventObj 的虚函数表指针放入 CEventObj 虚指针地址+ 0 处,将 CDoc 类指针放入 CEventObj 虚指针地址 + 10h 处。
退出构造函数之后,会间接调用 QueryInterface 函数,即 CEventObj 虚表的第一个函数 CEventObj::PrivateQueryInterface,在当中会调用 AddRef 增加 CEventObj 的一个引用计数,即 CEventObj +4 处的值由 1 增加到 2。因为 IE 采用 C++ 编写,里面包含了各种类的封装,所有的COM接口都继承了IUnknown,每个接口的 vtbl 中的前三个函数都是 QueryInterface、AddRef、Release(如图3-1)。这样所有COM接口都可以被当成IUnknown接口来处理。
类比于内核中的对象体,当引用计数归为零时,该对象体就没有存在的必要,会被系统清除。此处可对漏洞的理解更深入一点,即因为某种原因,CEventObj 中的 CElement 类的引用计数被归为零,但指向它的指针却被保留。
那么正常情况下,当引用计数为零,这个指针是如何清理掉的呢?
在 PlainRelease 中,若检查到引用为0,会释放内存。再进行一些操作后:
接着,若 CEventObj::Create 传入的 CMarkup* 参数后的 int 参数为 NULL 则跳转(对 EVENTPARAM 结构体进行构造):
注意 MemAlloc 函数返回的是分配的内存空间是没有清零的,还存储着别的数据:
在 EVENTPARAM::EVENTPARAM 函数中会对该地址进行覆盖,注意此处 esi 存着传入的参数 EventParam*:
此处是将原来的 EventParam 结构体复制到新的 EventParam 中。
退出函数后,将新写入的 EventParam 指针放入 CEventObj 虚指针偏移 18h 的地方。
执行完 SetAttributes 函数后会调用 CEventObj 虚表偏移 34h 的 CWindow::Document 函数,该函数返回 [ECX+28h] 处的值。取出的值为空后,会调用 CServer::UndoManager 取出 CMarkup 虚表偏移 50h 的值存入 CEventObj 虚指针偏移 28h 处。
而后,调用 CPeerEnumerator::AddRef 给 CSecurityContext 的引用计数+1,再调用 CEventObj 虚表偏移 8 的函数 CBase::PrivateRelease ,减少 CEventObj 的引用计数至 1,和之前在调用 CBase 函数之后的引用计数值相同。而增加 1 可以通过调试得知是在前面执行 mshtml!CStyleSheetRuleArray::QueryInterface 函数时增加的。
到此,CreateEventObj 函数执行完毕。简单梳理一下,此时的数据结构在内存中的值:
CImgElement 比较特殊,没有被清理掉,且就算漏洞触发时其引用计数仍然为 0xC .
其实看到这里就已经能看到问题所在了,CEventObj 因为是分配内存创建而来此时的引用计数为 1 是正常的,而内部存储着复制过来的 EventParam* 指针,即存在引用。但是我没有在这个结构体中找到对应的引用计数值,且没有定位到具体的一个释放过程。
因为这里的时间跨度太长,平常调试的时间都很零碎,要追溯、整理的东西比较杂,这里就只记录了我调试的历程,没有很精细的整理。实际上EventParam 的释放就是因为一个标志位和 aPixelFormats 引用计数清空导致,但何处引发这个标志位的改变以及如何引发就没有更进一步的分析了。
再次调试,本次对 EventParam 结构体创建后的 +4 +8 地址下读写断点(因为在找引用计数时发现这两个地址处的值会发生变化且比较可疑),会断在下图函数中:
6.释放的过程
继续单步进入跳转后的函数中,查看当前栈调用,很明显是在将 html 代码清空后引发的 Release 操作:
此时各个寄存器的值为如下:
查看内存中的寄存器地址的值:
ecx
esi:
eax:
这玩意很像数据的引用计数
继续单步,没什么重要的操作。继续运行到函数返回到下图,此处将在调用返回的函数前对 CTreeNode +9 处的一字节的第二位置为 0(后来发现其实就因为这个,感兴趣的可以自行调试,不感兴趣的可以结束阅读了,后面的重复调试只是帮助我自己厘清思路):
再次返回后,此处因为 CTreeNode 该字节处的第4位不为 0,则没有释放 CTreeNode +0 (CImgElement):
继续运行,会在下图断住:
触发断点的原因是因为一句汇编代码:mov al, [esi+9],注意这个函数,后面会再次见到:
此处比较的值相同,不跳转,函数返回后继续执行到:
此时寄存器的值:
从要执行的函数名可以知道,这是要删除其它指向 01c623e0 的内存。
在 DelLookasidePtr 中执行了 Remove 后删除了一个地方的引用(忘记截图纪念了),并且继续修改标志位(将第四位和第五位置0):
函数返回到 RemoveRef 后不会跳转,而是会执行内存释放操作,释放 CTreeNode:
接着执行到:
此时再搜索内存,查看一下哪些地方存储 CTreeNode 的结构体地址
0012e744 是我每次运行该 POC 都能看到的存储它的地址,我就对它下断点了,看看它什么时候会被释放(因为之前经过了多次调试,这个地址在漏洞触发点就会消失)。继续运行,断住后再次查看:
这是在分配块了,并且是在 mshtml 中的函数,应该是 IE 自己维护了一个可用空间内存块表。再次搜索
发现已经消失了大部分,对比前后两次搜索,找找不同。可以发现消失的地址都是小于等于 0012e744 的,而之前查看多次的栈调用可以发现,0012e744 及比它小的地址都是栈空间。因此,这些存储着 CTreeNode 指针的地址都是局部变量。
7.此时还是没有发现明显的区别,再次查看函数名中存在 CTreeNode 以及 Ref 的函数:
注意此处的 ecx+9 ,ecx 是 CTreeNode 指针,而在调用函数 GetLookasidePtr 之前会对 ecx+9 进行检测,且前面我们也能看到 GetLookasidePtr 函数中也会对此进行检测,此处不是多余的,而是为了函数的通用性。而 GetLookasidePtr 函数中的比较则更耐人寻味——ecx+9 的第4位为0,则不会 GetDocPtr 和 LookUp:
不会去查询则表明没有可以搜索的,否则会出现问题。那么是不是 ecx+9 的第四位就代表 CTreeNode 是否为空?动态调试该函数(不需要管此时的 CTreeNode 是否是触发漏洞时的),因为其第四位为1会查找下去:
接着调用 LookUp 函数找到 01c62e40 ,查看一下这是一个什么结构体:
接着退出GetLookasidePtr 函数,然后调用 mshtml!PlainAddRef (7e279115):
而在 PlainAddRef 函数中会为 aPixelFormats 结构体增加一个引用:
那么是否 CTreeNode 的释放似乎还是没有找到关键的地方,再次调试一次,注意数据之间的关联。
8.找到关联
此时再回忆一下之前调试过的 NodeRelease ,对 7e289aa3 下断点,看它是怎么释放节点的:
PlainRelease 是释放结构体的统一函数,结合寄存器值可以判断,此处减少的计数就是 aPixelFormats 的:
重新下断点(createCEventObject 断住后找到 CTreeNode +9 ,下单字节读写断点),运行后断住:
FD == 11111101 ,此处断住是因为第二位被置 0(Dead 标志位,后面 EventParam 的释放就是这个标志位),而 VoidCachedNodeInfo 函数返回后会将 [esi+4] 清空,清除了非 CImagElement 的另一个元素。与我们关心的无关,继续运行,内存读写断点断住后:
和前面一样,[esi+9] 为 0x48,第四位为1,没有清理内存,继续运行会暂停在前面提到过的地方:
此时查看栈调用,可以发现,此处的栈和
这里能正常获取到 AsidePtr 并且返回 aPixelFormats 虚指针:
跟前面一样,会对 aPixelFormats 的引用计数-1,此处直接对 01c61564 下读写断点,步过 aPixelFormats 时会断住,此处会进行跳转:
跳转后如下图所示,若不跳转则后面会执行释放到底:
调用 CElement 的 Release 函数:
进入函数后会比较两个标志位,若满足条件则不需要调用 CElement::GetMarkupPtr 直接调用 Release 函数:
CBase::PrivateRelease 函数会根据传入的 this 指针进行 - - [esi+4] 操作,若 [esi+4] 不等于 0,则函数正常返回。
函数返回后,esi 是 aPixelFormats 虚指针,因 eax 不为 0,所以会跳转:
接着会执行 CTreeNode::RemoveRef 函数:
因为前面已经调试过 RemoveRef 函数中的 DelLookasidePtr 函数,这里简单说一下流程。 首先会对 CTreeNode + 9 处的一字节的第4位进行检测,此处因为不为0,所以不会跳转,执行完 CElement::GetDocPtr ,接着用这个函数返回的值作为参数执行 CHtPvPv::Remove 函数,然后将第四位置0,然后退出。此时的值为 40
函数返回后,因为第二位在前面调用 VoidCachedNodeInfo 前置为 0 ,会将 EventParam 释放:
之后会执行:
此函数中会执行:
此处与前面不同,会在第一个判断处跳转,执行 Release 函数将计数从 4 变为 3,后正常返回:
到此,释放操作完成。总结一下:+9 处一字节的第二位和第四位对释放都有影响,不过这里的情况只涉及第二位。
修补后增加了EventParam的一个元素——引用计数
在修补后的 DLL 中搜索 ”CTreeNode:: “,可以找到一个叫 addref 的函数:
注意这条指令:lea eax, ds:0FFFFFFF8h[eax*8] ,是相对基址变址寻址方式,得到的结果是 eax = eax*8 + FFFFFFF8h
理解一下 AddRef 的汇编函数,从0开始计算:
可以看到,它其实和之前类型的引用计数不太意义,它是从第四位开始计算,每次都从第四位开始加一
而若是从1开始计算,与上相同,不过最低位一直保持为1:
再看一下 NodeRelease 函数:
只通过静态分析可以发现,应该一开始引用计数就被置为1。