CVE-2021-33742:Internet Explorer MSHTML堆越界写漏洞分析

  • 1、漏洞背景
  • 2、漏洞简介
  • 3、分析环境
    • 3.1、提取漏洞模块
    • 3.2、关闭ASLR
  • 4、漏洞复现
  • 5、Internet Explorer DOM树的结构
    • 5.1、以文本为中心的设计
    • 5.2、增加复杂性层次结构
    • 5.3、原来的DOM没有经过封装
  • 6、漏洞原理分析
    • 6.1、逆向mshtml.dll中此漏洞的相关类
      • 6.1.1、CSpliceTreeEngine
      • 6.1.2、CTreeNode
      • 6.1.3、CTreePos
      • 6.1.4、CTreeDataPos
        • 6.1.4.1、Tree::TextData
        • 6.1.4.2、CTxtPtr
    • 6.2、漏洞PoC所对应的DOM树
    • 6.3、漏洞产生的根本原因分析
  • 7、漏洞修复
  • 8、参考链接

漏洞背景

2021年07月14日Google威胁分析团队(TAG:Threat Analysis Group)发布了一篇标题为“How We Protect Users From 0-Day Attacks”的文章。这篇文章公布了2021年Google威胁分析团队发现的4个在野利用的0day漏洞的详细信息。Google Chrome中的CVE-2021-21166和CVE-2021-30551,Internet Explorer中的CVE-2021-33742和Apple Safari中的CVE-2021-1879。

2021年4月,TAG发现了一项针对亚美尼亚用户的攻击活动,该活动通过恶意的Office文档调用Internet Explorer加载远程的恶意Web页面来利用Internet Explorer渲染引擎中的一个漏洞进行攻击。该恶意文档通过使用Shell.Explorer.1 OLE对象嵌入远程ActiveX对象或通过VBA宏生成Internet Explorer进程并导航到恶意网页来实现攻击。此攻击中使用的漏洞被分配为CVE-2021-33742,并于2021年6月由Microsoft修复。

微软计划将于2022年6月停用Internet Explorer 11,用微软推出的新版本浏览器Microsoft Edge来替代它。为了兼容旧网站,Microsoft Edge内置了Internet Explorer模式。按理说,继续研究Internet Explorer漏洞,不再有较大意义,但是今年还是发生了多个Internet Explorer 0day漏洞在野利用的攻击事件,例如:CVE-2021-26411、CVE-2021-40444,所以研究Internet Explorer漏洞,还是存在一定的意义。

本文要分析的漏洞是存在于Trident渲染引擎/排版引擎中的一个漏洞。如今,在最新版的Windows11中,依旧可以看到Trident渲染引擎(mshtml.dll)和EdgeHTML渲染引擎(edgehtml.dll)的身影。Trident是Internet Explorer使用的排版引擎。它的第一个版本随着1997年10月发布的Internet Explorer 4发布,之后不断的加入新的技术并随着新版本的Internet Explorer发布。在Trident7.0(Internet Explorer 11使用)中,微软对Trident排版引擎做了重大的变动,除了加入新的技术之外,并增加了对网页标准的支持。EdgeHTML是由微软开发并用于Microsoft Edge的专有排版引擎。该排版引擎是Trident的一个分支,但EdgeHTML移除所有旧版Internet Explorer遗留下来的代码,并重写主要的代码以和其他现代浏览器的设计精神互通有无。

在Google威胁分析团队发布了上面所说的那篇文章之后,又在Google Project Zero的博客上公布了这些漏洞的细节。本文章就是对Internet Explorer中的CVE-2021-33742漏洞的分析过程的一个记录。我之前分析过老版本的Internet Explorer的漏洞,这是第一次比较正式的分析新版本Internet Explorer的漏洞,如有错误和不足之处,还望见谅。

 

漏洞简介

CVE-2021-33742是存在于Internet Explorer的Trident渲染引擎(mshtml.dll)中的一个堆越界写漏洞。这个漏洞是由于通过JavaScript使用DOM innerHTML属性对内部html元素设置内容(包含文本字符串)时触发的。通过innerHTML属性修改标签之间的内容时,会造成IE生成的DOM树/DOM流的结构发生改变,IE会调用CSpliceTreeEngine类的相关函数对IE的DOM树/DOM流的结构进行调整。当调用CSpliceTreeEngine::RemoveSplice()去删除一些DOM树/DOM流结构时,恰好这些结构中包含文本字符串时,就有可能会造成堆越界写。

