前言

想研究一下 PHP 的 unserialize 流程的想法是从网鼎杯的一道 PHP 反序列化题开始的,当时发现本地 public 序列化的字段可以在远程反序列化成 protected,就觉得很神奇,于是计划阅读源码跟进一下。

本文主要侧重类的非静态成员变量。


测试用类

class Twings {
    public $a = "Aluvion";
    protected $b = 1;
    private $c = array("PHP", "Java", 7);
    private String $d;
}

class

class 的编译及成员变量类型检测

php 代码的词法分析略,这部分太长了,直接到解析 AST,获取成员变量数据进行检测的部分,在 zend_compile.c 的 zend_compile_prop_decl 函数:

void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t flags) /* {{{ */
{
    ...
    if (ce->ce_flags & ZEND_ACC_INTERFACE) {
        zend_error_noreturn(E_COMPILE_ERROR, "Interfaces may not include member variables");
    }

    if (flags & ZEND_ACC_ABSTRACT) {
        zend_error_noreturn(E_COMPILE_ERROR, "Properties cannot be declared abstract");
    }

    for (i = 0; i < children; ++i) {
        ...

        if (type_ast) {
            type = zend_compile_typename(type_ast, 0);

            if (ZEND_TYPE_CODE(type) == IS_VOID || ZEND_TYPE_CODE(type) == IS_CALLABLE) {
                zend_error_noreturn(E_COMPILE_ERROR,
                    "Property %s::$%s cannot have type %s",
                    ZSTR_VAL(ce->name),
                    ZSTR_VAL(name),
                    zend_get_type_by_const(ZEND_TYPE_CODE(type)));
            }
        }

        /* Doc comment has been appended as last element in ZEND_AST_PROP_ELEM ast */
        ...

        if (flags & ZEND_ACC_FINAL) {
            ...
        }

        if (zend_hash_exists(&ce->properties_info, name)) {
            ...
        }

        if (value_ast) {
            zend_const_expr_to_zval(&value_zv, value_ast);

            if (ZEND_TYPE_IS_SET(type) && !Z_CONSTANT(value_zv)) {
                if (Z_TYPE(value_zv) == IS_NULL) {
                    if (!ZEND_TYPE_ALLOW_NULL(type)) {
                        ...
                    }
                } else if (ZEND_TYPE_IS_CLASS(type)) {
                    ...
                } else if (ZEND_TYPE_CODE(type) == IS_ARRAY || ZEND_TYPE_CODE(type) == IS_ITERABLE) {
                    if (Z_TYPE(value_zv) != IS_ARRAY) {
                        ...
                    }
                } else if (ZEND_TYPE_CODE(type) == IS_DOUBLE) {
                    if (Z_TYPE(value_zv) != IS_DOUBLE && Z_TYPE(value_zv) != IS_LONG) {
                        ...
                    }
                    convert_to_double(&value_zv);
                } else if (!ZEND_SAME_FAKE_TYPE(ZEND_TYPE_CODE(type), Z_TYPE(value_zv))) {
                    ...
                }
            }
        } else if (!ZEND_TYPE_IS_SET(type)) {
            ZVAL_NULL(&value_zv);
        } else {
            ZVAL_UNDEF(&value_zv);
        }

        zend_declare_typed_property(ce, name, &value_zv, flags, doc_comment, type);
    }
}

flags 指成员变量的修饰符(如 public、protected、private),不合法的修饰符(如 final)会在这里检测并抛出错误。

type 是从 type_ast 里面解析出来的指成员变量的类型,这里的类型是在 PHP 代码里面规定好的类型,没有规定类型的成员变量的 type 为 0,同样也有不合法类型的检测(如 void)。顺便在这里贴一个 PHP 底层类型的定义,这里的 type 是内部定义好的代表类型的数字 x4 之后的结果:

/* regular data types */
#define IS_UNDEF                    0
#define IS_NULL                        1
#define IS_FALSE                    2
#define IS_TRUE                        3
#define IS_LONG                        4
#define IS_DOUBLE                    5
#define IS_STRING                    6
#define IS_ARRAY                    7
#define IS_OBJECT                    8
#define IS_RESOURCE                    9
#define IS_REFERENCE                10

/* constant expressions */
#define IS_CONSTANT_AST                11

/* internal types */
#define IS_INDIRECT                 13
#define IS_PTR                        14
#define IS_ALIAS_PTR                15
#define _IS_ERROR                    15

