【技术分享】微软如何手工修复Office Equation内存破坏漏洞(CVE-2017-11882)

https://p4.ssl.qhimg.com/t01a74ce54ba1708086.jpg

译者:eridanus96

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

历史悠久的可执行文件

在11月14日微软发布的安全补丁更新中,对一个相当古老的公式编辑器进行了更新,以修复由Embedi报告的缓冲区溢出漏洞。

这个公式编辑器是微软Office早先版本中的一个组件,目前的Office使用的则是集成的公式编辑器。通过查看EQNEDT32.EXE的属性,我们发现它确实有着悠久的历史:

http://p9.qhimg.com/t01d1f2d791b7deb8a5.png

我们可以看到,文件版本是2000.11.9.0(也就是2000年的版本),而修改时间是在2003年,与其签名的时间相匹配。此外,根据其PE头的时间戳信息(3A0ACEBF),我们发现该文件是2000年11月9日的,与上面版本号中的日期完全一致。

http://p7.qhimg.com/t01e92ad6e922313100.png

所以,这个带有漏洞的EQNEDT32.EXE,从2000年起就存在于Office中,长达17年,可以说是一个拥有相当长生命周期的软件。

然而现在,该程序因为这个漏洞宣告终结。在获知这个漏洞后,微软根据Embedi的PoC,成功进行了复现,修正代码中的问题,重新编译了EQNEDT32.EXE,并将修复后的版本提供给用户作为更新。目前该可执行文件的版本为2017.8.14.0。

然而,出于某种原因,微软并没有选择在源代码中修复此问题,而是使用汇编方式进行的手工修复。

手工修复的EXE

正如小标题的字面含义那样,一些有足够经验的逆向工程师定位了EQNEDT32.EXE中有缺陷的代码,在准确定位原指令所占用的空间之后,通过手工覆盖原有指令来修复了这一问题。

而我们又是如何发现的呢?

通常来说,一个C/C++编译器,在重新编译修改的源代码后,特别是对多个函数的源代码都进行修改的前提下,作为一个500KB以上的可执行文件,不可能拥有与之前完全相同的地址。由此,我们得出了这一推断,微软是通过手工方式修复EXE的。

为了证明这一点,让我们看看修复后与修复前(2017.8.14.0版本为第一个文件,2000.11.9.0版本为第二个文件)的EQNEDT32.EXE在BinDiff中的结果:

http://p4.qhimg.com/t0176fefde8e15dfdf2.png

如果你经常比较二进制文件,你会发现一些亮点:所有的EA主值都与相应函数的EA次值相同。即使是在底部列出的相对应但明显不同的函数,在两个版本的EQNEDT32.EXE中都是相同的地址。

正如我此前在推特上提到的,微软修改了EQNEDT32.EXE中的5个函数,也就是上图所示的最后5个函数。让我们先来看看改动最大的,也就是位于4164FA的函数,左边所示的是修复后版本,右边的则是修复前版本。

t01a909067af3aa9699.png

该函数使用了一个指向目标缓冲区的指针,并在循环中将用户提供的字符串逐一复制到这一缓冲区。这也就是Embedi发现的漏洞所在,该函数并没有检查目标缓冲区是否足够容纳用户提供的字符串。此外,如果公式对象提供的字体名称太长,也可能会导致缓冲区溢出。

微软的修复方案是:为该函数引入了一个额外的参数,并指定了目标缓冲区的长度。然后,修改了字符复制循环的逻辑,不仅在到达源字符串末尾时会结束循环,并且在达到目标缓冲区长度时也会终止循环,以确保防止缓冲区溢出。此外,目标缓冲区中复制的字符串将在完成复制后被清空,以防其达到目标缓冲区的长度,从而造成字符串不能结束。

让我们看看加注释后的汇编代码(左侧为修复后函数,右侧为修复前函数):

t018dbb8df6dd5d1161.png

如你所见,修复此函数的人不仅在其中添加了一个缓冲区长度的检查,并且还设法使函数比原来减少了14字节,并在相邻函数前面使用0xCC进行填充。这一点确实厉害。

修复调用方

我们继续进行分析。通常来说,如果被修复的函数有了一个新增加的参数,那么所有调用者也需要进行相应的改动。在地址43B418和4181FA,有该函数的两个调用方(Caller),而在修复后的版本中,它们都有一个在调用之前添加的push指令,分别定义了其缓冲区,0x100和0x1F4的长度。

现在,带有32位字操作数(Literal Operand)的push指令占用5字节。为了将该指令添加到这两个函数中,同时保持原始代码在紧凑的空间中不发生变化(其逻辑也必须保持不变),补丁进行了下面的操作:

针对位于地址43B418的函数,修补后的版本将一些以后会用到的值临时存储在ebx中,而非像之前那样,存储在本地基于栈的变量中。特别要说的是,当局部变量不再使用时,其空间仍然是由栈组成的,否则sub esp 0x10C将会变成sub esp 0x108。

t018850a2cf4cfb6050.png

对于其他调用方,在地址为4181FA的函数,其修复方式是:在不对其他代码进行修改的前提下,将push指令简单直接地加入,这样就能产生所需的额外空间。

t01c80aad1c9d64d41a.png

如上图所示,push指令是在黄色方块的开始处加入的,并且该黄色方块中的全部原有指令都后移了5字节。但是,为什么不另找地方去覆盖掉原来代码中的5个字节呢?这就好像是在补丁可以安全覆盖的代码块之后,已经存在了5字节或更多未使用的空间。

为了解答这一问题,我们来看看加入注释后的汇编代码:

t0197d95e9f9bdc30bb.png

我们惊讶地发现,在修复前的版本中,要修改的代码块末尾有一个额外的jmp loc_418318指令。这样一来,就使得该块中的代码可以向后移动5个字节,从而为最前面的push指令提供空间。

额外的安全检查

到目前为止,我们详细分析了CVE-2017-11882是如何对Embedi发现的漏洞进行修复的。至此,通过检查目标缓冲区的长度,可以有效防止缓冲区溢出。然而,除此之外,新版本的EQNEDT32.EXE在地址41160F和4219F0的地址处,还对另外两个函数进行了修改,让我们再来分析一下。

在修复后的可执行文件中,这两个函数增加了一组边界检查,以确保复制到的是0x20字节的缓冲区。这些检查看起来都一样:ecx(复制的计数器)将与0x21进行比较,如果ecx大于或等于0x21,就会将ecx赋值为0x20。上述这些检查都是在内联memcpy操作之前加入的。我们以其中的一个为例,再研究一下如何为新指令腾出空间。

t015c717df70ba07bf4.png

如上图所示,在内联memcpy代码之前,插入了一个检查的环节。我们需要注意的是,在32位代码中,memcpy通常使用movsd(移动双字)指令来复制块中的前4个字节,而所有剩余的字节则都使用movsb(移动字节)进行复制。之前的这一设计可以有效提升性能。但对漏洞修复者来说,可以很轻易地注意到,我们可以通过仅使用movsb的方法,释放掉一些空间,而代价只是牺牲掉1-2纳秒的运行速度。借助于此,我们让代码在逻辑上能够保持一致,并且已经有了一个可以做安全检查的空间,同时还有了初始化后的复制字符串。该方法同样是一个令人非常印象深刻的方法。

在两个修改过的函数中,共新增了6处这样的长度检查。尽管该变化与CVE-2017-11882无关,但我们认为是微软注意到了一些附加的攻击向量也有导致缓冲区溢出的潜在风险,因此决定主动修复它们。

在修复了相应代码,并手工编译了新版本的公式编辑器后,补丁将EQNEDT32.EXE的版本号改为2017.8.14.0,并将PE头中的时间戳设置为2017年8月14日(十六进制的5991FA38)。自接到提交之日起,整个漏洞分析及修复过程仅进行了10天。

结语

在不对修改后的源代码进行重新编译的前提下,修改一个项目的汇编代码是非常困难的。我们仅能推测,微软使用这种方式是由于源代码已经丢失。

考虑到旧版本的数学公式编辑器(Equation Editor)已经进入大家的视野,有可能性之后还会发现它的其他相关漏洞。虽然Office自2007年起就已经有了一个新的数学公式编辑器,但我们还是不能删除旧版EQNEDT32.EXE。因为,目前依然有大量此前的文档包含数学公式,简单地删除旧版本EXE,将会导致我们含有数学公式的文件无法正常编辑。

那么我们如何通过0patch的微补丁(Micropatch)解决该漏洞呢?实际会比想象容易得多。我们不需要减少现有的代码来为加入的代码腾出空间,因为0patch会保证我们能得到需要的位置。同样,我们也不需要优化memcpy,不需要找到替代的地方来暂时存储一个值以备之后使用。正是这种自由和灵活的特点,使得通过内存中微补丁的方案比在文件中修改的方案要更加容易、迅速。我也建议微软今后可以考虑使用内存中的微补丁来修复关键的漏洞。

(完)