分析环境

使用的环境 备注
操作系统 Windows 10 1809 Pro x64
Windows 10 Enterprise LTSC 2019 x64
版本号1:10.0.17763.864(Updated Nov 2019)
版本号2:10.0.17763.316(Updated March 2019)
调试器 WinDbg 版本号:v10.0.16299.15(x64)
反汇编器 IDA Pro 版本号:7.5
漏洞软件 Internet Explorer 版本号: 11.864.17763.0
更新版本:11.0.160(KB4525106)
漏洞模块 mshtml.dll 版本号1:11.0.17763.831(逆向)
版本号2:11.0.17763.1911(补丁前)
版本号3:11.0.17763.1999(补丁后)

提取漏洞模块

Windows 10 x64版本内置32位和64位两个版本的Internet Explorer,分别在“C:\Program Files (x86)\Internet Explorer”和“C:\Program Files\internet explorer”两个文件夹下。但是相应架构的Internet Explorer的Trident渲染引擎(mshtml.dll)位于“C:\Windows\SysWOW64\mshtml.dll”和“C:\Windows\System32\mshtml.dll”。64位操作系统能够独立运行32位和64位版本软件,“Program Files (x86)”和“SysWOW64”存放32位软件的软件模块,“Program Files”和“System32”存放64位软件的软件模块。32位软件并不能在64位系统中直接运行,所以微软设计了WoW64(Windows-on-Windows 64-bit),通过Wow64.dll、Wow64win.dll、Wow64cpu.dll三个dll文件进行32位和64位系统的切换来运行32位软件。

本次分析,我使用的是32位Internet Explorer的Trident渲染引擎(mshtml.dll),也就是“C:\Windows\SysWOW64\mshtml.dll”。

关闭ASLR

关闭了ASLR后,可以更方便的进行调试,dll模块的加载基址不会在每次调试时发生改变,造成调试障碍。Windows10是通过Windows Defender来关闭Windows缓解措施的。打开Windows Defender后,选择“应用和浏览器控制”,然后找到“Exploit Protection”,选择“Exploit Protection 设置”。注意:设置界面拥有两个选项卡,“系统设置”和“程序设置”。我们先看“系统设置”,与ASLR有关系的是“强制映像随机化(强制性ASLR)”、“随机化内存分配(自下而上ASLR)”、“高熵ASLR”,我们都将其设为关闭状态。先关闭“高熵ASLR”,然后再关闭其他两项。

“强制映像随机化(强制性ASLR)”,不管编译时是否使用“/DYNAMICBASE”编译选项进行编译,开启了“强制性ASLR”后,会对所有软件模块的加载基址进行随机化,包括未使用“/DYNAMICBASE”编译选项编译的软件模块。关于编译时是否使用了“/DYNAMICBASE”编译选项进行编译,可以使用“Detect It Easy”查看PE文件的“IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> DllCharacteristics -> IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE”标志位是否进行了设置。

“随机化内存分配(自下而上ASLR)”,开启了该选项后,当我们使用malloc()或HeapAlloc()在堆上申请内存时,得到的堆块地址将在一定程度上进行随机化。

“高熵ASLR”,这个选项需要配合“随机化内存分配(自下而上ASLR)”选项使用,开启了该选项后,会在“随机化内存分配(自下而上ASLR)”基础上,更大程度的随机化堆块的分配地址。

Picture1:Exploit_Protection_System_Setting

接下来,我们来看“程序设置”。由于Windows10可以对单独的应用程序设置缓解措施的开启或关闭,并且替换“系统设置”中的设置,造成关闭了“系统设置”中所有与ASLR相关的缓解措施后,dll模块的加载基址还是在变化。切换到“程序设置”选项卡后,找到iexplore.exe,点击编辑,将所有与ASLR有关的设置的“替代系统设置”的勾去掉。

Picture2:Exploit_Protection_Program_Setting

设置完成后,重启一下操作系统。

这样设置完后,你可能会发现,软件模块的加载基址仍然不是一个确定的值,这时,就需要使用16进制编辑器将PE文件头中的NT Headers->Optional Header->DllCharacteristics->IMAGE_DLL_CHARACTERISTICS_ DYNAMIC_BASE设置为0,用其替换原有的软件模块。这样就彻底关闭了Internet Explorer的ASLR了。这里推荐使用010Editor,借助它的Templates功能,可以很方便的修改该标志位。

