前言
在过去的几个月中,微软安全响应中心(MSRC)发布了许多Windows更新,陆续修复了由FortiGuard实验室发现的多个UAF(Use-After-Free)漏洞。我们在此前发布的文章中曾提及过,我们在发现UAF漏洞后,向微软安全响应中心提交了一份详细的技术细节,随后MSRC将该漏洞定级为高危,并分配了编号CVE-2018-0797。在本文中,我们将与大家分享发现该漏洞的过程,探究造成该漏洞的根本原因,并分析微软为修复这一漏洞所部署的缓解措施。
本文的分析过程是在Windows 7 32位系统上使用Microsoft Word 2010进行的。
使用AlleyCat、Lighthouse和BinDiff进行差异化分析
漏洞研究人员应该了解,安全审计的过程非常困难,特别是针对代码量较大的软件。然而更具挑战性的是,Microsoft Office并没有提供任何调试符号,调试符号有助于识别并区分已经解析的函数名称、参数或本地变量。因此,我们如果要对Microsoft Office的补丁进行深入分析,就要花费比平常更多的时间来进行逆向工程。
然而幸运的是,我们正站在巨人的肩膀上,我们可以使用一些非常方便的工具来有效解决分析大型二进制文件过程中产生的一些困难。事不宜迟,接下来就开始进行我们的分析。在以下的差异分析过程中,我们针对14.0.7191.5000(未修复)和14.0.7192.5000(已修复)版本的wwlib.dll进行了比较。
首先,我们借助BinDiff,比较了未修复和已修复版本的wwlib.dll:
正如我们在BinDiff输出结果中看到的那样,此次补丁中包含许多更新。但实际上,我们发现差异结果的置信度较低,这也就是说BinDiff在此次分析中可能会产生误报的情况。为了保证分析的准确性,就不能只依赖于BinDiff来找到与UAF漏洞相关的函数内容修改。目前需要解决这两个问题:
1、将函数范围缩小到我们需要关注的部分;
2、找到与漏洞相关函数的代码路径,并进行分析。
在阅读Embedi的博客之后,我们了解到一个名为AlleyCat的优秀IDA Pro脚本,该脚本由devttys0开发,可以使IDA Pro能够自动查找两个或两个以上函数之间的路径。毫无疑问,借助这个脚本就能够解决我们的第二个问题,但这一脚本仍然有着局限性,如何缩小范围仍然是一个问题。下图非常直观地描述了当前我们面临的困难:
上图中所展现的调用过程,是借助AlleyCat脚本,通过以下步骤生成的:
1、首先,我们在Microsoft Word的易受攻击版本上运行RTF PoC文件,从而触发漏洞所在的相应函数,该函数使用红框标出;
2、在漏洞所在的函数wwlib_cve_2018_0197中,运行AlleyCat,选择View(视图) —> Graphs(图形) —> Find paths to the current function from …(从当前函数寻找路径) —> Pick FMain(选择FMain) —> OK(确定)。
默认情况下,AlleyCat会从入口点FMain开始,寻找与wwlib_cve_2018_0197直接或间接相关的所有函数,因此生成的调用图非常复杂。而我们的目标是只需要找到与我们的PoC相关的函数。针对这一情况,可以通过人工操纵代码覆盖来进行。我们需要借助DynamoRio套件中的相应命令行工具(如ddrun.exe),使用Microsoft Word打开PoC文档。随后,drrun.exe将生成覆盖文件。之后我们就可以使用Lighthouse插件,获取解析器代码,来解析由DrCov插件生成的覆盖信息,并将该覆盖信息当作过滤器,应用在我们得到的第一个调用图之中。最终,我们得到以下调用图:
正如上图所示的那样,我们目前已经确定了哪些函数是需要进行进一步研究的。甚至,现在可以使用BinDiff来确定哪个函数的更新是为了修复特定漏洞的。改进后的调用图一大优点在于,它能让我们只关注与PoC相关的函数。例如,我们在进行了一些回溯之后,可以迅速识别出函数sub_31B22D39和sub_31B25BD5中的RTF解析例程,这样就为我们节省出大量的分析事件。最重要的是,Lighthouse提供的覆盖绘图(Coverage Painting)功能也使得我们的静态分析工作变得更加容易。
**在这里需要提一句,尽管我们通过Lighthouse获取了大部分DrCov解析器代码,但是我们发现了Lighthouse的一个小BUG,可能会在特定情况下导致覆盖输出结果不一致。在发现之后,我们修复了这一错误,并将修复后的版本作为Lighthouse的Upstream提交。如果各位读者有兴趣,可以在这里查看关于该BUG的描述:https://github.com/gaasedelen/lighthouse/pull/33/files 。不过,由于DrCov的解析器功能以及通过AlleyCat实现代码覆盖后过滤的函数实现非常简单,因此我们没有在这里提供增强后AlleyCat的源代码。
样式表控制字结构定义
在深入讨论漏洞的细节之前,我们首先要了解RTF样式表的数据结构,这将有助于我们理解这一漏洞。正如前面所说,由于我们没有wwlib.dll的调试符号,所以只能是通过逆向工程来了解它的数据结构。
通过进一步研究,我们确定了样式表的数据结构。假设现在有一个RTF文件,其中定义了以下样式表控制字(Stylesheet Control Word):
使用WinDBG调试器查看,可以在以下内存转储中看到其原始数据。
样式表对象头部的定义如下:
在头部定义之后的指针样式表数组:
根据内存转储的输出,我们获知,在示例RTF中定义的styledef控制字在内存地址0x1022af60中以指针的形式存储(0x5338768、0xfdf2768和0xfb00768)。并且,我们可以将这种内存结构解释为以下C语言的数据结构:
struct _strucStyleSheet{
DWORD dwCountStyles;
DWORD dwTotalStyles;
DWORD dwSizeofPtr;
DWORD dwSizeofHeader;
DWORD dwUnknown1;
DWORD dwUnknown2;
void *pUnknown;
DWORD dwUnknown3;
void *pStyleDefs[dwTotalStyles];
}strucStyleSheet;
在阅读之后,就很容易理解它的内存结构。pStyleDefs中索引为0到14的数组是用来存储默认styledef指针,这一部分并不重要。我们需要重点关注的是索引在15及以上的数组,它们通常由我们在RTF文档中定义的styledef指针(s1、s2和s3)组成。我们尝试去弄明白这些数组的原始数据的含义,但最终还是没能完全理解pStyleDefs的完整数据结构。不过,只理解其中一部分定义,已经足够用来分析漏洞。在放弃之后,我们开始使用styledef中的控制字,并将第一个RTF样本修改为:
我们在第二个RTF样本中,添加了sbasedon1控制字。引用自微软官方的RTF规范,sbasedonN控制字定义了当前样式所基于的样式句柄。简而言之,就是要告诉s2应该从s1继承样式。此外,我们还注意到pStyleDefs[16]中的更改。在这里请注意,第一个styledef s1的数组索引为15,第二个styledef s2的数组索引为16,位于内存转储的偏移量2处:
通过分析样式表分析例程,我们发现styledef索引可以从位于偏移量2的styledef指针中检索到。如上图所示,位于地址0xf6e0790的偏移量2处的值表示在sbasedonN控制字中指定的pStyledefs数组索引,在将值右移4位(0xF1 >> 4)后得到0xF。大家可能会注意到,sbasedon1指向了s1。因此,需要注意数组pStyleDefs的索引始终以0xF(十进制的15)开头,用于第一个控制字,例如RTF文档中所使用的sN、sbasedonN和slinkN。
漏洞的根本原因
所谓UAF,是指允许攻击者在释放内存后访问内存的漏洞,这样的漏洞可能会导致程序崩溃、任意代码执行,甚至会导致远程代码执行的风险。
事实上,借助于我们增强后的AlleyCat脚本,我们很快就能找到CVE-2018-0797漏洞修补程序所修复的部分。在我们缩小函数的范围之后,就可以使用BinDiff查看更新的内容,从而注意到发生崩溃的wwlib_cve_2018_0797函数中添加了一个代码块。
这一次我们比较幸运,因为修复的函数恰好是发生了崩溃的函数。以我们以往的经验来说,补丁通常会打在不同函数上,研究人员就需要花费一定时间来寻找和定位修复的位置。当然,这也取决于漏洞的性质。无论如何,能够定位到这一函数就胜利了一大半,我们接下来从该函数中探寻UAF漏洞的产生原因。
在对修复后的函数进行分析之后,我们发现,添加代码块的目的是为了确保循环始终能获取到更新后的pStyleDefs指针。当遇到sbasedonN后,会更新pStyleDefs指针,以分配更多空间,从而存储应该继承的styledef的其他信息。在这种情况下,Microsoft Word使用更大的缓冲区空间调用堆重新分配函数,来替换pStyleDefs指针。需要说明的是,在这里没有出现悬垂指针(Dangling Pointer)。
在原始函数中,只要发生堆的重新分配,就会释放旧的pStyleDefs指针。正因如此,如果一个函数试图将一个样式链接到另一个样式,在其返回给调用者时,仍然保存着旧的pStyleDefs指针,如下图中代码(2)所示。而当pStyleDefs在漏洞所在函数内某个地方被间接引用时,就会发生UAF,如下图中代码(1)所示。
为了修复上述问题,该函数通过添加代码块来确保能及时更新pStyleDefs指针,该代码块负责将更新后的pStyleDefs指针返回给调用方,如下图中标红的代码所示。
接下来的一个问题就是,我们如何触发wwlib_cve_2018_0797。经过更为深入的分析后,我们发现RTF解析器负责styledef索引引用表的初始化和维护工作。举例来说,第二个RTF样本中就具有如下的styledef引用表:
在RTF解析器中有一个条件检查,用于确定当前styledef索引以及引用表中styledef索引的完整性。进行引用表的完整性检查是非常具有挑战性的,一旦检查到当前styledef索引与styledef引用表中的初始化索引不匹配时,解析器例程就会尝试重新构造样式表数据结构, 并调用wwlib_cve_2018_0797函数。
然而,有多种方式可以造成这种不匹配的情况。其中最典型的方法就是在sN控制字和RTF文件中附加的stylesheet控制字中定义N=0:
该RTF样本会导致新的styledef引用表产生,如下所示:
请注意,由于在更新的RTF样本中定义了s0,所以引用索引现在是从0开始,如上图所示。此外,当RTF解析器解析第二个stylesheet控制字时,其引用索引应该是0,而不是4,并且它应该是针对当前styledef(s4)的s0的引用索引。
当RTF解析器想要获取当前styledef的索引以查询引用表时,将会返回styledef索引0xF,但实际上当前styledef索引(s4)中的值却是0x14。
以上就是这一漏洞的产生原因。
总之,在以下情况下,可以触发该UAF漏洞:
1、定义了多个stylesheet控制字,其中一个样式控制字会导致RTF解析器初始化引用表中的引用索引为0。
2、sbasedonN控制字触发堆的重新分配,以扩展存储在数据结构中的样式属性。数据结构的扩展导致样式对象的初始指针在漏洞所在函数中被间接引用时会被释放并且变为无效,进而会从无效的指针中访问某些数据。
总结
在本篇文章中,我们分享了借助多个开源工具来分析大型二进制文件的思路,这些工具可以帮助我们将分析所需的时间从几周缩短到几天。我们在了解这一开源代码实现方式的同时,也发现了该代码中的一些问题,并意识到这些代码存在一些限制,随后我们修复了开源工具并将更新后的版本回馈给开源社区。在最后,我们详细阐述了这一漏洞产生的根本原因。根据我们的分析,我们觉得微软允许复制样式表控制字,而这也就是在使用第二个样式表控制字时微软之所以没有修正styledef引用表中styledef索引错位的原因。然而,这一猜测还没有百分百地确认,需要我们继续进行深入研究。
本分析报告由FortiGuard Lion团队发表。