×

IE DOM 树概览

96
o_0xF2B8F2B8
2017.06.14 15:31* 字数 3658

[TOC]

一、DOM 流简介

DOM (Document Object Model)作为现代浏览器的基础,其设计和实现方式影响着整个浏览器的表现。对安全研究者而言,了解DOM 的结构更是有着特殊的意义。在对 DOM 结构有了了解之后进行相关漏洞的分析,分析工作将会有一个清晰的脉络和方向,使得分析工作变得更加简单,对漏洞的理解也会更加深刻和具体。

IE 作为一度占据很大市场份额的主流浏览器其 DOM 的组织和结构值得我们去研究一番。IE 浏览器在刚刚推出时还只是实现简单的文本解析功能,随着页面功能的丰富和各种新特性的加入,IE 浏览器的功能也变得越来越复杂,但是作为浏览器骨架的 DOM 结构却仍然延续了最初的设计,微软尝试通过不断添加新的属性以优化其表现使其适应现代浏览器的功能需要。

出于以上的历史原因,IE 浏览器的 DOM 结构并不是一个真正意义上的树结构,而是被设计成为一个流结构,页面内容解析出的节点在逻辑上以线性形式排列,对于 DOM 的访问和修改均以流的方式进行。微软在有关Markup 的设计文档中描述了这一特点 。

以如下 Html 文档举例

My <B>dog</B> has fleas.

按照树形结构进行构建,上述页面会被解析成如下的一棵树

              ROOT
                |
          +-----+------+
          |     |      |
         "My"   B  "has fleas."
                |
              "dog"

但是如果将上述页面进行修改变成

Where do <B>you <I>want to</B> go</I> today?

在这个页面中 <B> 标签和 <I> 标签相互嵌套,这样一来页面便无法被简单的表示成为树结构,此时便选择使用流来表示页面,并通过范围操作操作页面内容。

本文将通过对 IE 8 版本浏览器进行分析和逆向,阐述 IE 中 DOM 的结构,以及这种结构如何在浏览器中发挥作用。(为了方便描述下文统一将 Html 文档中的标签成为 tag,而将由 tag 解析而的到的对象称为 Element。)

二、DOM 流的结构

在 IE 的 DOM 流中,页面元素 Element 并不直接相关联,而是间接通过一个叫做 CTreeNode 的类来完成这一功能。CTreeNode 类结构如下(本文中所列出的数据结构和函数均根据 IE 8 逆向而来,IE9、IE10、IE11 中有所不同, 但是核心思想均是相同的)

class CTreeNode
{
public: 
    CElement * element;
    CTreeNode * parent;
    BYTE        _etag;                              // 0-7:     element tag
    BYTE        _fFirstCommonAncestorNode : 1;    // 8:       for finding common ancestor
    BYTE        _fInMarkup : 1;    // 9:       this node is in a markup and shouldn't die
    BYTE        _fInMarkupDestruction : 1;    // 10:      Used by CMarkup::DestroySplayTree
    BYTE        _fHasLookasidePtr : 2;    // 11-12    Lookaside flags
    BYTE        _fBlockNess : 1;    // 13:      Cached from format -- valid if _iFF != -1
    BYTE        _fHasLayout : 1;    // 14:      Cached from format -- valid if _iFF != -1
    BYTE        _fUnused : 1;    // 15:      Unused
    SHORT       _iPF;                               // 16-31:   Paragraph Format
                                                // DWORD 2
    SHORT       _iCF;                               // 0-15:    Char Format
    SHORT       _iFF;

    CTreePos    _tpBegin;
    CTreePos    _tpEnd;
    DWORD      unknow1;
    DWORD      unknow2;
    DWORD      unknow3;
};

每个 CTreeNode 都与一个 Element 相关联。一个 CTreeNode 中包含了两个 CTreePos 结构,分别在逻辑上代表着一个 Element 对应的 tag 的头标签和尾标签,这些 CTreePos 按照 tag 在 html 文档中的排布顺序依次前后相连,DOM 流即是这些 CTreePos 对象链接而成的双向链表。

