涉及所有主流开发语言的Hash安全漏洞

导语:近来IT技术领域热点事件频发,特别是Hash Collision DoS (通过Hash碰撞进行的拒绝式服务攻击),更是吸引了广大技术人员的注意。通过Hash Collision DoS,能让受攻击的服务器变得巨慢无比。而这个漏洞早在2003年时就有过相关论文报告进行了预警,但好像并没有引起当时正蓬勃发展的Java和其他开发语言设计者的注意。直到现在,这个漏洞几乎影响到了所有的开发语言,而且截止发稿时,业界也没有找到一条完美的解决之道。

Hash Collision DoS事件及影响

Hash Collision DoS能让受攻击的服务器变得巨慢无比。

这不是因为服务器的编码原因或是疏忽造成的,而是程序语言自身的问题,Hash Collision DoS利用了各语言中Hash算法的非随机性可以制造出N多不一样的value,但是key一样数据,然后让Hash表成为一张单向链表,从而导致整个网站或是程序的运行性能以级数下降。有数据说,10kb的数据量就会导致一个i7CPU马上占用率飙升100%,这真是恐怖。不幸的是,除了Perl之外,这个漏洞使得包括Java, JRuby, PHP, Python在内的以下各种开发语言和许多常用软件都纷纷中招:

  • Java, 所有版本
  • JRuby <= 1.6.5 (目前fix在 1.6.5.1)
  • PHP <= 5.3.8, <= 5.4.0RC3 (目前fix在 5.3.9,  5.4.0RC4)
  • Python, all versions
  • Rubinius, all versions
  • Ruby <= 1.8.7-p356 (目前fix在 1.8.7-p357, 1.9.x)
  • Apache Geronimo, 所有版本
  • Apache Tomcat <= 5.5.34, <= 6.0.34, <= 7.0.22 (目前fix在 5.5.35,  6.0.35,  7.0.23)
  • Oracle Glassfish <= 3.1.1 (目前fix在mainline)
  • Jetty, 所有版本
  • Plone, 所有版本
  • Rack <= 1.3.5, <= 1.2.4, <= 1.1.2 (目前fix 在 1.4.0, 1.3.6, 1.2.5, 1.1.3)
  • V8 JavaScript Engine, 所有版本
  • ASP.NET 没有打MS11-100补丁

事实上,Hash Collision DoS 漏洞并不是突然出现,早在2003年的一篇论文《通过算法复杂性进行拒绝式服务攻击》中就有相关报告进行了预警,但好像并没有引起当时正蓬勃发展的Java和其它开发语言的注意。以上不幸中招开发语言的列表还在继续更新之中,最新情况可查看: oCERT2011-003报告

更详细的内容,可参考这里。

原理及解决方法:Java相关

这些语言使用的Hash算法都是非随机的比如JavaOracle使用的Hash函数:

  1. staticinthash(inth)   
  2. {   
  3. h ^= (h >>> 20) ^ (h >>> 12);   
  4. returnh ^ (h >>> 7) ^ (h >>> 4);   
  5. }  

所谓非随机的” Hash算法,就可以猜。比如:

1)在Java里, AaBB这两个字符串的hash code(hash key) 是一样的,也就是Collision 

2)于是,可以通过这两个种子生成更多的拥有同一个hash key的字符串。如:”AaAa”, “AaBB”, “BBAa”, “BBBB”。这是第一次迭代。其实就是一个排列组合,写个程序就搞定了。

3)然后,可以用这4个长度的字符串,构造8个长度的字符串,如下所示:

  1. “AaAaAaAa”“AaAaBBBB”“AaAaAaBB”“AaAaBBAa”,   
  2. “BBBBAaAa”“BBBBBBBB”“BBBBAaBB”“BBBBBBAa”,   
  3. “AaBBAaAa”“AaBBBBBB”“AaBBAaBB”“AaBBBBAa”,   
  4. “BBAaAaAa”“BBAaBBBB”“BBAaAaBB”“BBAaBBAa”,  

4)同理,就可以生成16个长度的、以及256个长度的字符串,总之,很容易生成N多的这样的值。

在攻击时,只需要把这些数据做成一个HTTP POST 表单,然后写一个无限循环的程序,不停地提交这个表单。用浏览器就可以实现。当然,如果做得更精妙一点的话,把这个表单做成一个跨站脚本,然后找一些网站的跨站漏洞,放上去,于是能过SNS的力量就可以找到N多个用户从不同的IP来攻击某服务器。

