CVE-2012-1875 UAF Analysis

Intro

大牛ga1ois在《关于泄漏的艺术》一文中提到CVE-2012-1875是一个可以从脚本层面直接访问的UAF。以前没分析过这种类型的UAF,这两天趁着年后不忙看了一下漏洞成因。

POC

POC来自看雪,按照惯例添加了一些代码方便跟踪整个过程:

调试的IE版本:

崩溃信息

打开POC之后崩溃信息如下:

看起来是某个CElement(实际上是CImgElement,后文会提到)的UAF。根据栈中的调用信息,发现这个CElement来自下面的代码:

635E7AA4的CElementAryCacheItem::MoveTo以及635E7AAB的CElementAryCacheItem::GetNext是通过调试得到的。可以看到,这个CElement保存在一个类型为CElementAryCacheItem的对象里。

CElementAryCacheItem&CImplPtrAry

根据调试分析,在这个版本的IE中,CElementAryCacheItem的虚表地址是6363b284:

显然这有点像一个vector。通过进一步的分析,发现是CImplPtrAry的一个子类,对象中部分偏移所代表的含义如下:

如此看来,这个UAF的原因是某个CElement被保存在一个CElementAryCacheItem对象里,但这个CElement被释放之后,并没有从这个数组中移除,之后的代码直接取出对象并使用,导致UAF。
因此,通过监视CElementAryCacheItem添加和删除对象的情况,可以得知是哪一个CElementAryCacheItem对象以及哪一个CElement出的问题。经过测试,IE中只使用了CElementAryCacheItem::AppendElement(CElement *)进行添加对象,CElementAryCacheItem创建之初并没有分配Buffer,而是在第一次Append的时候分配空间,之后每一次Append会检查是否超过了当前的容量,如果超过则分配新的空间,然后复制原始数据,并且Append新的对象,最后释放掉旧的空间,这一点上很像vector;释放的时候使用的是CElementAryCacheItem::ResetContents(void),ResetContents只是重置了个数,并没有清除掉数组Buffer中的内容。

对象跟踪

需要说明的是,通过调试可以发现,那个UAF的CElement对象实际上的类型是CImgElement,是CElement的子类。最初我是通过CElement::CElement以及CElement::~CElement进行监视,这样的Log比较多,后来通过KK的文章以及调试,确定了对象是CImgElement,所以,用于监视的WinDbg脚本应该是这样的:

分段列出Log,便于分析:

上述代码是执行x[0].fireEvent(“onMouseOver”);前的Log,可以看到,创建的CImgElement对象是00215ae8,被插入到了00226950和00225d38两个CElementAryCacheItem中。

这里是触发onMouseOver后执行crash函数中的Log,可以看到CImgElement对象00215ae8被加入到了02de91a0这个CElementAryCacheItem中。

这一段是执行testfaild.innerHTML = testfaild.innerHTML;时的Log,KK的文章中说这一段代码会导致对象被释放并且重新创建,这个Log展示的和描述相符。其中,CImgElement对象00215ae8已经被释放。
现在,00215ae8被加入到了三个CElementAryCacheItem中,分别是00226950、00225d38以及02de91a0,通过最后一段日志,可以看到发生问题的:

可以看到,出问题的对象确实是00215ae8的那个CImgElement,而由于00225d38和00226950已经reset。所以,目前可以确定,第一次进入crash函数的时候创建的CElementAryCacheItem中残留了已经释放的CImgElement。

另一个CElementAryCacheItem

然而,实际情况有些不太一样。我尝试着调试crash函数中的Append相关的代码,发现被Append到这个CElementAryCacheItem中的元素,是HTML代码中包含了相同名字的元素,在上一层代码,也就是CCollectionCache::BuildNamedArray函数中,会遍历另外一个CElementAryCacheItem,然后进行比较Id,只有Id为imgTest才会被Append。CCollectionCache::BuildNamedArray完毕之后,会返回CCollectionCache::GetDisp,然后执行到这个地方:

这段代码会分配一个新的CElementAryCacheItem,然后使用CImplPtrAry::Copy将之前监视到的那个CElementAryCacheItem复制到新的CElementAryCacheItem中,之前的那个只是一个局部变量。之后,这个CElementAryCacheItem会保存在CCollectionCache中。构造完毕的结构如下(由于是另外一个快照的,所以CImgElement的地址变成了001f8850,新的CElementAryCacheItem地址是001ebf50):

解释一下,CCollectionCache偏移0xC处,是一个数组的Buffer地址。这个数组元素的结构未知,这里延续KK文章中的描述,称之为CACHE_ITEMS。CACHE_ITEMS的大小是0x3C,总共有0xF个,发生UAF的元素保存在最后一项也就是下标为0xE。CACHE_ITEMS的偏移0x8处是一个CElementAryCacheItem结构,这个结构之前已经给出,偏移0x14处是数组的Buffer,里面保存着CImgElement和CDivElement,也就是001f8850和001d8228。内存Dump展示如下:

此时,可以通过硬件断点,跟踪这个CACHE_ITEMS的访问,最后可以发现,确实是这个数组中的CElementAryCacheItem被取出,进而读取里面已经释放的CImgElement。

成因分析

有了对CElementAryCacheItem的理解,以及对象释放后对三个包含了CElementAryCacheItem的观察,发现在CImgElement释放后,这些CElementAryCacheItem中依然保存着CImgElement的指针。不同的是,另外两个CElementAryCacheItem在使用前检查了某个标志,然后调用CElementAryCacheItem::ResetContents避免了UAF的发生。所以猜测这个UAF的原因是某处代码忘记调用CElementAryCacheItem::ResetContents所致。
为了证明这个观点,我企图通过调试补丁后的程序得到答案。然而,通过修改WinDbg脚本监视ResetContents并没有得到我想要的结果,在补丁后的IE中,并不会Reset那个CElementAryCacheItem::ResetContents。此时,可以确定是某处标志设置或检查不严格导致了这个UAF。
经过漫长的对比调试,得到了最终的结果。最后出现不同的代码位于此处:

在这里,ebx是CCollectionCache,所以eax就是CACHE_ITEMS的Buffer,而esi是0xE,也就是读取最后一个元素。此时,最后一个元素内存是这样的:

之后,会从CACHE_ITEMS的偏移0x24读出另外一个Index,再用这个Index去索引另外一个CACHE_ITEMS。可以看到偏移0x24处的值是8。也就是说,这里会用CACHE_ITEMS[0xE]和CACHE_ITEMS[0x8]的最后一个元素做对比。Patch前两个值的内容一样,均为0,跳转不会实现。而补丁后的值则不同,跳转不会实现。以下是在mshtml_8.00.6001.23580中得到的结果对比:

可见,两个的值不同,0x8的是2,0xE的是1。在补丁后的版本,也就是mshtml_8.00.6001.23580中,修改CACHE_ITEMS[0x8]标志位1,使得跳转失败,该POC还是会导致UAF。所以,可以确定,导致该UAF的原因是某处忘记设置了CACHE_ITEMS的标志。
有了这个地方,通过硬件断点即可找到补丁后的代码更新标志的地方:

而使用硬件断点监视补丁前的的程序,没有看到更新这个标志的地方。

结语

Coming soon…

参考文章

1. CVE-2012-1875:mshtml.dll Use-After-Free漏洞分析,http://bbs.pediy.com/showthread.php?t=152240
2. CVE-2012-1875:Microsoft IE SAME_ID UAF,http://zenhumany.blog.163.com/blog/static/1718066332013926101149471/

发表评论