漏洞分析:7zip CVE-2016-2334 HFS+代码执行漏洞

 

一、前言

2016年,Talos实验室公布了关于CVE-2016-2334漏洞的一份安全公告,该漏洞为远程执行代码漏洞,影响特定版本的7zip压缩软件。在文本中,我们会详细分析该漏洞的利用方法,构造可用的利用工具,我们的实验环境为Windows 7 x86系统,其中安装了存在漏洞的7zip软件(x86平台,15.05 beta版)。

 

二、漏洞分析

首先来看看7zip中存在漏洞的代码片段。读者可以阅读之前的安全公告了解更多技术细节。

当7zip解压缩HFS+文件系统上的某个压缩文件时就会触发该漏洞,漏洞位于CHandler::ExtractZlibFile函数内部。如上图所示,在代码中1575行,ReadStream_FALSE函数根据size参数获取需读取的字节数,将这些数据拷贝到名为buf的一个缓冲区中。CHandler::Extract函数中限定了buf缓冲区的大小为0x10000 + 0x10(固定值)。问题在于,用户可以控制size参数,程序会从文件中直接读取该参数值(第1573行),没有经过任何过滤处理。

 

总结一下:

1、size参数:32位值,攻击者可以完全控制该参数。

2、buf参数:大小固定的缓冲区(0x10010字节)。

3、ReadStream_FALSE函数:为ReadFile函数的封装函数,换句话说,覆盖buf缓冲区的数据直接来自于输入文件,并且没有任何字符限制。

注意:在堆溢出场景中,如果溢出现象由read/ReadFile等类似函数引发,而这些代码通常最终会由内核(kernel)来执行,因此如果我们启用页堆(page heap)选项,就不会出现溢出现象。内核能够感知不可用的页面(free/protected/guarded),直接导致系统调用(system call)返回错误代码。在启用page heap选项前我们需要记住这一点。

 

我们需要创建一个基于HFS+文件系统的镜像,然后再修改这个镜像,以触发该漏洞。我们可以使用Apple OSX系统来完成这一任务,在Windows平台上,我们也可以使用python脚本完成该任务。在OSX Snow Leopard 10.6及以上操作系统中,我们可以使用DiskUtil应用,带上–hfsCompressio参数来创建该镜像。接下来,我们会详细介绍如何修改该镜像来触发漏洞。目前,我们可以先来看看修改后的镜像,其结构如下所示:

c:> 7z l PoC.img

Scanning the drive for archives:
1 file, 40960000 bytes (40 MiB)
Listing archive: PoC.img
--

Path = PoC.img
Type = HFS
Physical Size = 40960000
Method = HFS+
Cluster Size = 4096
Free Space = 38789120
Created = 2016-07-09 16:41:15
Modified = 2016-07-09 16:59:06

Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2016-07-09 16:58:35 D....                            Disk Image
2016-07-09 16:59:06 D....                            Disk Image.fseventsd
2016-07-09 16:41:15 D....                            Disk Image.HFS+ Private Directory Data
2016-07-09 16:41:16 .....       524288       524288  Disk Image.journal
2016-07-09 16:41:15 .....         4096         4096  Disk Image.journal_info_block
2016-07-09 16:41:15 D....                            Disk Image.Trashes
2014-03-13 14:01:34 .....       131072       659456  Disk Imageksh
2014-03-20 16:16:47 .....         1164          900  Disk ImageWeb.collection
2016-07-09 16:41:15 D....                            Disk Image[HFS+ Private Data]
2016-07-09 16:59:06 .....          111         4096  Disk Image.fseventsd000000000f3527a
2016-07-09 16:59:06 .....           71         4096  Disk Image.fseventsd000000000f3527b
2016-07-09 16:59:06 .....           36         4096  Disk Image.fseventsdfseventsd-uuid
------------------- ----- ------------ ------------  ------------------------

2016-07-09 16:59:06             660838      1201028  7 files, 5 folders

 

三、准备测试环境

3.1 编译7zip 15.05 beta

为了让漏洞分析利用过程更加易懂,我们可以在7zip源代码中添加一些调试信息,然后编译生成7zip。我们可以修改编译文件(Build.mak),启用调试符号功能,如下所示:

Standard:

