加入收藏 | 设为首页 | 会员中心 | 我要投稿 均轻资讯网_我爱站长网 (https://www.52junqing.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 站长学院 > PHP教程 > 正文

Web安全实践13:针对PHP数组的DoS

发布时间:2022-10-12 12:41:52 所属栏目:PHP教程 来源:
导读:  这篇是在前一篇的基础上,因为PHP的数组在进行['key':'value']的存储时,也同样是使用x33算法做的,因此,可以使用前一篇的方法构造冲突串。这一篇的目的,其实更重要的是理解PHP数组—&

  这篇是在前一篇的基础上,因为PHP的数组在进行['key':'value']的存储时,也同样是使用x33算法做的,因此,可以使用前一篇的方法构造冲突串。这一篇的目的,其实更重要的是理解PHP数组——面试PHP的时候可能会用得上。
 
  首先看一下PHP数据的使用和实现。
 
  PHP 中的数组是一种强大且灵活的数据类型。首先看一下基本的使用。
 
  // 可以使用数字下标的形式定义数组
  $arr= ['Mike', 2 => 'JoJo'];
  echo $arr[0], $arr[2];  
  它的效果是在$arr[0]中存放了'Mike',$arr[1]空缺,$arr[1]中存放了'JoJo'。
 
  使用var_dump($arr)可以得到
 
  array(2) { [0]=> string(4) "Mike" [2]=> string(4) "JoJo" }
  也可以使用字符串下标定义数组 :
 
  $arr = ['name' => 'Mike', 'age' => 22];  
  得到的结果:
 
  array(2) { ["name"]=> string(4) "Mike" ["age"]=> int(22) }
  可以顺序读取数组中的数据
 
  foreach ($arr as $key => $value) {    
    echo $key,":",$value,"
  ";
  }
  echo current($arr);
  echo next($arr);  
  // 也可以随机读取数组中的数据
  $arr = ['name' => 'Mike', 'age' => 22];
  echo $arr['name'];  
  // 数组的长度是可变的 $arr = [1, 2, 3];
  $arr[] = 4;
  array_push($arr, 5);
  基于这些特性,我们可以很轻易的使用 PHP 中的数组实现集合、栈、列表、字典等多种数据结构。
 
  在了解了PHP数组的基本使用之后,来看一段代码:
 
  $arr = array();
  $arr[0] = 0;
  $arr[1] = 1;
  $arr= array('name' => 'Mike', 'age' => 22,5 => "mary",6=>"alice" );
  $b = array_values($arr);
  foreach ($arr as $key => $value) {
          echo $key,":", $arr[$key], "
  ";
  }
  foreach ($b as $key => $value) {
          echo $key,":", $b[$key], "
  ";
  }
  分析一下,以上代码的输出是什么呢?
 
  name:Mike
  age:22
  5:mary
  6:alice
  0:Mike
  1:22
  2:mary
  3:alice
  这里要注意的是,最开始的两个赋值,
 
  $arr[0] = 0;
  $arr[1] = 1;
  被后面的覆盖了。
 
  再看一段:
 
  $arr = array(1,2,3,4,5);
  $arr['name'] = "mary";
  $arr['first'] = "second";
  $b = array_values($arr);
  foreach ($arr as $key => $value) {
          echo $key,":", $arr[$key], "
  ";
  }
  foreach ($b as $key => $value) {
          echo $key,":", $b[$key], "
  ";
  }
  输出是:
 
  0:1
  1:2
  2:3
  3:4
  4:5
  name:mary
  first:second
  0:1
  1:2
  2:3
  3:4
  4:5
  5:mary
  6:second
  【array_values() 函数返回一个包含给定数组中所有键值的数组,但不保留键名。被返回的数组将使用数值键,从 0 开始并以 1 递增。】
 
  那么再看下面一段代码:
 
  $arr = array('name'=>'Mary','first'=>'Alice','last'=>'Smith','Age'=>'20');
  $arr[0] = 0;
  $arr[1] = 1;
  $b = array_values($arr);
  foreach ($arr as $key => $value) {
          echo $key,":", $arr[$key], "
  ";
  }
  foreach ($b as $key => $value) {
          echo $key,":", $b[$key], "
  ";
  }
  预期的结果是什么呢?下面的结果和预期是否一致呢?
 
  name:Mary
  first:Alice
  last:Smith
  Age:20
  0:0
  1:1
  0:Mary
  1:Alice
  2:Smith
  3:20
  4:0
  5:1
  从上面的结果我们可以看出PHP数组的重要特性:它既支持键值对数组,也即使用map的方法将键映射到整型的Index,同时还保存了一部分的顺序性——也即,数组在存放的时候是按照赋值的顺序保存的。
 
  另外再看一个删除元素的代码:
 
   $value) {
      unset($array[$i]);
  }
  print_r($array);
  // 添加一个单元(注意新的键名是 5,而不是你可能以为的 0)
  $array[] = 6;
  print_r($array);
  // 重新索引:
  $array = array_values($array);
  $array[] = 7;
  print_r($array);
  ?>
  以上代码的输出是:
 
  Array
  (
      [0] => 1
      [1] => 2
      [2] => 3
      [3] => 4
      [4] => 5
  )
  Array
  (
  )
  Array
  (
      [5] => 6
  )
  Array
  (
      [0] => 6
      [1] => 7
  )
  在unset的时候,元素并没有真正删除(导致数组中的元素重新建立索引),而仅仅是将原素设置为undefined;当使用array_values()的时候会真正进行元素重新建立索引。
 
  那么,什么样的底层实现可以达到这样的效果呢?
 
  如果对之前的内容有足够的了解,那么就应该知道为了实现键值对存储,肯定使用了hashtable;和之前的介绍不同的是,为了实现顺序性,PHP使用了两个数组,一个是用来存储值,一个是用来进一步存储哈希的结果与存储位置的对应关系,也即多了一个中间映射表。
 
  对比一下:
 
  PHP数组_php数组_php数组转换为js数组
 
  上图是一次哈希的,也即直接根据X33的结果,对数组的大小取模PHP数组,然后值就存在所得到的模所对应的位置。
 
  PHP中的做法是这样的:
 
  PHP数组_php数组转换为js数组_php数组
 
  在上面的图中,可以看到在获得了“name”这个键的哈希4之后,这个4被用在中间映射表中,然后在中间映射表的4的位置,存放的是存放真正的值的bucket数组的位置0(之所以为0是因为 name => mike是目前插入的第一个键值对)。
 
  实际上,中间映射表和Bucket是相邻的一块连续内存。zend_array 中并没有单独定义中间映射表,而是将其与 arData 放在一起,数组初始化时并不只分配 Bucket 大小的内存,同时还会分配相同大小空间的数据来作为中间映射表,其实现方式如图:
 
  php数组转换为js数组_PHP数组_php数组
 
  正如在一次哈希的过程中,需要使用对数组大小求余来找到具体的位置一样,PHP 中也需要做X33的结果进行一次处理来找到在中间映射表中的位置,采用如下方式对 hash code 进行散列:
 
  nIndex = key->h | nTableMask;
  这里有一个新的变量nTableMask。将哈希值和nTableMask进行或运算即可得出映射表的下标,其中nTableMask数值为nTableSize的负数。并且由于nTableSize的值为 2 的幂次方,所以key->h | nTableMask的取值范围在 [nTableMask, -1]之间,正好在映射表的下标范围内。本质上它就是求余,那为什么不直接使用简单的「取余」呢?而是这么麻烦地采用「按位或」运算呢?因为「按位或」运算的速度要比「取余」运算要快。PHP对与改进性能还是很注意的。
 
  接下来看一下哈希冲突。
 
  任何散列函数都会出现哈希冲突的问题,常见的解决哈希冲突的方法有以下几种:
 
  PHP 采用的是其中的链地址法,将冲突的 Bucket 串成链表,这样中间映射表映射出的就不是某一个元素,而是一个 Bucket 链表,通过散列函数定位到对应的 Bucket 链表时,需要遍历链表,逐个对比 Key 值,继而找到目标元素。
 
  新元素 Hash 冲突时的插入分为以下两步:
 
  php数组_php数组转换为js数组_PHP数组
 
  上图中假设存储了两个键值对,key1=>1;key2=>2; 并且X33时,key1和key2冲突。当存储key1=>1时,hash['key1']的结果是-2,然后中间映射表中-2这个位置存放了0,也即在bucket中的位置0处,存放了key1对应的value 1。当存放key2=>2的时候,计算出来得到中间映射表的值同样是-2,此时,将-2中原始存放的0放在key2=>2的元素的val.u2.next中,然后把-2中的值更新为1。也即,在bucket的位置1处存放了key2对应的value 2,同时value 2的val.u2.next中存放的上一个冲突的键值对存放的位置。
 
  此时访问$a['key1']的过程多了一些步骤。首先通过散列运算得出映射表下标为 -2 ,然后访问映射表发现其内容指向bucket数组中下标为 1 的元素。该元素的key和要访问的键名相比较,发现两者并不相等,意味着发生了冲突,该元素并非我们所想访问的元素,而该元素的val.u2.next保存的值正是下一个具有相同散列值的元素对应bucket数组的下标,所以我们可以不断通过next的值遍历直到找到键名相同的元素或查找失败。
 
  总结一下查找过程:
 
  HashTable 中的查找过程其实已经在上面说完了:
 
  使用 x33 算法对 key 值计算得到hash code使用散列函数计算 hash code 得到散列值 nIndex,即元素在中间映射表的下标通过 nIndex 从中间映射表中取出元素在 Bucket 中的下标 idx通过 idx 访问 Bucket 中对应的数组元素,该元素同时也是一个静态链表的头结点遍历链表,分别判断每个元素中的 key 值是否与我们要查找的 key 值相同如果相同,终止遍历
 
  扩容
 
  在 C 语言中,数组的长度是定长的,那么如果空间已满还需继续插入的时候怎么办呢?PHP 的数组在底层实现了自动扩容机制,当插入一个元素且没有空闲空间时,就会触发自动扩容机制,扩容后再执行插入。
 
  需要提出的一点是,当删除某一个数组元素时,会先使用标志位对该元素进行逻辑删除,而不会立即删除该元素所在的 Bucket,因为后者在每次删除时进行一次排列操作,从而造成不必要的性能开销。
 
  扩容的过程为:
 
  如果已删除元素所占比例达到阈值,则会移除已被逻辑删除的 Bucket,然后将后面的 Bucket 向前补上空缺的 Bucket,因为 Bucket 的下标发生了变动,所以还需要更改每个元素在中间映射表中储存的实际下标值。如果未达到阈值,PHP 则会申请一个大小是原数组两倍的新数组,并将旧数组中的数据复制到新数组中,因为数组长度发生了改变,所以 key-value 的映射关系需要重新计算,这个步骤为重建索引。
 
  PHP数组最小容量是8(2^3),最大容量是0×80000000(2^31),并向2的整数次幂圆整(即长度会自动扩展为2的整数次幂,如13个元素的哈希表长度为16;100个元素的哈希表长度为128)。
 
  ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)
  {
      uint i = 3;
      Bucket **tmp;
      SET_INCONSISTENT(HT_OK);
      //长度向2的整数次幂圆整
      if (nSize >= 0x80000000) {
          /* prevent overflow */
          ht->nTableSize = 0x80000000;
      } else {
          while ((1U << i) < nSize) {
              i++;
          }
          ht->nTableSize = 1 << i;
      }
      ht->nTableMask = ht->nTableSize - 1;
   ......
      return SUCCESS;
  好,基础介绍完毕。
 
  攻击:
 
  那么要对PHP的数组发起DOS攻击,思路和之前是一样的,也即需要让它失去hashtable本身快速检索的性能,让它遇到最坏情况,达到100%碰撞,所有的key都塞进一个bucket里去,hashtable就变成了一个长链表。此时的链表查询就变成了顺序查找,复杂度大大增加。
 
  一种最简单的方法是利用掩码规律制造碰撞。上文提到Zend HashTable的长度nTableSize会被圆整为2的整数次幂,假设我们构造一个2^16的哈希表,则nTableSize的二进制表示为:1 0000 0000 0000 0000,而nTableMask = nTableSize – 1为:0 1111 1111 1111 1111。接下来,可以以0为初始值,以2^16为步长,制造足够多的数据,可以得到如下推测:
 
  0000 0000 0000 0000 0000 & 0 1111 1111 1111 1111 = 0
 
  0001 0000 0000 0000 0000 & 0 1111 1111 1111 1111 = 0
 
  0010 0000 0000 0000 0000 & 0 1111 1111 1111 1111 = 0
 
  0011 0000 0000 0000 0000 & 0 1111 1111 1111 1111 = 0
 
  0100 0000 0000 0000 0000 & 0 1111 1111 1111 1111 = 0
 
  ……
 
  概况来说只要保证后16位均为0,则与掩码位于后得到的哈希值全部碰撞在位置0。
 
  【16也可以是其他的数字;因为mask的位数必定全部为1,所以只要key的值本身足够大,后面的位上的值都相同,那么必然会造成冲突】
 
  看一下攻击代码:
 
  ";
  $endTime = microtime(true);
  echo '插入 ', $size, ' 个恶意的元素需要 ', $endTime - $startTime, ' 秒', "
  ";
  echo "
 
  ";
  $startTime = microtime(true);
  $array = array();
  for ($key = 0, $maxKey = $size - 1; $key <= $maxKey; ++$key) {
      $array[$key] = 0;
      $len = count($array);
  }
  echo "普通数据数组长度为:     ".$len."
  ";
  $endTime = microtime(true);
  echo '插入 ', $size, ' 个普通元素需要 ', $endTime - $startTime, ' 秒', "
  ";
  ?>
 
 

(编辑:均轻资讯网_我爱站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!