Picture3:IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE

漏洞复现

我使用的是Google Project Zero的Ivan Fratric提供的PoC。

由于原始PoC过于精简,无法观察到执行效果,对我理解程序的执行流程造成了一定的障碍。所以我尝试了以下几种经过修改的PoC,用于观察执行效果。

执行效果如下:

Picture5:Result_of_Execution

我们可以得出以下结论:PoC通过HTML DOM方法document.createElement(),创建了一个“html”结点(同时创建“head”和“body”结点),并把新创建的“html”结点添加到原有的“body”结点中。然后,创建了一个Array数组并进行了初始化。最后将该数组转化为字符串,通过HTML DOM的innerHTML属性,添加到新创建的“html”结点中的“body”结点中。

原始PoC中,并未将创建的Array数组初始化,我们通过Chrome的开发者工具查看未初始化的Array数组转化为字符串后,得到的是什么。这有助于我们后面在调试PoC时,观察字符串所对应的内存数据。

Picture4:Array_Init_NotInit

可以看到,初始化后的Array数组转化成字符串后,每个元素是使用“,”分隔的。未初始化的Array数组转化成字符串后,只有一连串的“,”。其个数为Array数组元素个数减1。

经过测试,PoC2也可以成功造成Crash。关于document.createElement()的参数,只有“html”元素可以成功触发Crash,其他标签无法造成Crash(我不确定)。

好了,我们现在开始通过调试复现此漏洞。这里使用的是原始的PoC。首先打开Internet Explorer,拖入PoC,会弹出一个提示框“Internet Explorer已限制此网页运行脚本或ActiveX控件”,表示现在html中的javascript代码还没有得到执行。这时,我们打开WinDbg,附加到iexplore.exe上,输入g命令运行,然后在Internet Explorer界面点击提示框中的“允许阻止的内容”(可能需要刷新一下)。然后Internet Explorer会执行异常,WinDbg会捕获到异常并中断下来。以下是Crash的现场情况:

通过观察WinDbg的输出信息,可以发现PoC造成了异常代码为0xc0000005的内存访问违例异常。0x63a46809处的异常代码向一个内存访问权限为PAGE_NOACCESS(不可访问)的地址写入一个值,从而造成Crash。通过k命令打印栈回溯,可以知道发生异常的代码位于MSHTML!CSpliceTreeEngine::RemoveSplice()函数中。

Internet Explorer DOM树的结构

当如今的Web开发者想到DOM树时,他们通常会想到这样的一个树:

Picture6:A_Simple_Tree

这样的树看起来非常的简单,然而,现实是Internet Explorer的DOM树的实现是相当复杂的。

简单地说,Internet Explorer的DOM树是为了20世纪90年代的网页设计的。当时设计原始的数据结构时,网页主要是作为一个文档查看器(顶多包含几个动态的GIF图片和其他的静态图片)。因此,算法和数据结构更类似于为Microsoft Word等文档查看器提供支持的算法和数据结构。回想一下网页发展的早期,JavaScript还没有出现,并不能通过编写脚本操作网页内容,因此我们所了解的DOM树并不存在。文本是组成网页的主要内容,DOM树的内部结构是围绕快速、高效的文本存储和操作而设计的。内容编辑(WYSIWYG:What You See Is What You Get)和以编辑光标为中心用于字符插入和有限的格式化的操作范式是当时网页开发的特点。

以文本为中心的设计

由于其以文本为中心的设计,DOM的原始结构是为了文本后备存储,这是一个复杂的文本数组系统,可以在最少或没有内存分配的情况下有效地拆分和连接文本。后备存储将文本(Text)和标签(Tag)表示为线性结构,可通过全局索引或字符位置(CP:Character Position)进行寻址。在给定的CP处插入文本非常高效,复制/粘贴一系列的文本由高效的“splice(拼接)”操作集中处理。下图直观地说明了如何将包含“hello world”的简单标记加载到文本后备存储中,以及如何为每个字符和标签分配CP。

Picture7:Text_Backing_Store

文本后备存储为非文本实体(例如:标签和插入点)提供特殊的占位符。