要防守这样的攻击,有下面几招:

打补丁,把hash算法改了。

限制POST的参数个数,限制POST的请求长度。

最好还有防火墙检测异常的请求。

不过,对于更底层的或是其它形式的攻击,可能就有点麻烦了。

原理及解决方法:PHP相关

下面再结合PHP内核源码,聊一聊这种攻击的原理及实现。

PHP是使用单链表存储碰撞的数据,因此实际上PHP哈希表的平均查找复杂度为O(L),其中L为桶链表的平均长度;而最坏复杂度为O(N),此时所有数据全部碰撞,哈希表退化成单链表。下图PHP中正常哈希表和退化哈希表的示意图。

哈希表碰撞攻击就是通过精心构造数据,使得所有数据全部碰撞,人为将哈希表变成一个退化的单链表,此时哈希表各种操作的时间均提升了一个数量级,因此会消耗大量CPU资源,导致系统无法快速响应请求,从而达到拒绝服务攻击(DoS)的目的。

攻击者可以通过一些方法间接构造哈希表来进行攻击。例如PHP可利用POST方式进行攻击,针对这种方式的哈希碰撞攻击,目前PHP的防护措施是控制POST数据的数量。另外的防护方法是在Web服务器层面进行处理,例如限制http请求body的大小和参数的数量等,这个是现在用的最多的临时处理方案。这些方法只是限制POST数据的数量,而不能彻底解决这个问题。彻底的解决方案要从Zend底层HashTable的实现动手。一般来说有两种方式,一是限制每个桶链表的最长长度;二是使用其它数据结构如红黑树取代链表组织碰撞哈希(并不解决哈希碰撞,只是减轻攻击影响,将N个数据的操作时间从O(N^2)降至O(NlogN),代价是普通情况下接近O(1)的操作均变为O(logN))。

目前使用最多的仍然是POST数据攻击,因此建议生产环境的PHP均进行升级或打补丁。至于从数据结构层面修复这个问题,目前还没有任何方面的消息。

更多详细内容可参考原文

原理及解决方法:Nodejs相关

以 connect 为示例说明Nodejs防御此问题

使用 connect.limit 限制 request-body-size,直接上 connect.limit 模块解决:

  1. connect()   
  2.   .use(connect.limit(’1mb’))   
  3.   .use(handleRequest)  

修改 qs 模块,让其支持 keys-limit 和 allow-keys
querystring.js
PS: 提了pull request,但是估计在没有真实攻击示例放出来之前,是不会被接受的。

  1. /**  
  2.  * Parse the given str.  
  3.  */  
  4. function parseString(str, options) {   
  5.   var limit = options && options.limit;   
  6.   var keys = options && options.keys;   
  7.   if (keys && Array.isArray(keys)) {   
  8.     keys = {};   
  9.     for (var i = 0, l = options.keys.length; i < l; i++) {   
  10.       keys[options.keys[i]] = 1;   
  11.     }   
  12.   }   
  13.   return String(str)   
  14.     .split(‘&’, limit)   
  15.     .reduce(function(ret, pair){   
  16.       try{   
  17.         pair = decodeURIComponent(pair.replace(/\+/g, ’ ’));   
  18.       } catch(e) {   
  19.         // ignore   
  20.       }   
  21.       var eql = pair.indexOf(‘=’)   
  22.         , brace = lastBraceInKey(pair)   
  23.         , key = pair.substr(0, brace || eql);   
  24.       if (keys && !keys[key]) {   
  25.         return ret;   
  26.       }   
  27.       var val = pair.substr(brace || eql, pair.length)   
  28.       val = val.substr(val.indexOf(‘=’) + 1, val.length);   
  29.       // ?foo   
  30.       if ( == key) key = pair, val = ;   
  31.       return merge(ret, key, val);   
  32.     }, { base: {} }).base;   
  33. }   
  34.   
  35. /**
  36.  * Parse the given query `str` or `obj`, returning an object.  
  37.  * Options: (only effect on parse string)  
  38.  *  - `limit` parse string split limit.  
  39.  *  - `keys`  which keys need to be parse.  
  40.  * @param {String} str | {Object} obj  
  41.  * @param {Object} options  
  42.  * @return {Object}  
  43.  * @api public  
  44.  */  
  45.   
  46. exports.parse = function(str, options) {   
  47.   if (null == str ||  == str) return {};   
  48.   return ’object’ == typeof str   
  49.     ? parseObject(str)   
  50.     : parseString(str, options);   
  51. };  

