天天看點

如何安全的存儲使用者密碼?(上)

<a href="https://s4.51cto.com/wyfs02/M02/8E/95/wKioL1jGO5KjDCCmAABGFK4qfcE692.jpg" target="_blank"></a>

本文介紹了對密碼哈希加密的基礎知識,以及什麼是正确的加密方式。還介紹了常見的密碼破解方法,給出了如何避免密碼被破解的思路。相信讀者閱讀本文後,就會對密碼的加密有一個正确的認識,并對密碼正确進行加密措施。

作為一名Web開發人員,我們經常需要與使用者的帳号系統打交道,而這其中最大的挑戰就是如何保護使用者的密碼。經常會看到使用者賬戶資料庫頻繁被黑,是以我們必須采取一些措施來保護使用者密碼,以免導緻不必要的資料洩露。保護密碼的最好辦法是使用加鹽密碼哈希( salted password hashing)。

在對密碼進行哈希加密的問題上,人們有很多争論和誤解,可能是由于網絡上有大量錯誤資訊的原因吧。對密碼哈希加密是一件很簡單的事,但很多人都犯了錯。本文将會重點分享如何進行正确加密使用者密碼。

重要警告:請放棄編寫自己的密碼哈希加密代碼的念頭!因為這件事太容易搞砸了。就算你在大學學過密碼學的知識,也應該遵循這個警告。所有人都要謹記這點:不要自己寫哈希加密算法!存儲密碼的相關問題已經有了成熟的解決方案,就是使用 phpass,或者在defuse/password-hashing 或 libsodium 上的 PHP 、 C# 、 Java 和 Ruby 的實作。

密碼哈希是什麼?

<a href="https://s3.51cto.com/wyfs02/M01/8E/96/wKiom1jGO6aD9JL_AAA4Nf86c8Y538.jpg" target="_blank"></a>

雜湊演算法是一種單向函數。它把任意數量的資料轉換為固定長度的“指紋”,而且這個過程無法逆轉。它們有這樣的特性:如果輸入發生了一點改變,由此産生的哈希值會完全不同(參見上面的例子)。這個特性很适合用來存儲密碼。因為我們需要一種不可逆的算法來加密存儲的密碼,同時保證我們也能夠驗證使用者登陸的密碼是否正确。

在基于哈希加密的帳号系統中,使用者注冊和認證的大緻流程如下。

1.   使用者建立自己的帳号。

2.   密碼經過哈希加密後存儲在資料庫中。密碼一旦寫入到磁盤,任何時候都不允許是明文形式。

3.   當使用者試圖登入時,系統從資料庫取出已經加密的密碼,和經過哈希加密的使用者輸入的密碼進行對比。

4.   如果哈希值相同,使用者将被授予通路權限。否則,告知使用者他們輸入的登陸憑據無效。

5.   每當有人試圖嘗試登陸,就重複步驟3和4。

在步驟4中,永遠不要告訴使用者輸錯的究竟是使用者名還是密碼。就像通用的提示那樣,始終顯示:“無效的使用者名或密碼。”就行了。這樣可以防止攻擊者在不知道密碼的情況下枚舉出有效的使用者名。

應當注意的是,用來保護密碼的哈希函數,和資料結構課學到的哈希函數是不同的。例如,實作哈希表的哈希函數設計目的是快速查找,而非安全性。隻有加密哈希函數( cryptographic hash function)才可以用來進行密碼哈希加密。像SHA256 、 SHA512 、 RIPEMD 和 WHIRLPOOL 都是加密哈希函數。

人們很容易認為,Web開發人員所做的就是:隻需通過執行加密哈希函數就可以讓使用者密碼得以安全。然而并不是這樣。有很多方法可以從簡單的哈希值中快速恢複出明文的密碼。有幾種易于實施的技術,使這些“破解”的效率大為降低。網上有這種專門破解MD5的網站,隻需送出一個哈希值,不到一秒鐘就能得到破解的結果。顯然,單純的對密碼進行哈希加密遠遠達不到我們的安全要求。下一節将讨論一些用來破解簡單密碼哈希常用的手段。

如何破解哈希? 

字典攻擊和暴力攻擊( Dictionary and Brute Force Attacks)

字典攻擊

暴力攻擊

Trying apple         : failed

Trying aaaa : failed

Trying blueberry    : failed

Trying aaab : failed

Trying justinbeiber : failed

Trying aaac : failed

Trying letmein      :  failed

Trying acdb : failed

Trying s3cr3t       :  success!

Trying acdc : success!