为了存储非文本数据(例如:格式化和分组信息),另一组对象与后备存储分开进行维护:表示树位置的双向链表(TreePos对象)。TreePos对象在语义上等同于HTML源代码标记中的标签——每个逻辑元素都由一个开始和结束的TreePos表示。这种线性结构使得在深度优先前序遍历(几乎每个DOM搜索API和CSS/Layout算法都需要)DOM树时,可以很快的遍历整个DOM树。后来,微软扩展了TreePos对象以包括另外两种“位置”:TreeDataPos(用于指示文本的占位符)和PointerPos(用于指示诸如脱字符(“^大写字符”:用于表示不可打印的控制字符)、范围边界点之类的东西,并最终用于新特性,如:生成的内容结点)。

每个TreePos对象还包括一个CP对象,它充当标签的全局序数索引(对于遗留的document.all API之类的东西很有用)。从TreePos进入文本后备存储时需要用到CP,它可以使结点顺序的比较变得容易,甚至可以通过减去CP索引来得到文本的长度。

为了将它们联系在一起,一个TreeNode将成对的TreePos绑定在一起,并建立了JavaScript DOM所期望的“树”层次结构,如下图所示:

Picture8:Node_Text_DOM

增加复杂性层次结构

CP的设计造成了原有的DOM非常复杂。为了使整个系统正常工作,CP必须是最新的。因此,每次DOM操作(例如:输入文本、复制/粘贴、DOM API操作,甚至点击页面——这会在DOM中设置插入点)后都会更新CP。最初,DOM操作主要由HTML解析器或用户操作驱动,所以CP始终保持最新的模型是完全合理的。但是随着JavaScript和DHTML的兴起,这些操作变得越来越普遍和频繁。

为了保持原来的更新速度,DOM添加了新的结构以提高更新的效率,并且伸展树(SplayTree)也随之产生,伸展树是在TreePos对象上添加了一系列重叠的树连接。起初,增加的复杂性提高了DOM的性能,可以用O(log n)速度实现全局CP更新。然而,伸展树实际上仅针对重复的局部搜索进行了优化(例如:针对以DOM树中某个位置为中心的更改),并没有证明对JavaScript及其更多的随机访问模式具有同样的效果。

另一个设计现象是,前面提到的处理复制/粘贴的“Splice(拼接)”操作被扩展到处理所有的树突变。核心“Splice Engine(拼接引擎)”分三步工作,如下图所示:

Picture9:Splice_Engine_Algorithm

在步骤1中,引擎将通过从操作开始到结束遍历树的位置(TreePos)来“记录”拼接信息。然后创建一个拼接记录,其中包含此操作的命令指令(在浏览器的还原栈(Undo Stack)中重用的结构)。

在步骤2中,从树中删除与操作关联的所有结点(即TreeNode和TreePos对象)。请注意,在IE DOM树中,TreeNode/TreePos对象与脚本引用的Element对象不同,TreeNode/TreePos对象可以使标签重叠更容易,所以删除它们并不是一个功能性问题。

最后,在步骤3中,拼接记录用于在目标位置“Replay(重现)”(重新创建)新对象。例如,为了完成appendChild DOM操作,拼接引擎(Splice Engine)在结点周围创建了一个范围(从TreeNode的起始TreePos到其结束TreePos),将此范围“拼接”到旧位置之外,并创建新结点来表示新位置处的结点及其子结点。可以想象,除了算法效率低下之外,这还造成了大量内存分配混乱。

原来的DOM没有经过封装

这些只是Internet Explorer DOM复杂性的几个示例。更糟糕的是,原来的DOM没有经过封装,因此从Parser一直到Display系统的代码都对CP/TreePos具有依赖性,这需要许多年的开发时间来解决。

复杂性很容易带来错误,DOM代码库的复杂性对于软件的可靠性是一种负担。根据内部调查,从IE7到IE11,大约28%的IE可靠性错误源自核心DOM组件中的代码。而且这种复杂性也直接削弱了IE的灵活性,每个新的HTML5功能的实现成本都变得更高,因为将新理念实现到现有架构中变得更加困难。

漏洞原理分析

逆向mshtml.dll中此漏洞的相关类

逆向主要是通过微软提供的pdb文件,以及先前泄露的IE5.5源码完成的。

CSpliceTreeEngine

实际为SpliceTree工作的类,也就是上面所说的拼接引擎(Splice Engine)的核心类。SpliceTree可以对树的某个范围进行移除(Remove)、复制(Copy)、移动(Move)或还原移除(Undo a Remove)。当DOM树发生变化时就会调用到此类的相关函数。

以下是IE源代码中的关于此类功能的一些注释:

