此前,为了写好那篇关于7z PPMD压缩算法漏洞的文章,我阅读了大量的7-Zip源代码,然后从中发现了很多非常有价值的信息(这些信息可以更好地帮助我分析反病毒产品中的漏洞)。因此,我准备利用我手中所掌握的这些信息来分析一下Bitdefender的7z模块。
我之前曾写过一篇关于简化文件处理过程的文章,而Bitdefender 7z PPMD压缩算法的栈缓冲区溢出漏洞也是一个通过移除检测机制(实际上是移除了负责进行检测的相关源码)来简化文件处理流程的绝佳例子。
除此之外,这个漏洞也证明了有的时候向已存在的代码中添加新的代码是有多么的困难。但是一般来说,代码的修改永远都是不可避免的,哪怕是只修改其中的一小部分那也是需要考虑非常多因素的,如果考虑不周全的话,则很有可能会影响程序的内存分配以及文件访问的管理。而本文所要介绍的漏洞就是一个很好的例子,因为程序不正当地使用了内存分配函数并进一步导致Bitdefender的7z模块(7-Zip源代码)引起堆缓冲区溢出。
漏洞细节分析
当Bitdefender的7z模块在一份7z压缩文档中发现了一个EncodedHeader时,它会尝试使用LZMA解码器进行解压。该模块的代码似乎是基于7-Zip的源码进行开发的,但是Bitdefender的开发人员对代码进行了一些修改。
注:EncodedHeader的作用是在一份压缩文档中包含超过一个文件时对多个header进行压缩。关于7z文件格式的更多内容可以从7-Zip源码包中的DOC/7zFormat.txt中获取【传送门】。
简单来说,提取7z EncodedHeader的实现过程大致如下:
1.从7z EncodedHeader中读取unpackSize;
2.分配unpackSize字节数据;
3.使用7-Zip自带的LZMA解码器的C API来解码流数据;
下面给出的代码段显示了分配函数的调用过程:
1DD02A845FA lea rcx, [rdi+128h] //
1DD02A84601 mov rbx, [rdi+168h]
1DD02A84608 mov [rsp+128h], rsi
1DD02A84610 mov rsi, [rax+10h]
1DD02A84614 mov [rsp+0E0h], r15
1DD02A8461C mov edx, [rsi] //
1DD02A8461E call SZ_AllocBuffer
大家可以先回忆一下x64架构的函数调用惯例。实际上在这个代码段中,前两个整形参数是通过rcx和rdx传递的。
SZ_AllocBuffer是Bitdefender的7z模块中的一个函数,这个函数可以接受两个参数:
第一个参数result是一个指针,这个指针指向的是保存结果的内存地址。
第二个参数size则是待分配的内存空间大小。
接下来,让我们一起看一看该函数的代码实现:
260ED3025D0 SZ_AllocBuffer proc near
260ED3025D0
260ED3025D0 mov [rsp+8], rbx
260ED3025D5 push rdi
260ED3025D6 sub rsp, 20h
260ED3025DA mov rbx, rcx
260ED3025DD mov edi, edx //
260ED3025DF mov rcx, [rcx]
260ED3025E2 test rcx, rcx
260ED3025E5 jz short loc_260ED3025EC
260ED3025E7 call near ptr irrelevant_function
260ED3025EC
260ED3025EC loc_260ED3025EC:
260ED3025EC cmp edi, 0FFFFFFFFh //
260ED3025EF jbe short loc_260ED302606
260ED3025F1 xor ecx, ecx
260ED3025F3 mov [rbx], rcx
260ED3025F6 mov eax, ecx
260ED3025F8 mov [rbx+8], ecx
260ED3025FB mov rbx, [rsp+30h]
260ED302600 add rsp, 20h
260ED302604 pop rdi
260ED302605 retn
260ED302606 ; ------------------------------------
260ED302606
260ED302606 loc_260ED302606:
260ED302606 mov rcx, rdi //
260ED302609 call mymalloc
//[rest of the function omitted]
请注意其中的mymalloc函数,这是一个封装函数,它最终会调用malloc并返回处理结果。很明显,开发人员希望SZ_ALLocBuffer函数的size参数大小至少要32位以上,而这就是一个32位的值。
细心的同学可能已经发现了,编译器并没有成功地对上述代码中{*}所标注的地方(负责进行参数比较)进行优化。考虑到这里的比较结果还需要进行无符号比较(jbe),这就非常有意思了。
在SZ_AllocBuffer返回了处理结果之后,函数LzmaDecode将会被调用:
LzmaDecode(Byte *dest, SizeT *destLen, const Byte *src, SizeT *srcLen, /* further arguments omitted */)
需要注意的是,dest是SZ_AllocBuffer函数分配的一个缓冲区,而destLen应该是一个指向缓冲区大小的指针。
在引用实现中,SizeT被定义成了size_t。有趣的是,Bitdefender的7z模块同时使用了32位和64位版本的SizeT,而这两个版本都存在安全漏洞。我怀疑Bitdefender的开发人员这样做的目的是为了给32位和64位引擎提供不同的功能行为而设计的。
接下来,LZMA解码器会提取出参数中给定的src流数据,然后将*destLen字节数据写入到dest缓冲区中,其中的*destLen是7z EncodedHeader中的一个64位unpackSize,而最终的结果就是一个堆缓冲区溢出漏洞。
触发漏洞
为了触发这个漏洞,我们所创建的7z LZMA流数据中包含了我们需要写入堆内存中的数据。接下来,我们构建了一个7z EncodedHeader(unpackSize大小为(1),这样应该可以让函数SZ_AllocBuffer分配一个大小为一个字节的缓冲区空间了。