破解哈希加密最簡單的方法是嘗試猜測密碼,哈希每個猜測的密碼,并對比猜測密碼的哈希值是否等于被破解的哈希值。如果相等,則猜中。猜測密碼攻擊的兩種最常見的方法是字典攻擊和暴力攻擊。

字典攻擊使用包含單詞、短語、常用密碼和其他可能用做密碼的字元串的字典檔案。對檔案中的每個詞都進行哈希加密,将這些哈希值和要破解的密碼哈希值比較。如果它們相同,這個詞就是密碼。字典檔案是通過大段文本中提取的單詞構成,甚至還包括一些資料庫中真實的密碼。還可以對字典檔案進一步處理以使其更為有效:如單詞“hello” 按網絡用語寫法轉成 “h3110” 。

暴力攻擊是對于給定的密碼長度,嘗試每一種可能的字元組合。這種方式會消耗大量的計算,也是破解哈希加密效率最低的辦法,但最終會找出正确的密碼。是以密碼應該足夠長,以至于周遊所有可能的字元組合,耗費的時間太長令人無法承受,進而放棄破解。

目前沒有辦法來組織字典攻擊或暴力攻擊。隻能想辦法讓它們變得低效。如果密碼哈希系統設計是安全的,破解哈希的唯一方法就是進行字典攻擊或暴力攻擊周遊每一個哈希值了。

 查表法( LookupTables)

Searching:5f4dcc3b5aa765d61d8327deb882cf99: FOUND: password5

Searching:6cbe615c106f422d23669b610b564800:  not in database

Searching:630bf032efe4507f2c57b280995925a9: FOUND: letMEin12

Searching:386f43fab5d096a7a66d67c8f213e5ec: FOUND: mcd0nalds

Searching:d5ec75d5fe70d428685510fae36492d9: FOUND: p@ssw0rd!

對于破解相同類型的哈希值,查表法是一種非常高效的方式。主要理念是預先計算( pre-compute)出密碼字典中的每個密碼的哈希值,然後把他們相應的密碼存儲到一個表裡。一個設計良好的查詢表結構,即使包含了數十億個哈希值,仍然可以實作每秒鐘查詢數百次哈希。

如果你想感受查表法的速度有多快,嘗試一下用 CrackStation 的 free hash cracker 來破解下面的 SHA256。

<a href="https://s1.51cto.com/wyfs02/M02/8E/96/wKiom1jGO8OC5zNuAAA6hDad2yI637.jpg" target="_blank"></a>

 反向查表法(Reverse Lookup Tabs)

<a href="https://s5.51cto.com/wyfs02/M00/8E/96/wKiom1jGO87RZq3TAABWIgq9v6w719.jpg" target="_blank"></a>

這種攻擊允許攻擊者無需預先計算好查詢表的情況下同時對多個哈希值發起字典攻擊或暴力攻擊。

首先,攻擊者從被黑的使用者帳号資料庫建立一個使用者名和對應的密碼哈希表,然後,攻擊者猜測一系列哈希值并使用該查詢表來查找使用此密碼的使用者。通常許多使用者都會使用相同的密碼,是以這種攻擊方式特别有效。

 彩虹表( RainbowTables)

彩虹表是一種以空間換時間的技術。與查表法相似,隻是它為了使查詢表更小,犧牲了破解速度。因為彩虹表更小,是以在機關空間可以存儲更多的哈希值,進而使攻擊更有效。能夠破解任何最多8位長度的 MD5 值的彩虹表已經出現。

接下來,我們來看一種謂之“加鹽( salting)”的技術,能夠讓查表法和彩虹表都失效。

 加鹽( AddingSalt)

<a href="https://s3.51cto.com/wyfs02/M01/8E/96/wKiom1jGO9ijNAl0AABSA-qcibU034.jpg" target="_blank"></a>

查表法和彩虹表隻有在所有密碼都以完全相同的方式進行哈希加密才有效。如果兩個使用者有相同的密碼,他們将有相同的密碼哈希值。我們可以通過“随機化”哈希,當同一個密碼哈希兩次後,得到的哈希值是不一樣的,進而避免了這種攻擊。

我們可以通過在密碼中加入一段随機字元串再進行哈希加密,這個被加的字元串稱之為鹽值。如上例所示,這使得相同的密碼每次都被加密為完全不同的字元串。我們需要鹽值來校驗密碼是否正确。通常和密碼哈希值一同存儲在帳号資料庫中,或者作為哈希字元串的一部分。