CTreePos 对象结构如下,其中 _pLeft_pRight 用于构建 DOM 流;_cchLeft 表示当前 CTreePos 代表的 tag 在页面中左边有多少个字符,可以看做 CTreePos 在 DOM 流中的 index(这里需要经过计算)。

class CTreePos
{
public:
    DWORD       _cElemLeftAndFlags; 
    DWORD       _cchLeft;           // 左子树中的字符数量
    CTreePos*   _pFirstChild;      
    CTreePos*   _pNext;           
    CTreePos*   _pLeft;              //  当前 CTreePos 在 DOM 流中的左边
    CTreePos*   _pRight;            //  当前 CTreePos 在 DOM 流中的右边
}

以具体的例子来说明 IE 中 DOM 流的情况。考虑如下页面

<html>
    <body>
    <textarea>This is a Test</textarea>
    </body>
<html>

上述页面经过 IE 解析之后构成如下所示的 DOM 流,每个 Element 与一个 CTreeNode 相关联,CTreeNode 的两个 CTreePos 分别按照其 tag 在页面中的排布前后相连,html 的 tpBegin 与 body 的 tpBegin 之间省略的部分为页面解析时自动补全的 Head Element

 <html>   <body>    <textarea>    This is a Test   </textarea>    </body>    <html>
   |         |          |                |              |             |         |
tpBegin...tpBegin    tpBegin        DataTreePos       tpEnd        tpEnd      tpEnd
   |         |          |                               |             |         |
   |         |          ------------CTreeNode------------             |         | 
   |         |                       textarea                         |         |
   |         |                                                        |         | 
   |         ----------------------------body--------------------------         | 
   |                                                                            |
   --------------------------------------html -----------------------------------
 

三、DOM 流的遍历

为了将页面内容与解析出 DOM 流关联起来,IE 需要使用一个容器类来对 DOM 流进行管理,这个容器类称为 CMarkup 。在页面开始解析时,IE 会默认创建一个 CMarkup 用于管理解析过程中产生的一系列 DOM 节点(CTreeNode)和 DOM 操作,此后每一次针对 DOM 的操作均需要为其指定一个 CMarkup ,该次操作的 DOM 流即为这个 CMarkup 管理的 DOM。

IE 浏览器在访问页面 DOM 流时也按照流的规范进行,为此 IE 使用一个名叫 CMarkupPointer 的对象来作为 DOM 流的迭代器。CMarkupPointer 顾名思义是用于指示 CMarkup 即DOM 流中位置的指针。CMarkupPointer 并不是DOM 流的一部分,多数情况下它都是作为临时变量来使用。CMarkupPointer 可以被放置于页面的这些位置:element 的开始、element 的结束或者 text 之中。由于 CMarkupPointer 本身不包含内容,因此如果两个 CMarkupPointer 指向了同一个位置便会难以区分。CMarkupPointer 的放置工作通过 MoveAdjacentToElement 函数完成。函数原型如下

HRESULT MoveAdjacentToElement(
    CElement *elementTarget,
    ELEMENT_ADJACENCY
);

    enum ELEMENT_ADJACENCY {
         ELEMENT_ADJ_BeforeBegin
         ELEMENT_ADJ_AfterBegin
         ELEMENT_ADJ_BeforeEnd
         ELEMENT_ADJ_AfterEnd
    };

一旦 CMarkupPointer 被放置在了文档中, 浏览器便可以通过它来获取其所指向位置中 DOM 流的相关信息。这个功能使用 There 函数实现,函数原型如下

HRESULT There(
    BOOL fLeft,
    BOOL fMove,
    MARKUP_CONTEXT_TYPE pContextType,
    CElement **ppElement,
    long *plCch,
    OLE_CHAR *pch
);
  • 第一个参数指定获取指针右边还是左边的信息
  • 第二个参数指定指针是否可移动,若不可移动,则函数仅仅会返回指针周围内容的描述;否则,函数- 在返回周围内容描述的同时还会移动过去。
  • 第三个参数为返回值,返回pointer周围的内容类型。
