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

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

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

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

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

下面一些鏈接可以用來快速跳轉到本文的各章節。

  1. 爲什麼密碼需要進行哈希?
  2. 如何破解哈希加密
  3. 加鹽
  4. 無效的哈希方法
  5. 恰當使用哈希加密
  6. 常見問題

這裏也給出了一些基於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個字符串,它們的哈希值的第一字節包含了所有可能的情況。然後用它們去系統中嘗試登錄,並記錄系統返回結果所消耗的時間,耗時最長的那個就是第一字節猜對的那個。接下來用同樣的方式猜測第二字節、第三字節等等。直到攻擊者獲取了最夠長的哈希值片段,最後只需在自己的機器上破解即可,完全不受在線系統的限制。

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

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

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

privatestatic boolean slowEquals(byte[] a, byte[] b)
{
    intdiff = a.length ^ b.length;
    for(inti = 0; i < a.length && i < b.length; i++)
    diff |= a[i] ^ b[i];
    returndiff == 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

弄這麼麻煩幹嘛?

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


原文:http://blog.jobbole.com/61872/

另參考:http://www.cnblogs.com/jfzhu/p/4023439.html

發佈了64 篇原創文章 · 獲贊 25 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章