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如下


  
    
  
  x

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

你可能感兴趣的:(CVE-2016-0003 Microsoft Edge TextData Type Confusion Information Disclosure Vulnerability)