加鹽密碼雜湊:如何正確使用

2 贊 回覆發表於2014-03-20

| 2014-03-20 10:13   評論: 4 收藏: 4 分享: 5    

如果你是Web開發者,你很可能需要開發一個使用者賬戶系統。這個系統最重要的方面,就是怎樣保護使用者的密碼。存放帳號的資料庫經常成為入侵的目標,所以你必須做點什麼來保護密碼,以防網站被攻破時發生危險。最好的辦法就是對密碼進行加鹽雜湊,這篇文章將介紹它是如何做到這點。

在對密碼進行雜湊加密的問題上,人們有許多爭論和誤解,這大概是由於網路上廣泛的誤傳吧。密碼雜湊是一件非常簡單的事情,但是依然有很多人理解錯誤了。本文闡述的並不是進行密碼雜湊唯一正確的方法,但是會告訴你為什麼這樣是正確的。

鄭重警告:如果你在試圖編寫自己的密碼雜湊程式碼,趕緊停下來!那太容易搞砸了。即使你受過密碼學的高等教育,也應該聽從這個警告。這是對所有人說的:不要自己寫加密函式!安全儲存密碼的難題現在已經被解決了,請使用phpass或者本文給出的一些原始碼。

如果因為某些原因你忽視了上面那個紅色警告,請翻回去好好讀一遍,我是認真的。這篇文章的目的不是教你研究出自己的安全演算法,而是講解為什麼密碼應該被這樣儲存。

這裡也給出了一些基於BSD許可的雜湊函式原始碼:

為什麼密碼需要進行雜湊?

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366
hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542

雜湊演算法是一個單向函式。它可以將任何大小的資料轉化為定長的“指紋”,並且無法被反向計算。另外,即使資料來源只改動了一丁點,雜湊的結果也會完全不同(參考上面的例子)。這樣的特性使得它非常適合用於儲存密碼,因為我們需要加密後的密碼無法被解密,同時也能保證正確校驗每個使用者的密碼。

在基於雜湊加密的賬戶系統中,通常使用者註冊和認證的流程是這樣的:

  1. 使用者註冊一個帳號
  2. 密碼經過雜湊加密儲存在資料庫中。只要密碼被寫入磁碟,任何時候都不允許是明文
  3. 當使用者登入的時候,從資料庫取出已經加密的密碼,和經過雜湊的使用者輸入進行對比
  4. 如果雜湊值相同,使用者獲得登入授權,否則,會被告知輸入了無效的登入資訊
  5. 每當有使用者嘗試登入,以上兩步都會重複

在第4步中,永遠不要告訴使用者到底是使用者名稱錯了,還是密碼錯了。只需要給出一個大概的提示,比如“無效的使用者名稱或密碼”。這可以防止攻擊者在不知道密碼的情況下,列舉出有效的使用者名稱。

需要提到的是,用於保護密碼的雜湊函式和你在資料結構中學到的雜湊函式是不同的。比如用於實現雜湊表這之類資料結構的雜湊函式,它們的目標是快速查詢,而不是高安全性。只有加密雜湊函式才能用於保護密碼,例如SHA256,SHA512,RipeMD和WHIRLPOOL。

也許你很容易就認為只需要簡單地執行一遍加密雜湊函式,密碼就能安全,那麼你大錯特錯了。有太多的辦法可以快速地把密碼從簡單雜湊值中恢復出來,但也有很多比較容易實現的技術能使攻擊者的效率大大降低。駭客的進步也在激勵著這些技術的進步,比如這樣一個網站:你可以提交一系列待破解的雜湊值,並且在不到1秒的時間內得到了結果。顯然,簡單雜湊加密並不能滿足我們對安全性的需求。

那麼下一節會講到幾種常用的破解簡單雜湊加密的辦法。

如何破解雜湊加密

字典攻擊和暴力攻擊

Dictionary Attack
Trying apple : failed
Trying blueberry : failed
Trying justinbeiber : failed
...
Trying letmein : failed
Trying s3cr3t : success!

Brute Force Attack
Trying aaaa : failed
Trying aaab : failed
Trying aaac : failed
...
Trying acdb : failed
Trying acdc : success!

• 破解雜湊加密最簡單的辦法,就是去猜,將每個猜測值雜湊之後的結果和目標值比對,如果相同則破解成功。兩種最常見的猜密碼的辦法是字典攻擊暴力攻擊

