I found a double free vulnerability in the Standard PHP Library (SPL). While writing the exploit, I can't seem to find much write up on how PHP manages their heap internally. Through this blogpost I'll shed some light on this topic as well as my approach to exploit a double-free vulnerability like this within PHP.Root Cause Analysis The vulnerability is in SplDoublyLinkedList::offsetSet ( mixed $index , mixed $newval ) when passing in an invalid index. For example: <?php$var_1=new SplStack();$var_1->offsetSet(100,new DateTime('2000-01-01')); //DateTime will be double-freed When an invalid index (100 in this case) is passed into the function, the DateTime object is being freed for the first time in line 833: 832 if (index < 0 || index >= intern->llist->count) { 833 zval_ptr_dtor(value); 834 zend_throw_exception(spl_ce_OutOfRangeException, "Offset invalid or out of range", 0); 835 return; 836 } The second free occurs in Zend/zend_vm_execute.h:855 when it cleans up the call stack: 854 EG(current_execute_data) = call->prev_execute_data; 855 zend_vm_stack_free_args(call); PHP Internal Heap Management Internally within PHP, when doing heap allocations via calls such as ealloc(), it falls into 3 categories depending of the size: Small heap allocation (< 3072 bytes) Large heap adllocation ( < 2 megabytes) Huge heap allocation ( > 2 megabytes) Lets explore the small heap allocation/deallocation since that's what we are going to be using in this exploit. When dealing with memory handled by the small heap allocator, each chunk of memory is categorised into "bins" based of their size. For example:
When a memory chunk is freed internally within PHP via calls such as efree(), if the chunk in question is handled by the small heap deallocator, instead of releasing it back to the OS, it caches it and place it into an appropriate "Bin". This Bin is implemented as a single link list with freed chunks of memory being chained together. The first couple bytes of each freed chunk can be considered its header and it contains a pointer that points to the next freed chunk. Visually this is what it looks like in the memory: Step 0: Evaluate if this is feasibly exploitable. This vulnerability is triggered by trying to insert an object into an invalid SplDoublyLinkedList index. This triggers a fatal error as such: Fatal error: Uncaught OutOfRangeException: Offset invalid or out of range Upon the fatal error, PHP exits immediately. This prevents us from running any user code in "userland" after the double free. Thus in order for us to exploit this vulnerability, we need to trap the error and make PHP not exit immediately after the double free. This can be done via the set_exception_handler(); Step 1: We need to decide on what object we want to trigger the double free on. I have chosen to use SplFixedArray for this due to the following reasons:
Step 2: We need to do some heap massaging. First let's do some cleaning up and clear any free chunks of memory in the Bin associated with SplFixedArray's size. We can do that by allocating many instances of the object. This also ensures that the SplFixedArrays allocated are in contiguous chunk of memory
On line 5 above, we free the 50th SplFixedArray. This creates a hole in the contiguous chunk of memory allocated and can be visualized as: We immediately allocate a new SplFixedArray (which causes it to occupy the 50th slot) and trigger the double free vulnerability on it:
The reasons for all these heap manipulation is to ensure that the double free vulnerability is triggered on a location that is part of my controlled contiguous chunk of memory. This gives me relative good control of the memory layout as well as references to its neighboring chunk of memory (49th and 51st SplFixedArray). After the first free, this is what the memory looks like: After the 2nd free, this is what the memory looks like: Step 3: Let's now exploit this abnormally in the heap. Notice that there's only 1 free chunk of memory? However the link list now has two arrows which points to the same chunk of memory. At this point, the heap is corrupted and PHP think there's 2 free chunk of memory when there's only 1. This means that we can allocate 2 free chunk ( from Bin #12) and they will occupy the same memory space:
In the code above we allocate a string (line 2) and an object mySpecialSplFixedArray (line 3) which will both occupy and the same space between the 49th SplFixedArray and 51st SplFixedArray. Notice that in line 3 I allocated mySpecialSplFixedArray which is just an extended class of SplFixedArray but with the offsetUnset method being overwritten. To understand why, lets take a look at the PHP internal structure of a String vs SplFixedArray:
Step 4: Gaining Code execution. At this point:
Assuming I want to execute code at 0xdeadbeef, here's the approach I'll take:
Achieving (1) is easy because we can write into the 51st SplFixArray and sore our fake Handler structure there.Achieving (2) is also easy because $s and mySpecialSplFixedArray shares the same memory space. The tricky part here is that when we create the fake Handler structure, we do not know the address of where is the exact address fake structure. Without knowing the address, we can't point mySpecialSplFixedArray to our fake handler structure. The solution to that would be to free the 51st and 52nd SplFixArray: Due to this free, the first 8 bytes of the 51st SPLFixedArray chunk now points to the 52nd SPLFixedArray memory chunk. By using $s to read into the first 8 bytes of the 51st SplFixedArray, one can figure out the exact memory address that one is in. Here's the exploit code in its full glory:
Original bug report can be found here |
Blog >