移除(Remove):

1、此SpliceTree的行为是移除指定范围内的所有文本(Text),以及完全落入该范围内的所有元素(Element)。

2、语义是这样的,如果一个元素不完全在一个范围内,它的结束标签(End-Tags)将不会相对于其他元素进行移动。但是,可能需要减少该元素的结点数。发生这种情况时,结点将从右边界(Right Edge)移除。

3、范围内的不具有cling的指针(CTreeDataPos)最终会出现在开始标签(Begin-Tags)和结束标签(End-Tags)之间的空间中(可以说,它们应该放在开始标签和结束标签之间)。带有cling的指针会被删除。

复制(Copy):

1、复制指定范围内的所有文本(Text),以及完全落在该范围内的元素(Element)。

2、与左侧范围重叠的元素被复制;开始边界(Begin-Edges)隐含在范围的最开始处,其顺序与开始边界在源中出现的顺序相同。

3、与右侧范围重叠的元素被复制;结束边界(End-Edges)隐含在范围的最末端,其顺序与结束边界在源中出现的顺序相同。

移动(Move):

1、指定范围内的所有文本(Text),以及完全落入该范围内的元素(Element),都被移动(移除并插入到新位置,而不是复制)。

2、使用与移除(Remove)相同的规则修改与右侧或左侧重叠的元素,然后使用与复制(Copy)相同的规则将其复制到新位置。

还原移除(Undo a Remove):

1、这种对SpliceTree的操作只能从还原代码(Undo Code)中调用。本质上,它是由先前移除(Remove)中保存的数据驱动的移动(Move)。更复杂的是,我们必须将保存的数据编织到已经存在的树中。

下面是我经过逆向得出的IE11中CSpliceTreeEngine类对象的大部分成员。

下面是我经过逆向得出的IE11中CSpliceTreeEngine类的构造函数。

CTreeNode

html代码中,每一对标签在IE中都会对应一个CTreeNode对象,每个CTreeNode对象的tpBegin和tpEnd成员分别用来标识对应标签的起始标签和结束标签。IE11中CTreeNode对象的第三个DWORD的低12位为标签的类型,通过IE5.5源代码中的enum ELEMENT_TAG枚举变量和pdb文件中全局g_atagdesc表,可以得出当前版本mshtml.dll渲染引擎中大部分标签对应的枚举值。

下面是我经过逆向得出的IE11中CTreeNode类对象的部分成员。

CTreePos

每个标签的开始标签和结束标签都有一个对应的CTreePos对象,其包含在CTreeNode对象中。通过CTreePos对象可以找到任何一个标签在DOM流中的位置,以及在DOM树中的位置。IE通过CTreePos对象的pFirstChild和pNext成员构成了实际的DOM树,通过pLeft和pRight成员构成了DOM流(双链表)。

下面枚举变量EType是CTreePos对象所对应的元素的类型。

下面枚举变量是某一个CTreePos对象在DOM树中与相连的CTreePos对象的关系,以及CTreePos对象的类型。

下面是我经过逆向得出的IE11中CTreePos类对象的完整成员。

CTreeNode::InitBeginPos()函数用于初始化起始标签对应的CTreePos对象。

CTreeNode::InitEndPos()函数用于初始化结束标签对应的CTreePos对象。

CTreePos::GetCch()函数用于获取当前CTreePos对象对应的元素所占用的字符数量。起始标签和结束标签对应的字符数量为1,文本字符串为实际拥有的字符数,指针数据字符数的获取在CTreePos::GetContentCch()中(为0或1)。前面介绍DOM流结构时,在“以文本为中心的设计”中有提到过。

CTreeDataPos

CTreeDataPos继承于CTreePos。CTreeDataPos类为CTreePos类的扩展,用于表示文本数据和指针数据。此漏洞所涉及到的关键类,就是该类。

下面是我经过逆向得出的IE11中CTreeDataPos类对象的完整成员。

Tree::TreeWriter::AllocData1Pos()函数为指针数据类的CTreeDataPos对象分配内存,并初始化。IE8中此函数为CMarkup::AllocData1Pos()。

Tree::TreeWriter::AllocData2Pos()函数为文本数据类的CTreeDataPos对象分配内存,并初始化。IE8中此函数为CMarkup::AllocData2Pos()。