鹽值無需加密。由于随機化了哈希值,查表法、反向查表法和彩虹表都會失效。因為攻擊者無法事先知道鹽值,是以他們就沒有辦法預先計算查詢表或彩虹表。如果每個使用者的密碼用不同的鹽再進行哈希加密,那麼反向查表法攻擊也将不能奏效。

接下來,我們看看加鹽哈希通常會有哪些不正确的措施。

錯誤的方法:短鹽值和鹽值複用

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

 鹽值複用( SaltReuse)

一個常見的錯誤是每次都使用相同的鹽值進行哈希加密,這個鹽值要麼被寫死到程式裡,要麼隻在第一次使用時随機獲得。這樣的做法是無效的,因為如果兩個使用者有相同的密碼,他們仍然會有相同的哈希值。攻擊者仍然可以使用反向查表法對每個哈希值進行字典攻擊。他們隻是在哈希密碼之前,将固定的鹽值應用到每個猜測的密碼就可以了。如果鹽值被寫死到一個流行的軟體裡,那麼查詢表和彩虹表可以内置該鹽值,以使其更容易破解它産生的哈希值。

使用者建立帳号或者更改密碼時,都應該用新的随機鹽值進行加密。

 短鹽值( ShortSlat)

如果鹽值太短,攻擊者可以預先制作針對所有可能的鹽值的查詢表。例如,如果鹽值隻有三個 ASCII 字元,那麼隻有 95x95x95=857,375種可能性。這看起來很多,但如果每個查詢表包含常見的密碼隻有 1MB,857,375個鹽值總共隻需837GB,一塊時下不到100美元的 1TB硬碟就能解決問題了。

出于同樣的原因,不應該将使用者名用作鹽值。對每一個服務來說,使用者名是唯一的,但它們是可預測的,并且經常重複應用于其他服務。攻擊者可以用常見使用者名作為鹽值來建立查詢表和彩虹表來破解密碼哈希。

為使攻擊者無法構造包含所有可能鹽值的查詢表,鹽值必須足夠長。一個好的經驗是使用和哈希函數輸出的字元串等長的鹽值。例如, SHA256 的輸出為256位(32位元組),是以該鹽也應該是32個随機位元組。

 雙重哈希和古怪的哈希函數

本節将介紹另一種常見的密碼哈希的誤解:古怪哈希的算法組合。人們很容易沖昏頭腦,嘗試不同的哈希函數相結合一起使用,希望讓資料會更安全。但在實踐中,這樣做并沒有什麼好處。它帶來了函數之間互通性的問題,而且甚至可能會使哈希變得更不安全。永遠不要試圖去創造你自己的哈希加密算法,要使用專家設計好的标準算法。有人會說,使用多個哈希函數會降低計算速度,進而增加破解的難度。但是使破解過程變慢還有更好的辦法,我們将在後面講到。

下面是在網上見過的古怪的哈希函數組合的一些例子。

·        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))))

不要使用其中任何一種。

注意:此部分是有争議的。我收到了一些電子郵件,他們認為古怪的哈希函數是有意義的,理由是,如果攻擊者不知道系統使用哪個哈希函數,那麼攻擊者就不太可能預先計算出這種古怪的哈希函數彩虹表,于是破解起來要花更多的時間。

當攻擊者不知道哈希加密算法的時候,是無法發起攻擊的。但是要考慮到柯克霍夫原則,攻擊者通常會獲得源代碼(尤其是免費或者開源軟體)。通過系統中找出密碼-哈希值對應關系,很容易反向推導出加密算法。使用一個很難被并行計算結果的疊代算法(下面将予以讨論),然後增加适當的鹽值防止彩虹表攻擊。

如果你真的想用一個标準的“古怪”的哈希函數,如 HMAC ,亦無不可。但是,如果你目的是想降低哈希計算速度,那麼可以閱讀下面有關密鑰擴充的部分。

如果創造新的哈希函數,可能會帶來風險,構造希函數的組合又會導緻函數互通性的問題。它們帶來一點的好處和這些比起來微不足道。很顯然,最好的辦法是,使用标準、經過完整測試的算法。

 哈希碰撞( HashCollisions)

由于哈希函數将任意大小的資料轉化為定長的字元串,是以,必定有一些不同的輸入經過哈希計算後得到了相同的字元串的情況。加密哈希函數( Cryptographic hash function)的設計初衷就是使這些碰撞盡量難以被找到。現在,密碼學家發現攻擊哈希函數越來越容易找到碰撞了。最近的例子是MD5算法,它的碰撞已經實作了。

