加鹽密碼哈希:如何正確使用


加鹽

  1. hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
  2. hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
  3. hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
  4. hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007

查表法和彩虹表只有在所有密碼都以相同方式進行哈希加密時才有效。如果兩個用戶密碼相同,那么他們密碼的哈希值也是相同的。我們可以通過“隨機化”哈希來阻止這類攻擊,於是當相同的密碼被哈希兩次之后,得到的值就不相同了。

比如可以在密碼中混入一段“隨機”的字符串再進行哈希加密,這個被字符串被稱作鹽值。如同上面例子所展示的,這使得同一個密碼每次都被加密為完全不同的字符串。為了校驗密碼是否正確,我們需要儲存鹽值。通常和密碼哈希值一起存放在賬戶數據庫中,或者直接存為哈希字符串的一部分。

鹽值並不需要保密,由於隨機化了哈希值,查表法、反向查表法和彩虹表都不再有效。攻擊者無法確知鹽值,於是就不能預先計算出一個查詢表或者彩虹表。這樣每個用戶的密碼都混入不同的鹽值后再進行哈希,因此反向查表法也變得難以實施。

下面講講我們在實現加鹽哈希的過程中通常會犯哪些錯誤

錯誤一:短鹽值和鹽值重復

最常見的錯誤就是在多次哈希加密中使用相同的鹽值或者太短的鹽值。

鹽值重復

每次哈希加密都使用相同的鹽值是很容易犯的一個錯誤,這個鹽值要么被硬編碼到程序里,要么只在第一次使用時隨機獲得。這樣加鹽的方式是做無用功,因為兩個相同的密碼依然會得到相同的哈希值。攻擊者仍然可以使用反向查表法對每個值進行字典攻擊,只需要把鹽值應用到每個猜測的密碼上再進行哈希即可。如果鹽值被硬編碼到某個流行的軟件里,可以專門為這個軟件制作查詢表和彩虹表,那么破解它生成的哈希值就變得很簡單了。

用戶創建賬戶或每次修改密碼時,都應該重新生成新的鹽值進行加密。

短鹽值

如果鹽值太短,攻擊者可以構造一個查詢表包含所有可能的鹽值。以只有3個ASCII字符的鹽值為例,一共有95x95x95=857,375種可能。這看起來很多,但是如果對於每個鹽值查詢表只包含1MB最常見的密碼,那么總共只需要837GB的儲存空間。一個不到100美元的1000GB硬盤就能解決問題。
同樣地,用戶名也不應該被用作鹽值。盡管在一個網站中用戶名是唯一的,但是它們是可預測的,並且經常重復用於其他服務中。攻擊者可以針對常見用戶名構建查詢表,然后對用戶名鹽值哈希發起進攻。

為了使攻擊者無法構造包含所有可能鹽值的查詢表,鹽值必須足夠長。一個好的做法是使用和哈希函數輸出的字符串等長的鹽值,比如SHA256算法的輸出是256bits(32 bytes),那么鹽值也至少應該是32個隨機字節。

錯誤二:兩次哈希和組合哈希函數


(譯注:此節標題原文中的Wacky Hash Functions直譯是古怪的哈希函數,大概是由於作者不認可這種組合多種哈希函數的做法,為了便於理解,本文還是翻譯為組合哈希函數)

這節講述了另一種對密碼哈希的誤解:使用組合哈希函數。人們經常不由自主地認為將不同的哈希函數組合起來,結果會更加安全。實際上這樣做幾乎沒有好處,僅僅造成了函數之間互相影響的問題,甚至有時候會變得更加不安全。永遠不要嘗試發明自己的加密方法,只需只用已經被設計好的標准算法。有的人會說使用多種哈希函數會使計算更慢,從而破解也更慢,但是還有其他的辦法能更好地減緩破解速度,后面會提到的。

這里有些低端的組合哈希函數,我在網上某些論壇看到它們被推薦使用:

  • md5(sha1(password))
  • md5(md5(salt) + md5(password))
  • sha1(sha1(password))
  • sha1(str_rot13(password + salt))
  • md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))

不要使用其中任何一種。

