[CVE-2016-5771] Use After Free Vulnerability in PHP GC algorithm and unserialize
[Vulnerability Analysis]
本文译自 bugs.php.net 并适当修改
描述
在使用PHP的垃圾回收算法(以下简称GC)与其他特定的PHP对象进行交互时,我们发现了一个严重的释放后重用的漏洞。该漏洞影响广泛,可使反序列化的漏洞利用代码(exploit)成功在远程主机实现命令执行。
在分析PHP的反序列化功能时,我们发现PHP内部GC算法存在严重缺陷。该缺陷可被本地利用以及基于反序列化的远程利用。
本报告中的漏洞不影响PHP 7,而第二个报告中的漏洞会影响所有PHP>=5.3.0的版本。
该PoC表明:我们可以恶意利用垃圾收集器来释放(free)一个目标数组(array)。
此时攻击者可以构造一个假的容器(zval),并通过接管EIP/RIP来攻击PHP进程。
漏洞说明
GC算法被用来清理循环引用的变量容器(zval)。在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾。PHP会将其在内存中销毁,以防止内存溢出。当一个PHP线程结束时,当前占用的所有内存空间都会被销毁,当前程序中所有对象同时被销毁。
PHP会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的zval,如果需要修改则需要修改源代码 Zend/zend_gc.c
中的常量 GC_ROOT_BUFFER_MAX_ENTRIES
然后重新编译。
每当一个zval被销毁时,GC算法会判断这个zval是否为“根缓冲区”的候选者(比如array和object),判断通过后将其移入根缓冲区中。这个过程会循环进行,直到以下事件发生:
a) gc_collect_cycles()
函数被手动调用。
b) 根缓冲区已满。这时系统会自动调用gc_collect_cycles()
进行回收。
接着,gc_collect_cycles()
函数会执行一个标记算法,该算法分为以下几个部分:
1) gc_mark_roots(TSRMLS_C);
该函数对所有根缓冲区的元素,按照深度优先算法遍历所有子节点。将所有遍历到的zval节点的引用计数(refcount)减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其执行gc_mark_grey
,标记为灰色。
2) gc_scan_roots(TSRMLS_C);
再次对每个缓冲区中的根zval深度优先遍历,对所有引用计数值为0的zval使用gc_mark_white
标记为“白”。不为0则使用gc_mark_black
标记为黑色。
3) gc_collect_roots(TSRMLS_C);
恢复所有zval的引用计数, 并将标记为白色的zval筛选出来。
4) 销毁被标记为白色的zval,回收其内存。
不幸的是,这个GC算法可以被多次递减的条目欺骗,尽管这些条目已被标记为灰色。
请参考以下例子:
我们初始化一个数组对象(ArrayObject),它引用了另一个数组。(这在反序列化中很容易实现)
一旦GC算法试图访问该数组对象里的元素,它将执行上文提到的 gc_mark_grey
方法:
(Zend/zend_gc.c, gc_mark_grey method)
HashTable *props = get_gc(pz, &table, &n TSRMLS_CC);
这个方法想要调用被查询对象的GC方法,但是我们的数组对象不具有自己的GC。
在对象不具有GC方法时,其get_properties
方法会作为替代品被调用。
因此,spl_array_get_properties
方法会被替代调用。
此方法将检索内部数组(intern->array)内的所有元素,由于这个array里的元素是一个引用,该方法将返回目标数组的哈希表。
对以上场景加以利用,可以多次减少特定数组内所有元素的引用计数。
如果利用的好,可以使这个元素及其所有子节点都被标记为白色并被GC算法清除,尽管系统内仍存在未释放的指针。
该漏洞无需手动调用gc_mark_roots(TSRMLS_C);
反序列化可触发GC,导致该漏洞可被远程利用。
对该漏洞的远程利用已被验证可行。
此外,“反序列化”仅是该漏洞是一个可选项,该漏洞还适用于更多场景。
这个漏洞发现过程很艰难,因为它涉及到的多个组件以一个复杂的方式协同工作。出于某些原因,本报告可能会遗漏一些细节和进一步的说明,我们将在漏洞确认之后写一份详尽的说明文档。
测试脚本
<?php
// Fill any potential freed spaces until now.
$filler = array();
for($i = 0; $i < 100; $i++)
$filler[] = "";
// Create our payload and unserialize it.
$serialized_payload = 'a:3:{i:0;r:1;i:1;r:1;i:2;C:11:"ArrayObject":19:{x:i:0;r:1;;m:a:0:{}}}';
$free_me = unserialize($serialized_payload);
// We need to increment the reference counter of our ArrayObject s.t. all reference counters of our unserialized array become 0.
$inc_ref_by_one = $free_me[2];
// The call to gc_collect_cycles will free '$free_me'.
gc_collect_cycles();
// We now have multiple freed spaces. Fill all of them.
$fill_freed_space_1 = "filler_zval_1";
$fill_freed_space_2 = "filler_zval_2";
var_dump($free_me);
预期结果
返回的Array中有两个reference和一个ArrayObject
实际结果
string(13) "filler_zval_2"
修复建议
确保 ext/spl/spl_array.c
有更加合适的GC算法 (就像PHP 7所做的那样)。
参考
[1] http://php.net/manual/de/features.gc.collecting-cycles.php
[2] https://github.com/php/php-src/commit/4e03ba4a6ef4c16b53e49e32eb4992a797ae08a8
[3] https://hackerone.com/reports/73235