碰撞攻擊是指存在一個和使用者密碼不同的字元串,卻有相同的哈希值。然而,即使是像MD5這樣的脆弱的哈希函數找到碰撞也需要大量的專門算力( dedicatedcomputing power),是以在實際中“意外地”出現哈希碰撞的情況不太可能。對于實用性而言,加鹽MD5 和加鹽 SHA256 的安全性一樣。盡管如此,可能的話,要使用更安全的哈希函數,比如 SHA256 、 SHA512 、RipeMD 或 WHIRLPOOL 。

如何正确進行哈希加密

本節介紹了究竟應該如何對密碼進行哈希加密。第一部分介紹基礎知識,這部分是必須的。後面闡述如何在這個基礎上增強安全性,使哈希加密變得更難破解。

 基礎知識:加鹽哈希(Hashing with Salt)

我們已經知道,惡意攻擊者使用查詢表和彩虹表,破解普通哈希加密有多麼快。我們也已經了解到,使用随機加鹽哈希可以解決這個問題。但是,我們使用什麼樣的鹽值,又如何将其混入密碼中?

鹽值應該使用加密的安全僞随機數生成器( Cryptographically Secure Pseudo-Random Number Generator,CSPRNG )産生。CSPRNG和普通的僞随機數生成器有很大不同,如“ C ”語言的rand()函數。顧名思義, CSPRNG 被設計成用于加密安全,這意味着它能提供高度随機、完全不可預測的随機數。我們不希望鹽值能夠被預測到,是以必須使用 CSPRNG 。下表列出了一些目前主流程式設計平台的 CSPRNG 方法。

Platform

CSPRNG

PHP

mcrypt_create_iv,  openssl_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.   将鹽值混入密碼,并使用标準的密碼哈希函數進行加密,如Argon2、 bcrypt 、scrypt 或 PBKDF2 。

3.   将鹽值和對應的哈希值一起存入使用者資料庫。

校驗密碼的步驟:

1.   從資料庫檢索出使用者的鹽值和對應的哈希值。

2.   将鹽值混入使用者輸入的密碼,并且使用通用的哈希函數進行加密。

3.   比較上一步的結果,是否和資料庫存儲的哈希值相同。如果它們相同,則表明密碼是正确的;否則,該密碼錯誤。

 在 Web 應用中,永遠在服務端上進行哈希加密

如果您正在編寫一個 Web 應用,你可能會疑惑究竟在哪裡進行哈希加密,是在使用者的浏覽器上使用JavaScript 對密碼進行哈希加密呢,還是将明文發送到服務端上再進行哈希加密呢?

就算浏覽器上已經用JavaScript哈希加密了,但你你還是要在服務端上将得到的密碼哈希值再進行一次哈希加密。試想一個網站,将使用者在浏覽器輸入的密碼經過哈希加密,而不是在傳送到服務端再進行哈希。為了驗證使用者,這個網站将接受來自浏覽器的哈希值,并和資料庫中的哈希值進行比對即可。因為使用者的密碼從未明文傳輸到服務端,這樣子看上去更安全,但事實并非如此。

問題是,從用戶端的角度來看,經過哈希的密碼,從邏輯上成為使用者的密碼了。所有使用者需要做的認證就是将它們的密碼哈希值告訴服務端。如果一個攻擊者得到了使用者的哈希值,他們可以用它來通過認證,而不必知道使用者的明文密碼!是以,如果攻擊者使用某種手段拖了網站的資料庫,他們就可以随意使用每個人的帳号直接通路,而無需猜測任何密碼。

這并不是說你不應該在浏覽器進行哈希加密,但是如果你這樣做了,你一定要在服務端上再進行一次哈希加密。在浏覽器中進行哈希加密無疑是一個好主意,但實作的時候要考慮以下幾點:

·        用戶端密碼哈希加密不是 HTTPS(SSL/TLS)的替代品。如果浏覽器和服務端之間的連接配接是不安全的,那麼中間人攻擊可以修改 JavaScript 代碼,删除加密函數,進而擷取使用者的密碼。

·        某些浏覽器不支援 JavaScript ,還有一些使用者在浏覽器中禁用 JavaScript 功能。是以,為了更好的相容性,您的應用應該檢測浏覽器是否支援 JavaScript ,如果不支援,就需要在服務端模拟用戶端進行哈希加密。

