對抗明文口令泄露 —— Web 前端慢 Hash


(更新:https://www.cnblogs.com/index-html/p/frontend_kdf.html

0x00 前言

天下武功,唯快不破。但在密碼學中則不同。算法越快,越容易破。

0x01 暴力破解

密碼破解(嚴格地說應該是賬號口令的破解),就是把散列值還原成明文口令。這貌似有不少方法,但事實上都得走一條路:暴力窮舉。(也許你會說還可以查表,瞬間就出結果。雖然查表不用窮舉,但表的制造過程仍然需要。查表只是將窮舉提前了而已)

因為散列計算是單向的,是不可逆的,所以只能窮舉。窮舉的原理很簡單。只要知道密文用的是什么 Hash 算法,我們也用同樣的算法把常用詞組跑一遍。若有結果和密文一樣,那就猜中了。

窮舉的速度有多快?這和算法有關。Hash 一次有多快,猜一次也這么快。

例如 MD5 就非常快的,若每次 Hash 耗費 1 微秒,那破解時猜一個詞組,也只需 1 微秒(假設機器都性能一樣,詞組長度相近),攻擊者一秒鍾就能猜 100 萬個!(而且這還只是單線程的速度)

所以,Hash 算法越快,破解起來就越容易。

0x02 慢 Hash

如果能提高 Hash 時間,顯然也能增加破解時間。如果 Hash 一次提高到 10 毫秒,那么攻擊者每秒只能猜 100 個,破解速度就慢了一萬倍。

怎樣才能讓 Hash 變慢?最簡單的,就是對 Hash 后的結果再 Hash,反復多次。例如原本 1 微秒,重復一萬次,就慢一萬倍了:

function slow_md5(x)
    for i = 0 to 10000
        x = md5(x)
    return x
end

攻擊者破解時,也必須用這套算法跑字典。於是,破解時間就大幅增加了。

事實上,這樣的「慢 Hash」算法早有現成的方案,例如 bcryptPBKDF2 等等。它們都有一個難度系數因子,可以控制 Hash 次數,想多慢就多慢。

所以 Hash 過程越慢,破解也就越費勁。

0x03 慢 Hash 應用

最需要慢 Hash 的場合,就是網站數據庫里的賬號口令。

近幾年,經常能聽到網站被「拖庫」的新聞。用戶資料都是明文存儲,泄露了也無法挽回。唯獨口令,還可以和攻擊者對抗一下。

然而不少網站,使用的都是快速 Hash 算法,因此輕易就能破解出一堆弱口令賬號。

當然,有時只想破解某個特定人物的賬號。只要不是特別復雜的詞匯,跑上幾天,很可能就破出來。

但網站用了慢 Hash,結果可能就不一樣了。如果把 Hash 時間提高 100 倍,破解時間就得長達數月,變得難以接受。

即使數據泄露,也能保障「明文口令」這最后一道隱私。

0x04 慢 Hash 缺點

不過,慢 Hash 也有明顯的缺點:消耗大量計算資源。

使用慢 Hash 的網站,如果同時來了多個用戶,服務器 CPU 可能就不夠用了。要是遇到惡意用戶,發起大量的登錄請求,甚至造成資源被耗盡。

性能和安全總是難以兼得。所以,一般也不會使用太高的強度。

一些大型網站,甚至為此投入集群,用來處理大量的 Hash 計算。但這需要不少的成本。

有沒有什么方法,可以讓我們使用算力強勁、同時又免費的計算資源?

0x05 前端計算

在過去,個人電腦和服務器的速度,還是有較大差距的。但如今,隨着硬件發展進入瓶頸,這個差距正縮小。在單線任務處理上,甚至不相上下。

客戶端擁有強大的算力,能不能分擔一些服務器的工作?

尤其像「慢 Hash」這種算法開源、但計算沉重的任務,為何不交給客戶端來完成?

傳統方案,提交的幾乎是明文口令;現在,提交的則是明文口令的「Hash 結果」。(無論是注冊,還是登陸)

而服務端則無需任何改動。先前是怎么保存的,現在還是怎么保存。

這樣就算被拖庫,攻擊者破解出來的也只是「Hash 結果」,還需再破解一次,才能還原出「明文口令」。

事實上,這個「Hash 結果」是不可能還原出來的。為什么這么說呢?因為它是毫無規律的隨機串,而字典都是有意義的詞組,幾乎不可能跑到它!除非字節逐個窮舉,但這將是個天文數字。

所以中間值,是無法通過數據庫泄露的數據「跑」出來的!

當然,即使不知道這個中間值,也沒影響明文的破解。攻擊者可以把前端 Hash + 后端的 Hash,組合成一個新的函數:

f(x) = back_hash( front_hash(x) )

然后使用這個新函數來跑字典。這樣,理論上還是可以跑出來的。但是,有 front_hash 這個重量級的函數存在,跑字典的速度就大幅降低了,於是就能增加攻擊者的破解成本!

0x06 對抗預先計算

不過前端的一切都是公開的,所以 front_hash 的算法大家都知道。

攻擊者可以用這套算法,把常用詞組的「慢 Hash 結果」提前算出來,制作成一個「新字典」。將來拖庫后,就可以直接跑這個新字典了。

對抗這種方法,還得用經典的手段:加鹽。最簡單的,將用戶名作為鹽值:

front_hash(password, username)

這樣即使相同的口令,對於不同的用戶,「Hash 結果」也變得不一樣了。

除了用戶名,還可以將網站的域名、或者其他固定信息,也加入到鹽值中,這樣不同的網站也不能共享同個彩虹表了。使得破解方案更不通用。

0x07 強度策略

密碼學上的問題到此結束,下面討論實現上的問題。現實中,用戶的算力是不均衡的。有人用的是神級配置,也有的是古董機。這樣,Hash 的次數就很難設定。如果古董機用戶登錄會卡上幾十秒,那肯定是不行的。因此必要得有控制強度的方案。

1.強度固定

根據大眾的配置,制定一個適中的強度,絕大多數用戶都可接受。

但如果超過規定時間還沒完成,就把算到一半的 Hash 和步數提交上來,剩余部分讓服務器來完成。

[前端] 完成 70% ----> [后端] 計算 30%

不過,這需要「可序列化」的算法,才能在服務端還原進度。如果計算過程涉及大量的內存,這種方案就不可行了。

相比過去 100% 后端慢 Hash,這種少量用戶「前后參半」的方式,可以節省不少服務器資源。

2.強度可變

如果后端不提供任何協助,那只能根據自身條件做取舍了。配置差的用戶,Hash 次數少一點。

用戶注冊時,算法不限步數放開跑,看看特定時間里能算到多少步:

# [注冊階段] 算力評估(線程 1 秒后中止)
while
	x = hash(x)
	iter = iter + 1
end

這個步數,就是 Hash 強度,會保存到賬號信息里。之后每次登錄時,先獲取這個強度值,然后再做相應次數的 Hash:

# 先獲取用戶的強度值
...

# 重復 Hash 相應次數
for i = 0 to iter
	x = hash(x)
end

使用這個方案,可以讓 高配置的用戶享受更高的安全性;低配置的用戶,也不會影響基本使用。(用上好電腦還能提升安全性,很有優越感吧~)

但這有個重要的前提:注冊和登錄,必須在性能相近的設備上 —— 如果是在高配置電腦上注冊的賬號,某天去古董機登錄,那就悲劇了,可能半天都算不出來。。。

3.動態調整方案

上述情況,現實中是普遍存在的。比如 PC 端注冊的賬號,在移動端登錄,算力可能就不夠用。

如果沒有后端協助,那只能等。要是經常在低端設備上登陸,那每次都得干等嗎?

等一兩次就算了,如果每次都等,不如重新估量下自己的能力吧。把強度動態調低,更好的適應當前環境。

將來如果不用低端設備了,再自動的調整回來。讓強度值,能動態適應常用的設備的算力。

0x08 性能優化

1.為什么要優化

或許你會問,「慢 Hash」不就是希望計算更慢嗎,為什么還要去優化?

假如這是一個自創的隱蔽式算法,並且混淆到外人根本無法讀懂,那不優化也沒事。甚至可以在里面放一些空循環,故意消耗時間。但事實上,我們選擇的肯定是「密碼學家推薦」的公開算法。它們每一個操作,都是有數學上的意義的。

原本一個操作只需一條 CPU 指令,因為不夠優化,用了兩條指令,那么額外的時間就是內耗。導致用時更久,強度卻未提升。

2.前端計算軟肋

如果是本地程序,根本不用考慮這個問題,交給編譯器就行。但在 Web 環境里,我們只能用瀏覽器計算!相比本地程序,腳本要慢的多,因此內耗會很大。

腳本為什么慢?主要還是這幾點:

  • 弱類型

  • 解釋型

  • 沙箱

3.弱類型

腳本,是用來處理簡單邏輯的,並不是用來密集計算的,所以沒必要強類型。不過如今有了一個黑科技:asm.js。它能通過語法糖,為 JS 提供真正的強類型。這樣計算速度就大幅提升了,可以接近本地程序的性能!

但是不支持 asm.js 的瀏覽器怎么辦?例如,國內還有大量的 IE 用戶,他們的算力是非常低的。好在還有個后補方案 —— Flash,它有各種高性能語言的特征。類型,自然不在話下。相比 asm.js,Flash 還是要慢一些,但比 IE 還是快多了。

4.解釋型

解釋型語言,不僅需要語法分析,更是失去了「編譯時深度優化」帶來的性能提升。

好在 Mozilla 提供了一個可以從 C/C++ 編譯成 asm.js 的工具:emscripten。有了它,就不用裸寫了。而且編譯時經過 LLVM 的優化,生成的代碼質量會更高。

事實上,這個概念在 Flash 里早有了。曾經有個叫 Alchemy 的工具,能把 C/C++ 交叉編譯成 Flash 虛擬機指令,速度比 ActionScript 快不少。

5.沙箱

一些本地語言看似很簡單的操作,在沙箱里就未必如此。例如數組操作:

vector[k] = v

虛擬機首先得檢查索引是否越界,否則會有嚴重的問題。如果「前端慢 Hash」算法涉及到大量內存隨機訪問,那就會有很多無意義的內耗,因此得慎重考慮。

不過有些特殊場合,腳本速度甚至能超過本地程序!例如開頭提到的 MD5 大量反復計算,比本地程序還快。這其實不難解釋:

首先,MD5 算法很簡單。沒有查表這樣的內存操作,使用的都是局部變量。

其次,emscripten 的優化能力,並不比本地編譯器差。

最后,本地程序編譯之后,機器指令就不會再變了;而如今腳本引擎,都有 JIT 這個利器,運行時生成更優化的機器指令。

所以選擇算法時,還得兼顧實際運行環境,揚長避短,發揮出最大功效。

0x09 對抗 GPU

眾所周知,跑密碼使用 GPU 可以快很多倍。GPU 可以想象成一個有幾百核的處理器,但只能執行一些簡單的指令。雖然單核速度不及 CPU,但可以通過數量取勝。暴力窮舉時,可以從字典里取出上千個詞匯同時跑,破解效率就提高了。

那能否在算法里添加一些特征,正好命中 GPU 的軟肋呢?

1.顯存瓶頸

大家聽過說「萊特幣」吧。不同於比特幣,萊特幣挖礦使用了 scrypt 算法。這種算法對內存依賴非常大,需要頻繁讀寫一個表。GPU 雖然每個線程都能獨立計算,但顯存只有一個,大家共享使用。

這意味着,同時只有一個線程能操作顯存,其他有需要的只能等待了。這樣,就極大遏制了並發的優勢。

2.移植難度

山寨幣遍地開花的時候,還出現了一個叫 X11Coin 的幣,據稱能對抗 ASIC。它的原理很簡單,里面摻雜了 11 種不同的算法。這樣,制造出相應的 ASIC 復雜度大幅增加了。

盡管這不是一個長久的對抗方案,但思路還是可以借鑒的。如果一件事過於復雜,很多攻擊者就望而生畏了,不如去做更容易到手的事。

3.其他想法

之所以 GPU 能大行其道,是因為目前的 Hash 算法,都是簡單的公式運算。這對 CPU 並沒太大的優勢。能否設計一個算法,充分依賴 CPU 的優勢?

CPU 有很多隱藏的強項,例如流水線。如果算法中有大量的條件分支,也許 GPU 就不擅長了。

當然,這里只是設想。自己創造密碼學算法,是非常困難的,也不推薦這么做。

0x0A 額外意義

慢 Hash 除了能降低破解速度,還有一些其他意義:

1.減少泄露風險

用戶輸入的明文口令,在瀏覽器內存里就已被散列化了。泄露風險,在用戶輸入后就就已結束。

而傳統的方案,明文口令會一直傳遞服務器的業務程序中才消失,這中間存在極大的泄露風險。

2.增加撞庫成本

「前端慢 Hash」需要消耗用戶的計算資源。這個缺點,有時也是件好事。

對於正常用戶來說,登錄時多等一秒影響並不大;但對於頻繁登錄的用戶來說,這就是一個障礙了。

誰會頻繁登錄?也許就是撞庫攻擊者。他們無法拖下這個網站的數據庫,於是就用在線登錄的方式,不斷的測試弱口令賬號。

如果通過 IP 來控制頻率,攻擊者可以找大量的代理 —— 網速有多快,就能試多快。但使用了前端慢 Hash,攻擊者每次測試,就得消耗大量的計算,於是將瓶頸卡在硬件上 —— 能算多快,才能試多快。

所以,這里有點類似 PoW(Proof-of-Work,工作量證明)的意義。關於 PoW,以后我們會詳細介紹。

0x0B 無法做到的

盡管「前端慢 Hash」有不少優勢,但也不是萬能的。如果環境本身就有問題,那么任何隱私都有泄露。

下面我們來思考一個場景:某網站使用了「前端慢 Hash」,但沒有使用 HTTPS —— 這會導致鏈路被竊聽。

回顧 0x05 小節,如果拿到 Hash 結果,就可以直接登上賬號,即使不知道明文口令。

的確如此。但請仔細想一想,這不也降低損失了嗎?

本來不僅賬號被盜用,而且明文口令也會泄露;而如今,只是賬號被盜用,明文口令對方仍無法獲得。

所以,前端慢 Hash 的真正保護的是 明文口令,而不是賬號的授權。簡單地說:賬號被盜,密碼拿不到!

當然,如果攻擊者不僅能竊聽,還能控制流量的話,就可以往頁面注入 JS 腳本,這樣倒是可以拿到明文口令的。不過這和電腦中毒、攝像頭偷窺一樣,都屬於「環境本身有問題」,不在本文討論范圍內。本文討論的是數據庫泄露的場景。

0x0C 多線程

用戶的配置越來越好,不少都是四核、八核處理器。能否利用多線程的優勢,將慢 Hash 計算進行分解?

如果每一步計算都依賴之前的結果,是無法進行拆解的。例如:

for i = 0 to 10000
	x = hash(x)
end

這是一個串行的計算。然而只有並行的問題,才能分解成多個小任務。

不過,換一種方式的多線程也是可以的。例如我們使用 4 個線程:

# 線程 1
x1 = hash(password, salt1)
for i = 0 to 2500
	x1 = hash(x1)
end

# 線程 2
x2 = hash(password, salt2)
for i = 0 to 2500
	x2 = hash(x2)
end

# ...

最終將 4 個結果合並起來,再做一次散列,作為結果。

finalHash = hash(x1, x2, x3, x4)

這樣,每 Hash 一個明文口令,就需要數倍的算力資源。同樣,破解起來成本也變得更大了。而對於用戶來說,只要支持多線程,總體花費的時間幾乎沒有增加,並不影響體驗。

當然,這個細節不用自己實現,現實中早已有這樣的算法。例如 2015 年《Password Hashing Competition》勝出者 argon2 算法,就可設置並行數量。(這個算法很先進,包括了 GPU 抵抗等特性)

0x0D 總結

前端慢 Hash,就是讓每個用戶貢獻少量的計算資源,使得 Hash 計算變得更強勁。使得數據泄露后,攻擊者需要花費更大的成本去破解。


免責聲明!

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



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