/* fake types used only for type hinting (Z_TYPE(zv) can not use them) */
#define _IS_BOOL                    16
#define IS_CALLABLE                    17
#define IS_ITERABLE                    18
#define IS_VOID                        19
#define _IS_NUMBER                    20

成员变量重复定义的检测是通过查询类里面的哈希表 properties_info 实现的,这个哈希表在后面创建的时候再说。

value_zv 是从 value_ast 里面解析出来的成员变量的默认值。这里会有更详细的变量类型的检测,具体什么样的是不合法的类型可以自行阅读源码,宏定义配合报错信息很容易理解。这里提一下两个宏:ZEND_TYPE_IS_SET 和 ZEND_TYPE_CODE,ZEND_TYPE_IS_SET 虽然叫 is,其实是个 > 3 的比较,在规定的类型里面除了未定义其他类型都会返回 true,而 ZEND_TYPE_CODE 是个将 type / 4 转换成内部定义数字的宏。

完成检测之后,会调用 zend_declare_typed_property 函数写入内存。

class 结构体

class 在 PHP 内存中是一个 _zend_class_entry 结构体:

struct _zend_class_entry {
    char type;
    zend_string *name;
    /* class_entry or string depending on ZEND_ACC_LINKED */
    union {
        zend_class_entry *parent;
        zend_string *parent_name;
    };
    int refcount;
    uint32_t ce_flags;

    int default_properties_count;
    int default_static_members_count;
    zval *default_properties_table;
    zval *default_static_members_table;
    ZEND_MAP_PTR_DEF(zval *, static_members_table);
    HashTable function_table;
    HashTable properties_info;
    HashTable constants_table;

    struct _zend_property_info **properties_info_table;

    zend_function *constructor;
    zend_function *destructor;
    zend_function *clone;
    zend_function *__get;
    zend_function *__set;
    zend_function *__unset;
    zend_function *__isset;
    zend_function *__call;
    zend_function *__callstatic;
    zend_function *__tostring;
    zend_function *__debugInfo;
    zend_function *serialize_func;
    zend_function *unserialize_func;

    ...
};

本类和父类的各种信息、各种魔术方法和静态成员变量暂且不提,可以看到跟非静态成员变量相关的数据主要是这几个:

  • default_properties_count
  • default_properties_table
  • properties_info
  • properties_info_table

成员变量写入内存

继续看代码,将非静态成员变量写入用户类的关键代码如下:

ZEND_API int zend_declare_typed_property(zend_class_entry *ce, zend_string *name, zval *property, int access_type, zend_string *doc_comment, zend_type type) /* {{{ */
{
    zend_property_info *property_info, *property_info_ptr;

    ...
    property_info = zend_arena_alloc(&CG(arena), sizeof(zend_property_info));
    ...

    if (!(access_type & ZEND_ACC_PPP_MASK)) {
        access_type |= ZEND_ACC_PUBLIC;
    }
    if (access_type & ZEND_ACC_STATIC) {
        ...
    } else {
        zval *property_default_ptr;
        if ((property_info_ptr = zend_hash_find_ptr(&ce->properties_info, name)) != NULL &&
            (property_info_ptr->flags & ZEND_ACC_STATIC) == 0) {
            ...
        } else {
            property_info->offset = OBJ_PROP_TO_OFFSET(ce->default_properties_count);
            ce->default_properties_count++;
            ce->default_properties_table = perealloc(ce->default_properties_table, sizeof(zval) * ce->default_properties_count, ce->type == ZEND_INTERNAL_CLASS);

            /* For user classes this is handled during linking */
            if (ce->type == ZEND_INTERNAL_CLASS) {
                ...
            }
        }
        property_default_ptr = &ce->default_properties_table[OBJ_PROP_TO_NUM(property_info->offset)];
        ZVAL_COPY_VALUE(property_default_ptr, property);
        Z_PROP_FLAG_P(property_default_ptr) = Z_ISUNDEF_P(property) ? IS_PROP_UNINIT : 0;
    }
    if (ce->type & ZEND_INTERNAL_CLASS) {
        ...
    }

    if (access_type & ZEND_ACC_PUBLIC) {
        property_info->name = zend_string_copy(name);
    } else if (access_type & ZEND_ACC_PRIVATE) {
        property_info->name = zend_mangle_property_name(ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
    } else {
        ZEND_ASSERT(access_type & ZEND_ACC_PROTECTED);
        property_info->name = zend_mangle_property_name("*", 1, ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
    }

    property_info->name = zend_new_interned_string(property_info->name);
    property_info->flags = access_type;
    property_info->doc_comment = doc_comment;
    property_info->ce = ce;
    property_info->type = type;

    zend_hash_update_ptr(&ce->properties_info, name, property_info);

    return SUCCESS;
}

不符合 public、protected、private 的变量修饰符会变成 public。

然后开始将非静态成员变量的数据写入内存,首先需要一个计算非静态成员变量数目的变量,也就是 default_properties_count,然后是计算 offset。

先说一下非静态成员变量的存放方式,非静态成员变量的值存放在 default_properties_table,其它信息则是先存放在 property_info,最后会更新到哈希表 properties_info 里面。property_info 的结构如下:

typedef struct _zend_property_info {
    uint32_t offset; /* property offset for object properties or
                          property index for static properties */
    uint32_t flags;
    zend_string *name;
    zend_string *doc_comment;
    zend_class_entry *ce;
    zend_type type;
} zend_property_info;

flags 修饰符等标识,其他数据都好理解,而 offset 用来表示内存中对象的成员变量距离头部的偏移:

#define OBJ_PROP_TO_OFFSET(num) ((uint32_t)(XtOffsetOf(zend_object, properties_table) + sizeof(zval) * (num)))

所以 offset 就是 0x28 + 0x10 * default_properties_count,有什么用途以后再说。

然后将 property(即代表成员变量值的 zval,包括类型和值两个数据)复制到 default_properties_table 的相应位置上,default_properties_table 实质上就是一个 zval 数组。

接下里则是将成员变量的相关信息存放进 property_info 里面,首先是按照修饰符修改成员变量名,public 不改,protected 加上 \x00*\x00,private 加上 \x00类名\x00,就编程了我们在序列化字符串中看到的样子。然后则是将修饰符、类指针、规定的变量类型都放入 property_info,最后调用 zend_hash_update_ptr 将其更新到哈希表 properties_info 中,哈希表的结构如下:

typedef struct _Bucket {
    zval              val;
    zend_ulong        h;                /* hash value (or numeric index)   */
    zend_string      *key;              /* string key or NULL for numerics */
} Bucket;

typedef struct _zend_array HashTable;

struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    _unused,
                zend_uchar    nIteratorsCount,
                zend_uchar    _unused2)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;
    Bucket           *arData;
    uint32_t          nNumUsed;
    uint32_t          nNumOfElements;
    uint32_t          nTableSize;
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};

数据存放在 arData 中,数量则用 nNumUsed 记录。如 property_info 就是作为一个 ptr 类型的数据存放在 val 中,key 则是根据修饰符修改前的变量名,h 是根据 key 生成的一个哈希。

至此,类非静态成员变量的处理及相关的 default_properties_count、default_properties_table、properties_info 等结构体成员的构成都大致清楚了,非静态成员变量相关的数据已经基本齐全,剩下的 properties_info_table 其实跟 properties_info 的数据存在重复,具体可以看 zend_build_properties_info_table 函数,它其实就是存放在 properties_info 里的 property_info。

他们的区别就在于 properties_info 存放了根据修饰符修改前的变量名,所以可以通过偏移和查找 key 来访问,而 properties_info_table 只可以通过偏移来访问。但是 properties_info_table 也有一个优点,那就是结构比较简单,可以直接查找该变量的规定类型(如 zend_get_typed_property_info_for_slot 函数)。

顺带一提,在继承的情况下,在分析编译完字类之后,要将父类的数据继承到字类,即按照父类对字类做一些修改,修改的逻辑在 zend_do_inheritance_ex 函数中。如果重写了父类的成员变量,修改后字类的 default_properties_count、default_properties_table 和 properties_info 属性都会改变,前两者加上父类的相应数据,复制到一个新的 zval 数组里再赋值回 default_properties_table,不知道为什么是倒着来复制的,子类的放在后面:

if (parent_ce->default_properties_count) {
    if (ce->default_properties_count) {
        zval *table = pemalloc(sizeof(zval) * (ce->default_properties_count + parent_ce->default_properties_count), ce->type == ZEND_INTERNAL_CLASS);
        src = ce->default_properties_table + ce->default_properties_count;
        end = table + parent_ce->default_properties_count;
        dst = end + ce->default_properties_count;
        ce->default_properties_table = table;
        do {
            dst--;
            src--;
        ZVAL_COPY_VALUE_PROP(dst, src);
        } while (dst != end);
        pefree(src, ce->type == ZEND_INTERNAL_CLASS);
        end = ce->default_properties_table;
    }
    ...
    src = parent_ce->default_properties_table + parent_ce->default_properties_count;
    ...
    do {
        dst--;
        src--;
        ZVAL_COPY_PROP(dst, src);
        if (Z_OPT_TYPE_P(dst) == IS_CONSTANT_AST) {
            ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED;
        }
        continue;
    } while (dst != end);
    ce->default_properties_count += parent_ce->default_properties_count;
}

properties_info 的处理如下:

ZEND_HASH_FOREACH_PTR(&ce->properties_info, property_info) {
    if (property_info->ce == ce) {
        if (property_info->flags & ZEND_ACC_STATIC) {
            property_info->offset += parent_ce->default_static_members_count;
        } else {
            property_info->offset += parent_ce->default_properties_count * sizeof(zval);
        }
    }
} ZEND_HASH_FOREACH_END();

if (zend_hash_num_elements(&parent_ce->properties_info)) {
    zend_hash_extend(&ce->properties_info,
        zend_hash_num_elements(&ce->properties_info) +
        zend_hash_num_elements(&parent_ce->properties_info), 0);

    ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->properties_info, key, property_info) {
        do_inherit_property(property_info, key, ce);
    } ZEND_HASH_FOREACH_END();
}

主要有两个部分,首先是遍历字类的 properties_info 然后修改里面 propertie_info 的偏移,因为前面复制 default_properties_table 的时候将父类的变量放在前面,所以需要加上相应的偏移。然后就是遍历父类的 properties_info,如果 key 不存在就加上去,因为父类放在前面,所以偏移不需要改变:

if (UNEXPECTED(ce->type & ZEND_INTERNAL_CLASS)) {
    child_info = zend_duplicate_property_info_internal(parent_info);
} else {
    child_info = parent_info;
}
_zend_hash_append_ptr(&ce->properties_info, key, child_info);

而如果字类中存在这个 key,即重写了父类中的成员变量,而且父类中这个变量的修饰符不是 private:

if (UNEXPECTED((child_info->flags & ZEND_ACC_PPP_MASK) > (parent_info->flags & ZEND_ACC_PPP_MASK))) {
    zend_error_noreturn(...);
} else if ((child_info->flags & ZEND_ACC_STATIC) == 0) {
    int parent_num = OBJ_PROP_TO_NUM(parent_info->offset);
    int child_num = OBJ_PROP_TO_NUM(child_info->offset);

    /* Don't keep default properties in GC (they may be freed by opcache) */
    zval_ptr_dtor_nogc(&(ce->default_properties_table[parent_num]));
    ce->default_properties_table[parent_num] = ce->default_properties_table[child_num];
    ZVAL_UNDEF(&ce->default_properties_table[child_num]);
    child_info->offset = parent_info->offset;
}

if (UNEXPECTED(ZEND_TYPE_IS_SET(parent_info->type))) {
    inheritance_status status = property_types_compatible(parent_info, child_info);
    if (status == INHERITANCE_ERROR) {
        emit_incompatible_property_error(child_info, parent_info);
    }
    if (status == INHERITANCE_UNRESOLVED) {
        add_property_compatibility_obligation(ce, child_info, parent_info);
    }
} else if (UNEXPECTED(ZEND_TYPE_IS_SET(child_info->type) && !ZEND_TYPE_IS_SET(parent_info->type))) {
    zend_error_noreturn(...);
}

修饰符等级不能变大,如 protected 不能变成 private。

修改 default_properties_table,将字类的重复变量覆盖到前面的父类上面,修改字类该变量的偏移,然后尝试清空字类里面的多余数据。

最后检测父类该变量是否有规定类型,若有规定类型则检测父类和字类重复变量的类型是否能共存,具体判断就不说了。

如果是 private 类型就不会进行改动,但是会给字类的这个变量加上一个 ZEND_ACC_CHANGED 的标识,后面实例化的时候会有不同的处理。

class 在内存中的存放

为了方便理解,最好一边调试一边看内存,class:

pwndbg> p *ce
$5 = {
  default_properties_count = 4, 
  default_properties_table = 0x7ffff3e6c240, 
  properties_info = {
    gc = {
      refcount = 1, 
      u = {
        type_info = 23
      }
    }, 
    u = {
      v = {
        flags = 16 '\020', 
        _unused = 0 '\000', 
        nIteratorsCount = 0 '\000', 
        _unused2 = 0 '\000'
      }, 
      flags = 16
    }, 
    nTableMask = 4294967280, 
    arData = 0x7ffff3e5c7c0, 
    nNumUsed = 4, 
    nNumOfElements = 4, 
    nTableSize = 8, 
    nInternalPointer = 0, 
    nNextFreeElement = 0, 
    pDestructor = 0x0
  }, 
  properties_info_table = 0x7ffff3e03368, 
}

default_properties_count,非静态成员变量数目。

default_properties_table,zval 结构体数组,前 8 字节表示值,后 8 字节表示类型等数据:

pwndbg> x/20xg ce->default_properties_table
0x7ffff3e6c240:    0x00007ffff3e6b5a0    0x0000000000000006
0x7ffff3e6c250:    0x0000000000000001    0x0000000000000004
0x7ffff3e6c260:    0x00007ffff3e562a0    0x0000000000000307
0x7ffff3e6c270:    0x00007ffff3e562a0    0x0000000100000000

properties_info,长度为 4 的哈希表,arData 为 Bucket 数组,其中后 16 字节中的前 8 字节为 hash,后 8 字节为 zend_string 类型的 key:

pwndbg> x/20xg ce->properties_info->arData
0x7ffff3e5c7c0:    0x00007ffff3e032c8    0xffffffff0000000e
0x7ffff3e5c7d0:    0x800000000002b606    0x00000000016437d0
0x7ffff3e5c7e0:    0x00007ffff3e032f0    0xffffffff0000000e
0x7ffff3e5c7f0:    0x800000000002b607    0x0000000001643800
0x7ffff3e5c800:    0x00007ffff3e03318    0xffffffff0000000e
0x7ffff3e5c810:    0x800000000002b608    0x0000000001643830
0x7ffff3e5c820:    0x00007ffff3e03340    0xffffffff0000000e
0x7ffff3e5c830:    0x800000000002b609    0x0000000001643860

前 16 字节为 ptr 类型的 zval,指向一个 zend_property_info 结构体:

pwndbg> x/20xg (zend_property_info*)ce->properties_info->arData->val->value->ptr
0x7ffff3e032c8:    0x0000000100000028    0x00000000016437d0
0x7ffff3e032d8:    0x0000000000000000    0x00007ffff3e03100
0x7ffff3e032e8:    0x0000000000000000    0x0000000200000038
0x7ffff3e032f8:    0x00007ffff3e6b640    0x0000000000000000
0x7ffff3e03308:    0x00007ffff3e03100    0x0000000000000000
0x7ffff3e03318:    0x0000000400000048    0x00007ffff3e01aa0
0x7ffff3e03328:    0x0000000000000000    0x00007ffff3e03100
0x7ffff3e03338:    0x0000000000000000    0x0000000400000058
0x7ffff3e03348:    0x00007ffff3e01ac8    0x0000000000000000
0x7ffff3e03358:    0x00007ffff3e03100    0x0000000000000018
pwndbg> p *(zend_property_info*)ce->properties_info->arData->val->value->ptr
$17 = {
  offset = 40, 
  flags = 1, 
  name = 0x16437d0, 
  doc_comment = 0x0, 
  ce = 0x7ffff3e03100, 
  type = 0
}

properties_info_table,指向 properties_info 内 zend_property_info 结构体的指针数组,可以对照上面的存放地址来看:

pwndbg> x/20xg ce->properties_info_table
0x7ffff3e03368:    0x00007ffff3e032c8    0x00007ffff3e032f0
0x7ffff3e03378:    0x00007ffff3e03318    0x00007ffff3e03340

class 管理

在将一个 class 的相关数据都写入内存之后,自然需要有一个统一的管理,不然 new 的时候都不知道去哪里找这个类。PHP 底层用来管理类的就是一个哈希表 class_table,可以根据类名来查找类。class_table 存放 class 的结构大致如下:

pwndbg> p *(Bucket*)(CG(class_table)->arData+158)
$38 = {
  val = {
    value = {
      ptr = 0x7ffff3e03100, 
    }, 
    u1 = {
      v = {
        type = 14 '\016', 
        type_flags = 0 '\000', 
        u = {
          extra = 0
        }
      }, 
      type_info = 14
    }, 
  }, 
  h = 9223378990928643873, 
  key = 0x7ffff3e6b520
}
pwndbg> x/10s (*(Bucket*)(CG(class_table)->arData+158))->key->val
0x7ffff3e6b538:    "twings"

object

object 结构体

对象结构体如下:

struct _zend_object {
    zend_refcounted_h gc;
    uint32_t          handle; // TODO: may be removed ???
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};

比类的结构体简单很多,跟成员变量相关的是 properties 和 properties_table 两个成员。

根据类名查找类

PHP 底层有一个函数 zend_lookup_class_ex,可以根据类名查找类:

ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, zend_string *key, uint32_t flags) /* {{{ */
{
    zend_class_entry *ce = NULL;
    zval args[1], *zv;
    zval local_retval;
    zend_string *lc_name;
    zend_fcall_info fcall_info;
    zend_fcall_info_cache fcall_cache;
    zend_class_entry *orig_fake_scope;

    if (key) {
        lc_name = key;
    } else {
        if (name == NULL || !ZSTR_LEN(name)) {
            return NULL;
        }

        if (ZSTR_VAL(name)[0] == '\\') {
            lc_name = zend_string_alloc(ZSTR_LEN(name) - 1, 0);
            zend_str_tolower_copy(ZSTR_VAL(lc_name), ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1);
        } else {
            lc_name = zend_string_tolower(name);
        }
    }

    zv = zend_hash_find(EG(class_table), lc_name);
    if (zv) {
        if (!key) {
            zend_string_release_ex(lc_name, 0);
        }
        ce = (zend_class_entry*)Z_PTR_P(zv);
        ...
        return ce;
    }
    ...
}

可以看到命名空间 \ 处理,类名小写处理以及查询哈希表的操作,源码里的很多标识(zend_ACC_*)都可以在 zend_compile.h 里面找到定义,结合注释和标识名可以更好理解。这个函数后面还有一段查询失败后调用 autoload 来动态加载类的代码,这里就不提了。

根据类新建对象

获取到类后,就要根据类来新建对象,处理代码在 _object_and_properties_init 函数中:

static zend_always_inline int _object_and_properties_init(zval *arg, zend_class_entry *class_type, HashTable *properties) /* {{{ */
{
    if (UNEXPECTED(class_type->ce_flags & (ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT|ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS))) {
        if (class_type->ce_flags & ZEND_ACC_INTERFACE) {
            zend_throw_error(NULL, "Cannot instantiate interface %s", ZSTR_VAL(class_type->name));
        } else if (class_type->ce_flags & ZEND_ACC_TRAIT) {
            zend_throw_error(NULL, "Cannot instantiate trait %s", ZSTR_VAL(class_type->name));
        } else {
            zend_throw_error(NULL, "Cannot instantiate abstract class %s", ZSTR_VAL(class_type->name));
        }
        ZVAL_NULL(arg);
        Z_OBJ_P(arg) = NULL;
        return FAILURE;
    }

    if (UNEXPECTED(!(class_type->ce_flags & ZEND_ACC_CONSTANTS_UPDATED))) {
        if (UNEXPECTED(zend_update_class_constants(class_type) != SUCCESS)) {
            ZVAL_NULL(arg);
            Z_OBJ_P(arg) = NULL;
            return FAILURE;
        }
    }

    if (class_type->create_object == NULL) {
        zend_object *obj = zend_objects_new(class_type);

        ZVAL_OBJ(arg, obj);
        if (properties) {
            object_properties_init_ex(obj, properties);
        } else {
            _object_properties_init(obj, class_type);
        }
    } else {
        ZVAL_OBJ(arg, class_type->create_object(class_type));
    }
    return SUCCESS;
}

新建一个空白对象后,会调用 _object_properties_init 初始化成员变量:

static zend_always_inline void _object_properties_init(zend_object *object, zend_class_entry *class_type) /* {{{ */
{
    if (class_type->default_properties_count) {
        zval *src = class_type->default_properties_table;
        zval *dst = object->properties_table;
        zval *end = src + class_type->default_properties_count;

        if (UNEXPECTED(class_type->type == ZEND_INTERNAL_CLASS)) {
            do {
                ZVAL_COPY_OR_DUP_PROP(dst, src);
                src++;
                dst++;
            } while (src != end);
        } else {
            do {
                ZVAL_COPY_PROP(dst, src);
                src++;
                dst++;
            } while (src != end);
        }
    }
}