Value Are Example
CONTEXT_TYPE_None pointer左边或者右边没有东西 [p1]<HTML></HTML>[p2]
CONTEXT_TYPE_Text pointer左边或者右边是一个text tex[p]t
CONTEXT_TYPE_EnterScope 如果是Left,则point左边是一个End tag;如果是Right,pointer的右边是一个Begin tag 。 </B>[p]<B>
CONTEXT_TYPE_ExitScope 如果是Left,则point左边是一个Begin tag;如果是Right,pointer的右边是一个End tag 。 <B>[p]</B>
CONTEXT_TYPE_NoScope pointer的左边或者右边不是一个可以成对的标签 <BR>[p]<BR>
  • 第四个参数返回 pointer 左边或者右边的element
  • 第五个参数用来限定读取的text范围,同时也用来返回获取的text 的大小
  • 第六个参数返回pointer 左边或者右边的 text

下面以具体的页面举例说明

[p1]Where [p2]<I>do </I>[p3]<B>you <BR>[p4]want</B> to go today[p5]?

对于页面上的五个 CMarkupPointer 分别调用 There()函数,调用的参数及结果如下表

Ptr Derection Type Element cch in cch out Text
p1 left None - - - -
p1 right Text - 2 2 Wh
p1 right Text - -1 6 -
p1 right Text - 345 6 Where
p2 left Text - NULL - -
p2 right EnterScope I - - -
p3 left ExitScope I - - -
p4 left NoScope BR - - -
p5 left Text I 100 12 NULL

下面通过实际的 js 操作来说明 IE 是如何通过 CMarkupPointer 来对流结构进行遍历的

previousSibling,nextSibling,firstChild,lastChild

js 中可以通过 Node 的这四个函数获取一个 Element 周围 DOM 的信息,这里以 previousSibling 作为典型来说明,previousSibling 在 IE 8 中的函数逻辑如下所示

HRESULT CElement::GetpreviousSiblingHelper(CElement *this, CElement **previousSibling)
{
  CMarkupPointer * markupPointer;
  CDoc* cDoc;
  HRESULT result;

  cDoc = CElement::Doc(this);
  CMarkupPointer::CMarkupPointer(markupPointer, cDoc);   // 创建 CMarkupPointer

  result = markupPointer->MoveAdjacentToElement( this, ELEMENT_ADJ_BeforeBegin);    // 放置 CMarkupPointer
    if ( result == S_OK )
    {
      cDoc = CElement::Doc(this);
      result = sub_74D4A0B3(cDoc , markupPointer, &nextSibling);       // 通过 CMarkupPointer 获取 Element
    }

  result = CBase::SetErrorInfo(markupPointer, result);
  CMarkupPointer::~CMarkupPointer(markupPointer);
  return result;
}

函数首先新建一个 CMarkupPointer 对象,接着将该 CMarkupPointer 放置于目标节点的 ELEMENT_ADJ_BeforeBegin 位置,而后通过该 CMarkupPointer 来检查周围的内容,这里使用 CMarkupPointer::There 函数来获取 CMarkupPointer 所指的位置的对应节点信息。逻辑上来说即 Element->_tpBegin->_pLeft->There(left)

nextSiblingfirstChildlastChild 其函数逻辑也大体相同,有所区别的即是在调用 MoveAdjacentToElement 时的传入参数,分别为 ELEMENT_ADJ_AfterEndELEMENT_ADJ_AfterBeginELEMENT_ADJ_BeforeEnd

这里也非常容易理解,以如下页面流为例子。调用 div.nextSibling ,首先使用一个 CMarkupPointer 对象指向在 div 的 ELEMENT_ADJ_AfterEnd位置,即下面页面中[ae]的位置,再调用 There 获取 [ae] 右边的内容,即为下面页面中的 <a> 标签,其他同理。

<p></p>[bb]<div>[ab]<image/><input/>[be]</div>[ae]<a></a>

childNodes

childNodes 节点属性则是通过 CMarkupPointer 遍历对应 Element 节点而实现,在 IE 8 中其主要的功能函数为 CElement::DOMEnumerateChildren ,该函数逆向后主要功能代码如下