·        用戶端的哈希加密同樣需要加鹽。顯而易見的解決方案是使用戶端腳本向服務端請求使用者的鹽值。但是不提倡這樣做,因為它可以讓攻擊者能夠在不知道密碼的情況下檢測使用者名是否有效。既然你已經在服務端上對密碼進行了加鹽哈希(使用合格的鹽值),那麼在用戶端,将使用者名(或郵箱)加上網站特有的字元串(如域名)作為用戶端的鹽值也是可行的。

 使密碼更難破解:慢哈希函數( Slow Hash Function)

加鹽可以確定攻擊者無法使用像查詢表和彩虹表攻擊那樣對大量哈希值進行破解,但依然不能阻止他們使用字典攻擊或暴力攻擊。高端顯示卡( GPU )和定制的硬體每秒可以進行十億次哈希計算,是以這些攻擊還是很有效的。為了降低使這些攻擊的效率,我們可以使用一個叫做密鑰擴充( key stretching)的技術。

這樣做的初衷是為了将哈希函數變得非常慢,即使有一塊快速的 GPU 或定制的硬體,字典攻擊和暴力攻擊也會慢得令人失去耐心。終極目标是使哈希函數的速度慢到足以令攻擊者放棄,但由此造成的延遲又不至于引起使用者的注意。

密鑰擴充的實作使用了一種 CPU 密集型哈希函數( CPU-intensive hash function)。不要試圖去創造你自己的疊代哈希加密函數。疊代不夠多的話,它可以被高效的硬體快速并行計算出來,就跟普通的哈希一樣。要使用标準的算法,比如 PBKDF2 或 bcrypt 。你可以在這裡找到 PBKDF2 在 PHP 上的實作。

這類算法采取安全因子或疊代次數作為參數。此值決定哈希函數将會如何緩慢。對于桌面軟體或智能手機應用,确定這個參數的最佳方式是在裝置上運作很短的性能基準測試,找到使哈希大約花費半秒的值。通過這種方式,程式可以盡可能保證安全而又不影響使用者體驗。

如果您想在一個 Web 應用使用密鑰擴充,須知你需要額外的計算資源來處理大量的身份認證請求,并且密鑰擴充也容易讓服務端遭受拒絕服務攻擊( DoS )。盡管如此,我還是建議使用密鑰擴充,隻不過要設定較低一些的疊代次數。這個次數需要根據自己伺服器的計算能力和預計每秒需要處理的認證請求次數來設定。消除拒絕服務的威脅可以通過要求使用者每次登陸時輸入驗證碼( CAPTCHA )來做到。系統設計時要将疊代次數可随時友善調整。

如果你擔心計算帶來負擔,但又想在 Web 應用中使用密鑰擴充,可以考慮在浏覽器中使用 JavaScript 完成。斯坦福大學的 JavaScript 加密庫就包含了 PBKDF2 的實作。疊代次數應設定足夠低,以适應速度較慢的用戶端,如移動裝置。同時,如果使用者的浏覽器不支援 JavaScript ,服務端應該接手進行計算。用戶端密鑰擴充并不能免除服務端端進行哈希加密的需要。你必須對用戶端生成的哈希值再次進行哈希加密,就跟普通密碼的處理一樣。

 不可能破解的哈希加密:密鑰哈希和密碼哈希裝置

隻要攻擊者可以使用哈希來檢查密碼的猜測是對還是錯,那麼他們可以進行字典攻擊或暴力攻擊。下一步是将密鑰( secret key)添加到哈希加密,這樣隻有知道密鑰的人才可以驗證密碼。有兩種實作的方式,使用ASE算法對哈希值加密;或者使用密鑰雜湊演算法 HMAC 将密鑰包含到哈希字元串中。

實作起來并沒那麼容易。這個密鑰必須在任何情況下,即使系統因為漏洞被攻陷,也不能被攻擊者擷取。如果攻擊者完全進入系統,密鑰不管存儲在何處,總能被找到。是以,密鑰必須密鑰必須被存儲在外部系統,例如專用于密碼驗證一個實體上隔離的服務端,或者連接配接到服務端,例如一個特殊的硬體裝置,如 YubiHSM 。

我強烈建議所有大型服務(超過10萬使用者)使用這種方式。我認為對于任何超過100萬使用者的服務托管是非常有必要的。