注意:這節內容是有爭議的。我已經收到的大量的郵件,為組合哈希函數而辯護。他們的理由是如果攻擊者不知道系統使用的哪種哈希函數,那么也就很難預先為這種組合構造出彩虹表,於是破解起來會花費更多的時間。

誠然,攻擊者在不知道加密算法的時候是無法發動攻擊的,但是不要忘了Kerckhoffs’s principle,攻擊者通常很容易就能拿到源碼(尤其是那些免費或開源的軟件)。通過系統中取出的一些密碼-哈希值對應關系,很容易反向推導出加密算法。破解組合哈希函數確實需要更多時間,但也只是受了一點可以確知的因素影響。更好的辦法是使用一個很難被並行計算出結果的迭代算法,然后增加適當的鹽值防止彩虹表攻擊。

當然你實在想用“標准的”組合哈希函數,比如HMAC,也是可以的。但如果只是為了使破解起來更慢,那么先讀讀下面講到的密鑰擴展。

創造新的哈希函數可能帶來安全問題,構造哈希函數的組合又可能帶來函數間互相影響的問題,它們帶來的一丁點好處和這些比起來真是微不足道。顯然最好的做法是使用標准的、經過完整測試的算法。

哈希碰撞

哈希函數將任意大小的數據轉化為定長的字符串,因此其中一定有些輸入經過哈希計算之后得到了相同的結果。加密哈希函數的設計就是為了使這樣的碰撞盡可能難以被發現。隨着時間流逝,密碼學家發現攻擊者越來越容易找到碰撞了,最近的例子就是MD5算法的碰撞已經確定被發現了。

碰撞攻擊的出現表明很可能有一個和用戶密碼不同的字符串卻和它有着相同的哈希值。然而,即使在MD5這樣脆弱的哈希函數中找到碰撞也需要耗費大量的計算,因此這樣的碰撞“意外地”在實際中出現的可能性是很低的。於是站在實用性的角度上可以這么說,加鹽MD5和加鹽SHA256的安全性是一樣的。不過可能的話,使用本身更安全的哈希函數總是好的,比如SHA256、SHA512、RipeMD或者WHIRLPOOL

正確的做法:恰當使用哈希加密

本節會准確講述應該如何對密碼進行哈希加密。其中第一部分介紹最基本的要素,也是在哈希加密中一定要做到的;后面講解怎樣在這個基礎上進行擴展,使得加密更難被破解。

基本要素:加鹽哈希

忠告:你不僅僅要用眼睛看文章,更要自己動手去實現后面講到的“讓密碼更難破解:慢哈希函數”。

在前文中我們已經看到,利用查表法和彩虹表,普通哈希加密是多么容易被惡意攻擊者破解,也知道了可以通過隨機加鹽的辦法也解決這個問題。那么到底應該使用怎樣的鹽值呢,又如何把它混入密碼?

鹽值應該使用基於加密的偽隨機數生成器(Cryptographically Secure Pseudo-Random Number Generator – CSPRNG)來生成。CSPRNG和普通的隨機數生成器有很大不同,如C語言中的rand()函數。物如其名,CSPRNG專門被設計成用於加密,它能提供高度隨機和無法預測的隨機數。我們顯然不希望自己的鹽值被猜測到,所以一定要使用CSPRNG。下面的表格列出了當前主流編程語言中的CSPRNG方法:

