文/玄魂
1.Hash與Hash碰撞
Hash,簡單來講,是一種將任意長度的輸入變換成固定長度的輸出,固定長度的輸出在“實際應用場景”下可以代表該輸入。Hash函數通常被翻譯成散列函數。Hash通常用來校驗信息的一致性。
Hash函數的實現多種多樣,在安全領域應用最為廣泛的是SHA-x系列和MDx系列。Hash函數也划分為帶密鑰的Hash函數和不帶密鑰的Hash函數,通常所說的Hash函數是不帶密鑰的Hash函數。這里對Hash算法的實現原理不做更多的探討。
篇外補充: Hash函數的定義和性質 |
由於Hash固定長度輸出的特性,必然會存在多個不同輸入產生相同輸出的情況。如果兩個輸入串的hash函數的值一樣,則稱這兩個串是一個碰撞(Collision)。在理論范圍內,存在一個輸出串對應無窮多個輸入串,所以碰撞具有其必然性。
如果找到碰撞,那么意味着我們可以破壞信息的一致性而不被接收方察覺,搜尋指定輸入的Hash碰撞值的過程被稱作“Hash破解”。這里需要說明的是,Hash函數必須是不可逆的,所以不存在從散列值到原始輸入的破解(這里不包括暴力破解,使用彩虹表是暴力破解的最佳方式,但是仍然無法保證破解到的數據是原始數據)。設計不良的Hash算法,很容易讓人找到碰撞值,例如(參考網址:http://www.laruence.com/2011/12/30/2435.html):
“
在PHP中,如果鍵值是數字, 那么Hash的時候輸入值就是數字本身, 一般的時候都是, index & tableMask. 而tableMask是用來保證數字索引不會超出數組可容納的元素個數值, 也就是數組個數-1.
PHP的Hashtable的大小都是2的指數, 比如如果你存入10個元素的數組, 那么數組實際大小是16, 如果存入20個, 則實際大小為32, 而63個話, 實際大小為64. 當你的存入的元素個數大於了數組目前的最多元素個數的時候, PHP會對這個數組進行擴容, 並且從新Hash.
現在, 我們假設要存入64個元素(中間可能會經過擴容, 但是我們只需要知道, 最后的數組大小是64, 並且對應的tableMask為63:0111111), 那么如果第一次我們存入的元素的鍵值為0, 則hash后的值為0, 第二次我們存入64, hash(1000000 & 0111111)的值也為0, 第三次我們用128, 第四次用192…
”
一個“優良”的hash函數 f 應當滿足以下三個條件:
- 任意y,找x,使得f(x)=y,非常困難。
- 給定x1,找x2,使得f(x1)=f(x2),非常困難。
- 找x1,x2,使得f(x1)=f(x2),非常困難。
上面的“非常困難”的意思是除了枚舉外不可能有別的更快的方法。幾乎所有的尋找碰撞的方法都從第三條入手,即找到兩個不一樣的輸入得到相同的輸出。
尋找Hash碰撞的方法也很多如用於一般性攻擊的生日攻擊和模差分法,用於攻擊具有分組鏈結構的Hash方案的中間相遇攻擊,於攻擊基於模算術的Hash函數的修正分組攻擊。這里我簡要介紹以下四種尋找碰撞的方法:
- 相等子串法
- 生日攻擊法
- 中間相遇法
- 模差分法
“相等子串法”是針對某些Hash函數具有相同的字符串組合在上下文中相同位置的Hash值都相同的特性來構造碰撞的。比如f(“string1”)=f(“string2”),那么字符串“aaastring1bbb”與字符串“aaastring2bbb”中,“string1”與“string2”具有相同的Hash值。針對這個特性我們可以構造任意多的碰撞,比如“Ly”和“nz”的Hash值相同,那么“LyLy”、“nznz”、“Lynz”、“nzLy”的Hash值都相同。
設h:X->Y是一個Hash函數,X和Y都是有限的,並且|X|>=2|Y|,記|X|=m,|Y|=n。顯然至少有n個碰撞,問題是如何去找到這些碰撞。一個很自然的方法是隨機選擇k個不同的元素x1,x2,x3,••••••,xk ∈X,計算yI=h(xi),1<=i<=k,然后確定是否有一個碰撞發生。這個過程類似於把k個球隨機地扔到n個箱子里邊,然后檢查看是否某一箱子里邊至少有兩個球。k個球對應於k個隨機數x1,x2,x3,••••••,xk,n個箱子對應於Y中的n個可能的元素。我們將計算用這種方法找到一個碰撞的概率的下界,該下界只依賴於k和n,而不依賴於m。 |
因為我們關心的是碰撞概率的下界,所以可以假定對所有y∈Y,有|h-1(y)|≈m/n。這個假定是合理的,這是因為如果原像集h-1(y)( y∈Y)不是近似相等的,那么找到一個碰撞的概率將增大。 |
因為原像集h-1(y)( y∈Y)的個數都近似相等,並且xI(1<=i<=k)是隨機選擇的,所以可將yI=h(xi),1<=i<=k視作Y中的隨機元素(yi(1<=i<=k)未必不同)。但計算k個隨機元素y1,y2, ••••••yk∈Y是不同的概率是一件容易的事情。依次考慮y1,y2, ••••••yk。y1可任意地選擇;y2 ≠y1的概率為1-1/n;y3 ≠y1 ,y2的概率為1-2/n;••••••;yk ≠y1,y2, ••••••,yk-1的概率為1-(k-1)/n。 |
因此,沒有碰撞的概率是(1-1/n)(1-2/n)••••••(1-(k-1)/n)。如果x是一個比較小的實數,那么1-x≈e-x,這個估計可由下式推出:e-x=1-x+x2/2!-x3/3!+ ••••••。現在估計沒有碰撞的概率(1-1/n)(1-2/n)••••••(1-(k-1)/n)約為1-e-k(k-1)/2n。我們設ε是至少有一個碰撞的概率,則ε≈1-e-k(k-1)/2n,從而有k2-k≈nln(1/(1-ε)2)。去掉-k這一項,我們有k2≈nln(1/(1-ε)2),即k≈sqrt(2nln(1/(1-ε)2))。 |
如果我們取ε=0.5,那么k≈1.17 sqrt(n)。這表明,僅sqrt(n)個X的隨機的元素就能以50%的概率產生一個碰撞。注意ε的不同選擇將導致一個不同的常數因子,但k與sqrt(n)仍成正比例。 |
如果X是一個教室中的所有學生的集合,Y是一個非閏年的365天的集合,h(x)表示學生x的生日,這時n=365,ε=0.5,由k≈1.17 sqrt(n)可知,k≈22.3。因此,此生日問題的答案為23。 |
生日攻擊隱含着消息摘要的長度的一個下界。一個40比特長的消息摘要是很不安全的,因為僅僅用220(大約一百萬)次隨機Hash可至少以1/2的概率找到一個碰撞。為了抵抗生日攻擊,通常建議消息摘要的長度至少應取為128比特,此時生日攻擊需要約264次Hash。安全的Hash標准的輸出長度選為160比特是出於這種考慮。 |
“中間相遇法”是生日攻擊的一種變形,它不比較Hash值,而是比較鏈中的中間變量。這種攻擊主要適用於攻擊具有分組鏈結構的Hash方案。中間相遇攻擊的基本原理為:將消息分成兩部分,對偽造消息的第一部分從初試值開始逐步向中間階段產生r1個變量;對偽造消息的第二部分從Hash結果開始逐步退回中間階段產生r2個變量。在中間階段有一個匹配的概率與生日攻擊成功的概率一樣。
“模差分法”是山東大學王小雲教授提出的Hash分析方法,具有較高的執行效率。模差分的算法請參考http://wenku.baidu.com/view/f0bf451414791711cc7917b5.html?from=related。
2.HashTable與HashTable退化
上面我們了解了Hash函數的特性和Hash攻擊的可行性。我在這里沒有詳細說明攻擊的算法細節,也沒有給出具體的代碼實現,因為這些不是本文要關注的重點,之后如果有機會我會專門討論Hash攻擊的各種方案及代碼實現。下面我們來了解和Hash函數密切相關的數據結構—HashTable(參考內容:http://en.wikipedia.org/wiki/Hash_table )。
HashTable(散列表,也叫哈希表),是一種根據鍵值(key-value)進行直接訪問的數據結構。
HashTable結合了鏈表和數組的雙向優勢,所以增、刪、改、查各種操作速度都很快。在HashTable中,Hash函數的作用是通過Key得到Value的地址(數組的下標),這和我們前面說到的在安全領域保證數據完整性的Hash算法的功能有所區別,因為要返回的是數組下標,所以Hash值必須是整數。因此信息安全領域的標准Hash算法在這里就無用武之地了,不同的應用開發平台通常實現自己的Hash算法,或者使用在HashTable構造算法中常用的Hash函數,比如DJBX33A算法。
前面我們說話Hash無法避免Hash碰撞的問題,那么HashTable如何處理碰撞問題呢,通常有兩種做法:開放定址法,鏈接法。
開放定址法(Open addressing)這種方法就是在計算一個key的哈希的時候,發現目標地址已經有值了,即發生沖突了,這個時候通過相應的函數在此地址后面的地址去找,直到沒有沖突為止。這個方法常用的有線性探測,二次探測,再哈希。這種解決方法有個不好的地方就是,當發生沖突之后,會在之后的地址空間中找一個放進去,這樣就有可能后來出現一個key哈希出來的結果也正好是它放進去的這個地址空間,這樣就會出現非同義詞的兩個key發生沖突。
鏈接法(Separate chaining)鏈接法是通過數組和鏈表組合而成的。當發生沖突的時候只要將其加到對應的鏈表中即可。如圖1-2.
圖1-2
與開放定址法相比,鏈接法有如下幾個優點:
①鏈接法處理沖突簡單,且無堆積現象,即非同義詞決不會發生沖突,因此平均查找長度較短;
②由於鏈接法中各鏈表上的結點空間是動態申請的,故它更適合於造表前無法確定表長的情況;
③開放定址法為減少沖突,要求裝填因子α較小,故當結點規模較大時會浪費很多空間。而鏈接法中可取α≥1,且結點較大時,拉鏈法中增加的指針域可忽略不計,因此節省空間;
④在用鏈接法構造的散列表中,刪除結點的操作易於實現。只要簡單地刪去鏈表上相應的結點即可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結點的空間置為空,否則將截斷在它之后填人散列表的同義詞結點的查找路徑。這是因為各種開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。因此在 用開放地址法處理沖突的散列表上執行刪除操作,只能在被刪結點上做刪除標記,而不能真正刪除結點。
當然鏈接法也有其缺點,拉鏈法的缺點是:指針需要額外的空間,故當結點規模較小時,開放定址法較為節省空間,而若將節省的指針空間用來擴大散列表的規模,可使裝填因子變小,這又減少了開放定址法中的沖突,從而提高平均查找速度。
以鏈接法為例,如果每次插入的值都產生碰撞,那么HashTable最終就變成了一個鏈表,我們稱之為HashTable退化。如圖1-3.
圖1-3
HashTable退化成一個鏈表之后,性能會急劇的下降。
3.拒絕服務攻擊
HashTable在所有的Web應用框架上都有應用,我們對Web應用每次發起請求所提交的參數,服務器端都會將其存儲在HashTable中供后台代碼調用。比如在Asp.NET應用中,我們使用Request.Form[key]和Request.QueryString[key]的方式來獲取客戶端提交的參數,參數就是被存儲在HashTable中的,我們傳入參數名稱作為Key,通過Hash函數轉換成對應的Value的數組下標,然后Value值被返回。
在正常的應用場景下這沒什么問題,現在我們回到上面提到的HashTable退化的問題,如果客戶端根據Web應用框架采用的Hash函數來通過某種Hash攻擊的方式獲得大量的碰撞,那么HashTable就會退化成鏈表,服務器有可能處理一次請求要花上十幾分鍾甚至幾個小時的時間,一台PC機就可以搞定一台服務器,根本不用分布式攻擊。當然攻擊能否成功的先決條件是Web應用框架采用的Hash機制存在漏洞。如果存在這樣的漏洞,攻擊者可以輕而易舉的實施拒絕服務攻擊。
下面我們來看看現實世界中,流行的Web框架對HashTable退化的防御能力。(參考內容:http://www.nruns.com/_downloads/advisory28122011.pdf)
3.1 PHP5
PHP5的HashTable使用的函數是DJBX33A。
DJBX33A算法,也叫time33算法,它是php、apache,、perl、bsddb的默認Hash算法。 |
下面的代碼體現了DJBX33A算法的基本思想 uint32_t time33(char const *str, int len) { unsigned long hash = 0; for (int i = 0; i < len; i++) { hash = hash *33 + (unsigned long) str[i]; } return hash; } |
對於為什么使用33這個數,有這樣的解釋: 1到256之間的所有奇數,都能達到一個可接受的哈希分布,平均分布大概是86%。而其中33,17,31,63,127,129這幾個數在面對大量的哈希運算時有一個更大的優勢,就是這些數字能將乘法用位運算配合加減法替換,這樣運算速度會更高。並不是所有基於DJBX33A的算法都使用33作為倍數,如Ngix使用的是time31,Tokyo Cabinet使用的是time37。 |
PHP版本的DJBX33A算法如下所示: inline unsigned time33(char const*str, int len) |
對於DJBX33A算法,我們完全可以通過上面提到的“相等子串法”來找到碰撞,進行攻擊。
目前PHP官方建議通過配置來限制表單提交的最大長度來抵御該攻擊。
3.2 ASP.NET
Asp.NET使用Request.Form對象來獲取表單提交的變量。內部的Hash函數是DJBX33X(Dan Bernstein's times 33, XOR)。
DJBX33X算法思想如下所示:
static ulong DJBX33X (char *arKey, uint nKeyLength)
{
ulong h = 5381;
char *arEnd = arKey + nKeyLength;
while (arKey < arEnd) {
h += (h << 5);
h ^= (ulong) *arKey++;
}
return h;
}
針對DJBX33X算法的特點,我們可以采用上面提到的中間相遇攻擊的方法來尋找碰撞。
微軟已經發布了針對該漏洞的補丁,如果您擔心該漏洞會對您的網站造成麻煩,請更新補丁。
3.3 Java
Java 的Hash函數是對DJBX33A的改造(使用的31而不是33,另外初始值為0而不是5381),但我們仍然可以使用相等子串法來獲取該Hash函數的碰撞。
基於Java的Tomcat服務器存在這樣的漏洞。
3.4 其他
Python、Ruby和V8同樣有這樣的漏洞。