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操作码处理过程中:
- 首先获取数组元素地址
- 检查变量合法性(可能触发错误处理)
- 执行加法操作
- 将结果赋值回原元素
关键问题:错误处理中可能导致数组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:检查元素是否仍在
array->arData相对位置- 无法解决ABA问题(内存被释放后又被相同结构占用)
-
简单方法2:将
check_var放在最前面- 无法处理多维数组赋值情况
3. 利用技术分析
3.1 利用思路
- 触发数组resize,释放butterfly内存
- 快速抢占被释放的内存
- 控制
null写入的位置
3.2 关键步骤
3.2.1 构造fake zval
- 创建混合数组:
$a1_str = 'eeee';
$victim_arr = array(
'a1' => $a1_str,
'a2' => 1,
// ...共8个元素
);
- 通过错误处理抢占内存:
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原语
- 在可控内存上布置fake array
- 控制fake array引用计数为1
- 使用fakeZval包装fake_array
- 触发UAF后立即申请相同array
- 通过
$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 可能的修复方案
- 多索引支持:修改
ZEND_ASSIGN_DIM和ZEND_ASSIGN_DIM_OP支持多索引 - 延迟错误处理:将数组赋值过程中发生的错误延迟到赋值完成后处理
4.2 修复挑战
- 需要修改大量array fetch操作
- 需要处理object assignment和fetch的类似问题
- 在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. 关键知识点总结
- PHP数组在扩容时会realloc内存,可能导致地址变化
- 错误处理可能产生副作用,使之前获取的地址失效
- 利用UAF可以构造fake zval和addressOf原语
- 通过控制php://memory结构可以实现任意地址读写
- 修复此问题需要考虑多维数组和对象操作的情况
该问题展示了编程语言虚拟机中IR设计的重要性,以及副作用处理不当可能引发的安全问题。