Group of Software Security In Progress

GoSSIP @ LoCCS.Shanghai Jiao Tong University

CVE-2016-3078 PHP ZipArchive Integer Overflow 分析

这个漏洞的影响范围是PHP 7.0.6版本以前的所有PHP 7.x 版本。PHP的源码可以在这里下到,https://github.com/php/php-src/

PHP源码架构

  • PHP源码里的核心库是在Zend目录下。负责php脚本的解析,执行等核心功能。
  • TSRM目录下是关于PHP多线程的库。
  • ext目录下是实现各种PHP扩展功能的代码。如:ftp,ssl,xml等,也包括这次主要要分析的zip解析的功能。

漏洞详情

关于CVE-2016-3078,在社区上有人发过:http://seclists.org/bugtraq/2016/Apr/159

主要问题是,当PHP在x86的机器上编译的时候,其中的zend_ulong类型会被编译成不同的长度。

Fig

以上代码来自Zend/zend_long.h。可以看到,如果是在x64环境下编译的话,zend_ulong是64位长度的;如果是x86下的话,就是32位长度的。然后,在php_zip_get_from()函数里,会把一个64位的长度赋值给一个zend_ulong类型的变量,形成整型溢出,然后是堆溢出,通过合理构造输入可以达到任意地址写。

执行流程

一个可以触发漏洞的简单的php脚本如下:

Fig

在php脚本里,解析zip文件时,先调用ZipArchive::open()来把zip文件读到内存里。在php源码里对应的代码在php_zip.c里:

Fig

其中,open()初始化了一个_ze_zip_object结构体:

Fig

其中的za指向了一个zip结构体,这个结构体存放的是与被解析的zip文件的内容相关的东西。

Fig

其中的zip_source_t *src指向的结构体与zip文件里的数据相关的东西。

Fig

其中的cb是一个union结构体,里面放的是在解压zip压缩包里的文件时调用的回调函数。在open()函数里,这个回调函数会被初始化成read_data()

Fig

再转回php脚本。open()完成之后,然后调用getFromIndex()或者getFromName()读取zip压缩包里的具体文件数据。在php源码里,这两个函数的都直接调用了php_zip_get_from()函数,而这个就是存在漏洞的函数。

在php_zip_get_from()函数里,会先从Executor Globals里把传入的参数读出来。然后会解析zip文件的dirEntry对应的文件目录,然后更新一个zip_stat结构,存放结果。

Fig

其中size是uint64的,对应zip Entry里的UncompressedSize位。之后会检查从php脚本里传入的参数len是不是小于1,如果是就会把这个size赋值到len。注意,这里len的类型是zend_long。

Fig

然后,调用zip_fopen_index()来解析zip结构,然后更新zip结构体的数据。在解析过程中,这个函数会对zip文件的压缩方式做区分。分别是encrypted,compressed,和store。

encrypted是加密,需要password;compressed是压缩;store是不压缩,直接存放原始文件数据。然后会在原来的zip结构体之上再重新封装一层zip结构体,并把新的结构体的回调函数注册成相应的解密、解压、crc检查的函数。并返回这个新的结构体的指针。

接下来,回到php_zip_get_from()函数。zend_string_alloc()函数是触发整型溢出的点。然后下面的zip_fread()函数是堆溢出的点。

Fig

在zend_string()里会先对len做一个边界对齐,会在原始len的大小上加上0x14然后再mask 0xFFFFFFFC。攻击中可以把UncompressedSize设成0xFFFFFFFE,然后会分配一个0x10大小的堆。

Fig

这里面,调用了pemalloc分配堆块。它是php内部实现的一个Memory Allocator。源码在zend_alloc.c里,具体就不展开了。它里面对小的堆块的分配做了优化,所有小堆块都连续分布在一个或多个连续的内存页上,每个空闲的小堆块都用一个单向链表的形式组织起来。第一个空闲的小堆块的地址会放在全局的chunk head的free_slot里。

Fig

当要分配小堆块的内存的时候,会先从free_slot里拿出那个地址,然后根据链表,把下一个free的block地址放到free_slot里。代码如下:

Fig

所以,这就给堆溢出带来了机会。可以先溢出改写下一个free block的头,然后再分配一次,把构造的free block头写入free_slot,然后再分配一次,构造出任意地址写。

实际的堆溢出发生在后面的zip_fread()函数里。他会递归地调用之前提到的zip结构体里注册的回掉函数。于是,就会先调用之前提到的decrypt/inflate/crc check,然后再调用在open()的时候注册的read_data()函数。这里基本上可以看作是memcpy。