IE11的CTreeDataPos拥有一个新的成员_pTextData,IE8及以前是没有的。以前文本数据是存在CTxtArray类中的,并通过CTxtPtr类对其进行访问。在IE11中并没有废除以前的方式,而是添加了一种新的用于存储文本数据的方式,即Tree::TextData类。

CTreeDataPos::SetTextData()函数用于设置CTreeDataPos对象中_pTextData成员存储的Tree::TextData类对象指针。

CTreeDataPos::GetTextLength()函数可以从两种存储文本字符串的结构CTxtArray和Tree::TextData中获取到文本字符串的长度。此漏洞的根本原因就在于CTreeDataPos类中DATAPOSTEXT结构体的cch成员(25bit)与Tree::TextData类中cch成员(32bit)的大小不同,而在使用时进行混用,从而导致了堆块的越界写。具体原因,见后面漏洞的根本原因分析。

CTreeDataPos::AppendText()用于在原来的字符串后面附加新的字符串。

Tree::TextData

下面是我经过逆向得出的IE11中Tree::TextData类对象的完整成员。

Tree::TextData::AllocateMemory()函数用于为Tree::TextData对象分配内存。

Tree::TextData::Create()函数用于根据传入的参数字符串创建一个Tree::TextData对象,并将字符串复制到Tree::TextData对象的空间,然后返回Tree::TextData对象的指针。

下面函数是上面函数的重载。能够添加额外的字符串。

Tree::TextData::GetText()函数用于从Tree::TextData对象获取到文本字符串的指针和长度。

CTxtPtr

CTxtPtr继承于CRunPtr。提供对后备存储区中字符数组的访问(即CTxtArray)。

CSpliceTreeEngine::RecordSplice()函数是CSpliceTreeEngine引擎用于记录DOM树的拼接的函数。

漏洞PoC所对应的DOM树

这里调试时用的PoC是Google Project Zero的Ivan Fratric提供的PoC,未经修改。

重新调试,附加IE进程,在初始断点断下后,设置以下两个断点。

以下内容是WinDbg调试输出的结果:

我们通过漏洞Crash附近两次调用CTreePos::GetCp()时,传入的参数ptpSourceL和ptpSourceR,再结合CTreePos中的pLeft和pRight,形成的DOM流双链表结构,以及CTreeNode中tpBegin和tpEnd相对于CTreeNode对象起始地址的偏移关系,可以获取到DOM流中所有的元素内容。

以下是ROOT标签的CTreeNode、起始标签和结束标签对应的CTreePos的对象内存数据:

以下是html标签的CTreeNode、起始标签和结束标签对应的CTreePos的对象内存数据:

以下是head标签的CTreeNode、起始标签和结束标签对应的CTreePos的对象内存数据:

以下是body标签的CTreeNode、起始标签和结束标签对应的CTreePos的对象内存数据:

以下是DOM流中除了标签结点以外,链入的CTreeDataPos(Text)和CTreeDataPos(Pointer)对象的内存数据:

我根据CTreePos中的pFirstChild和pNext成员,可以还原出此漏洞PoC所对应的DOM树结构如下图所示:

Picture10:pFirstChild_pNext_DOM_Tree

我根据CTreePos中的pLeft和pRight成员,可以还原出此漏洞PoC所对应的DOM流结构如下图所示:

Picture11:pLeft_pRight_DOM_Stream

漏洞产生的根本原因分析

以下是动态调试过程中,关键部分的WinDbg输出内容:

下面是存在漏洞的函数CSpliceTreeEngine::RemoveSplice()的关键部分代码(逆向所得):

造成堆越界写的根本原因是,用于标识文本字符串在DOM树/DOM流中的位置的CTreeDataPos类对象中有两个结构用于记录文本字符串的长度,一个是结构体DATAPOSTEXT的cch成员(25bit),一个是Tree::TextData对象中的cch成员(32bit)。由于它们的大小不同,当文本字符串的长度超过25bit能够表示的长度后,在向结构体DATAPOSTEXT的cch成员赋值时,会造成其存储的是截断后的长度。之后调用CSpliceTreeEngine::RemoveSplice()函数删除文本字符串在DOM树/DOM流的结构时,会使用CTreePos::GetCp()函数获得要删除的DOM树/DOM流结构所占用的字符数(包含截断的文本字符串长度),并用其申请一段内存。然后,调用Tree::TextData::GetText()函数获得Tree::TextData对象中的cch成员中存储的未截断文本字符串长度,并用其作为索引,对前面申请的内存进行赋值操作,从而造成了堆越界写漏洞。