Platform CSPRNG
PHP mcrypt_create_ivopenssl_random_pseudo_bytes
Java java.security.SecureRandom
Dot NET (C#, VB) System.Security.Cryptography.RNGCryptoServiceProvider
Ruby SecureRandom
Python os.urandom
Perl Math::Random::Secure
C/C++ (Windows API) CryptGenRandom
Any language on GNU/Linux or Unix Read from /dev/random or /dev/urandom

對於每個用戶的每個密碼,鹽值都應該是獨一無二的。每當有新用戶注冊或者修改密碼,都應該使用新的鹽值進行加密。並且這個鹽值也應該足夠長,使得有足夠多的鹽值以供加密。一個好的標准的是:鹽值至少和哈希函數的輸出一樣長;鹽值應該被儲存和密碼哈希一起儲存在賬戶數據表中。

存儲密碼的步驟

  1. 使用CSPRNG生成一個長度足夠的鹽值
  2. 將鹽值混入密碼,並使用標准的加密哈希函數進行加密,如SHA256
  3. 把哈希值和鹽值一起存入數據庫中對應此用戶的那條記錄

校驗密碼的步驟

  1. 從數據庫取出用戶的密碼哈希值和對應鹽值
  2. 將鹽值混入用戶輸入的密碼,並且使用同樣的哈希函數進行加密
  3. 比較上一步的結果和數據庫儲存的哈希值是否相同,如果相同那么密碼正確,反之密碼錯誤

文章最后有幾個加鹽密碼哈希的代碼實現,分別使用了PHP、C#、Java和Ruby。

在Web程序中,永遠在服務器端進行哈希加密

如果你正在開發一個Web程序,你可能會疑惑到底在哪進行加密。是使用JavaScript在用戶的瀏覽器上操作呢,還是將密碼“裸體”傳送到服務器再進行加密?

即使瀏覽器端用JavaScript加密了,你仍然需要在服務端再次進行加密。試想有個網站在瀏覽器將密碼經過哈希后傳送到服務器,那么在認證用戶的時候,網站收到哈希值和數據庫中的值進行比對就可以了。這看起來比只在服務器端加密安全得多,因為至始至終沒有將用戶的密碼明文傳輸,但實際上不是這樣。

問題在於,從客戶端來看,經過哈希的密碼邏輯上成為用戶真正的密碼。為了通過服務器認證,用戶只需要發送密碼的哈希值即可。如果有壞小子獲取了這個哈希值,他甚至可以在不知道用戶密碼的情況通過認證。更進一步,如果他用某種手段入侵了網站的數據庫,那么不需要去猜解任何人的密碼,就可以隨意使用每個人的帳號登錄。

這並不是說你不應該在瀏覽器端進行加密,但是如果你這么做了,一定要在服務端再次加密。在瀏覽器中進行哈希加密是個好想法,不過實現的時候注意下面幾點:

• 客戶端密碼哈希並不能代替HTTPS(SSL/TLS)。如果瀏覽器和服務器之間的連接是不安全的,那么中間人攻擊可以修改JavaScript代碼,刪除加密函數,從而獲取用戶密碼。

• 有些瀏覽器不支持JavaScript,也有的用戶禁用了瀏覽器的JavaScript功能。為了最好的兼容性,你的程序應該檢測JavaScript是否可用,如果答案為否,需要在服務端模擬客戶端的加密。

• 客戶端哈希同樣需要加鹽,很顯然的辦法就是向服務器請求用戶的鹽值,但是不要這么做。因為這給了壞蛋一個機會,能夠在不知道密碼的情況下檢測用戶名是否有效。既然你已經在服務端對密碼進行了加鹽哈希,那么在客戶端把用戶名(或郵箱)加上網站特有的字符串(如域名)作為鹽值是可行的。

讓密碼更難破解:慢哈希函數

加鹽使攻擊者無法采用特定的查詢表和彩虹表快速破解大量哈希值,但是卻不能阻止他們使用字典攻擊或暴力攻擊。高端的顯卡(GPU)和定制的硬件可以每秒進行數十億次哈希計算,因此這類攻擊依然可以很高效。為了降低攻擊者的效率,我們可以使用一種叫做密鑰擴展的技術。

這種技術的思想就是把哈希函數變得很慢,於是即使有着超高性能的GPU或定制硬件,字典攻擊和暴力攻擊也會慢得讓攻擊者無法接受。最終的目標是把哈希函數的速度降到足以讓攻擊者望而卻步,但造成的延遲又不至於引起用戶的注意。

密鑰擴展的實現是依靠一種CPU密集型哈希函數。不要嘗試自己發明簡單的迭代哈希加密,如果迭代不夠多,是可以被高效的硬件快速並行計算出來的,就和普通哈希一樣。應該使用標准的算法,比如PBKDF2或者bcrypt這里可以找到PBKDF2在PHP上的一種實現。

這類算法使用一個安全因子或迭代次數作為參數,這個值決定了哈希函數會有多慢。對於桌面軟件或者手機軟件,獲取參數最好的辦法就是執行一個簡短的性能基准測試,找到使哈希函數大約耗費0.5秒的值。這樣,你的程序就可以盡可能保證安全,而又不影響到用戶體驗。

如果你在一個Web程序中使用密鑰擴展,記得你需要額外的資源處理大量認證請求,並且密鑰擴展也使得網站更容易遭受拒絕服務攻擊(DoS)。但我依然推薦使用密鑰擴展,不過把迭代次數設定得低一點,你應該基於認證請求最高峰時的剩余硬件資源來計算迭代次數。要求用戶每次登錄時輸入驗證碼可以消除拒絕服務的威脅。另外,一定要把你的系統設計為迭代次數可隨時調整的。

如果你擔心計算量帶來的負載,但又想在Web程序中使用密鑰擴展,可以考慮在瀏覽器中用JavaScript完成。Stanford JavaScript Crypto Library里包含了PBKDF2的實現。迭代次數應該被設置到足夠低,以適應速度較慢的客戶端,比如移動設備。同時當客戶端不支持JavaScript的時候,服務端應該接手計算。客戶端的密鑰擴展並不能免除服務端進行哈希加密的職責,你必須對客戶端傳來的哈希值再次進行哈希加密,就像對付一個普通密碼一樣。

無法破解的哈希加密:密鑰哈希和密碼哈希設備

只要攻擊者可以檢測對一個密碼的猜測是否正確,那么他們就可以進行字典攻擊或暴力攻擊。因此下一步就是向哈希計算中增加一個密鑰,只有知道這個密鑰的人才能校驗密碼。有兩種辦法可以實現:將哈希值加密,比如使用AES算法;將密鑰包含到哈希字符串中,比如使用密鑰哈希算法HMAC

聽起來很簡單,做起來就不一樣了。這個密鑰需要在任何情況下都不被攻擊者獲取,即使系統因為漏洞被攻破了。如果攻擊者獲取了進入系統的最高權限,那么不論密鑰被儲存在哪,他們都可以竊取到。因此密鑰需要儲存在外部系統中,比如另一個用於密碼校驗的物理服務器,或者一個關聯到服務器的特制硬件,如YubiHSM

我強烈推薦大型服務(10萬用戶以上)使用這類辦法,因為我認為面對如此多的用戶是有必要的。

如果你難以負擔多個服務器或專用的硬件,仍然有辦法在一個普通Web服務器上利用密鑰哈希技術。大部分針對數據庫的入侵都是由於SQL注入攻擊,因此不要給攻擊者進入本地文件系統的權限(禁止數據庫服務訪問本地文件系統,如果它有這個功能的話)。這樣一來,當你隨機生成一個密鑰存到通過Web程序無法訪問的文件中,然后混入加鹽哈希,得到的哈希值就不再那么脆弱了,即便這時數據庫遭受了注入攻擊。不要把將密鑰硬編碼到代碼里,應該在安裝時隨機生成。這當然不如獨立的硬件系統安全,因為如果Web程序存在SQL注入點,那么可能還存在其他一些問題,比如本地文件包含漏洞(Local File Inclusion),攻擊者可以利用它讀取本地密鑰文件。無論如何,這個措施比沒有好。

請注意密鑰哈希不代表無需進行加鹽。高明的攻擊者遲早會找到辦法竊取密鑰,因此依然對密碼哈希進行加鹽和密鑰擴展很重要。

其他安全措施

哈希加密可以在系統發生入侵時保護密碼,但這並不能使整個程序更加安全。首先還有很多事情需要做,來保證密碼哈希(和其他用戶數據)不被竊取。

即使經驗豐富的開發者也需要額外學習安全知識,才能寫出安全的程序。這里有個關於Web程序漏洞的資源:The Open Web Application Security Project (OWASP),還有一個很好的介紹:OWASP Top Ten Vulnerability List。除非你了解列表中所有的漏洞,才能嘗試編寫一個處理敏感數據的Web程序。雇主也有責任保證他所有的開發人員都有資質編寫安全的程序。

對你的程序進行第三方“滲透測試”是一個不錯的選擇。最好的程序員也可能犯錯,因此有一個安全專家審查你的代碼尋找潛在的漏洞是有意義的。找尋值得信賴的機構(或招聘人員)來對你的代碼進行審查。安全審查應該從編碼的初期就着手進行,一直貫穿整個開發過程。

監控你的網站來發現入侵行為也是很重要的,我推薦至少雇佣一個人全職負責監測和處理安全隱患。如果有個漏洞沒被發現,攻擊者可能通過網站利用惡意軟件感染訪問者,因此檢測漏洞並且及時應對是十分重要的

常見問題

我應該使用什么哈希算法?

應該使用:

  • 本文末尾的PHP source code, Java source code, C# source code or the Ruby source code
  • OpenWall的Portable PHP password hashing framework
  • 任何先進的、被良好測試過的哈希加密算法,比如SHA256,SHA512,RipeMD,WHIRLPOOL,SHA3等等
  • 設計良好的密鑰擴展算法,如PBKDF2bcryptscrypt
  • 安全的crypt()版本($2y$,$5$,$6$)

不要使用:

  • 過時的函數,比如MD5或SHA1
  • 不安全的crypt()版本($1$,$2$,$2x$,$3$)
  • 任何你自己設計的加密算法。只應該使用那些在公開領域中的,並且被密碼學家完整測試過的技術

盡管還沒有一種針對MD5或SHA1非常效率的攻擊手段,但是它們太古老也被廣泛地認為不足以勝任存儲密碼的工作(某種程度上甚至是錯誤的),因此我也不推薦使用它們。但是有個例外,PBKDF2中頻繁地使用了SHA1作為它底層的哈希函數。

當用戶忘記密碼的時候,怎樣進行重置?

我個人的觀點是,當前所有廣泛使用的密碼重置機制都是不安全的。如果你對安全性有極高的要求,比如一個加密服務,那么不要允許用戶重置密碼。
大多數網站向那些忘記密碼的用戶發送電子郵件來進行身份認證。首先,需要隨機生成一個一次性的令牌,它直接關聯到用戶的賬戶。然后將這個令牌混入一個重置密碼的鏈接中,發送到用戶的電子郵箱。最后當用戶點擊這個包含有效令牌的鏈接時,提示他們可以設置新的密碼。要確保這個令牌只對一個賬戶有效,以防攻擊者從郵箱獲取到令牌后,用來重置其他用戶的密碼。

令牌必須在15分鍾內使用,並且一旦被使用就立即失效。當用戶重新請求令牌時,或用戶登錄成功時(說明他還記得密碼),使原令牌失效也是一個好做法。如果一個令牌始終不過期,那么它一直可以用於入侵用戶的帳號。電子郵件(SMTP)是一個純文本協議,並且網絡上有很多惡意路由在截取郵件信息。在用戶修改密碼后,那些包含重置密碼鏈接的郵件在很長一段時間內依然缺乏保護。因此應該盡早使令牌過期,降低把用戶信息暴露給攻擊者的可能。

攻擊者是可以篡改令牌的,所以不要把賬戶信息和失效時間存儲在里面。這些信息應該以不可猜解的二進制形式存在,並且只用來識別數據庫中某條用戶的記錄。

永遠不要通過電子郵件向用戶發送新密碼,同時也記得在用戶重置密碼的時候隨機生成一個新的鹽值用於加密,不要重復使用之前密碼的那個鹽值。

當賬戶數據庫被泄漏或入侵時,應該怎么做?

你首先需要做的,是查看系統被暴露到什么程度了,然后修復這個攻擊者利用的漏洞。如果你沒有應對入侵的經驗,我強烈推薦雇一個第三方安全機構來做這件事。

將一個漏洞精心掩蓋期待沒有人能注意到,是否聽起來很省事而又誘人呢?但是這樣只會讓你顯得更糟糕,因為你在用戶不知情的情況下,將他們的密碼和個人信息暴露在危險之中。即使用戶還無法理解到底發生了什么,你也應該盡快履行告知的義務。比如在首頁放置一個鏈接,指向對此問題更詳細的說明,可能的話還可以通過電子郵件告知用戶目前的情況。

向你的用戶說明你是如何保護他們的密碼的——最好是使用了加鹽哈希——即便如此惡意黑客也能使用字典攻擊和暴力攻擊。設想用戶可能在很多服務中使用相同的密碼,攻擊者會用找到的密碼去嘗試登錄其他網站。提示你的用戶應該修改所有相似的密碼,不論它們被使用在哪個服務上,並且強制用戶下次登錄你的網站時修改密碼。大部分用戶會嘗試將密碼“修改”為和之前相同的以便記憶,你應該使用老密碼的哈希值來確保用戶無法這么做。

即使有加鹽哈希的保護,攻擊者也很可能快速破解其中一些脆弱的密碼。為了減少攻擊者使用的它們機會,你應該對這些密碼的帳號發送認證電子郵件,直到用戶修改了密碼。可以參考上一個問題,其中有一些實現電子郵件認證的要點。

另外也要告訴你的用戶,網站到底儲存了哪些個人信息。如果你的數據庫中有用戶的信用卡號,你應該指導用戶檢查自己近期的賬單,並且注銷掉這張信用卡。

我應該使用什么樣的密碼規則?是否應該強制用戶使用復雜的密碼?

如果你的服務對安全性沒有嚴格的要求,那么不要對用戶進行限制。我推薦在用戶輸入密碼的時候,頁面上顯示出密碼強度,由用戶自己決定需要多安全的密碼。如果你的服務對安全有特殊的需求,那就應該強制用戶輸入長度至少為12個字符的密碼,並且其中至少包括兩個字母、兩個數字和兩個符號。

不要過於頻繁地強制你的用戶修改密碼,最多6個月1次,因為那樣做會使用戶疲於選擇一個強度足夠好的密碼。更好的做法是指導用戶在他們感覺密碼可能泄漏的時候去主動修改,並且提示用戶不要把密碼告訴任何人。如果這是在商業環境中,鼓勵你的員工利用工作時間熟記並使用他們的密碼。

如果攻擊者入侵了我的數據庫,他們難道不能把其中的密碼哈希替換為自己的值,然后登錄系統么?

當然可以,但是如果他已經入侵了你的數據庫,那么很可能已經有權限訪問你服務器上任何東西了,因此完全沒必要登錄賬戶去獲取他想要的。對密碼進行哈希加密的手段,(對網站而言)不是保護網站免受入侵,而是在入侵已經發生時保護數據庫中的密碼。

通過為數據庫連接設置兩種權限,可以防止密碼哈希在遭遇注入攻擊時被篡改。一種權限用於創建用戶:它對用戶表可讀可寫;另一種用於用戶登錄,它只能讀用戶表而不能寫。

為什么我非得用像HMAC那種特殊的算法?為什么不能簡單地把密鑰混入密碼?

像MD5、SHA1和SHA2這類哈希函數是基於Merkle–Damgård構造的,因此在長度擴展攻擊面前非常脆弱。就是說如果已經知道一個哈希值H(X),對於任意的字符串Y,攻擊者可以計算出H(pad(X) + Y)的值,而不需要知道X是多少,其中pad(X)是哈希函數的填充函數(padding function,比如MD5將數據每512bit分為一組,最后不足的將填充字節)。

在攻擊者不知道密鑰(key)的情況下,他仍然可以根據哈希值H(key + message)計算出H(pad(key + message) + extension)。如果這個哈希值用於身份認證,並且依靠其中的密鑰來防止攻擊者篡改消息,這個辦法已經行不通了。因為攻擊者無需知道密鑰,也能構造出包含message + extension的一個有效的哈希值。

目前還不清楚攻擊者能否用這個辦法更快破解密碼,但是由於這種攻擊的出現,在密鑰哈希中使用上述哈希函數已經被認為是差勁的實踐了。也許某天高明的密碼學家會發現一個利用長度擴展攻擊的新思路,從而更快地破解密碼,所以還是使用HMAC吧。

鹽值應該加到密碼前面還是后面?

都行,但是在一個程序中應該保持一致,以免出現互操作方面的問題。目前看來加到密碼之前是比較常用的做法。

為什么本文中的代碼在比較哈希值的時候,都是經過固定的時間才返回結果?

讓比較過程耗費固定的時間可以保證攻擊者無法對一個在線系統使用計時攻擊,以此獲取密碼的哈希值,然后進行本地破解工作。

比較兩個字節序列(字符串)的標准做法是,從第一字節開始,每個字節逐一順序比較。只要發現某字節不相同了,就可以立即返回“假”的結果。如果遍歷整個字符串也沒有找到不同的字節,那么兩個字符串就是相同的,並且返回“真”。這意味着比較字符串的耗時決定於兩個字符串到底有多大的不同。

舉個例子,使用標准的方法比較“xyzabc”和“abcxyz”,由於第一個字符就不同,不需要檢查后面的內容就可以馬上返回結果。相反,如果比較“aaaaaaaaaaB”和“aaaaaaaaaaZ”,比較算法就需要遍歷最后一位前所有的“a”,然后才能知道它們是不相同的。

假設攻擊者妄圖入侵一個在線系統,並且此系統限制了每秒只能嘗試一次用戶認證。還假設他已經知道了密碼哈希所有的參數(鹽值、哈希函數的類型等等),除了密碼的哈希值和密碼本身(顯然啊,否則還破解個什么)。如果攻擊者能精確測量在線系統耗時多久去比較他猜測的密碼和真實密碼,那么他就能使用計時攻擊獲取密碼的哈希值,然后進行離線破解,從而繞過系統對認證頻率的限制。

首先攻擊者准備256個字符串,它們的哈希值的第一字節包含了所有可能的情況。然后用它們去系統中嘗試登錄,並記錄系統返回結果所消耗的時間,耗時最長的那個就是第一字節猜對的那個。接下來用同樣的方式猜測第二字節、第三字節等等。直到攻擊者獲取了最夠長的哈希值片段,最后只需在自己的機器上破解即可,完全不受在線系統的限制。

乍看之下在網絡上進行計時攻擊是不可能做到的,然而有人已經實現了,並運用到實際中了。因此本文提供的代碼才使用固定的時間去比較字符串,不論它們有多相似。

“慢比較”的代碼是如何工作的?

上一個問題解釋了為什么“慢比較”是有必要的,現在來講解一下代碼具體是怎么實現的。

  1. private static boolean slowEquals(byte[] a, byte[] b)
  2. {
  3. int diff = a.length ^ b.length;
  4. for(int i = 0; i < a.length && i < b.length; i++)
  5. diff |= a[i] ^ b[i];
  6. return diff == 0;
  7. }

代碼中使用了異或運算符“^”(XOR)來比較兩個整數是否相等,而不是“==”。當且僅當兩位相等時,異或的結果才是0。因為0 XOR 0 = 0, 1 XOR 1 = 0, 0 XOR 1 = 1, 1 XOR 0 = 1。應用到整數中每一位就是說,當且僅當字節兩個整數各位都相等,結果才是0。

代碼中的第一行,比較a.length和b.length,相同的話diff是0,否則diff非0。然后使用異或比較數組中各字節,並且將結果和diff求或。如果有任何一個字節不相同,diff就會變成非0的值。因為或運算沒有“置0”的功能,所以循環結束后diff是0的話只有一種可能,那就是循環前兩個數組長度相等(a.length == b.length),並且數組中每一個字節都相同(每次異或的結果都非0)。
我們使用XOR而不是“==”來比較整數的原因是:“==”通常被翻譯/編譯/解釋為帶有分支的語句。例如C語言中的“diff &= a == b”可能在x86機器成被編譯為如下匯編語言:

  1. MOV EAX, [A]
  2. CMP [B], EAX
  3. JZ equal
  4. JMP done
  5. equal:
  6. AND [VALID], 1
  7. done:
  8. AND [VALID], 0

其中的分支導致代碼運行的時間不固定,決定於兩個整數相等的程度和CPU內部的跳轉預測機制(branch prediction)。

而C語言代碼“diff |=a ^ b”會被編譯為下面的樣子,它執行的時間和兩個整數是什么樣的情況無關。

  1. MOV EAX, [A]
  2. XOR EAX, [B]
  3. OR [DIFF], EAX

弄這么麻煩干嘛?

用戶在你的網站上輸入密碼,說明他們相信你會保障密碼的安全。如果你的數據庫被黑了,又沒有對用戶密碼加以保護,惡意黑客就可以使用這些密碼去入侵用戶在其他網站或服務的賬戶(大部分人會在各處使用相同的密碼)。這不僅僅關乎你網站的安全,更關系到用戶的。你需要對用戶的安全負責。

PHP PBKDF2 密碼哈希代碼

下面是PBKDF2在PHP中一種安全的實現,你也可以在這個頁面找到測試用例和基准測試的代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM