CVE-2013-3897 UAF Analysis

前几天面试管家,让我分析一个IE10以上的漏洞并给出详细报告,我挑了个CVE-2014-0322。从前都是从崩溃点之后开始分析ShellCode,这次把前前后后的细节都看了一遍,受益匪浅。趁着有感觉,今天又看了一下CVE-2013-3897,发现出错的逻辑是一样的,均和AddRef有关。POC来自这里

分析的目标是Windows XP Professional SP3下IE8的mshtml.dll文件,详细信息如下:

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

这里只记录一下崩溃点之前对象的LifeTime,这也是UAF的主要原因。

IE内部对对象的管理,几乎都使用了这种方法。摘录一段:

Using a reference count to manage an object’s lifetime allows multiple clients to obtain and release access to a single object without having to coordinate with one another in managing the object’s lifetime. As long as the client object conforms to certain rules of use, the object, in effect, provides this management. These rules specify how to manage references between objects. (COM does not specify internal implementations of objects, although these rules are a reasonable starting point for a policy within an object.)

AddRef和Release的功能不言而喻,类似于智能指针,这俩函数用于维护引用计数。当有函数引用对象的时候,应该调用AddRef增加计数,在用完之后调用Release减少计数。而当对象内部计数为0的时候,Release内部会自动释放掉对象。

这个设计的初衷是好的,但是手动档实在不好,程序员在使用的时候经常会手贱多加一个AddRef或者遗漏Release。忘记AddRef可能导致UAF,忘记Release则导致内存泄漏。

通过POC的崩溃点可以很容易的知道,被UAF的是一个CDisplayPointer对象,在进入CDisplayPointer::ScrollIntoView的时候,该对象还是好的。

.text:638CAAE6 ; public: virtual long __stdcall CDisplayPointer::ScrollIntoView(void)
.text:638CAAE6 ?ScrollIntoView@CDisplayPointer@@UAGJXZ proc near
.text:638CAAE6                                         ; DATA XREF: .text:636399D8o
.text:638CAAE6
.text:638CAAE6 arg_0           = dword ptr  8
.text:638CAAE6
.text:638CAAE6                 mov     edi, edi
.text:638CAAE8                 push    ebp
.text:638CAAE9                 mov     ebp, esp
.text:638CAAEB                 mov     eax, [ebp+arg_0]
.text:638CAAEE                 push    dword ptr [eax+44h] ; int
.text:638CAAF1                 mov     ecx, [eax+20h]
.text:638CAAF4                 lea     edx, [eax+8]    ; edx => 即将被UAF的CDisplayPointer对象,此时对象还没有被释放。
.text:638CAAF7                 add     eax, 1Ch
.text:638CAAFA                 neg     edx
.text:638CAAFC                 sbb     edx, edx
.text:638CAAFE                 and     edx, eax
.text:638CAB00                 push    edx             ; struct IUnknown *
.text:638CAB01                 push    ecx             ; int
.text:638CAB02                 call    ?ScrollPointerIntoView@CDoc@@QAEJPAUIMarkupPointer@@HW4_POINTER_SCROLLPIN@@@Z ; CDoc::ScrollPointerIntoView(IMarkupPointer *,int,_POINTER_SCROLLPIN)
.text:638CAB07                 pop     ebp
.text:638CAB08                 retn    4
.text:638CAB08 ?ScrollIntoView@CDisplayPointer@@UAGJXZ endp

之后的流程会往CDoc::ScrollPointerIntoView走,走了很多很多代码之后,最后会到这里:

.text:6375B6AB ; public: virtual void __thiscall CSelectTracker::Destroy(void)
.text:6375B6AB ?Destroy@CSelectTracker@@UAEXXZ proc near ; DATA XREF: .text:6363D008o
.text:6375B6AB
.text:6375B6AB ; FUNCTION CHUNK AT .text:6385D723 SIZE 00000047 BYTES
.text:6375B6AB
.text:6375B6AB                 mov     edi, edi
.text:6375B6AD                 push    ebx
.text:6375B6AE                 push    esi
.text:6375B6AF                 mov     esi, ecx
.text:6375B6B1                 lea     eax, [esi+44h]  ; esi + 44 => CDisplayPointer.
.text:6375B6B4                 call    ?ClearInterfaceFn@@YGXPAPAUIUnknown@@@Z ; ClearInterfaceFn(IUnknown * *) ; Free!

