php中的变量是以符号表
的方式进行存储的,实际上也是个HashTable
,哈希表是经过特定的哈希算法将索引转换成特定的index
而后映射到对应的槽中,而后采用拉链法
,在一个槽中使用链表将数据进行存储,链表的时间复杂度为O(n)。php
php中的hashtable
的结构定义在Zend/zend_hash.h
文件中:web
//保存数据的单链表结构 typedef struct bucket { ulong h; /* Used for numeric indexing */ uint nKeyLength; //key长度 void *pData; //指向bucket中保存的数据的指针 void *pDataPtr; //指针数据 struct bucket *pListNext; //指向hashtable桶列中下一个元素 struct bucket *pListLast; //指向hashtable桶列前一个元素 struct bucket *pNext; //指向具备同一个hash index的桶列的后一个元素 struct bucket *pLast; //指向具备同一个hash index的桶列的前一个元素 const char *arKey; //必须是最后一个成员,key的名称 } Bucket;
每一个数据元素bucket
有一个键名key
,它在整个hashtable中是惟一的,不能重复;根据键名能够惟一肯定hashtable中的数据元素
在php的数组中,键的值能够是整型或字符串,在这里也只有这两种形式:算法
若是key为字符串的话:字符串arKey
做为键名,该字符串的长度为nKeyLength
,h
字段保存arKey
对应的hash值json
索引方式: 这时nKeyLength
为0,索引值存储在h
上,也就是数据元素的键名数组
bucket
是保存在hashtable
上的一个双向链表,其向前和向后的元素分别用pLast
和pNext
来表示。新插入的Becket放在该桶的最前面
在Bucket
中,实际上数据是保存在pData
指针指向的内存块中的,一般这个内存块是系统额外分配的。可是存在例外,当Bucket
保存的数据是一个指针的时候,Hashtable不会另外请求系统分配空间来保存这个指针,而是直接将该指针保存到pDataPtr
中,而后再将pData
指向本结构成员的地址服务器
hashtable
中全部的bucket
经过pListNext,pListLast
构成了一个双向链表。最新插入的Bucket放在双向链表的最后面数据结构
//hashtable结构体 typedef struct _hashtable { uint nTableSize; uint nTableMask; uint nNumOfElements; ulong nNextFreeElement; Bucket *pInternalPointer; /* Used for element traversal */ Bucket *pListHead; Bucket *pListTail; Bucket **arBuckets; dtor_func_t pDestructor; zend_bool persistent; unsigned char nApplyCount; zend_bool bApplyProtection; #if ZEND_DEBUG int inconsistent; #endif } HashTable;
hash
表的主要保存的数据包括两部分:哈希表中存储的数据的个数;保存数据的容器
在HashTable
结构中,nTableSize
指定了HashTable
的大小,同时也限定了哈希表中能保存的Bucket
的数量,该数值越大,系统为HashTable
分配的内存就越多。为了提升计算效率, 系统自动会将nTableSize
调整到最小一个不小于nTableSize
的2的整数次方,这个其实不太懂arBuckets
是HashTable
的关键,HashTable
会自动向系统申请一块内存,并将其地址赋值给arBuckets
,该内存大小正好容纳nTableSize
指针。咱们能够将arBuckets
看做一个大小为nTableSize
的数组,每一个数组元素都是一个指针,用于指向存放数据的Buckets
。在初始化的时候每一个指针都为null函数
nTableMask
的值永远是nTableSize - 1
,引入这个字段的主要目的是为了提升计算效率,为了快速计算Buckets
键名在arBuckets
数组中的索引post
nNumberOfElements
记录了HastTable
当前保存的数据元素的个数。当nNumberOfElements
大于nTableSize
的时候,HashTable
就自动扩展为原来的两倍大小ui
nNextFreeElement
记录HashTable
中下一个可用于插入数据元素的arBuckets
索引
pListHead,pListTail
分别表示Buckets
双向链表的第一个和最后一个元素,这些元素一般是根据插入的顺序排列的。也能够根据对应的排序函数对其进行从新排列。pInternalPointer
则用于在遍历HashTable
时记录当前遍历的位置,它是一个指针,指向当前遍历到的Bucket
,初始值是pListHead
pDestroyctor
是一个函数指针,在HashTable
的增长,修改,删除Bucket
时自动调用,用于处理相关数据的清理工做
persistent
标志位指出了Bucket
内存分配的方式。若是persisient
为TRUE,则使用操做系统自己的内存分配函数为Bucket
分配内存,不然使用PHP的内存分配函数。具体请参考PHP的内存管理。
php中为了使用的哈希算法是DJBX33A
php中是使用单链表去存储碰撞的数据的,因此实际上php在哈希表上的平均查找复杂度为O(L)
,其中L为桶链表的平均长度;而最坏的时间复杂度为O(N)
,此时因此存储到hashtable上的数据所有发生碰撞,哈希表退化成为单链表。
哈希表碰撞就是精心设计的数据使全部的数据发生碰撞。人为的将一个哈希表
退化成为一个单链表
,在哈希表上的各类操做的时间会提高一个数量级,大量消耗CPU,致使系统没法快速响应请求,从而达到拒绝服务(DDOS
)的目的
<?php $size = pow(2, 16); $startTime = microtime(true); $array = array(); for ($key = 0, $maxKey = ($size - 1) * $size; $key <= $maxKey; $key += $size) { var_dump($key); //$array[$key] = 0; } $endTime = microtime(true); echo $endTime - $startTime, ' seconds', "\n"; ?>
通常状况下很难直接修改php的代码去制造哈希碰撞攻击,可是在表单参数$_POST
是一个Array,内部就是经过Zend HashTable
来实现的,攻击者只要构造一个含有大量碰撞的key的post请求,就能够达到ddos攻击的目的
以上说了经过http post请求中的表单数据进行攻击,防护的方式能够:
在php5.3以上的版本中,post参数的数量存在最大的限制max_input_vars => 1000
在web服务器层面进行处理,如经过限制请求body大小和参数的数量等
,这个也是目前使用最多的解决方案
其实以上的两种解决方案并不能解决问题,由于只是单纯的在参数的数量上进行限制了,可是入股请求的数据中包含json数据,但其中的数据就是碰撞的array。理论上,只要php代码某处构造array的数据依赖于外部输入,则均可能形成这个问题,所以完全的解决方案是更改Zend底层的HashTable
实现
限制每一个桶链表的最长长度
使用其余数据结构如红黑树取代链表组织碰撞哈希(并不解决哈希碰撞,只是减轻攻击影响,将N个数据的操做时间从O(N^2)降至O(NlogN),代价是普通状况下接近O(1)的操做均变为O(logN))
参考:
php内核探索:http://www.nowamagic.net/libr...