如果您難以負擔多個服務端或專用硬體的費用,依然有辦法在标準的Web服務端上使用密鑰哈希技術。大多數資料庫被拖庫是由于 SQL 注入攻擊,是以,不要給攻擊者進入本地檔案系統的權限(禁止資料庫服務通路本地檔案系統,如果有此功能的話)。如果您生成一個随機密鑰并将其存儲在一個通過 Web 無法通路的檔案上,然後進行加鹽哈希加密,那麼得到的哈希值就不會那麼容易被破解了,就算資料庫已經遭受注入攻擊,也是安全的。不要将密鑰寫死到代碼中,應該在安裝應用時随機生成。這麼做并不像使用一個獨立的系統那樣安全,因為如果 Web 應用存在 SQL 注入點,那麼有可能存在其他一些問題,如本地檔案包含漏洞( Local File Inclusion ),攻擊者可以利用它讀取本地密鑰檔案。無論如何,這個措施總比沒有好。

請注意,密鑰哈希并不意味着無需進行加鹽。高明的攻擊者最終會想方設法找到密鑰,是以,對密碼哈希仍然需要進行加鹽和密鑰擴充,這一點非常重要。

 其他安全措施

密碼哈希僅僅在安全受到破壞時保護密碼。它并不能使整個應用更加安全。首先有很多事必須完成,來保證密碼哈希值(和其他使用者資料)不被竊取。

即使是經驗豐富的開發人員也必須學習安全知識,才能編寫安全的應用。此處有關于Web應用漏洞的重要資源: The Open Web ApplicationSecurity Project (OWASP)。還有一個很好的介紹: OWASP Top TenVulnerability List 。除非你了解了清單中的所有漏洞,否則不要去嘗試編寫一個處理敏感資料的Web應用程式。雇主也有責任確定所有開發人員在安全應用開發方面經過充分的教育訓練。

對您的應用進行第三方“滲透測試”是一個很好的主意。即使最好的程式員也可能會犯錯,是以,讓安全專家審計代碼尋找潛在的漏洞是有意義的。找一個值得信賴的機構(或招聘人員)來定期審計代碼。安全審計應該從開發初期就着手進行,并貫穿整個開發過程。

監控您的網站來發現入侵行為也很重要。我建議至少雇用一名全職人員負責監測和處理安全漏洞。如果某個漏洞沒被發現,攻擊者可能通過網站利用惡意軟體感染通路者,是以,檢測漏洞并及時處理是極為重要的。

常見疑問 我應該使用什麼樣的雜湊演算法?

可以使用:

·        精心設計的密鑰擴充算法如 PBKDF2 、bcrypt 和scrypt 。

·        OpenWall的的 Portable PHP password hashing framework。

·        PBKDF2在PHP、C#、Java和Ruby的實作。

·        crypt 的安全版本。

不可使用:

·        快速加密哈希函數,如 MD5 、SHA1、SHA256、SHA512、RipeMD、WHIRLPOOL、SHA3等。

·        crypt()的不安全版本。

·        任何自己設計的加密算法。隻應該使用那些在公開領域中的、由經驗豐富的密碼學家完整測試過的技術。

盡管目前還沒有一種針對MD5或SHA1非常高效的攻擊手段,但它們過于古老以至于被廣泛認為不足以用來存儲密碼(可能有些不恰當)。是以我不推薦使用它們。但是也有例外,PBKDF2中經常使用SHA1作為它底層的哈希函數。

 當使用者忘記密碼時如何重置密碼?

這是我個人的觀點:當下所有廣泛使用的密碼重置機制都是不安全的。如果你對高安全性有要求,如加密服務,那麼就不要讓使用者重設密碼。

大多數網站向那些忘記密碼的使用者發送電子郵件來進行身份認證。要做到這一點,需要随機生成一個一次性使用的令牌( token ),直接關聯到使用者的帳号。然後将這個令牌混入一個重置密碼的連結中,發送到使用者的電子郵箱。當使用者點選包含有效令牌的密碼重置連結,就提示他們輸入新密碼。確定令牌隻對一個帳号有效,以防攻擊者從郵箱擷取到令牌後用來重置其他使用者的密碼。

令牌必須在15分鐘内使用,且一旦使用後就立即廢棄。當使用者登入成功時(表明還記得自己的密碼), 或者重新請求令牌時,使原令牌失效是一個好做法。如果令牌永不過期,那麼它就可以一直用于入侵使用者的賬号。電子郵件(SMTP)是一個純文字協定,網絡上有很多惡意路由在截取郵件資訊。在使用者修改密碼後,那些包含重置密碼連結的郵件在很長時間内缺乏保護,是以,盡早使令牌盡快過期,來降低使用者資訊暴露給攻擊者的風險。

攻擊者能夠篡改令牌,是以不要把帳号資訊和失效時間存儲在其中。它們應該以不可猜測的二進制形式存在,并且隻用來識别資料庫中某條使用者的記錄。