• 字典攻擊需要使用一個字典檔案,它包含單詞、短語、常用密碼以及其他可能用作密碼的字串。其中每個詞都是進過雜湊後儲存的,用它們和密碼雜湊比對,如果相同,這個詞就是密碼。字典檔案的構成是從大段文字中分解出的單詞,甚至還包括一些資料庫中真實的密碼。然後還可以對字典檔案進行更進一步的處理使它更有效,比如把單詞中的字母替換為它們的“形近字”(hello變為h3110)。

• 暴力攻擊會嘗試每一個在給定長度下各種字元的組合。這種攻擊會消耗大量的計算,也通常是破解雜湊加密中效率最低的辦法,但是它最終會找到正確的密碼。因此密碼需要足夠長,以至於遍歷所有可能的字串組合將耗費太長時間,從而不值得去破解它。

• 我們沒有辦法阻止字典攻擊和暴擊攻擊,儘管可以降低它們的效率,但那也不是完全阻止。如果你的密碼雜湊系統足夠安全,唯一的破解辦法就是進行字典攻擊或者暴力遍歷每一個雜湊值。

查表法

Searching: 5f4dcc3b5aa765d61d8327deb882cf99: FOUND: password5
Searching: 6cbe615c106f422d23669b610b564800: not in database
Searching: 630bf032efe4507f2c57b280995925a9: FOUND: letMEin12
Searching: 386f43fab5d096a7a66d67c8f213e5ec: FOUND: mcd0nalds
Searching: d5ec75d5fe70d428685510fae36492d9: FOUND: p@ssw0rd!

查表法對於破解一系列演算法相同的雜湊值有著無與倫比的效率。主要的思想就是預計算密碼字典中的每個密碼,然後把雜湊值和對應的密碼儲存到一個用於快速查詢的資料結構中。一個良好的查表實現可以每秒進行數百次雜湊查詢,即使表中儲存了幾十億個雜湊值。

如果你想更好地體驗查表法的速度,嘗試使用CrackStation的free hash cracker來破解下圖中四個SHA256加密的雜湊值吧。

c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc
08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7
e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904
5206b8b8a996cf5320cb12ca91c7b790fba9f030408efe83ebb83548dc3007bd

反向查表法

Searching for hash(apple) in users' hash list... : Matches [alice3, 0bob0, charles8]
Searching for hash(blueberry) in users' hash list... : Matches [usr10101, timmy, john91]
Searching for hash(letmein) in users' hash list... : Matches [wilson10, dragonslayerX, joe1984]
Searching for hash(s3cr3t) in users' hash list... : Matches [bruce19, knuth1337, john87]
Searching for hash(z@29hjja) in users' hash list... : No users used this password

這種方法可以使攻擊者同時對多個雜湊值發起字典攻擊或暴力攻擊,而不需要預先計算出一個查詢表。

首先攻擊者構造一個基於密碼-使用者名稱的一對多的表,當然資料需要從某個已經被入侵的資料庫獲得,然後猜測一系列雜湊值並且從表中查詢擁有此密碼的使用者。通常許多使用者可能有著相同的密碼,因此這種攻擊方式也顯得尤為有效。

彩虹表

彩虹表是一種在時間和空間的消耗上找尋平衡的破解技術。它和查表法很類似,但是為了使查詢表佔用的空間更小而犧牲了破解速度。因為它更小,於是我們可以在一定的空間記憶體儲更多的雜湊值,從而使攻擊更加有效。能夠破解任何8位及以下長度MD5值的彩虹表已經出現了。

下面我們會講到一種讓查表法和彩虹表都失去作用的技術,叫做加鹽

加鹽

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
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個字串,它們的雜湊值的第一位元組包含了所有可能的情況。然後用它們去系統中嘗試登入,並記錄系統返回結果所消耗的時間,耗時最長的那個就是第一位元組猜對的那個。接下來用同樣的方式猜測第二位元組、第三位元組等等。直到攻擊者獲取了最夠長的雜湊值片段,最後只需在自己的機器上破解即可,完全不受線上系統的限制。

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

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

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

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

程式碼中使用了異或運算子“^”(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機器成被編譯為如下組合語言:

MOV EAX, [A]
CMP [B], EAX
JZ equal
JMP done
equal:
AND [VALID], 1
done:
AND [VALID], 0

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

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

MOV EAX, [A]
XOR EAX, [B]
OR [DIFF], EAX

弄這麼麻煩幹嘛?

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

PHP PBKDF2 密碼雜湊程式碼

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

 

文章和程式碼由Defuse Security編寫。

原文連結: Crackstation   翻譯: 伯樂線上 蔣生武

譯文連結: http://blog.jobbole.com/61872/

加鹽密碼雜湊:如何正確使用

相關文章