Hash Collision DoS事件及影响
Hash Collision DoS能让受攻击的服务器变得巨慢无比。
这不是因为服务器的编码原因或是疏忽造成的,而是程序语言自身的问题,Hash Collision DoS利用了各语言中Hash算法的“非随机性”可以制造出N多不一样的value,但是key一样数据,然后让Hash表成为一张单向链表,从而导致整个网站或是程序的运行性能以级数下降。有数据说,10kb的数据量就会导致一个i7的CPU马上占用率飙升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和其它开发语言的注意。以上不幸中招开发语言的列表还在继续更新之中,最新情况可查看: oCERT的2011-003报告。
更详细的内容,可参考这里。
原理及解决方法:Java相关
这些语言使用的Hash算法都是“非随机的”,比如Java和Oracle使用的Hash函数:
staticinthash(inth)
{
h ^= (h >>> 20) ^ (h >>> 12);
returnh ^ (h >>> 7) ^ (h >>> 4);
}
所谓“非随机的” Hash算法,就可以猜。比如:
1)在Java里, Aa和BB这两个字符串的hash code(或hash key) 是一样的,也就是Collision 。
2)于是,可以通过这两个种子生成更多的拥有同一个hash key的字符串。如:”AaAa”, “AaBB”, “BBAa”, “BBBB”。这是第一次迭代。其实就是一个排列组合,写个程序就搞定了。
3)然后,可以用这4个长度的字符串,构造8个长度的字符串,如下所示:
"AaAaAaAa", "AaAaBBBB", "AaAaAaBB", "AaAaBBAa",
"BBBBAaAa", "BBBBBBBB", "BBBBAaBB", "BBBBBBAa",
"AaBBAaAa", "AaBBBBBB", "AaBBAaBB", "AaBBBBAa",
"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 模块解决:
connect()
.use(connect.limit('1mb'))
.use(handleRequest)
修改 qs 模块,让其支持 keys-limit 和 allow-keys
querystring.js
PS: 提了pull request,但是估计在没有真实攻击示例放出来之前,是不会被接受的。
/**
* Parse the given str.
*/
function parseString(str, options) {
var limit = options && options.limit;
var keys = options && options.keys;
if (keys && Array.isArray(keys)) {
keys = {};
for (var i = 0, l = options.keys.length; i < l; i++) {
keys[options.keys[i]] = 1;
}
}
return String(str)
.split('&', limit)
.reduce(function(ret, pair){
try{
pair = decodeURIComponent(pair.replace(/\+/g, ' '));
} catch(e) {
// ignore
}
var eql = pair.indexOf('=')
, brace = lastBraceInKey(pair)
, key = pair.substr(0, brace || eql);
if (keys && !keys[key]) {
return ret;
}
var val = pair.substr(brace || eql, pair.length)
val = val.substr(val.indexOf('=') + 1, val.length);
// ?foo
if ('' == key) key = pair, val = '';
return merge(ret, key, val);
}, { base: {} }).base;
}
/**
* Parse the given query `str` or `obj`, returning an object.
*
* Options: (only effect on parse string)
*
* - `limit` parse string split limit.
* - `keys` which keys need to be parse.
* @param {String} str | {Object} obj
* @param {Object} options
* @return {Object}
* @api public
*/
exports.parse = function(str, options) {
if (null == str || '' == str) return {};
return 'object' == typeof str
? parseObject(str)
: parseString(str, options);
};
还需要让 connect.query 模块 传递options参数给 qs.parse()
module.exports = function query(options){
return function query(req, res, next){
req.query = ~req.url.indexOf('?')
? qs.parse(parse(req.url).query, options)
: {};
next();
};
};
同样 connect.urlencoded 模块也需要将options参数传递给 qs.parse()
req.on('end', function(){
try {
req.body = buf.length
? qs.parse(buf, options)
: {};
next();
} catch (err){
next(err);
}
});
全部组合起来
var qsOptions = { limit: 100 };
connect()
.use(connect.limit('1mb'))
.use(connect.query(qsOptions))
.use(connect.bodyParser(qsOptions))
.use(handleRequest)
防范 http header 攻击
请求的 http header 也会导致hash冲突,在V8层面未修复hash算法之前,可以通过简单的 http_patch.js 修复此问题:
var http = require('http');
var IncomingMessage = http.IncomingMessage;
var _addHeaderLine = IncomingMessage.prototype._addHeaderLine;
// limit http header number
IncomingMessage.prototype._addHeaderLine = function(field, val) {
if (!this.__headerCount__) {
this.__headerCount__ = 0;
} else if (this.__headerCount__ >= 100) {
return;
}
_addHeaderLine.apply(this, arguments);
this.__headerCount__++;
};
业界人士看法
@Laruence:今天尝试了随机增长Hashtable大小来克服Hash Ddos, 不过, 后来证明, 这种方法只能防止Number Key, 而对于String Key,
攻击者总能找到一些特殊的Key, 他们在DJB Hash以后的结果相同.
目前新的修复方案进度搁置. 5.3.9可能只能发布包含限制max_input_vars的修复措施.
@beyondme37 :问题是出在计算key的hash函数。比如我们的存入一个哈希表的数据为整数,我们计算key的hash函数如下:
int hash(int x)
{
return (x % 5);
}
那我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算法,一定要保证必然碰撞事件的概率降低,也就是说提高生产位数,带来的痛苦就是请求效率降低。防御之法是做好异常检测,理性区分正常与恶意的数据请求。
资料参考:
Defense hash algorithm collision 防御hash算法冲突导致拒绝服务器
Multiple programming languages vulnerable to DoS via hash algorithm collision