CVE-2021-1665分析

译文声明

本文原作者Hardik Shah,原文来源https://www.mcafee.com/blogs/other-blogs/mcafee-labs/analyzing-cve-2021-1665-remote-code-execution-vulnerability-in-windows-gdi/

介绍

CVE-2021-1665是Windows GDI+ 中的一个远程代码执行漏洞,GDI+即Microsoft Windows 图形设备接口+,各种应用程序使用GDI+在视频显示器和打印机上使用不同的图形功能,Windows应用程序不直接访问设备驱动程序等图形硬件,但它们与GDI交互,然后GDI又与设备驱动程序交互,这样Windows应用程序就有了一个抽象层和一组供每个人使用的通用API。

GDI+因为格式复杂,因此有各种已知的漏洞历史。我们在McAfee上不断地fuzz各种开源和闭源软件,包括Windows GDI+,在过去几年中,我们向Microsoft报告了包括GDI在内的各种Windows组件的各种问题,并收到了它们的CVE。

本文中我们详细分析了我们使用WinAFL发现的一个此类漏洞:CVE-2021-1665 – GDI+ 远程代码执行漏洞,该问题已于 2021年1月作为 Microsoft 补丁的一部分得到修复。

winafl介绍

WinAFL是一个流行的Linux AFL fuzzer的Windows端口,由谷歌Zero项目的Ivan Fratric维护,WinAFL使用DynamoRIO进行动态二进制检测,它需要一个线束程序,线束是一个简单的程序,它调用我们想要fuzz的API。

WinAFL已经提供了一个简单的线束,我们可以在代码中启用“Image->GetThumbnailImage”代码,该段代码做了注释,以下是Fuzz GDI + Image和GetThumbnailimage API的线束代码:

如您所见,这一段代码是从提供的输入文件创建一个新的图像对象,然后调用另一个函数来生成缩略图图像,这是一个优异的攻击向量,使用缩略图的各种Windows应用程序都可能会受到影响,此外这只需要很少的用户交互,因此使用GDI+并调用GetThumbnailImage API的软件很脆弱。

语料库收集

良好的语料库会为fuzz提供一个好的基础,除了从其他软件和公共EMF文件中获取测试语料库之外,我们还可以使用Google或GitHub获取测试语料库,这些文件是针对其他漏洞发布的,我们已经生成了一些测试文件,我们通过对Microsoft网站上提供的示例代码进行修改生成一些测试文件,下面的示例使用一个带有EMFplusDrawString和其他记录的EMF文件:

参考

最小化语料库

在我们收集了初始语料库文件后,我们需要将其最小化,我们可以使用名为winafl-cmin.py的实用程序将其最小化,如下所示:

winafl-cmin.py -D D:\\work\\winafl\\DynamoRIO\\bin32 -t 10000 -i inCorpus -o minCorpus -covtype edge -coverage_module gdiplus.dll -target_module gdiplus_hardik.exe -target_method fuzzMe -nargs 2 — gdiplus_hardik.exe @@

Winafl如何工作

WinAFL使用内存模糊的概念,我们需要向WinAFL提供一个函数名,它将在函数开始时保存程序状态,并从语料库中取出一个输入文件,使其变异,并将其提供给函数。

它将监控任何新的代码路径或崩溃,如果它找到了一个新的代码路径,它将把新文件视为一个有趣的测试用例,并将其添加到队列中进行进一步的变异,如果发现任何崩溃,它将保存崩溃文件到的崩溃文件夹中。

以下图片显示fuzz流程:

使用Winafl进行fuzzing

我们编译了harness程序,收集并最小化了语料库,我们就可以运行这个命令来用WinAFL模糊我们的程序:

afl-fuzz.exe -i minCorpus -o out -D D:\work\winafl\DynamoRIO\bin32 -t 20000 —coverage_module gdiplus.dll -fuzz_iterations 5000 -target_module gdiplus_hardik.exe -target_offset 0x16e0 -nargs 2 — gdiplus_hardik.exe @@

结果

我们发现了一些崩溃并对这些崩溃作了分类,我们在“gdiplus!”BuiltLine::GetBaselineOffset "中发现了一个崩溃,它的调用堆栈如下所示:

如上图所示,程序在试图从edx+8指向的内存地址读取数据时崩溃,我们可以看到它的寄存器ebx, ecx和edx包含c0c0c0c0,这意味着二进制文件启用了页堆,我们还可以看到c0c0c0c0被作为参数传递给“gdiplus!”FullTextImager: :RenderLine”函数。

根据补丁寻找根本原因

要找出根本原因,我们可以使用补丁差异——即我们可以使用IDA BinDiff插件来确定补丁对文件做了哪些更改,幸运的话我们可以通过查看更改的代码轻松地找到根本原因,所以我们可以生成gdiplus.dll补丁和未补丁版本的IDB文件,然后运行IDA BinDiff插件来查看更改。

我们可以看到在补丁文件中添加了一个新函数,这个函数好像是BuiltLine对象的析构函数:

我们还可以看到,有一些函数的相似度小于1,其中一个函数是FullTextImager::BuildAllLines,如下所示:

现在,为了确认这个函数是否真的是被修补的那个,我们可以在windbg中运行我们的测试程序和POC,并在这个函数上设置断点,我们可以看到命中了这个断点,程序不再崩溃了:

下一步我们需要确定这个函数中有什么变化修复了这个漏洞,我们可以检查一下这个函数的流程图,我们可以看到如下内容,因为有太多的变化来识别漏洞,需要看看差异:

左边显示未修补的dll,右边显示修补过的dll:

1、绿色表示补丁块和未补丁块相同。

2、黄色块表示未打补丁和打补丁的dll之间有一些不同。

3、红色块显示dll中的差异。

如果我们放大黄色方块,我们可以看到下面的内容:

我们可以注意到一些变化,修补的DLL中删除了几个块,所以补丁差异将不足以识别这个问题的根本原因,但这为使用其他方法(如windbg)进行调试查找何处以及查找什么提供了有价值的提示,我们可以从上面的混合输出中找到一些观察结果:

1、在未修补的DLL中,如果我们仔细检查,我们可以看到一个对“GetuntrimmedCharacterCount”函数的调用,稍后还有另一个对“SetSpan::SpanVector”函数的调用。

2、在这个补丁DLL中,我们可以看到有一个对“GetuntrimmedCharacterCount”的调用,其中EAX寄存器中存储的返回值被检查,如果它是0,那么控制跳转到另一个位置- BuiltLine对象的析构函数,这是在补丁DLL中新添加的代码:

wKg0C2DdcnqAEbrSAAA1wBsnzns547.png

所以我们可以假设这是漏洞固定的地方,现在我们需要弄清楚以下几点:

1、为什么我们的程序和提供的PoC文件崩溃了?

2、文件中的哪个字段导致了这个崩溃?

3、该字段的值是多少?

4、程序中的哪些条件导致这个崩溃?

5、这个崩溃如何修复?

EMF文件格式

EMF也被称为增强元文件格式,它用于独立存储图形图像设备,EMF文件由不同长度的记录组成,它可以包含各种图形对象的定义、绘图命令和其他图形属性。

资料来源:MS EMF文档。

通常EMF文件包括以下记录:

1、EMF标题,包含关于EMF结构的信息。

2、EMF记录,可以是各种可变长度的记录,包含有关图形属性、绘制顺序等信息。

3、EMF EOF记录,EMF文件中的最后一条记录。

EMF文件格式的详细规格可在Microsoft网站上看到

在EMF文件中定位漏洞记录

通常,EMF中的大多数问题都是由于格式错误或损坏的记录造成的,我们需要找出什么记录类型导致了这次崩溃,我们查看调用堆栈可以看到以下内容:

我们可以注意到对“gdiplus!”GdipPlayMetafileRecordCallback”函数的调用:

在这个函数上设置断点并检查参数可以看到:

我们可以看到EDX寄存器包含一些内存地址,我们可以看到这个函数的参数是00x00401c、0x00000000和0x00000044。此外检查EDX寄存器指向的位置时可以看到以下内容:

检查我们的POC EMF文件可以看到这个数据属于来自偏移量ox15c的文件:

通过查看EMF规范并手动解析记录,我们可以很容易地发现这是一个“EmfPlusDrawString”记录,其格式如下所示:

在我们的例子中:

Record Type = 0x401c EmfPlusDrawString record

Flags = 0x0000

Size = 0x50

Data size = 0x44

Brushid = 0x02

Format id = 0x01

Length = 0x14

Layoutrect = 00 00 00 00 00 00 00 00 FC FF C7 42 00 00 80 FF

String data =

现在我们找到了可能导致崩溃的记录,之后要做的是找出为什么我们的程序会崩溃的原因,我们调试和检查代码可以看到控件会到达“gdiplus!FullTextImager::BuildAllLines”函数,当我们反编译这段代码时,我们可以看到如下内容:

下面是函数调用的层次结构:

执行流程总结

1、“Builtline::BuildAllLines”函数中有一个while循环,程序在该函数中分配0x60字节内存,然后调用Builtline:: Builtline函数。

2、“Builtline:: Builtline”函数将数据移动到新分配的内存中,然后调用“Builtline:: GetUntrimmedCharacterCount”函数。

3、将“BuiltLine::GetUntrimmedCharacterCount”的返回值添加到循环计数器ECX寄存器中,这个过程不断重复,直到循环计数器ECX<ECX寄存器的string length,这里是0x14。

4、循环从0开始,0x13终止,或者当“GetUntrimmedCharacterCount”的返回值为0时终止。

5、脆弱的DLL中程序不会因为循环计数器增加而终止,这里“BuiltLine::GetUntrimmedCharacterCount”返回0,它被添加到循环计数器ECX中,并不增加ECX值,它分配0x60字节的内存并创建另一行,破坏随后导致程序崩溃的数据,循环执行21次,而不是20次。

细节

1.“Builtline::BuildAllLines”分配0x60或96字节的内存,调试器中它看起来如下:

2.然后调用“BuiltLine::BuiltLine”函数,并将数据移动到新分配的内存中:

3.这发生在side a while循环中,并且有一个函数调用“BuiltLine::GetUntrimmedCharacterCount”。

4.“BuiltLine::GetUntrimmedCharacterCount”的返回值存储在0x12ff2ec位置,该值为1,如下图所示:

5.该值添加到ECX中:

6.然后有一个检查判断ECX<EAX,如果为true,它将继续循环,反之它将跳转到另一个位置:

7.现在在存在漏洞的版本中如果“BuiltLine::GetUntrimmedCharacterCount”的返回值为0,则循环不存在,这个0将被添加到ECX中,这意味着ECX不会增加,因此循环将使用0x13的“ECX”值再执行1次,这将导致循环执行21次,而不是20次,这就是问题的根源。

此外经过一些调试,我们可以弄清楚为什么EAX寄存器包含14,它从POC文件中读取了偏移量:0x174:

如果我们回忆一下,这是EmfPlusDrawString记录,0x14是我们之前提到的长度,之后程序到达“FullTextImager::Render”函数会破坏EAX的值,因为它读取未使用的内存:

这将作为参数传递给“FullTextImager::RenderLine”函数:

然后程序将在访问该位置时崩溃:

当访问一个无效的内存位置和处理字符串数据字段时,程序在处理EMF文件中的EmfPlusDrawString记录时崩溃了,主要是该程序没有验证“gdiplus!BuiltLine::GetUntrimmedCharacterCount”函数的返回值,导致采取不同的程序路径,损坏寄存器和各种内存值,最终导致崩溃。

如何解决问题

正如我们通过上面的补丁差异发现的那样,补丁添加了一个检查,它决定了“gdiplus!”BuiltLine: GetUntrimmedCharacterCount”函数的返回值。

如果返回值为0,则程序xor ebx,ebx后的EBX寄存器包含计数器并跳转到一个位置,该位置调用Builtline对象的析构函数:

下面是阻止这个问题的析构函数:

结论

GDI+是一个非常常用的Windows组件,像这个的漏洞可能会影响全球数十亿个系统,我们建议用户适当的更新并保持其Windows部署的最新状态。

我们在McAfee上正在持续fuzz各种开源和封闭的源库,并与供应商合作解决这些问题,通过负责地披露这些问题,给他们适当的时间来修复问题,并根据需要发布更新。

我们感谢Microsoft与我们一起修复了这个问题并发布了更新。

查看原文