- CFLAGS = $(CFLAGS) -nologo -c -Fo$O/ -WX -EHsc -Gy -GR-
- CFLAGS_O1 = $(CFLAGS) -O1
- CFLAGS_O2 = $(CFLAGS) -O2
- LFLAGS = $(LFLAGS) -nologo -OPT:REF -OPT:ICF

With debug: 

+ CFLAGS_O1 = $(CFLAGS) -Od
+ CFLAGS_O2 = $(CFLAGS) -Od
+ CFLAGS = $(CFLAGS) -nologo -c -Fo$O/ -W3 -WX -EHsc -Gy -GR- -GF -ZI
+ LFLAGS = $(LFLAGS) -nologo -OPT:REF -DEBUG

7zip源码编译完成后,我们可以先用PoC测试一下,看溢出前的堆布局结构,命令如下:

"C:Program FilesWindows Kits10Debuggersx86windbg.exe" -c"!gflag -htc -hfc -hpc" t:projectsbugs7zipsrc7z1505-srcCPP7zipinstalled7z.exe x PoC.hfs

注意:调试会话中记得使用!gflag命令来关闭所有堆选项。

来看看该缓冲区后面的内存块,如下所示:

看上去我们很有希望能够利用这种堆结构。我们可以找到带有vftable(虚函数表)的一些对象。这些对象有可能能够用来操纵代码的执行流程。利用我们自己的数据覆盖vftable后,我们可以绕过现代操作系统中的堆溢出保护机制,接管代码执行流程。

做个测试,在不该变PoC的前提下,覆盖调试会话中的对象,继续执行,如下所示:

根据测试结果,程序会调用溢出点之后被覆盖的对象,调用过程非常快,因此没有其他内存操作(如alloc/free)能够影响调用点之前已损坏的堆结构。如果不满足这种情况,程序就会出现崩溃。现在我们需要确认此时此刻堆结构与标准版本的7zip一致。需要注意的是,调试版7zip的堆结构与正式版的可能有明显不同,这一点非常重要。

3.2 定位ExtractZLibFile函数

为了确认标准版7zip的堆结构,我们需要找到ExtractZLibFile函数,这个函数中会调用ReadStream_FALSE函数。

为了定位该函数,我们可以在IDA中查找该函数体中使用的某个变量:

0x636D7066

(备注:我们事先已经修改了IDA中的函数名)

进入.text1001D9D9地址后,我们的确能够找到那个函数。

我们可以在ReadStream_FALSE调用处(0x1001D7AB)设置断点,分析buf附近的堆结构。

小贴士:请注意edx指向的是buf缓冲区所在的地址。

堆布局如下所示:

不幸的是,我们发现使用标准版的7zip会得到一个不同的堆布局。比如,在buf缓冲区之后([size 0x10010]),我们无法找到包含vftable的任何对象。

请注意:即使没有加载调试符号或者RTTI,我们也可以在WinDBG中使用!heap -p -h命令来显示带有vftable的对象。如下所示:

013360b0 0009 0007  [00]   013360b8    0003a - (busy)
013360f8 0007 0009  [00]   01336100    00030 - (busy)     ←-- object with vftable

? 7z!GetHashers+246f4

01336130 0002 0007  [00]   01336138    00008 - (free)
01336140 9c01 0002  [00]   01336148    4e000 - (busy)
* 01384148 0100 9c01  [00]   01384150    007f8 - (busy)

我们的目标是开发在实际环境中能够使用的利用工具,因此我们需要找到能够修改堆结构的方法,再重新调整堆布局,以便利用这个漏洞。

 

四、规划利用路线

堆结构主要受到我们构造的PoC.hfs文件的内容及其内部数据结构的影响。如果我们想要修改当前的堆布局,我们需要创造一个合理且可靠的HFS+镜像文件生成器,利用这个生成器将符合要求的HFS+数据添加到文件镜像中,使我们能够重新调整堆分配情况,确保在我们的buf缓冲区后可以看到带有vtable的对象。

我们并不需要构造能够实现所有结构、配置及功能的超级HFS+镜像文件生成器。这个生成器只要能够支持生成我们所需要的元素,使得我们能够重新调整堆布局、触发漏洞即可。

大家可以查看这份文档了解有关HFS+文件结构的详细知识,深入理解HFS+文件格式更有助于理解这次调试过程。

4.1 识别能够改变堆结构的元素