简单来说就是遍历类的 default_properties_table,然后将其中数据复制到对象的 properties_table 中。这个时候,对象里面只有变量的值,用于根据 key 查询的 properties 哈希表还是空的,而哈希表的初始化需要调用 Z_OBJPROP_P,其实就是 object_handlers 的 zend_std_get_properties 函数:

ZEND_API HashTable *zend_std_get_properties(zval *object) /* {{{ */
{
    zend_object *zobj;
    zobj = Z_OBJ_P(object);
    if (!zobj->properties) {
        rebuild_object_properties(zobj);
    }
    return zobj->properties;
}
...
ZEND_API void rebuild_object_properties(zend_object *zobj) /* {{{ */
{
    if (!zobj->properties) {
        zend_property_info *prop_info;
        zend_class_entry *ce = zobj->ce;
        uint32_t flags = 0;

        zobj->properties = zend_new_array(ce->default_properties_count);
        if (ce->default_properties_count) {
            zend_hash_real_init_mixed(zobj->properties);
            ZEND_HASH_FOREACH_PTR(&ce->properties_info, prop_info) {
                if (!(prop_info->flags & ZEND_ACC_STATIC)) {
                    flags |= prop_info->flags;

                    if (UNEXPECTED(Z_TYPE_P(OBJ_PROP(zobj, prop_info->offset)) == IS_UNDEF)) {
                        HT_FLAGS(zobj->properties) |= HASH_FLAG_HAS_EMPTY_IND;
                    }

                    _zend_hash_append_ind(zobj->properties, prop_info->name,
                        OBJ_PROP(zobj, prop_info->offset));
                }
            } ZEND_HASH_FOREACH_END();
            if (flags & ZEND_ACC_CHANGED) {
                while (ce->parent && ce->parent->default_properties_count) {
                    ce = ce->parent;
                    ZEND_HASH_FOREACH_PTR(&ce->properties_info, prop_info) {
                        if (prop_info->ce == ce &&
                            !(prop_info->flags & ZEND_ACC_STATIC) &&
                            (prop_info->flags & ZEND_ACC_PRIVATE)) {
                            zval zv;

                            if (UNEXPECTED(Z_TYPE_P(OBJ_PROP(zobj, prop_info->offset)) == IS_UNDEF)) {
                                HT_FLAGS(zobj->properties) |= HASH_FLAG_HAS_EMPTY_IND;
                            }

                            ZVAL_INDIRECT(&zv, OBJ_PROP(zobj, prop_info->offset));
                            zend_hash_add(zobj->properties, prop_info->name, &zv);
                        }
                    } ZEND_HASH_FOREACH_END();
                }
            }
        }
    }
}

遍历类的 properties_info,从里面取出该成员变量的变量名、标识以及在对象中存储的偏移,再根据偏移获取 properties_table 中的 zval 数据存入 properties 中,需要注意的是这里的 key 是根据修饰符修改后的 key,所以 properties 存储的其实就是 properties_table 的数据,里面没有父类的重复变量。

就像上文所说,在继承的情况下,如果重写了父类的私有变量,就会有一个 ZEND_ACC_CHANGED 的标识,在遍历完子类的 properties_info 之后,还会往上遍历父类的 properties_info,将父类的重复变量也放进 properties 里面。这是因为私有属性无法被继承,但是可以被父类的方法访问,所以需要保留下来。

反序列化

反序列化数据结构体

typedef struct {
    zend_long used_slots;
    void *next;
    zval *data[VAR_ENTRIES_MAX];
} var_entries;

typedef struct {
    zend_long used_slots;
    void *next;
    zval data[VAR_DTOR_ENTRIES_MAX];
} var_dtor_entries;

struct php_unserialize_data {
    var_entries      *last;
    var_dtor_entries *first_dtor;
    var_dtor_entries *last_dtor;
    HashTable        *allowed_classes;
    HashTable        *ref_props;
    zend_long         cur_depth;
    zend_long         max_depth;
    var_entries       entries;
};

cur_depth、max_depth 很好理解,就是反序列化的深度和最大深度。

allowed_classes 也很好理解,允许反序列化的类。

