PHP之殇 : 一个IR设计缺陷引发的蝴蝶效应
字数 1296 2025-08-23 18:31:24

PHP IR设计缺陷引发的安全问题深度分析

1. 问题概述

PHP中存在一个由IR设计缺陷引发的安全问题,该问题自PHP7以来一直存在,可能导致内存读写错误甚至段错误(segfault)。

1.1 问题重现

$array = range(0, 7);
set_error_handler(function($err, $msg) {
    global $array;
    $array[] = 1; //force resize;
});
function crash() {
    global $array;
    $array[0] += $var; //undefined notice
}
crash();

1.2 问题本质

ZEND_ASSIGN_DIM_OP操作码处理过程中:

  1. 首先获取数组元素地址
  2. 检查变量合法性(可能触发错误处理)
  3. 执行加法操作
  4. 将结果赋值回原元素

关键问题:错误处理中可能导致数组resize和realloc,使之前获取的元素地址失效。

2. 技术细节分析

2.1 PHP数组内部结构

PHP使用zend_array结构表示数组,包含:

  • arData: 指向第一个元素的指针
  • nTableMask: 用于哈希计算的掩码
  • nNumUsed: 已使用元素数
  • nNumOfElements: 实际元素数
  • nTableSize: 总容量

2.2 问题操作码流程

伪代码表示ZEND_ASSIGN_DIM_OP处理过程:

arr_base = get_base_addr_of(array);
elem_addr = get_addr_by_index(array_base, index);
elem = get_elem_from_addr(elem_addr);
check_var(var);  // 可能触发错误处理
res = add(elem, var);
assign_var_to_elem(elem, res);

2.3 修复难点

  1. 简单方法1:检查元素是否仍在array->arData相对位置

    • 无法解决ABA问题(内存被释放后又被相同结构占用)
  2. 简单方法2:将check_var放在最前面

    • 无法处理多维数组赋值情况

3. 利用技术分析

3.1 利用思路

  1. 触发数组resize,释放butterfly内存
  2. 快速抢占被释放的内存
  3. 控制null写入的位置

3.2 关键步骤

3.2.1 构造fake zval

  1. 创建混合数组:
$a1_str = 'eeee';
$victim_arr = array(
    'a1' => $a1_str,
    'a2' => 1,
    // ...共8个元素
);
  1. 通过错误处理抢占内存:
set_error_handler(function() {
    $victim_arr['a9'] = 1;
    $user_str = str_repeat('b', $user_str_length);
});

3.2.2 地址泄露

利用PHP弱类型转换:

$victim_arr['a1'] = true;
$victim_arr['a1'] .= null;
var_dump($victim_arr['a1']); // 输出: string(1) "1"

3.2.3 构造addressOf原语

  1. 在可控内存上布置fake array
  2. 控制fake array引用计数为1
  3. 使用fakeZval包装fake_array
  4. 触发UAF后立即申请相同array
  5. 通过$hax[0] = $val操作泄露地址

3.3 任意读写原语

利用php://memory包装器:

typedef struct {
    char *data;
    size_t fpos;
    size_t fsize;
    size_t smax;
    int mode;
} php_stream_memory_data;

通过控制这个结构体的指针实现任意地址读写。

4. 修复建议

4.1 可能的修复方案

  1. 多索引支持:修改ZEND_ASSIGN_DIMZEND_ASSIGN_DIM_OP支持多索引
  2. 延迟错误处理:将数组赋值过程中发生的错误延迟到赋值完成后处理

4.2 修复挑战

  1. 需要修改大量array fetch操作
  2. 需要处理object assignment和fetch的类似问题
  3. 在PHP JIT中实现延迟错误处理机制较为复杂

5. 相关数据结构

5.1 zval结构

typedef union _zend_value {
    zend_long lval;
    double dval;
    zend_refcounted *counted;
    // ...其他类型指针
} zend_value;