ClearInterfaceFn将调用这个CDisplayPointer的Release函数,减少引用计数,并释放该对象。之后的代码,还会继续使用该对象,导致了UAF。

到这里,可以判断,CDisplayPointer的引用计数一定出了问题,至于是在CSelectTracker::Destroy手贱多写了一句,还是在CDisplayPointer::ScrollIntoView之前就忘记AddRef了,不得而知。
对于这种情况,只能通过补丁后的代码进行对比,分析是什么地方出了问题。用于进行对比的文件信息如下:

文件名:mshtml.dll
文件版本:8.00.6001.23569
MD5:427C63C2075ABF62FAA897BBD3DE44F4
SHA1:8F45A10A6F040512B0DC694E51C9C38A64A59E91

首先对比CSelectTracker::Destroy,看看是否手贱:

.text:3C6CF1A5 ; public: virtual void __thiscall CSelectTracker::Destroy(void)
.text:3C6CF1A5 ?Destroy@CSelectTracker@@UAEXXZ proc near ; DATA XREF: .text:3C5EA020o
.text:3C6CF1A5
.text:3C6CF1A5 ; FUNCTION CHUNK AT .text:3C7DFC5F SIZE 00000047 BYTES
.text:3C6CF1A5
.text:3C6CF1A5                 mov     edi, edi
.text:3C6CF1A7                 push    ebx
.text:3C6CF1A8                 push    esi
.text:3C6CF1A9                 mov     esi, ecx
.text:3C6CF1AB                 lea     eax, [esi+44h]
.text:3C6CF1AE                 call    ?ClearInterfaceFn@@YGXPAPAUIUnknown@@@Z ; ClearInterfaceFn(IUnknown * *)
.text:3C6CF1B3                 lea     eax, [esi+48h]

没有问题。那肯定就是在CDisplayPointer::ScrollIntoView之前忘记了AddRef。在CDisplayPointer::ScrollIntoView下断点观察之后,发现补丁后的版本,在进入CDisplayPointer::ScrollIntoView的时候,该CDisplayPointer的引用计数为2。所以,目前已经十分明显,在CDisplayPointer::ScrollIntoView之前的代码中,某处对CDisplayPointer的操作忘记了AddRef。

要找到这个地方只能通过补丁后的版本来跟踪。跟踪的方法和简单:首先,找个地方停下来,拍个快照,此举是为了保证堆地址不会变化;然后记录所有CDisplayPointer的构造函数,对比进入CDisplayPointer::ScrollIntoView时候的CDisplayPointer对象,得到我们需要的CDisplayPointer对象地址;接下来恢复快照,对这个对象+4的地方进行内存写入操作监视,对于IE的大部分对象来说,+4偏移处的含义是该对象的引用计数。在进入CDisplayPointer::ScrollIntoView之前的最后一次AddRef,也便是原来忘记添加的AddRef。

以下代码说明了这个补丁的原因以及修补方法。8.00.6001.18702中CDisplayPointer对象的虚表:

.text:63639990 ; const CDisplayPointer::`vftable'
.text:63639990 ??_7CDisplayPointer@@6B@ dd offset ?QueryInterface@CDisplayPointer@@UAGJABU_GUID@@PAPAX@Z
.text:63639990                                         ; DATA XREF: CDisplayPointer::~CDisplayPointer(void)+6o
.text:63639990                                         ; CDisplayPointer::CDisplayPointer(CDoc *)+Bo
.text:63639990                                         ; CDisplayPointer::QueryInterface(_GUID const &,void * *)
.text:63639994                 dd offset ?AddRef@CBaseEnum@@UAGKXZ ; CBaseEnum::AddRef(void)
.text:63639998                 dd offset ?Release@CDisplayPointer@@UAGKXZ ; CDisplayPointer::Release(void)

对应的代码片段:

.text:639D1135                 call    ?Select@CSelectionManager@@QAEJPAUIMarkupPointer@@0W4_SELECTION_TYPE@@PAH@Z ; CSelectionManager::Select(IMarkupPointer *,IMarkupPointer *,_SELECTION_TYPE,int *)
.text:639D113A                 mov     ebx, eax
.text:639D113C                 test    ebx, ebx
.text:639D113E                 jl      short loc_639D1161
.text:639D1140                 cmp     [ebp+var_C], 0
.text:639D1144                 jz      short loc_639D1161
.text:639D1146                 mov     eax, [esi+48h]
.text:639D1149                 mov     eax, [eax+50h]
.text:639D114C                 test    eax, eax
.text:639D114E                 jz      short loc_639D1161
.text:639D1150                 cmp     dword ptr [eax+0Ch], 2
.text:639D1154                 jnz     short loc_639D1161
.text:639D1156                 mov     eax, [eax+44h]
.text:639D1159                 mov     ecx, [eax]
.text:639D115B                 push    eax
.text:639D115C                 call    dword ptr [ecx+48h]

8.00.6001.23569中CDisplayPointer对象的虚表:

.text:3C5E8080 ; const CDisplayPointer::`vftable'
.text:3C5E8080 ??_7CDisplayPointer@@6B@ dd offset ?QueryInterface@CDisplayPointer@@UAGJABU_GUID@@PAPAX@Z
.text:3C5E8080                                         ; DATA XREF: CDisplayPointer::~CDisplayPointer(void)+6o
.text:3C5E8080                                         ; CDisplayPointer::CDisplayPointer(CDoc *)+Bo
.text:3C5E8080                                         ; CDisplayPointer::QueryInterface(_GUID const &,void * *)
.text:3C5E8084                 dd offset ?AddRef@CBaseEnum@@UAGKXZ ; CBaseEnum::AddRef(void)
.text:3C5E8088                 dd offset ?Release@CDisplayPointer@@UAGKXZ ; CDisplayPointer::Release(void)

