PHP7:反序列化漏洞案例及分析(上)

阅读量    95775 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

http://p7.qhimg.com/t0147059c5092b26485.jpg


1.漏洞历史

对于黑客来说,如果能够利用服务器端错误,那简直相当于中了头彩。因为用户倾向于将他们的数据保存在服务器中,如果黑客能够利用这个错误,就能针对某一个目标,从而获取更大的收益。PHP脚本语言是时下最流行的web服务器端语言。为了消除PHP开发过程中不同类型的漏洞,人们采用了多种安全编码方案。

然而,安全编码方案不能掩盖语言本身的缺陷。PHP是用相对低级的语言写成的,其中常见的漏洞有内存损坏漏洞,而use-after-free漏洞最为普遍。

这些年来,PHP语言得到了不断的改进,在2015年12月,一个重要的新版本——PHP7被公布出来。这个版本的内部结构与PHP5有很大不同,分配器已经发生了改变,而变量的内部表示(zvals)也完全不同了。

通过一个反序列化漏洞,Check Point研究小组成功演示了一项对PHP7的利用。在这篇报告中,我们将会一步步解释这是如何完成的。

 2.技术背景

为了更好地解释这项利用,我们首先要回顾一些关键的技术细节。

(1)值和对象

在PHP-7中,用来保存值的结构与php-5有所不同。

在内部保存值的结构是zval(_zval_struct)。这个结构的第一个字段是zend_value,其中包含指向PHP基本类型的指针和结构,而主要类型有Boolean、integer、double、string、object和array 等。

我们需要关注的类型是String、Object和Array,它们在内部中被表示为zend_string、zend_object和zend_array结构。

zend_string是用于保存字符串的结构。当引擎创建了一个新的字符串后,它会分配足够的字节给zend_string结构,对字符串的大小进行扩充。然后,它会用字符串的数据填补这个结构的字段,并在结构的末尾添加上字符串的内容。因此,字符串创建为我们提供了一种在不同的尺寸中进行分配的方法:sizeof(zend_string)+ strlen(str)= 16 + strlen(str)。这样,我们就没法再伪造一个字符串zval,并让它指向我们想要的地方了,这和使用PHP-5时有所不同。

zend_object用来表示对象的基本结构。它通常被嵌入在一个代表着不同类型对象的结构中。当zval保存了一个对象时,它的value 字段是一个指向zend_object的指针。

zend_array(又名HashTable)是保存键值存储的结构。这是一个对哈希表数据结构的直接应用,其中的arData字段指向Bucket结构内的一个数组。

总体来说,我们可以看到,PHP-7值系统更倾向于嵌入结构(PHP-5相比)。这种改变可以提高代码的效率(减少分配),让我们难以利用与内存相关的bug(更少的引用)。

2)PHP-7内存分配器

在PHP-7中,内存分配器的工作原理不同于PHP-5。小的分配(slot)由一个free list完成。每个分配大小都有一个对应的free list。free list通过一个或多个连续页(bin)进行初始化,而free list的初始化使得每一个slot指向下一个slot。一旦free list耗尽,一个新的bin会被分配出来。

重点:

•一个slot的元数据是基于所在页面进行检索的。(地址对齐到最近的chunk)

•下一个分配的位置可能是当前分配的位置+分配的大小。例如,如果分配器以0 x28的大小返回到地址0xf7e10000,那么下一个大小为0 x28的分配就位于0 xf7e10028。为了简单起见,我们假定这是真实的。注意,在最后一个primitive(下文Writing Memory / 64中会提到),我们设计了一个不依赖这一假设,但仍能触发错误的方法。

•分配大小被四舍五入成了某个预定义的大小。

 (3)反序列化

unserialize函数被用于将格式化字符串内的对象进行实例化,在反序列化期间,每个解析元素都有一个索引号,号码从1开始。

在内部,每个解析值都被放到了php_unserialize_data_t的两个数组中。第一个数组是values-array,第二个是destructor-array。在反序列化期间,值可以重新定义,即在stdClass(最基本的PHP的对象——一个键值存储)中,同一个key可以用不同的值反序列化两次。如果是这样的话,第一个定义会被覆盖,引用也会从数值数组中被移除。然而引用会被保存在destructor-array中。当反序列化结束时,destructor-array中每个值的引用数都会被减少,如果减少到零,它就会被释放。

所以请记住,在反序列化过程中,值不能被释放,只有最后的过程中才可以。