千萬不要通過電子郵件向使用者發送新密碼。記得在使用者重置密碼時随機生成一個新的鹽值用來加密,不要重複使用已用于密碼哈希加密的舊鹽值。

 如果帳号資料庫被洩漏或入侵,應該怎麼做?

你的首要任務是,确定系統被暴露到什麼程度,然後修複攻擊者利用的的漏洞。如果你沒有應對入侵的經驗,我強烈建議聘請第三方安全公司來做這件事。

捂住一個漏洞并期待沒人知道,是不是很省事,又誘人?但是這樣做隻會讓你的處境變得更糟糕,因為你在使用者不知情的情況下,将它們的密碼和個人資訊置于暴露風險之中。就算你還沒有完全發生什麼事情時,你也應該盡快通知使用者。例如在首頁放置一個連結,指向對此問題更為詳細的說明;如果可能的話通過電子郵件發送通知給每個使用者告知目前的情況。

向使用者說明他們的密碼究竟是如何被保護的:最好是使用了加鹽哈希。但是,即使用了加鹽哈希,惡意黑客仍然可以使用字典攻擊和暴力攻擊。如果使用者在很多服務使用相同的密碼,惡意黑客會利用他們找到的密碼去嘗試登陸其他網站。告知使用者這個風險,建議他們修改所有類似的密碼,不論密碼用在哪個服務上。強制他們下次登入你的網站時更改密碼。大多數使用者會嘗試“修改”自己的密碼為原始密碼,以便記憶。您應該使用目前密碼哈希值以確定使用者無法做到這一點。

就算有加鹽哈希的保護,也存在攻擊者快速破解其中一些弱密碼密碼的可能性。為了減少攻擊者使用這些密碼的機會,應該對這些密碼的帳号發送認證電子郵件,直到使用者修改了密碼。可參考前面提到的問題:當使用者忘記密碼時如何重置密碼?這其中有一些實作電子郵件認證的要點。

另外告訴你的使用者,網站存儲了哪些個人資訊。如果您的資料庫包括信用卡号碼,您應該通知使用者仔細檢查近期賬單并銷掉這張信用卡。

 應該使用什麼樣的密碼政策?是否應該使用強密碼?

如果您的服務沒有嚴格的安全要求,那麼不要對使用者進行限制。我建議在使用者輸入密碼時,頁面顯示出密碼強度,由他們自己決定需要多安全的密碼。如果你有特殊的安全需求,那就應該實施長度至少為12個字元的密碼,并且至少需要兩個字母、兩個數字和兩個符号。

不要過于頻繁地強制你的使用者更改密碼,最多每半年一次,超過這個次數,使用者就會感到疲勞。相反,更好的做法是教育使用者,當他們感覺密碼可能洩露時主動修改,并且提示使用者不要把密碼告訴任何人。如果這是一個商業環境,鼓勵員工利用工作時間熟記并使用他們的密碼。

 如果攻擊者入侵了資料庫,他不能直接替換哈希值登陸任意帳号麼?

是的,但如果有人入侵您的資料庫,他們很可能已經能夠通路您的服務端上的所有内容,這樣他們就不需要登入到您的帳号,就可以獲得他們想要的東西。密碼哈希(對網站而言)的目的不是為了保護被入侵的網站,而是在入侵已經發生時保護資料庫中的密碼。

你可以通過給資料庫連接配接設定兩種權限,防止密碼哈希在遭遇注入攻擊時被篡改。一種權限用于建立使用者,一種權限用于使用者登陸。“建立使用者”的代碼應該能夠讀寫使用者表;但“使用者登陸”的代碼應該隻能夠讀取使用者表而不能寫入。

 為什麼要使用一種像HMAC的特殊算法,而不是隻将密鑰混入密碼?

如MD5、SHA1、SHA2 和 Hash 函數使用 Merkle–Damgrd,這使得它們很容易受到所謂的長度擴充攻擊( length extension attack)。意思是給定的哈希值 H(X),對于任意的字元串 Y,攻擊者可以計算出 H(pad(X)+Y) 的值,而無需知道 X 的值。其中, pad(X) 是哈希函數的填充函數。

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

目前尚不清楚攻擊者如何利用這種攻擊來快速破解密碼哈希。然而,由于這種攻擊的出現,不建議使用普通的哈希函數對密鑰進行哈希加密。将來也許某個高明的密碼學家有一天發現利用長度擴充攻擊的新思路,進而更快的破解密碼,是以還是使用 HMAC 為好。

 鹽值應該加到密碼之前還是之後?