漏洞修复

分析此漏洞时,使用的环境是Windows 10 1809 Pro x64。在此漏洞的MSRC公告页面,可以找到当前环境该漏洞的补丁号为KB5003646。在补丁详情页面,我们可以知道此补丁只适用于LTSC版本。当前环境,此补丁无法安装成功。所以我使用Windows 10 Enterprise LTSC 2019环境来进行补丁安装并进行补丁分析。我用的是2019年03月发布的Windows 10 Enterprise LTSC 2019,成功安装此漏洞补丁需要先安装2021年5月11日之后发布的服务堆栈更新(SSU),这里安装的是KB5003711,安装完之后再安装此漏洞的补丁KB5003646,就可以成功安装。

由于KB5003646补丁是2021年6月8日发布的一个累计更新,如果补丁分析时所用的两个漏洞模块文件是两个更新时间相差较大的环境提取出来的,会造成不好定位补丁位置。所以我们需要知道2021年5月发布的累计更新补丁编号。这可以通过KB5003646在Microsoft更新目录详情页面的信息得到。

Picture12:KB5003646_Package_Details

以下是KB5003171和KB5003646补丁对应的mshtml.dll的版本号:

补丁编号 mshtml.dll版本号
KB5003171 11.0.17763.1911
KB5003646 11.0.17763.1999

接下来我们将这两个补丁环境的mshtml.dll提取出来,使用IDA打开并生成IDB文件,再使用BinDiff进行补丁比较。不同的IDA版本和不同的BinDiff版本可能会出现不兼容的情况,我这里使用的是IDA Pro7.5+BinDiff6。分析完成后,得到如下结果:

Picture13:mshtml_unpatch_patched_bindiff

根据前面的根本原因分析,我们可以知道此漏洞是和文本字符串相关的。再来看BinDiff分析出来的结果,存在差异的函数中只有Tree::TreeWriter::NewTextPosInternal()和CTreeDataPos::GetPlainTextLength()是与文本字符串有关的。通过IDA静态分析这两个函数后,可以确定补丁位置位于Tree::TreeWriter::NewTextPosInternal()函数中。因为CTreeDataPos::GetPlainTextLength()函数中调用了Tree::TextData::GetText()函数,从之前给出的逆向出的Tree::TextData::GetText()函数代码可知,Tree::TextData::GetText()函数是从Tree::TextData对象获取文本字符串的指针和长度的。Tree::TextData对象中的cch用于存储文本字符串的长度,它的长度为32bit。而CTreeDataPos对象中结构体DATAPOSTEXT的cch成员也是用于存储文本字符串的长度,它的长度为25bit。如果字符串长度超过了25bit所能表示的范围,在向结构体DATAPOSTEXT的cch成员存入字符串长度时,就会造成截断。补丁代码应该是在向结构体DATAPOSTEXT的cch成员写入文本字符串长度时,对文本字符串的长度进行判断。所以补丁位置并不在CTreeDataPos::GetPlainTextLength()函数中。

下图为Tree::TreeWriter::NewTextPosInternal()函数中添加的补丁代码:

Picture14:mshtml_patched_code

如下是,经过处理的补丁前后Tree::TreeWriter::NewTextPosInternal()函数的IDA反编译代码:

可以看到打了补丁后的Tree::TreeWriter::NewTextPosInternal()函数在向CTreeDataPos对象中结构体DATAPOSTEXT的_cch成员写入文本字符串长度之前,进行了一个判断。如果SrcTextCch < 0×2000000,就会触发断言失败。普通断言(assert())只有在debug版本的文件中会得到执行,而在release版本的文件中不会得到执行。这里使用的是一种由C++提供的,可以添加到release版本的文件中的断言函数Release_Assert()。断言失败后,通过SetUnhandledExceptionFilter()函数设置异常处理函数,并会抛出一个断点异常。之后会一直在异常处理流程中,并不会造成IE执行堆越界写的代码。

参考链接

1、Google Project Zero – CVE-2021-33742: Internet Explorer out-of-bounds write in MSHTML

2、Google Threat Analysis Group – How we protect users from 0-day attacks

3、weolar – 丢几个好东西,完整可编译的ie2、ie5.5源码,嘿嘿

4、o_0xF2B8F2B8 – IE DOM树概览

5、Microsoft Edge Team – Modernizing the DOM tree in Microsoft Edge

 

Written by