struct _zval_struct {
    zend_value value;
    union {
        uint32_t type_info;
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar type,
                zend_uchar type_flags,
                uint16_t extra
            )
        } v;
    } u1;
    // ...其他union
};

5.2 zend_string结构

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong h;    // hash值
    size_t len;      // 长度
    char val[1];     // 字符串内容
};

5.3 Bucket结构(数组元素)

typedef struct _Bucket {
    zval val;        // 元素值
    zend_ulong h;    // 哈希值或数字索引
    zend_string *key; // 字符串键或NULL
} Bucket;

6. 关键知识点总结

  1. PHP数组在扩容时会realloc内存,可能导致地址变化
  2. 错误处理可能产生副作用,使之前获取的地址失效
  3. 利用UAF可以构造fake zval和addressOf原语
  4. 通过控制php://memory结构可以实现任意地址读写
  5. 修复此问题需要考虑多维数组和对象操作的情况

该问题展示了编程语言虚拟机中IR设计的重要性,以及副作用处理不当可能引发的安全问题。

PHP IR设计缺陷引发的安全问题深度分析 1. 问题概述 PHP中存在一个由IR设计缺陷引发的安全问题,该问题自PHP7以来一直存在,可能导致内存读写错误甚至段错误(segfault)。 1.1 问题重现 1.2 问题本质 在 ZEND_ASSIGN_DIM_OP 操作码处理过程中: 首先获取数组元素地址 检查变量合法性(可能触发错误处理) 执行加法操作 将结果赋值回原元素 关键问题 :错误处理中可能导致数组resize和realloc,使之前获取的元素地址失效。 2. 技术细节分析 2.1 PHP数组内部结构 PHP使用 zend_array 结构表示数组,包含: arData : 指向第一个元素的指针 nTableMask : 用于哈希计算的掩码 nNumUsed : 已使用元素数 nNumOfElements : 实际元素数 nTableSize : 总容量 2.2 问题操作码流程 伪代码表示 ZEND_ASSIGN_DIM_OP 处理过程: 2.3 修复难点 简单方法1 :检查元素是否仍在 array->arData 相对位置 无法解决ABA问题(内存被释放后又被相同结构占用) 简单方法2 :将 check_var 放在最前面 无法处理多维数组赋值情况 3. 利用技术分析 3.1 利用思路 触发数组resize,释放butterfly内存 快速抢占被释放的内存 控制 null 写入的位置 3.2 关键步骤 3.2.1 构造fake zval 创建混合数组: 通过错误处理抢占内存: 3.2.2 地址泄露 利用PHP弱类型转换: 3.2.3 构造addressOf原语 在可控内存上布置fake array 控制fake array引用计数为1 使用fakeZval包装fake_ array 触发UAF后立即申请相同array 通过 $hax[0] = $val 操作泄露地址 3.3 任意读写原语 利用 php://memory 包装器: 通过控制这个结构体的指针实现任意地址读写。 4. 修复建议 4.1 可能的修复方案 多索引支持 :修改 ZEND_ASSIGN_DIM 和 ZEND_ASSIGN_DIM_OP 支持多索引 延迟错误处理 :将数组赋值过程中发生的错误延迟到赋值完成后处理 4.2 修复挑战 需要修改大量array fetch操作 需要处理object assignment和fetch的类似问题 在PHP JIT中实现延迟错误处理机制较为复杂 5. 相关数据结构 5.1 zval结构 5.2 zend_ string结构 5.3 Bucket结构(数组元素) 6. 关键知识点总结 PHP数组在扩容时会realloc内存,可能导致地址变化 错误处理可能产生副作用,使之前获取的地址失效 利用UAF可以构造fake zval和addressOf原语 通过控制php://memory结构可以实现任意地址读写 修复此问题需要考虑多维数组和对象操作的情况 该问题展示了编程语言虚拟机中IR设计的重要性,以及副作用处理不当可能引发的安全问题。