首先我们需要确定我们文件数据在堆上的具体位置(数据大小可变)。我们可以先寻找负责解析HFS+格式的具体代码。

请注意:7zip在解析特定的格式之前可能会先执行一些命令。比如,7zip可能会执行一些指令来“动态”检测文件格式。

一步一步调试跟踪负责处理PoC.hfs的代码后,我们可以找到在文件解析过程中负责将数据写到堆上的所有函数。

回到源代码中,我们首先可以找到这一个函数:

进一步跟进,可以找到这个函数:

经过一些测试后,我们可以识别出如下这样一段代码:

LoadName函数的代码如下:

其中每个属性都包含一个名称(UTF-16字符串),名称字符串在堆上分配,其大小可变。这看起来是一个绝佳的利用点。我们可以添加尽可能多的属性,将属性名作为喷射点加以利用。这里唯一的限制是attr.ID必须设置为除file.ID之外的其他值。

4.2 编写HFS+生成器

我们希望生成的文件结构如下所示:

7zip开发者在实现HFS+文件系统解析器时,并没有直接遵循标准的HFS+文档。因此,我们首先需要分析7zip,看看7zip究竟如何解析HFS+。我们公布了一个文件生成脚本,该脚本可以创建漏洞利用所需的载荷文件,大家可以访问该网址下载此脚本。

图注:在文件格式解析过程中使用的010 Editor模板

如上所述,我们限制了生成器的功能,只能生成必要的文件结构,以触发本文描述的漏洞。将OVERFLOW_VALUE(用来溢出buf缓冲区的缓冲区大小)设置为0x10040后,我们可以生成触发漏洞的载荷文件,在调试会话中得到如下结果:

单步跟进代码执行过程,分析溢出点所在位置:

可以确认我们的HFS+生成器能正常工作。现在,将OVERFLOW_VALUE变量的值增加到0x10300,这个值足以溢出临近的大小为0x310字节的空闲块。同时,这个块中还包含带有vftable的一个对象。如下所示:

可以发现,跟在buf缓冲区后的空闲块变大了,这样一来我们无法成功溢出带有vftable的下一个对象。看来程序会根据我们的文件内容来分配内存空间。为了寻找这条指令的具体位置,我们可以设置如下断点条件:

bp ntdll!RtlAllocateHeap "r $t0=esp+0xc;.if (poi(@$t0) > 0xffff) {.printf "RtlAllocateHeap hHEAP 0x%x, ", poi(@esp+4);.printf "Size: 0x%x, ", poi(@$t0);.echo}.else{g}"

为了简化寻找过程,我们可以使用之前编译好的带有调试信息的7zip:

当缓冲区大小与我们的文件大小相同时,调试器就会触发断点。快速分析后,我们发现断点位于程序中负责启发式检测文件格式的代码片段中。

7zip会分配一个足够大的缓冲区,来处理整个文件内容的大小,在释放先前分配的缓冲区之前,7zip会尝试判断文件的格式。随后,在分配buf缓冲区时,会用到被释放的缓冲区内存空间。这也是为什么我们会在内存块后看到一个空闲区域,并且在增加载荷大小时,该空闲区域大小也会跟增增长。这是不是意味着我们无法成功利用该漏洞?并非如此,你有没有注意到我们保存文件时所使用的后缀名?如果我们想规避7zip的启发式文件检测机制,我们只需要使用正确的文件扩展名即可(本例中为.hfs扩展名)。如果我们使用这个扩展名,7zip就不会执行启发式函数,相应的堆结构如下所示:

 

五、调整利用思路

现在,我们总结一下前面已知的信息,然后再想办法找到可以使用的方法来构造利用程序:

1、我们的目标缓冲区(buf)大小为固定值:0x10010。

2、受此大小值影响,该缓冲区总是由堆后端来分配。详细信息可参考此处资料

3、在溢出之前,我们可以分配任意数量的对象,这些对象的大小可以为任意值。

4、我们无法在堆上执行或触发任何free操作。

5、我们无法在溢出点之后执行任何alloc或free操作。

考虑到上述情况,我们无法执行某些操作,并且Windows 7上还存在堆防护机制,因此我们可以采用如下方法:

1、我们应该寻找带有vftable的一个对象,并且程序在溢出点之后会尽可能快地调用该对象。这一点非常重要,因为如果vftable的调用位置与内存中的溢出点距离过远,那么在此期间程序很有可能会执行一些alloc/free操作,导致程序崩溃。

 