CElement::DOMEnumerateChildren(CPtrAry<CTreeNode__> children)
{
     cDoc = CElement::Doc(this);
     CMarkupPointer::CMarkupPointer(markupPtrBegin, cDoc);   
     CMarkupPointer::CMarkupPointer(markupPtrEnd, cDoc);    
     ......
     result = markupPtrBegin->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterBegin);    // 放置 MarkupPointer
     result = markupPtrEnd->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterEnd);    // 放置 MarkupPointer
     do{
        ......
        child = markupPointer->There()
        children.Append(child); 
        result = markupPtrBegin->MoveAdjacentToElement( child, ELEMENT_ADJ_AfterBegin);    // 放置 MarkupPointer
        ......
        }while( !markupPtrBegin->isLeftOf(markupPtrEnd) )
    ......
}

通过两个 CMarkupPointer 指针分别指向 Element 的开始位置 [ab] 和结尾位置 [ae],并从其开始位置开始依次遍历 ,其间所有的节点均为 Element 的子节点。

<div>[ab]<p></p><image/><input/><a></a></div>[ae]

四、DOM 流的修改

现代浏览器中,为了更好的用户体验,页面经常需要根据不同情况动态进行变化,DOM 流也需要相应的进行修改。为了提高对于流的访问效率,IE 浏览器采用 Splay tree 来对这个流进行操作。SplayTree 虽然名义上称作 Tree,其实并不是一个真正意义上树结构,其本质是为了高效操作流结构而产生的一套改进算法。

IE 中SpalyTree 的基本数据结构为 CTreePos,用于表示流中各数据在 SplayTree 中的逻辑关系,再看一遍 CTreePos 的数据结构,其_pFirstChild_pNext 指针便是用于描述当前节点在 SplayTree 中的逻辑关系。

class CTreePos
{
public:
    DWORD       _cElemLeftAndFlags;  // 左子树中的 Begin Element 数量以及一些与 tag 有关的属性
    DWORD       _cchLeft;           // 左子树中的字符数量
    CTreePos*   _pFirstChild;      //  指向第一个子节点
    CTreePos*   _pNext;             //  若当前节点为父节点的最后一个节点,则该指针指向父节点,否则指向兄弟节点
    CTreePos*   _pLeft;
    CTreePos*   _pRight;
}

SplayTree 的主要功能既是在保证流结构顺序的情况下,使得最后访问的节点处在树的最顶层,从而提升访问效率。以如下页面举例想要在在页面[p1]、[p2] 位置插入标签 <p>

<html>[p1]<textarea></textarea>[p2]<html>

首先访问 [p1] 位置,通过 Splay 操作将 [p1] 所指节点旋转置树顶,此时 SplayTree 如下左树所示;接着访问 [p2] 位置,SplayTree 变为如下右图,此时针对DOM 的操作只需要发生在 [p1] 的右子树上即可

      p1                                 p2
    /    \                             /    \
   hh    th                           p1    he
           \                         /  \
            te            =>       hh    th
              \                            \
               p2                           te 
                 \
                  he
// hh 表示 html tag 的头结点,he表示html tag 的尾节点。依次类推

SplayTree 的核心函数 Splay() 逆向如下,部分冗余代码没有给出

void CTreePos::Splay(CTreePos * t_node)
{
    DWORD t1;
    CTreePos *v1;
    CTreePos *v2;
    CTreePos *v3;
    CTreePos *v5;
    CTreePos *v6;
    CTreePos *v7;
    // v2 = t_node->parent->parent
    v1 = t_node->Parent();
    v2 = v1->Parent();

    while (v2)   // while grandparent
    {
        v3 = v2->Parent();
        // 如果 v3 没有(当前节点已经旋转到第三层,没有曾祖父节点了)则只进行一次单旋;否则进行一次之字旋或者一字旋;
        if (v3)
        {
            t1 = t_node->_cElemLeftAndFlags ^ v1->_cElemLeftAndFlags;
            if (t1 & TPF_LEFT_CHILD)  // 如果 t_node 与其父节点形成左右、或者右左的关系, 则进行之字形旋转
            {
                //           v3                         v3                               v3
                //          /  \                       /  \                            /    \
                //         v2   E                     v2   E                          t      E
                //        /  \                       /  \                           /    \ 
                //       D    v1          =>        D    t             =>         v2      v1
                //           /  \                       /  \                     /  \    /  \
                //          t    C                     A    v1                  D    A  B    C
                //         / \                             /  \
                //        A   B                           B    C
                v1->CTreePos::RotateUp(t_node, v2);   //RotateUp(t_node<eax>,v1<ecx>,v2);
                v5 = v2;
            }
            else           // 如果 t_node 与 其父节点均为左节点、或均为右节点,则进行一字型旋转
            {
                //               v3                            v3                              v3
                //              /  \                         /    \                           /  \
                //             v2   E                       v1     E                         t    E
                //            /  \                        /    \                            / \
                //           v1   D           =>         t      v2            =>           A   v1
                //          /  \                        / \    /  \                           /  \
                //         t    C                      A   B  C    D                         B    v2
                //        / \                                                                    /  \
                //       A   B                                                                  C    D
                v2->CTreePos::RotateUp(v1, v3);      //RotateUp(v1<eax>,v2<ecx>,v3);
                v5 = v1;
            }
            v6 = v3;
        }
        else
        {
            v5 = v1;
            v6 = v2;
        }
        v5->RotateUp(t_node, v6);     //RotateUp(t_node<eax>,v5<ecx>,v6);
        // v2 = t_node->parent->parent
        v1 = t_node->Parent();
        v2 = v1->Parent();
    }
    return;
}