还需要让 connect.query 模块 传递options参数给 qs.parse()

  1. module.exports = function query(options){   
  2.   return function query(req, res, next){   
  3.     req.query = ~req.url.indexOf(‘?’)   
  4.       ? qs.parse(parse(req.url).query, options)   
  5.       : {};   
  6.     next();   
  7.   };   
  8. };  

同样 connect.urlencoded 模块也需要将options参数传递给 qs.parse()

  1. req.on(‘end’, function(){   
  2.   try {   
  3.     req.body = buf.length   
  4.       ? qs.parse(buf, options)   
  5.       : {};   
  6.     next();   
  7.   } catch (err){   
  8.     next(err);   
  9.   }   
  10. });  

全部组合起来

  1. var qsOptions = { limit: 100 };   
  2. connect()   
  3.   .use(connect.limit(’1mb’))   
  4.   .use(connect.query(qsOptions))   
  5.   .use(connect.bodyParser(qsOptions))   
  6.   .use(handleRequest)  

防范 http header 攻击
请求的 http header 也会导致hash冲突,在V8层面未修复hash算法之前,可以通过简单的 http_patch.js 修复此问题:

  1. var http = require(‘http’);   
  2. var IncomingMessage = http.IncomingMessage;   
  3. var _addHeaderLine = IncomingMessage.prototype._addHeaderLine;   
  4. // limit http header number   
  5. IncomingMessage.prototype._addHeaderLine = function(field, val) {   
  6.   if (!this.__headerCount__) {   
  7.     this.__headerCount__ = 0;   
  8.   } else if (this.__headerCount__ >= 100) {   
  9.     return;   
  10.   }   
  11.   _addHeaderLine.apply(this, arguments);   
  12.   this.__headerCount__++;   
  13. };  

业界人士看法
@Laruence:今天尝试了随机增长Hashtable大小来克服Hash Ddos, 不过, 后来证明, 这种方法只能防止Number Key, 而对于String Key, 攻击者总能找到一些特殊的Key, 他们在DJB Hash以后的结果相同. 目前新的修复方案进度搁置. 5.3.9可能只能发布包含限制max_input_vars的修复措施.
@beyondme37 :问题是出在计算key的hash函数。比如我们的存入一个哈希表的数据为整数,我们计算key的hash函数如下:

  1. int hash(int x)   
  2. {   
  3. return (x % 5);   
  4. }  

那我x输入6, 11, 16, 21, 26 … 返回的计算出的key都是1,都会存在哈希表1位置,即1位置冲突,而一般解决哈希表冲突的方法为拉链法,即数据全部存在1位置成单链表了,所以查找速度会下降为O(n)级别。

 

@wenbo :可以简单理解为,tomcat 之类给你做了一个基础框架,这个框架会事先把提交的请求解析并装载在某个hash_map——那些会冲突的KEY可以理解为参数名,KEY=后面的值则是该参数的取值。然后,你的脚本不再需要解析http请求,而是直接从容器中用参数名取出各个参数的实际值即可。这个攻击针对的是第一步,即底层架构预先解析请求并将解析出的内容存储在某个容器内这个步骤。底层架构并不知道上层应用要如何解释这些参数,所以它只能把所有参数缓存起来等待处理;至于上层应用,此时并未被唤起,所以也无法对此决策。至多可以在部署时通过配置声明在我的所有网页里,最多会用到1000个参数,或者在我的应用里,参数及其内容加起来最多10K字节

目前暂无完美解决之道

虽然半个多月前Tomcat就紧急发布安全漏洞通知,同时微软也发布了相应的安全漏洞通知,但他们都是通过变通的方式来解决此拒绝服务漏洞:就是告知大家,将请求参数据缓存值设小,也就是说,一次性的请求量不会导致CPU被全部占满——这真是无奈之举。除此之外,就是设计好HASH算法,一定要保证必然碰撞事件的概率降低,也就是说提高生产位数,带来的痛苦就是请求效率降低。防御之法是做好异常检测,理性区分正常与恶意的数据请求。

4 条评论

  • nootropic

    Really wonderful info can be found on this site.

    28
  • click here

    I love your wonderful site. first class information. I hope you write more. I will carry on watching

    28
  • avitamin

    You have noted very interesting details! ps decent website.

    29
  • nootropic

    Cheers, I just stopped by to visit your website and thought I’d say I had a great visit.

    29

发表评论

发表您的评论...

有回复时邮件通知我

无觅相关文章插件,快速提升流量