2、找到这种对象后,使用与该对象大小相同的一些属性(名称)来喷射堆。之所以这么做,是因为分配对象时,如果对象大小与目标对象大小相同,并且个数大于0x10,大小小于0x4000(该值为低碎片堆(Low Fragmentation Heap,LFH)最大对象的大小值),那么我们就会激活LFH,为该大小的对象分配空闲内存块。这样溢出缓冲区后就会分配空闲的内存槽(free slot),这些对象会存放在这些槽中。

5.1 识别目标对象

制定利用策略后,我们需要寻找可以覆盖的合适对象。为了找到这个对象,我们可以使用WinDBG的一个简单的JS脚本,该脚本可以打印出带有vftable的对象及其堆栈跟踪(stack trace)情况。

大家可以访问此处链接下载该脚本。

执行结果如下:

首先我们可以先寻找溢出点所在函数(ExtractZlibFile)中是否会分配这些对象,如果存在,程序很有可能会在溢出后马上调用这些对象。根据上图结果,我们可以找到可能满足要求的两个候选者。

上述对象的定义语句如下所示:

Line 1504  CMyComPtr<ISequentialInStream> inStream;
(...)
Line 1560  CBufInStream *bufInStreamSpec = new CBufInStream;
Line 1561  CMyComPtr<ISequentialInStream> bufInStream = bufInStreamSpec;

当函数退出时,就会调用这些对象的析构函数(虚拟函数)。如果想最快触发析构动作,我们可以将溢出缓冲区的首字节设置为0xF:

5.2 移动这些对象

识别出待溢出的对象后,我们需要使用与对象大小一致的属性结构来喷射堆(属性结构中包含name字符串),这两个大小值分别为0x20及0x30。

我们可以使用如下代码来完成这个任务:

我们可以写一个WinDBG控制脚本,不断增加属性结构的数量,直至目标对象在溢出缓冲区后分配,或者我们也可以手动来完成这一过程。

我们决定手动完成这一任务,操作也很简单,就是逐步增加对象数量(依次为10、20、30),观察堆布局。当对象位置开始到达缓冲区所在位置时,我们开始逐一增加对象数量。

经过一些尝试后,我们确定所需的对象数量为139个:

139 (0x20 + 0x30 + 2 0x18)

此时堆结构如下所示:

看起来这种堆结构很有希望能够利用成功。将buf缓冲区的位置(该地址为0x12df9c8,计算方法为调用指令所在地址(0x12df9d0)减去8字节)与目标对象的地址(即0x12efdf8)相减,我们就能确定覆盖目标对象需要使用多少字节。为了确定我们载荷可以使用的空间大小,我选择堆上可用的最后一个地址(上图中没有显示出来),这样可以算出来最大可用空间。计算出空间后,我们可以更新OVERFLOW_VALUE变量的值为0x12618。

现在,我们可以再次生成载荷文件,执行应用程序,确认vftable已被成功覆盖:

根据上图所示,我们成功覆盖了vftable,现在我们可以开始编写漏洞利用工具。

5.3 检查当前的防御机制

为了进一步开发利用工具,我们需要检查当前7zip版本所使用的防御机制。10.05版7zip所使用的防御机制如下图所示:

如下图所示,7zip并不支持地址空间布局随机化(Address Space Layout Randomization,ASLR)或者数据执行保护(Data Execution Prevention,DEP)机制。去年漏洞安全公告发布以后,我们曾希望这种情况能有所改观,但事实证明并非如此:

如果你使用的是64位的7zip,操作系统会强制启动DEP机制。

5.4 寻找载荷

在寻找可用的gadget(指令代码)之前,我们可以先看一下栈中指向载荷的所有寄存器及指针。

如上图所示,有几个位置分别指向载荷中的不同部位,包括如下位置:

ESI
EDX
ESP
ESP-C
ESP+30
EBP+40
EBP-2C
EBP-68

我们需要确认从载荷到vftable对象的具体偏移量。由于ESI指向的是vftable对象,EDX指向的是我们的缓冲区,因此我们可以通过EDX减去ESI来计算出这个偏移量:

0:000> ?esi - edx 

Evaluate expression: 66608 = 00010430