void CTreePos::RotateUp(CTreePos* childNode, CTreePos* parentNode)
{
    CTreePos *v1;
    CTreePos *v2;
    CTreePos *v3;
    CTreePos *v4;
    CTreePos *v5;
    CTreePos *v6;
    DWORD v7;
    DWORD v8;

    if (childNode->IsLeftChild())
    {
        // 右旋
        //        this               child
        //       /    \              /    \
        //    child     c     =>    a     this
        //    /   \                       /   \
        //   a     b                     b     c 
        //
        // 得到 childNode 的左节点 ,通过 v2 指示出来
        v1 = childNode->_pFirstChild; 
        if (v1 && v1->IsLeftChild())
            v2 = v1;
        else
            v2 = NULL
        //得到 childNode 的右节点,通过 v3 表示
        v1 = childNode->_pFirstChild;
        v3 = 0;
        if (v1)
        {
            if (v1->IsLeftChild())
            {
                // 如果其左节点有兄弟节点,则该兄弟节点为右节点
                if (!v1->IsLastChild()) 
                    v3 = v1->_pNext;
            }
            else
            {
                v3 = v1;
            }
        }
        //得到 this 的右节点,通过 v5 表示
        v4 = this->_pFirstChild;
        v5 = 0;
        if (v4)
        {
            if (v4->IsLeftChild())
            {
                // 如果其左节点有兄弟节点,则该兄弟节点为右节点
                if (!v4->IsLastChild())
                    v5 = v4->_pNext;
            }
            else
            {
                v5 = v4;
            }
        }
        //替换 this 节点和 childNode 节点的上下关系
        this->ReplaceChild(childNode, parentNode);
        // 如果 childNode 有左节点,则该节点为 childNode 的第一个子节点,且该节点的兄弟节点应为 this
        // 如果 childNode 没有左节点,则 childNode 的第一个子节点为 this
        if (v2)
        {
            v2->MarkFirst();
            v2->_pNext = this;
        }
        else
        {
            childNode->_pFirstChild = this;
        }
        // 如果 childNode 有右节点,则 this 节点的第一个节点为该节点
        // 如果 childNode 没有右节点,则 this 节点的第一个节点为其右节点
        if (v3)
        {
            this->_pFirstChild = v3;
            v3->MarkLeft();
            // 如果 this 节点也有右节点,则此节点为原 childNode 右节点的兄弟节点
            // 如果 this 节点没有右节点,则此节点变为 this 最后一节点,需要为其设置父节点指针
            if (v5)
            {
                v3->MarkFirst();
                v3->_pNext = v5;
            }
            else
            {
                v3->MarkLast();
                v3->_pNext = this;
            }
        }
        else
        {
            this->_pFirstChild = v5;
        }
        //this 节点变为 childNode 节点的右节点,也即最后一个节点,将其父节点指针设置为 childNode
        this->MarkRight();
        this->MarkLast();
        this->_pNext = childNode;
        // 调整 this 节点和 childNode 节点的 subtree num
        v7 = ((childNode->_cElemLeftAndFlags >> TPF_FLAGS_SHIFT) << TPF_FLAGS_SHIFT);   //GetElemLeft : 清除flag 位的干扰
        this->_cElemLeftAndFlags - v7;
        this->SetFlag(_cElemLeftAndFlags);
        this->_cchLeft = this->_cchLeft - childNode->_cchLeft;
        // childNode 节点
        v8 = this->_cchLeft;
        if (childNode->IsNode())  // Begin,End
        {
            if (childNode->IsData2Pos())
            {
                this->_cchLeft = v8 - 1;
                if (childNode->IsBeginNode())  // NodeBeg
                    this->SetFlag(_cElemLeftAndFlags - 0x100);   // ElemLeft 减一
            }
        }
        else if (childNode->IsText())
        {
            v8 = v8 - (childNode->GetInterNode()->_tpEnd._cchLeft) & 0x3FFFFFFF;
            this->_cchLeft = v8;
        }
        return;
    }
    else
    {
        // 左旋
        //        child                 this
        //       /     \               /    \
        //      a     this    =>    child     c
        //           /   \          /   \
        //          b     c        a     b
        //代码总体和右旋差异不大,这里不再逆向
    }
}

在通过 SplayTree 高效的实现了 DOM 流的访问之后,IE 设计了一套专门用于操作 DOM 树的机制称为 CSpliceTreeEngine,对于 DOM 流的一系列修改操作均通过它来进行。SpliceTreeInternal() 函数部分功能逆向如下

CMarkup::SpliceTreeInternal( CTreePosGap *tpg_Begin,  CTreePosGap *tpg_End, CMarkup* target, CTreePosGap *tpg_tar, DWORD opt1,DWORD *opt2)
{

  CDoc *v1;
  CSpliceTreeEngine v2;
  HRESULT result;

  v1 = this->Doc();
  v2 = CSpliceTreeEngine::CSpliceTreeEngine(v1);
  EnsureTotalOrder(tpg1, tpg2);

  result = CSpliceTreeEngine::Init(this, tpg_Begin, tpg_End, target, tpg_tar, opt1,  opt2);
   // ...
   case copy:
    {
        result = v1->CSpliceTreeEngine::RecordSplice();
        result = v1->CSpliceTreeEngine::InsertSplice();
    }
   case move:
   {
    result = v1->CSpliceTreeEngine::RecordSplice();
    result = v1->CSpliceTreeEngine::RemoveSplice();
    result = v1->CSpliceTreeEngine::InsertSplice();
    }
   // ...
  CSpliceTreeEngine::~CSpliceTreeEngine(v1);
  return result;
}

函数首先调用 RecordSplice 函数将源 DOM 流中的节点信息备份一遍,接着根据操作要求决定是否将源 DOM 流中的节点信息删除,最后将之前备份的节点信息插入目标 DOM 流中。

对 DOM 流结构进行操作还需要有一个重要的结构 CTreePosGap,该结构用于指示两个 CTreePos 之间的内容,在对流进行插入和删除操作时都需要用到 CTreePosGap 结构来指示需要操作的区间。CTreePosGap 数据结构如下所示

class CTreePosGap{
    CElement    *_pElemRestrict; // 指定 CTreePosGap 所在的Element范围
    CTreePos    *_ptpAttach;    // 指示与 CTreePosGap 相关联的 CTreePos
    unsigned    _fLeft : 1;       // 当前 Gap 是否在  CTreePos 的左边
    unsigned    _eAttach : 2;
    unsigned    _fMoveLeft : 1;
}

当然上述操作均要通过 CMarkupPointer 来作为 DOM 流的指针才能完成。

通常情况下,一个页面内容被修改之后, 页面中的 CMarkupPointer 还会保留在之前未修改时的位置。举例来说

abc[p1]defg[p2]hij
abc[p1]deXYZfg[p2]hij

当第一个页面被修改为第二个页面之后,虽然页面的内容发生了改变,但是 CMarkupPointer 的相对位置仍然保持不变。但如果页面的修改发生在 CMarkupPointer 指向的位置,如上例中,向c、d之间插入一个Z,p 的位置就会出现二义性。

