- 5.1、以文本为中心的设计
- 5.2、增加复杂性层次结构
- 5.3、原来的DOM没有经过封装
- 6.3、漏洞产生的根本原因分析
Explorer进程并导航到恶意网页来实现攻击。此攻击中使用的漏洞被分配为CVE-,并于2021年6月由Microsoft修复。
0day漏洞在野利用的攻击事件,例如:CVE-、CVE-,所以研究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-漏洞的分析过程的一个记录。我之前分析过老版本的Internet Explorer的漏洞,这是第一次比较正式的分析新版本Internet Explorer的漏洞,如有错误和不足之处,还望见谅。
innerHTML属性对内部html元素设置内容(包含文本字符串)时触发的。通过innerHTML属性修改标签之间的内容时,会造成IE生成的DOM树/DOM流的结构发生改变,IE会调用CSpliceTreeEngine类的相关函数对IE的DOM树/DOM流的结构进行调整。当调用CSpliceTreeEngine::RemoveSplice()去删除一些DOM树/DOM流结构时,恰好这些结构中包含文本字符串时,就有可能会造成堆越界写。
设置”。注意:设置界面拥有两个选项卡,“系统设置”和“程序设置”。我们先看“系统设置”,与ASLR有关系的是“强制映像随机化(强制性ASLR)”、“随机化内存分配(自下而上ASLR)”、“高熵ASLR”,我们都将其设为关闭状态。先关闭“高熵ASLR”,然后再关闭其他两项。
“强制映像随机化(强制性ASLR)”,不管编译时是否使用“/DYNAMICBASE”编译选项进行编译,开启了“强制性ASLR”后,会对所有软件模块的加载基址进行随机化,包括未使用“/DYNAMICBASE”编译选项编译的软件模块。关于编译时是否使用了“/DYNAMICBASE”编译选项进行编译,可以使用“Detect It
“随机化内存分配(自下而上ASLR)”,开启了该选项后,当我们使用malloc()或HeapAlloc()在堆上申请内存时,得到的堆块地址将在一定程度上进行随机化。
“高熵ASLR”,这个选项需要配合“随机化内存分配(自下而上ASLR)”选项使用,开启了该选项后,会在“随机化内存分配(自下而上ASLR)”基础上,更大程度的随机化堆块的分配地址。
接下来,我们来看“程序设置”。由于Windows10可以对单独的应用程序设置缓解措施的开启或关闭,并且替换“系统设置”中的设置,造成关闭了“系统设置”中所有与ASLR相关的缓解措施后,dll模块的加载基址还是在变化。切换到“程序设置”选项卡后,找到iexplore.exe,点击编辑,将所有与ASLR有关的设置的“替代系统设置”的勾去掉。
设置完成后,重启一下操作系统。
由于原始PoC过于精简,无法观察到执行效果,对我理解程序的执行流程造成了一定的障碍。所以我尝试了以下几种经过修改的PoC,用于观察执行效果。
我们可以得出以下结论:PoC通过HTML DOM方法document.createElement(),创建了一个“html”结点(同时创建“head”和“body”结点),并把新创建的“html”结点添加到原有的“body”结点中。然后,创建了一个Array数组并进行了初始化。最后将该数组转化为字符串,通过HTML DOM的innerHTML属性,添加到新创建的“html”结点中的“body”结点中。
原始PoC中,并未将创建的Array数组初始化,我们通过Chrome的开发者工具查看未初始化的Array数组转化为字符串后,得到的是什么。这有助于我们后面在调试PoC时,观察字符串所对应的内存数据。
可以看到,初始化后的Array数组转化成字符串后,每个元素是使用“,”分隔的。未初始化的Array数组转化成字符串后,只有一连串的“,”。其个数为Array数组元素个数减1。
好了,我们现在开始通过调试复现此漏洞。这里使用的是原始的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()函数中。
当如今的Web开发者想到DOM树时,他们通常会想到这样的一个树:
这样的树看起来非常的简单,然而,现实是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。
文本后备存储为非文本实体(例如:标签和插入点)提供特殊的占位符。
为了存储非文本数据(例如:格式化和分组信息),另一组对象与后备存储分开进行维护:表示树位置的双向链表(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所期望的“树”层次结构,如下图所示:
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(拼接引擎)”分三步工作,如下图所示:
在步骤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功能的实现成本都变得更高,因为将新理念实现到现有架构中变得更加困难。
逆向主要是通过微软提供的pdb文件,以及先前泄露的IE5.5源码完成的。
以下是IE源代码中的关于此类功能的一些注释:
1、此SpliceTree的行为是移除指定范围内的所有文本(Text),以及完全落入该范围内的所有元素(Element)。
2、语义是这样的,如果一个元素不完全在一个范围内,它的结束标签(End-Tags)将不会相对于其他元素进行移动。但是,可能需要减少该元素的结点数。发生这种情况时,结点将从右边界(Right Edge)移除。
3、范围内的不具有cling的指针(CTreeDataPos)最终会出现在开始标签(Begin-Tags)和结束标签(End-Tags)之间的空间中(可以说,它们应该放在开始标签和结束标签之间)。带有cling的指针会被删除。
1、复制指定范围内的所有文本(Text),以及完全落在该范围内的元素(Element)。
2、与左侧范围重叠的元素被复制;开始边界(Begin-Edges)隐含在范围的最开始处,其顺序与开始边界在源中出现的顺序相同。
3、与右侧范围重叠的元素被复制;结束边界(End-Edges)隐含在范围的最末端,其顺序与结束边界在源中出现的顺序相同。
1、指定范围内的所有文本(Text),以及完全落入该范围内的元素(Element),都被移动(移除并插入到新位置,而不是复制)。
2、使用与移除(Remove)相同的规则修改与右侧或左侧重叠的元素,然后使用与复制(Copy)相同的规则将其复制到新位置。
1、这种对SpliceTree的操作只能从还原代码(Undo Code)中调用。本质上,它是由先前移除(Remove)中保存的数据驱动的移动(Move)。更复杂的是,我们必须将保存的数据编织到已经存在的树中。
下面是我经过逆向得出的IE11中CSpliceTreeEngine类对象的大部分成员。
html代码中,每一对标签在IE中都会对应一个CTreeNode对象,每个CTreeNode对象的tpBegin和tpEnd成员分别用来标识对应标签的起始标签和结束标签。IE11中CTreeNode对象的第三个DWORD的低12位为标签的类型,通过IE5.5源代码中的enum ELEMENT_TAG枚举变量和pdb文件中全局g_atagdesc表,可以得出当前版本mshtml.dll渲染引擎中大部分标签对应的枚举值。
下面是我经过逆向得出的IE11中CTreeNode类对象的部分成员。
每个标签的开始标签和结束标签都有一个对应的CTreePos对象,其包含在CTreeNode对象中。通过CTreePos对象可以找到任何一个标签在DOM流中的位置,以及在DOM树中的位置。IE通过CTreePos对象的pFirstChild和pNext成员构成了实际的DOM树,通过pLeft和pRight成员构成了DOM流(双链表)。
下面枚举变量EType是CTreePos对象所对应的元素的类型。
下面枚举变量是某一个CTreePos对象在DOM树中与相连的CTreePos对象的关系,以及CTreePos对象的类型。
下面是我经过逆向得出的IE11中CTreePos类对象的完整成员。
CTreePos::GetCch()函数用于获取当前CTreePos对象对应的元素所占用的字符数量。起始标签和结束标签对应的字符数量为1,文本字符串为实际拥有的字符数,指针数据字符数的获取在CTreePos::GetContentCch()中(为0或1)。前面介绍DOM流结构时,在“以文本为中心的设计”中有提到过。
CTreeDataPos继承于CTreePos。CTreeDataPos类为CTreePos类的扩展,用于表示文本数据和指针数据。此漏洞所涉及到的关键类,就是该类。
下面是我经过逆向得出的IE11中CTreeDataPos类对象的完整成员。
IE11的CTreeDataPos拥有一个新的成员_pTextData,IE8及以前是没有的。以前文本数据是存在CTxtArray类中的,并通过CTxtPtr类对其进行访问。在IE11中并没有废除以前的方式,而是添加了一种新的用于存储文本数据的方式,即Tree::TextData类。
下面是我经过逆向得出的IE11中Tree::TextData类对象的完整成员。
下面函数是上面函数的重载。能够添加额外的字符串。
指示CTxtArray中某一元素的索引 指示CTxtArray中某一元素的内容中的字符索引 |
漏洞PoC所对应的DOM树
重新调试,附加IE进程,在初始断点断下后,设置以下两个断点。
以下内容是WinDbg调试输出的结果:
以下是ROOT标签的CTreeNode、起始标签和结束标签对应的CTreePos的对象内存数据:
以下是html标签的CTreeNode、起始标签和结束标签对应的CTreePos的对象内存数据:
以下是head标签的CTreeNode、起始标签和结束标签对应的CTreePos的对象内存数据:
以下是body标签的CTreeNode、起始标签和结束标签对应的CTreePos的对象内存数据:
我根据CTreePos中的pLeft和pRight成员,可以还原出此漏洞PoC所对应的DOM流结构如下图所示:
漏洞产生的根本原因分析
以下是动态调试过程中,关键部分的WinDbg输出内容:
存储实际获得的文本长度的局部变量 返回值为文本字符串的指针,Tree::TextData对象偏移8字节处 edi,源文本字符串长度,未截断文本长度 eax,源文本字符串内存地址 edx,目的内存大小,截断文本长度 |
// 去除边界上带有cling的指针。这样做是为了让_ptpSourceL/R可以在非指针位置上重新定位。我们这样做是为了让元素能够在退出树通知中进行选择。 是文本数据则执行,不必须满足,CTreePos += TextLen; //下面会使用未截断的文本长度进行索引 |
造成堆越界写的根本原因是,用于标识文本字符串在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。在此漏洞的,可以找到当前环境该漏洞的补丁号为。在补丁详情页面,我们可以知道此补丁只适用于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在的信息得到。
接下来我们将这两个补丁环境的mshtml.dll提取出来,使用IDA打开并生成IDB文件,再使用BinDiff进行补丁比较。不同的IDA版本和不同的BinDiff版本可能会出现不兼容的情况,我这里使用的是IDA Pro7.5+BinDiff6。分析完成后,得到如下结果:
0×2000000,就会触发断言失败。普通断言(assert())只有在debug版本的文件中会得到执行,而在release版本的文件中不会得到执行。这里使用的是一种由C++提供的,可以添加到release版本的文件中的断言函数Release_Assert()。断言失败后,通过SetUnhandledExceptionFilter()函数设置异常处理函数,并会抛出一个断点异常。之后会一直在异常处理流程中,并不会造成IE执行堆越界写的代码。