将该偏移处所对应的值存放到载荷中,我们可以得到如下结果:

由于偏移量有所变化(+8),因此该值也发生了变化。现在我们可以开始寻找可用的gadget。

5.5 嵌套指针

由于我们要覆盖指向vftable的指针,因此我们需要识别gadget以及指向gadget的指针。

我们可以使用如下工具来完成这一任务:

1、RopGadgets

2、Mona

在分析过程中,我们可以搭配使用多种工具,找到尽可能多的gadget。

首先,使用RopGadgets生成7z.exe以及7z.dll可用的gadget:

接下来,根据这份清单,使用Mona来找到指向这些gadget地址的指针。

5.6 利用7zip中缺失的DEP机制

由于此7zip版本不支持DEP机制,因此利用该漏洞最简单的一种方法就是将代码执行流程重定向到位于堆上的缓冲区。回顾一下我们前面得到的那些指针,从中我们可以找到符合这些要求的一些指针,如下所示:

从上图可知,有多个地址包含相同的指针值。这些地址非常有用,因为在我们的gadget中,我们会使用ESP寄存器所指向的这些指针,来将代码执行流程重定向到我们的缓冲区。我们会将指向伪造的vftable的指针地址存放在ESI中,而这些地址包含的值与ESI所指向的值相同。

掌握这些信息后,我们需要识别指针对应反汇编代码中的哪条指令。

如你所见,POP ES这条指令会导致程序异常。此外,栈上并不会出现任何值被pop到ES上的情况。幸运的是,经过反汇编后,还有一条gadget地址能够得到较为可用的一条指令:

0x1007c748 - 8  = 0x1007c740

EDI指向的是一段可写的内存区域,因此我们应该能够执行这些指令。

我们还需要注意,这条指令中用到了我们用来填充缓冲区的那些字节(0xcc)。

考虑到这一点,在设置shellcode在缓冲区中的偏移位置时,我们需要忽略3个字节。

5.7 添加ShellCode

现在我们可以开始添加shellcode,其偏移地址为:

fake_vftable_ptr_offset = 0x00010430 + 3 ("0xCC")

我们可以使用Metasploit中包含的msfvenom工具来生成shellcode:

添加shellcode后,新的脚本如下所示:

5.8 测试利用程序

现在一切已准备就绪,我们可以生成HFS文件,测试漏洞利用程序,测试视频如下所示:

http://v.youku.com/v_show/id_XMzIwMTg4MTkxMg==.html

可以看到我们的shellcode能够成功执行。

5.9 稳定性分析

我们已经证实,可以使用大小为0x20以及0x30的对象来喷射堆,实现漏洞利用,但这种方法的稳定性如何呢?

相同版本的7zip在解析相同的HFS文件时应该会得到相同的堆布局,但我们还需要考虑堆上分配的一些不确定因素,如环境变量、命令行参数字符串、指向载荷文件的路径等。在不同系统上,这些因素可能有所不同,有可能会改变堆布局。

 

不幸的是,在本文案例中,这些不确定因素与我们的溢出缓冲区位于同一个堆上,至少我们所使用的命令行版7zip会面临这种情况。分析用于分配目标缓冲区的堆内存结构后,我们可以看到如下信息:

检查堆布局后,我们可以找到一个字符串,该字符串为需要解压的HFS文件的具体路径。这个字符串变量的长度会显著影响堆上的空闲及已分配空间,因此会影响堆喷射对象布局,导致漏洞利用失败。

 

如果想要解决空闲堆空间差异问题,一种方法是创建足够大的分配空间,耗尽堆上可用的空闲空间,创建过程中需要考虑文件路径、环境变量长度等系统限制因素。这种方法留待大家来完成,大家也可以顺便研究一下图形界面版的7zip的内存布局。

 

六、总结

现代系统中,虽然与Web浏览器漏洞利用相比,基于堆缓冲区溢出的应用级漏洞(如压缩程序、通用文件解析器等程序)无法灵活操纵堆布局,但我们还是可以探索这种漏洞利用方式。由于我们难以使用堆元数据损坏方法来利用漏洞,我们可以通过覆盖应用程序的数据来接管代码执行流程,完成漏洞利用任务。目前某些产品中仍然没有具备标准的漏洞利用防护机制,因此我们的利用过程并没有想象中那么难。

 

(完)