abcZ[p1]de  or  abc[p1]Zde

这时就需要引用另一个重要的概念gravity,每一个 CMarkupPointer 都有一个 gravity 值标识着其左偏或右偏。仍以上述页面为例

abc[p1,right]defg[p2,left]hij 

分别在p1,p2的位置插入一对 <B> 标签。这时由于gravity的存在,页面会变成如下

abc<B>[p1,right]defg[p2,left]</B>hij 

默认情况下 CMarkupPointergravity 值是 left。下面的函数负责查看或者修改CMarkupPointergravity

enum POINTER_GRAVITY {
    POINTER_GRAVITY_Left,
    POINTER_GRAVITY_Right
};

HRESULT Gravity(
    POINTER_GRAVITY *pGravityOut
);

HRESULT SetGravity(
    POINTER_GRAVITY newGravity
);

再考虑如下例子

[p2]ab[p1]cdxy

当bc 段被移动到 xy之间时p1的位置也出现了二义性,是应该随着bc移动,还是应该继续保持在原位呢

[p2]a[p1]dxbcy or [p2]adxb[p1]cy

这就需要 cling 的存在,如果p1指定了cling 属性,那么页面操作之后就会成为右边所示的情况,否则就会出现左边所示的情况

clinggravity 可以协同作用,考虑下面的例子

a[p1]bcxy

b移动到x、y之间,如果p1指定了 cling 属性,并且 gravity 值为 right,那么p1便会跟随b一起到xy之间。这种情况下如果b被删除,那么p1也会跟着从DOM 流中移除,但并不会销毁,因为p1还有可能重新被使用。cling相关的函数,函数原型如下

HRESULT Cling(
    BOOL *pClingOut
);

HRESULT SetCling(
    BOOL NewCling
);

下面通过实际的 js 操作来说明如何对 DOM 流进行修改的

appendChild

appendChild 意为在目标 Element 的最后添加一个子节点,其内部其实是通过 InsertBefore 来实现的,

CElement::InsertBeforeHelper()
{
    cDoc = CElement::Doc(this);
     CMarkupPointer::CMarkupPointer(markupPtr, cDoc);   

      markupPointer->MoveAdjacentToElement( this, ELEMENT_ADJ_BeforeEnd); 
     CDoc::InsertElement();
}

函数首先通过 CMarkupPointer 指定到 parent 的 BeforeEnd 位置,再调用 CDoc::InsertElement() -> CMarkup::InsertElementInternal 进行实际的插入操作,一般而言标签都是成对出现的,因此这里需要使用两个 CMarkupPointer 分别指定新插入标签的 Begin 和 End 位置

HRESULT CMarkup::InsertElementInternal(CMarkup *this, int a2, struct CElement *a3, struct CTreePosGap *a4, struct CTreePosGap *a5, unsigned __int32 a6)
{
    CTreePosGap::PartitionPointers(v10, a5, a3, v7);
    CTreePosGap::PartitionPointers(v12, v11, a3, v69);
    //......
    CTreePosGap::SetAttachPreference(((*((_BYTE *)a5 + 8) & 1) == 0) + 1, (int)&v78);
    CTreePosGap::MoveTo((CTreePosGap *)&v78, v62);
    CTreePosGap::SetAttachPreference(((*((_BYTE *)v11 + 8) & 1) == 0) + 1, (int)&v78);
    CTreePosGap::MoveTo((CTreePosGap *)&v75, v16);
    //.....
    CTreePosGap::MoveTo((CTreePosGap *)&v71, v63);
    v69 = (int)CTreePosGap::Branch(v19);
    v21 = CTreePosGap::Branch(v20);
    //......
      if ( CMarkup::SearchBranchForNodeInStory(v22, v21, v62) )
        v70 = 1;
      v23 = (CTreeNode *)HeapAlloc(g_hProcessHeap, 8u, 0x4Cu);
        //......
        v25 = CTreeNode::CTreeNode(v23, v66, 0, (int)v62);
        //......
        CElement::SetMarkupPtr(v24, v62);
        CElement::PrivateEnterTree(v26);
        //......
      v27 = CTreeNode::InitBeginPos(v24, v67 == 0);
      CMarkup::Insert(v28, a3, v27);
        //......
        v30 = CTreePos::GetCpAndMarkup(v29, 0, 0);
        //......
      v34 = CTreeNode::InitEndPos(v33, v70);
      CMarkup::Insert(v35, a3, v34);
      CTreePosGap::MoveImpl(v36, (int)&v71, 0, 0);
}