包含补丁的代码片段:

.text:3C96DC25                 call    ?Select@CSelectionManager@@QAEJPAUIMarkupPointer@@0W4_SELECTION_TYPE@@PAH@Z ; CSelectionManager::Select(IMarkupPointer *,IMarkupPointer *,_SELECTION_TYPE,int *)
.text:3C96DC2A                 mov     ebx, eax
.text:3C96DC2C                 test    ebx, ebx
.text:3C96DC2E                 jl      short loc_3C96DC61
.text:3C96DC30                 cmp     [ebp+var_C], 0
.text:3C96DC34                 jz      short loc_3C96DC61
.text:3C96DC36                 mov     eax, [esi+48h]
.text:3C96DC39                 mov     eax, [eax+50h]
.text:3C96DC3C                 test    eax, eax
.text:3C96DC3E                 jz      short loc_3C96DC61
.text:3C96DC40                 cmp     dword ptr [eax+0Ch], 2
.text:3C96DC44                 jnz     short loc_3C96DC61
.text:3C96DC46                 mov     esi, [eax+44h]
.text:3C96DC49                 test    esi, esi
.text:3C96DC4B                 jz      short loc_3C96DC53
.text:3C96DC4D                 mov     eax, [esi]
.text:3C96DC4F                 push    esi
.text:3C96DC50                 call    dword ptr [eax+4] ; !!!! Call AddRef
.text:3C96DC53
.text:3C96DC53 loc_3C96DC53:                           ; CODE XREF: CHTMLEditor::SelectRangeInternal(IMarkupPointer *,IMarkupPointer *,_SELECTION_TYPE,int)+97j
.text:3C96DC53                 mov     eax, [esi]
.text:3C96DC55                 push    esi
.text:3C96DC56                 call    dword ptr [eax+48h]

补丁后的代码在CHTMLEditor::SelectRangeInternal中增加了对AddRef的调用。可见,原来导致漏洞的原因就是因为在CHTMLEditor::SelectRangeInternal忘记对CDisplayPointer进行AddRef。如果把3C96DC4F的两句去掉,原来的POC还是会Crash掉IE,这也证明了漏洞的根源再次。至于为什么会在这,恐怕得十分熟悉mshtml才能解释了。

CVE-2013-3897 UAF Analysis》有4个想法

  1. Danny

    博主,还有一个问题,我没有明白为什么在定位CDisplayPointer引用计数问题的时候,只在CSelectTracker::Destroy调用ClearInterfaceFn前判断有没有多写Release呢?而不会在CSelectTracker::Destroy的其他位置或其他函数内调用Release?
    我对COM还不太熟,最近陆续在看微软的官方文档,ClearInterfaceFn是不是与QueryInterface对应的,用于释放对对象的引用。
    再次感谢分享~

    回复
    1. TheCjw 文章作者

      似乎我也忘记我怎么做的了。
      不过这样做是有缺陷的,建议使用内存断点来监视。
      上次有个哥们给我发邮件讨论了一下,我这样做有缺陷,但目前在忙别的,这块没深究了。

      回复

发表评论