last、entries,构成了一个类似栈链的结构,last 指向最后一个栈,entries 是第一个栈,存储反序列化过程数据。比如反序列化一个 Object,反序列化完前面 O 的部分后会将这个部分放进栈里,再去反序列化第一个 key。

first_dtor、last_dtor,同样是一个栈链的结构,存储反序列化中的临时变量。

ref_props,用来处理 PHP 的变量引用的类型问题。

反序列化流程

以 Object 为例。

PHP_FUNCTION(unserialize)

根据 options 配置反序列化参数,包括 max_depth 和 allowed_classes 等,然后调用 php_var_unserialize。

php_var_unserialize

调用 php_var_unserialize_internal 开始反序列化。

php_var_unserialize_internal

把 rval push 进栈里,rval 是一个存储反序列化过程数据的全局变量,然后开始词法分析。

词法分析过程很好理解,在分析出类名之后,调用 zend_lookup_class 尝试获取类,如果获取不到则尝试调用 unserialize_callback_func 动态加载类,加载完后会再次调用 zend_lookup_class 获取类。

获取到内存中的类后,会判断该类是否定义了 __unserialize 函数,定义了该函数的反序列化过程与一般的类不太一样,具体过程可以看这里,会将类成员作为一个数组然后调用函数。然后调用 object_init_ex 实例化一个新的对象,进入 object_common 方法。

object_common

如果定义了 __unserialize 函数,php 会调用 process_nested_data 方法将里面的数据反序列化为一个数组,然后跟对象一起放入栈中,在最后 php_var_unserialize_destroy 的时候调用。

如果没有定义,就会判断是否定义了 __wakeup,流程跟上面的一样。然后调用 Z_OBJPROP_P 建立对象的 properties 哈希表,同样调用 process_nested_data 反序列化对象内数据。

process_nested_data

首先是反序列化深度的检测,然后就开始根据对象/数组内部数据的数量逐个反序列化。哈希表的更新都很好懂,如果该变量已经有默认值,就将它更新为空,而在向哈希表里添加数据的时候,properties_table 因为哈希表没有对应的指针,所以不会添加。这里重点说一下 string_key 里面是怎么将处理成员变量名的。

首先会再次调用 php_var_unserialize_internal,解析出 key,然后关注 zend_unmangle_property_name_ex 函数:

ZEND_API int zend_unmangle_property_name_ex(const zend_string *name, const char **class_name, const char **prop_name, size_t *prop_len) /* {{{ */
{
    size_t class_name_len;
    size_t anonclass_src_len;

    *class_name = NULL;

    if (!ZSTR_LEN(name) || ZSTR_VAL(name)[0] != '\0') {
        *prop_name = ZSTR_VAL(name);
        if (prop_len) {
            *prop_len = ZSTR_LEN(name);
        }
        return SUCCESS;
    }
    if (ZSTR_LEN(name) < 3 || ZSTR_VAL(name)[1] == '\0') {
        // FAILURE;
    }

    class_name_len = zend_strnlen(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 2);
    if (class_name_len >= ZSTR_LEN(name) - 2 || ZSTR_VAL(name)[class_name_len + 1] != '\0') {
        // FAILURE;
    }

    *class_name = ZSTR_VAL(name) + 1;
    anonclass_src_len = zend_strnlen(*class_name + class_name_len + 1, ZSTR_LEN(name) - class_name_len - 2);
    if (class_name_len + anonclass_src_len + 2 != ZSTR_LEN(name)) {
        class_name_len += anonclass_src_len + 1;
    }
    *prop_name = ZSTR_VAL(name) + class_name_len + 2;
    if (prop_len) {
        *prop_len = ZSTR_LEN(name) - class_name_len - 2;
    }
    return SUCCESS;
}

这里会对词法解析出来的 key 进行拆解,如果是 public,即 key 不是 \x00 就会直接返回;然后开始拆解 key,从里面把类名扒出来,再将拆完剩下的最初的变量名放到 prop_name 里面,可以直接去类的哈希表 properties_info 里面查找这个变量在服务端的修饰符,这样变量修饰符就不会被序列化数据影响了。

后面就没有什么好说的了,之后会再调用 php_var_unserialize_internal,分析出变量值。反序列化结束后,会调用 php_var_unserialize_destroy,清理栈内数据,调用反序列化魔术方法。

整个反序列化过程其实就是个递归的过程。


Orz


Web PHP

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

PHP从编译到执行
利用反序列化进行JNDI注入