函数的主要逻辑为,首先通过一系列的 CTreePosGap 操作,指定 Begin 和 End 的位置;接着新建一个 CTreeNode 并与 Element 关联。调用 CTreeNode::InitBeginPos 初始化标签对象的 BeginPos ;接着调用 CMarkup::Insert 将 BeginPos 插入 DOM 流中,同时也插入 SpalyTree 中,并调用 CTreePos::GetCpAndMarkup 获取cp 信息,更新 SpalyTree 结构,同时触发 Notify ,进行响应事件的分发。完成了 BeginPos 的操作之后,对 EndPos 也执行相同的操作,最终完成功能。

replaceNode

replaceNode 用于将 DOM 流中一个节点替换为另一个节点,其主要功能函数我这里显示不出符号表,其逆向主要功能代码如下

HRESULT sub_74D359BA(CDOMTextNode *a1, int a2, int a3, struct CMarkupPointer *a4)
{
// .....
  CMarkupPointer::CMarkupPointer(v6, v7);
  CMarkupPointer::CMarkupPointer(v8, v7);
  result = CElement::GetMarkupPtrRange(v9, (struct CMarkupPointer *)&v15, (struct CMarkupPointer *)&v16, v13);
  if ( result == SUCCESS )
    v11 = CDoc::Move(v10, (struct CMarkupPointer *)&v15, (struct CMarkupPointer *)&v16, (struct CMarkupPointer *)1, v14);
  CMarkupPointer::~CMarkupPointer((CMarkupPointer *)&v16);
  CMarkupPointer::~CMarkupPointer((CMarkupPointer *)&v15);
  return v11;
}

函数的主要逻辑为,通过两个 CMarkupPointer 指针指定需要替换的目标节点在 DOM 流中的 Begin 和 End 位置,接着调用 CDoc::Move() 函数完成功能。CDoc::Move() 则直接通过调用 CDoc::CutCopyMove 来实现

HRESULT  CDoc::CutCopyMove(CDoc *this, int a2, struct CMarkupPointer *a3, struct CMarkupPointer *a4, struct CMarkupPointer *a5, int a6, DWORD a7)
{
    //......
      CTreePosGap::MoveTo(v12, TPG_LEFT);
      CTreePosGap::MoveTo(v13, TPG_RIGHT);
      CTreePosGap::MoveTo(v14, TPG_LEFT);
    // ......
      if ( v7 )
        result = CMarkup::SpliceTreeInternal((CMarkup *)&v19,v15,(struct CTreePosGap *)&v19,(struct CTreePosGap *)&v22,*(struct CMarkup **)(v7 + 28),(struct CTreePosGap *)&v16,(int)a5,a6);
      else
        result = CMarkup::SpliceTreeInternal((CMarkup *)&v19,v15,(struct CTreePosGap *)&v19,(struct CTreePosGap *)&v22,0,0,(int)a5,a6);
 
  return result;
}

CDoc::CutCopyMove 根据传入的 CMarkupPointer 位置信息构造三个 CTreePosGap 对象,并根据调用者的要求,选择是进行 Copy 操作还是 进行 Move 操作,最终将请求传递给 CSpliceTreeEngine

五、总结

IE 的这种 DOM 流结构是由于历史原因形成的一种特殊情况,随着浏览器功能的越来越丰富,这种 DOM 组织方式出现越来越多的问题。2016 年 10 月份的补丁之后微软在 Edge 中已经抛弃了 DOM 流的设计,转而构建了一个真正意义上的 DOM 树。 关于 Edge 中 DOM 树的结构将在以后的文档中再进行讨论。

IE 中与 DOM 相关的内容还有很多,这里仅仅列出了一点微小的工作,还有很多复杂的结构需要进一步分析。

六、Reference

[1] https://msdn.microsoft.com/en-us/library/bb508514(v=vs.85).aspx

漏洞分析
Web note ad 1