3.BUG (# 71311)

这里的bug是一个Use-After-Free bug,培训存在于标准php库内ArrayObject的反序列化函数中。

ArrayObject是一个SPL对象,它允许对象以数组的形式工作。在内部,它被表示为spl_array_object。这是该对象的序列化形式:

spacer.gif

C:11:"ArrayObject":37:{x:i:0;a:2:{i:0;i:0;i:1;i:1;};m:a:0:{}}

•37是括号内的字符数

•x:i:0;对应于结构中的nr_flags字段

•a:2:{i:0;i:0;i:1;i:1;}对应于结构中的数组字段(从这个角度,它被称为internal数组以区别于对象本身)

•m:a:0:{}对应于zend_object std字段内的properties字段(从这个角度,称为members数组)。

当对ArrayObject进行反序列化时,引擎首先会将一个默认的、拥有内部数组的ArrayObject实例化,然后解析ArrayObject的字段。当它解析到与内部数组相关的部分时,会释放初始的内部数组,然后通过引用,调用php_var_unserialize,并指向内部数组,目的是想让函数将它变成已经解析过的内部数组。内部数组可以是一个已经解析的数组的引用,在这种情况下,内部数组被修改为指向引用的数组,同时引用计数会有所增加。

在内部数组对自身进行引用时,错误出现了。这导致内部指针被分配给自己(即无操作),并指向释放了的数组,然后,数组的引用计数会增加。


 4.有漏洞的代码

我们利用的代码常被用于反序列化开发。我们建立了一个运行以下PHP脚本的apache服务器:

 http://p1.qhimg.com/t01aac8181c0b985ee8.jpg

这个脚本给了我们一个反馈。尽管我们对远程可利用性的要求有所降低,但在每一个情境中,反映到客户端的反序列化数据都是适合的。

我们通过向data参数内的脚本发送字符串进行了利用。在利用过程中,我们从返回的序列化字符串中推断出了一些内部信息。


5.触发这个错误

为了触发这个错误,ArrayObject的内部数组必须引用自身。如前所述,每个解析值会分配到一个索引值。

这是我们最初的字符串: 

http://p5.qhimg.com/t01cf7056a436f1e0ab.jpg

 反序列化这个字符串会触发错误,并导致ArrayObject::unserializ 内的intern→array 指针指向一个被释放了的slot,然后返回到再分配堆。然而,当对members数组进行反序列化时,这个slot被立即分配了(第1798行)。

如前所述,错误导致了堆的损坏。如果我们立即分配相同的slot,损坏的堆不能被修复。在这种情况下,我们没有办法安全地分配新对象。

 一个更好的解决方法是,将members数组引用到已经反序列化的数组中,避免它被分配到一个新的数组。

反序列化: 

http://p3.qhimg.com/t01e999ea617c82ed0b.jpg

现在,ArrayObject的内部数组正在引用自身, 错误已经引发。Members数组是对一个空数组(在stdClass中实例化的第一个对象)的引用。因此, free slot仍然在堆中,可以由我们分配。

接下来,我们需要修复损坏的堆。当我们引发错误时,内部数组的refcount增加了两次:第一次是在反序列化这个引用的时候,第二次是在引用destructors-array的时候。

zend_array的refcount是整个结构中的前面四个字节。当slot在进行去分配时,分配器会使用slot的前四个字节作为指针,指向bin 的free list内的下一个对象。所以,refcount的增量实际上是由于指针增加了2。

为了解决这个问题,我们需要通过引用让count / free list指向一个有效的已释放的slot。zend_array的大小为44个字节,因此它属于48字节大小的bin。可以假定下一个free slot在内部数组后面48字节处(在损坏之前)。为了解决这个问题,我们需要将refcount /指针增加46(2 + 46 = 48)。随着每个反序列化引用的增加,refcount 都会增加2,我们需要再添加23个对已释放数组的引用 (2 + 23 * 2 = 48)。

由此产生的字符串是这样的:

http://p2.qhimg.com/t0117570236c03280f0.jpg

现在我们可以对48-bin内的任何对象——即大小为41-48字节的对象进行反序列化,从而分配已释放的对象。

当我们用自己的对象占用已释放的slot后,还有一件事需要担心:当反序列化过程结束后, destructor-array中的所有引用都减少了。这意味着refcount将下降23。所以在分配后,我们至少要将引用计数增加23。如果我们增加的数量小于23,对象会被释放,这会导致它变成free list内的指针,然后降低更多,从而导致堆的损毁。

因此,稳定的触发器是以下的字符串:

http://p0.qhimg.com/t0131a54af47547e794.jpg

 

在这种情况下,我们仍然有一个分配给使用着的slot的空数组对象。当然,它不是很有用,但是很稳定,不会让引擎崩溃。如果我们少引用了一次数组,数组和其中所有对象都将被释放。我们可以利用这个性质来获得代码执行。

 重点:

 •ArrayObject认为内部数组的任何指向都是指向zend_array结构的指针。这意味着,我们选择分配给已释放slot的任何对象都必须与这个结构类似,即拥有有效的指针等。(在我们的触发字符串中,我们分配了一个真正的数组来避免这个问题)

•在反序列化后,PHP脚本本身可能需要分配一些对象,它可能会分配已释放的对象。为了避免这种情况,我们需要分配和释放几个适当大小的对象。

PHP7:反序列化漏洞案例及分析(下):http://bobao.360.cn/learning/detail/2992.html

报告原文:http://blog.checkpoint.com/wp-content/uploads/2016/08/Exploiting-PHP-7-unserialize-Report-160829.pdf

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多