無所謂,選擇一個并保持風格一緻即可,以免出現互操作方面的問題。鹽值加到密碼之前較為普遍。

 為何本文的哈希代碼都以固定時間比較哈希值?

使用固定的時間來比較哈希值可以防止攻擊者在線上系統使用基于時間差的攻擊,以此擷取密碼的哈希值,然後進行本地破解。

比較兩個位元組序列(字元串)是否相同的标準做法是,從第一個位元組開始,每個位元組逐一順序比較。隻要發現某個位元組不同,就可以知道它們是不同的,立即傳回false。如果周遊整個字元串沒有找到不同的位元組,可以确認兩個字元串就是相同的,可以傳回true。這意味着比較兩個字元串,如果它們相同的長度不一樣,花費的時間不一樣。開始部分相同的長度越長,花費的時間也就越長。

例如,字元串 “XYZABC” 和 “abcxyz” 的标準比較,會立即看到,第一個字元是不同的,就不需要檢查字元串的其餘部分。相反,當字元串“aaaaaaaaaaB” 和 “aaaaaaaaaaZ” 進行比較時,比較算法就需要周遊最後一位前所有的“a” ,然後才能知道他們是不同的。

假設攻擊者試圖入侵一個線上系統,這個系統限制了每秒隻能嘗試一次使用者認證。還假設攻擊者已經知道密碼哈希所有的參數(鹽值、哈希函數的類型等),除了密碼的哈希值和密碼本身。如果攻擊者能精确測量線上系統耗時多久去比較他猜測的密碼和真實密碼,那麼他就能使用時序攻擊擷取密碼的哈希值,然後進行離線破解,進而繞過系統對認證頻率的限制。

首先攻擊者準備256個字元串,它們的哈希值的第一位元組包含了所有可能的情況。他将每個字元串發送給線上系統嘗試登陸,并記錄系統響應所消耗的時間。耗時最長的字元串就是第一位元組相比對的。攻擊者知道第一位元組後,并可以用同樣的方式繼續猜測第二位元組、第三位元組等等。一旦攻擊者獲得足夠長的哈希值片段,他就可以在自己的機器上來破解,不受線上系統的限制。

在網絡上進行這種攻擊似乎不可能。然而,有人已經實作了,并已證明是實用的。這就是為什麼本文提到的代碼,它利用固定時間去比較字元串,而不管有多大的字元串。

 “慢比較(slowequals)”函數如何工作?

前一個問題解釋了為什麼“慢比較”是必要的,現在來解釋代碼如何工作。

private staticboolean slowEquals(byte[] a, byte[] b)

     {

         int diff =a.length ^ b.length;

         for(int i = 0; i&lt; a.length &amp;&amp; i &lt; b.length; i++)

             diff|= a[i] ^ b[i];

         return diff == 0;

     }

該代碼使用異或運算符“^”來比較兩個整數是否相等,而不是“==”運算符。下面解釋原因。當且僅當兩位相等時,異或的結果将是零。這是因為:0 XOR 0 = 0,1 XOR 1 = 0,0 XOR 1 = 1,1 XOR 0 = 1如果我們将其應用到整數中每一位,當且僅當位元組兩個整數各位都相等,結果才是0。

是以,在代碼的第一行中,如果a.length等于b.length ,相同的話得到0,否者得到非零值。然後使用異或比較數組中各位元組,并且将結果和diff求或。如果有任何一個位元組不相同,diff就會變成非零值。因為或運算沒有“置0”的功能,是以循環結束後diff是0的話隻有一種可能,那就是循環前兩個數組長度相等(a.length == b.length),并且數組中每一個位元組都相同(每次異或的結果都非0)。

我們需要使用XOR,而不是“==”運算符比較整數的原因是,“==”通常是編譯成一個分支的語句。例如,C語言代碼中“ diff &amp;= 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

 為何要進行哈希?

使用者在你的網站上輸入密碼,是因為他們相信你能保證密碼的安全。如果你的資料庫遭到黑客攻擊,而使用者的密碼又不受保護,那麼惡意黑客可以利用這些密碼嘗試登陸其他網站和服務(大多數使用者會在所有地方使用相同的密碼)。這不僅僅關乎你網站的安全,更關系到使用者的安全。你有責任負責使用者的安全。

本文轉自 wyait 51CTO部落格,原文連結:http://blog.51cto.com/wyait/1905878,如需轉載請自行聯系原作者