u8,u8国际,u8国际官方网站,u8国际网站,u8国际网址,u8国际链接,u8体育,u8体育官网,u8体育网址,u8注册,u8体育网址,u8官方网站,u8体育APP,u8体育登录,u8体育入口
哈希表是一种查找效率极高的数据结构,很多语言都在内部实现了哈希表。php中的哈希表是一种极为重要的数据结构,不但用于表示array数据类型,还在zend虚拟机内部用于存储上下文环境信息(执行上下文的变量及函数均使用哈希表结构存储)。
理想情况下哈希表插入和查找操作的时间复杂度均为o(1),任何一个数据项可以在一个与哈希表长度无关的时间内计算出一个哈希值(key),然后在常量时间内定位到一个桶(术语bucket,表示哈希表中的一个位置)。当然这是理想情况下,因为任何哈希表的长度都是有限的,所以一定存在不同的数据项具有相同哈希值的情况,此时不同数据项被定为到同一个桶,称为碰撞(collision)。哈希表的实现需要解决碰撞问题,碰撞解决大体有两种思路,第一种是根据某种原则将被碰撞数据定为到其它桶,例如线性探测——如果数据在插入时发生了碰撞,则顺序查找这个桶后面的桶,将其放入第一个没有被使用的桶;第二种策略是每个桶不是一个只能容纳单个数据项的位置,而是一个可容纳多个数据的数据结构(例如链表或红黑树),所有碰撞的数据以某种数据结构的形式组织起来。
不论使用了哪种碰撞解决策略,都导致插入和查找操作的时间复杂度不再是o(1)。以查找为例,不能通过key定位到桶就结束,必须还要比较原始key(即未做哈希之前的key)是否相等,如果不相等,则要使用与插入相同的算法继续查找,直到找到匹配的值或确认数据不在哈希表中。
php是使用单链表存储碰撞的数据,因此实际上php哈希表的平均查找复杂度为o(l),其中l为桶链表的平均长度;而最坏复杂度为o(n),此时所有数据全部碰撞,哈希表退化成单链表。下图php中正常哈希表和退化哈希表的示意图。
哈希表碰撞攻击就是通过精心构造数据,使得所有数据全部碰撞,人为将哈希表变成一个退化的单链表,此时哈希表各种操作的时间均提升了一个数量级,因此会消耗大量cpu资源,导致系统无法快速响应请求,从而达到拒绝服务攻击(dos)的目的。
可以看到,进行哈希碰撞攻击的前提是哈希算法特别容易找出碰撞,如果是md5或者sha1那基本就没戏了,幸运的是(也可以说不幸的是)大多数编程语言使用的哈希算法都十分简单(这是为了效率考虑),因此可以不费吹灰之力之力构造出攻击数据。下一节将通过分析zend相关内核代码,找出攻击哈希表碰撞攻击php的方法。
字段名很清楚的表明其用途,因此不做过多解释。重点明确下面几个字段:bucket中的“h”用于存储原始key;hashtable中的ntablemask是一个掩码,一般被设为ntablesize - 1,与哈希算法有密切关系,后面讨论哈希算法时会详述;arbuckets指向一个指针数组,其中每个元素是一个指向bucket链表的头指针。
php哈希表最小容量是8(2^3),最大容量是0×80000000(2^31),并向2的整数次幂圆整(即长度会自动扩展为2的整数次幂,如13个元素的哈希表长度为16;100个元素的哈希表长度为128)。ntablemask被初始化为哈希表长度(圆整后)减1。具体代码在zend/zend_hash.c的_zend_hash_init函数中,这里截取与本文相关的部分并加上少量注释。
知道了php内部哈希表的算法,就可以利用其原理构造用于攻击的数据。一种最简单的方法是利用掩码规律制造碰撞。上文提到zend hashtable的长度ntablesize会被圆整为2的整数次幂,假设我们构造一个2^16的哈希表,则ntablesize的二进制表示为:1 0000 0000 0000 0000,而ntablemask = ntablesize - 1为:0 1111 1111 1111 1111。接下来,可以以0为初始值,以2^16为步长,制造足够多的数据,可以得到如下推测:
上面的防护方法只是限制post数据的数量,而不能彻底解决这个问题。例如,如果某个post字段是一个json数据类型,会被php json_decode ,那么只要构造一个超大的json攻击数据照样可以达到攻击目的。理论上,只要php代码中某处构造array的数据依赖于外部输入,则都可能造成这个问题,因此彻底的解决方案要从zend底层hashtable的实现动手。一般来说有两种方式,一是限制每个桶链表的最长长度;二是使用其它数据结构如 红黑树 取代链表组织碰撞哈希(并不解决哈希碰撞,只是减轻攻击影响,将n个数据的操作时间从o(n^2)降至o(nlogn),代价是普通情况下接近o(1)的操作均变为o(logn))。