對抗拖庫 —— Web 前端慢加密

EtherDream發表於2015-12-01

0×00 前言

天下武功,唯快不破。但密碼加密不同。演算法越快,越容易破。

0×01 暴力破解

密碼破解,就是把加密後的密碼還原成明文密碼。似乎有不少方法,但最終都得走一條路:暴力窮舉。也許你會說還可以查表,瞬間就出結果。雖然查表不用窮舉,但表的製造過程仍然需要。查表只是將窮舉提前了而已。

密碼加密,用的都是單向雜湊計算。既然單向,那就是不可逆,那隻能窮舉。窮舉的原理很簡單。只要知道密文是用什麼演算法加密的,我們也用相同的演算法,把常用的片語跑一遍。若有結果和密文一樣,那就猜中了。

窮舉的速度有多快?這和加密演算法有關。加密一次有多快,猜一次也這麼快。例如 MD5 加密是非常快的。加密一次耗費 1 微秒,那破解時隨便猜一個片語,也只需 1 微秒(假設機器效能一樣,片語長度也差不多)。攻擊者一秒鐘就可以猜 100 萬個,而且這還只是單執行緒的速度。

所以,加密演算法越快,破解起來就越容易。

對抗拖庫 —— Web 前端慢加密

(題圖來自: netdna-ssl.com)

0×02 慢加密

如果能提高加密時間,顯然也能增加破解時間。如果加密一次提高到 10 毫秒,那麼攻擊者每秒只能猜 100 個,破解速度就慢了一萬倍。怎樣才能讓加密變慢?最簡單的,就是對加密後的結果再加密,重複多次。

例如,原本 1 微秒的加密,重複一萬次,就慢一萬倍了:

for i = 0 ~ 10000
    x = md5(x)
end

加密時多花一點時間,就可以換取攻擊者大量的破解時間。

事實上,這樣的「慢加密」演算法早已存在,例如 bcrypt、PBKDF2 等等。它們都有一個難度係數因子,可以控制加密時間,想多慢就多慢。

加密越慢,破解時間越長。

0×03 慢加密應用

最需要慢加密的場合,就是網站資料庫裡的密碼。

近幾年,經常能聽到網站被「拖庫」的新聞。使用者資料都是明文儲存,洩露了也無法挽回。唯獨密碼,還可以和攻擊者對抗一下。然而不少網站,使用的都是快速加密演算法,因此輕易就能破解出一堆弱口令賬號。當然,有時只想破解某個特定人物的賬號。只要不是特別複雜的詞彙,跑上幾天,很可能就破出來。

但網站用了慢加密,結果可能就不一樣了。如果把加密時間提高 100 倍,破解時間就得長達數月,變得難以接受。即使資料洩露,也能保障「密碼」這最後一道隱私。

0×04 慢加密缺點

不過,慢加密也有明顯的缺點:消耗大量計算資源。使用慢加密的網站,如果同時來了多個使用者,伺服器 CPU 可能就不夠用了。要是遇到惡意使用者,發起大量的登入請求,甚至造成資源被耗盡。效能和安全總是難以兼得。所以,一般也不會使用太高的強度。

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

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

0×05 前端加密

在過去,個人電腦和伺服器的速度,還是有較大差距的。但如今,隨著硬體發展進入瓶頸,這個差距正縮小。在單線任務處理上,甚至不相上下。客戶端擁有強大的算力,能不能分擔一些伺服器的工作?尤其像「慢加密」這種演算法開源、但計算沉重的任務,為何不交給客戶端來完成?

對抗拖庫 —— Web 前端慢加密

過去,提交的是明文密碼;現在,提交的則是明文密碼的「慢加密結果」。無論是註冊,還是登入。而服務端,無需任何改動。將收到的「慢加密結果」,當做原來的明文密碼 就行。以前是怎麼儲存的,現在還是怎麼儲存。這樣就算被拖庫,攻擊者破解出來的也只是「慢加密結果」,還需再破解一次,才能還原出「明文密碼」。

對抗拖庫 —— Web 前端慢加密

事實上,「慢加密結果」這個中間值,是不可能破解出來的!因為它是一個雜湊值 —— 毫無規律的隨機串,例如 32 位十六進位制字串,而字典都是有意義的片語,幾乎不可能跑到它!除非位元組逐個窮舉。但這有 16^32 種組合,是個天文數字。所以「慢加密結果」是無法通過資料庫裡洩露的密文「逆推」出來的。

或許你在想,即使不知道明文密碼,也可以直接用「慢加密結果」來登入。事實上後端儲存時再次加密,就無法逆推出這個雜湊值了。

當然,不能逆推,但可以順推。把字典裡的片語,用前後端的演算法依次執行一次:

back_fast_hash( front_slow_hash(password) )

然後對比密文,即可判斷有沒有猜中。這樣就可以用跑字典來破解。但是有 front_slow_hash 這個障礙,破解速度就大幅降低了。

0×06 對抗預先計算

不過,前端的一切都是公開的。所以 front_slow_hash 的演算法大家都知道。攻擊者可以用這套演算法,把常用片語的「慢加密結果」提前算出來,製作成一個「新字典」。將來拖庫後,就可以直接跑這個新字典了。

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

front_slow_hash(password + username)

這樣,即使相同的密碼,對於不同的使用者,「慢加密結果」也不一樣了。也許你會說,這個鹽值不合理,因為使用者名稱是公開的。攻擊者可以對某個重要人物的賬號,單獨為他建立一個字典。

那麼,是否可以提供一個隱蔽的鹽值?答案是:不可以。

因為這是在前端。使用者還沒登入,那返回誰的鹽值?登入前就能獲得賬號的鹽值,這不還是公開的嗎。所以,前端加密的鹽值無法隱藏,只能公開。當然,即使公開,單獨提供一個鹽值引數,也比使用者名稱要好。因為使用者名稱永遠不變,而獨立的鹽值可以定期更換。

鹽值可以由前端生成。例如註冊時:

# 前端生成鹽值
salt = rand()
password = front_slow_hash(password + salt)

# 提交時帶上鹽值
submit(..., password, salt)

後端將使用者的鹽值也儲存起來。登入時,輸完使用者名稱,就可以開始查詢使用者對應的鹽值:

對抗拖庫 —— Web 前端慢加密

當然要注意的是,這個介面可以測試使用者是否存在,所以得有一定的控制。

鹽值的更換,也非常簡單,甚至可以自動完成:

對抗拖庫 —— Web 前端慢加密

前端在加密當前密碼時,同時開啟一個新執行緒,計算新鹽值和新密碼。提交時,將它們全都帶上。如果「當前密碼」驗證成功,則用「新密碼」和「新鹽值」覆蓋舊的。這樣更換鹽值,還是隻用到前端的算力。

這一切都是自動的,相當於 在使用者無感知的情況下,定期幫他更換密碼!

密文變了,針對「特定鹽值」製作的字典,也就失效了。攻擊者得重新制作一次。

0×07 強度策略

密碼學上的問題到此結束,下面討論實現上的問題。

現實中,使用者的算力是不均衡的。有人用的是神級配置,也有的是古董機。這樣,加密強度就很難設定。如果古董機使用者登入會卡上幾十秒,那肯定是不行的。對於這種情況,只有以下選擇:

  • 強度固定
  • 強度可變

1.強度固定

根據大眾的配置,制定一個適中的強度,絕大多數使用者都可接受。但如果超過規定時間還沒完成,就把算到一半的 Hash 和步數提交上來,剩餘部分讓伺服器來完成。

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

不過,這需要「可序列化」的演算法,才能在服務端還原進度。如果計算中會有大量的臨時記憶體,這種方案就不可行了。相比過去 100% 後端慢加密,這種少量使用者「前後參半」的方式,可以節省不少伺服器資源。

對於請求協助的使用者,也必須有一定的限制,防止惡意透支伺服器資源。

2.強度可變

如果後端不提供任何協助,那隻能根據自身條件做取捨了。配置差的使用者,就少加密一點。使用者註冊時,加密演算法不限步數放開跑,看看特定時間裡能算到多少步:

# [註冊階段] 算力評估(執行緒 1 秒後中止)
while
    x = hash(x)
    step = step + 1
end

這個步數,就是加密強度,會儲存到他的賬號資訊裡。和鹽值一樣,強度也是公開的。因為在登入時,前端加密需要知道這個強度值。

# [登入階段] 先獲得 step
for i = 0 ~ step
    x = hash(x)
end

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

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

3.動態調整方案

上述情況,現實中是普遍存在的。比如 PC 端註冊的賬號,在移動端登入,算力可能就不夠用。如果沒有後端協助,那隻能等。要是經常在低端裝置上登入,那每次都得乾等嗎?等一兩次就算了,如果每次都等,不如重新估量下自己的能力吧。把加密強度動態調低,更好的適應當前環境。將來如果不用低端裝置了,再自動的調整回來。讓加密強度,能動態適應常用的裝置的算力。

實現原理,和上一節的自動更換鹽值類似。

4.異想天開方案

下面 YY 一個腦洞大開的方案,前提是網站有足夠大的訪問量。如果當前有很多線上使用者,它們不就是一堆免費的計算節點嗎?計算量大的問題,扔給他們來解決。

對抗拖庫 —— Web 前端慢加密

不過這樣做也有一些疑慮,萬一正好推送給了壞人怎麼辦?顯然,不能把太多的敏感資料放出去。節點只管計算,完全不用知道、也不能知道這個任務的最終目的。

但是,如果遇到惡作劇節點,故意把資料算錯怎麼辦?所以不能只推送給一個節點。多選幾個,最終結果一致才算正確。這樣風險概率就降低了。

相比 P2P 計算,網站是有中心、實名的,管理起來會容易一些。對於惡作劇使用者,可以進行懲罰;參與過幫助的使用者,也給予一定獎勵。

想象就到此,繼續討論實際的。

0×08 效能優化

1.為什麼要優化

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

假如這是一個自創的隱蔽式演算法,並且混淆到外人根本無法讀懂,那不優化也沒事。甚至可以在裡面放一些空迴圈,故意消耗時間。但事實上,我們選擇的肯定是「密碼學家推薦」的公開演算法。它們每一個操作,都是有數學上的意義的。原本一個操作只需一條 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 快不少。

Alchemy 現在改名 FlasCC,還有開源版的 crossbridge

5.沙箱

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

vector[k] = v

虛擬機器首先得檢查索引是否越界,否則會有嚴重的問題。如果「前端慢加密」演算法涉及到大量記憶體隨機訪問,那就會有很多無意義的內耗,因此得慎重考慮。不過有些特殊場合,指令碼速度甚至能超過本地程式!例如開頭提到的 MD5 大量反覆計算。

這其實不難解釋:

首先,MD5 演算法很簡單。沒有查表這樣的記憶體操作,使用的都是區域性變數。區域性變數的位置都是固定的,避免了越界檢查開銷。其次,emscripten 的優化能力,並不比本地編譯器差。最後,本地程式編譯之後,機器指令就不會再變了;而如今指令碼引擎,都有 JIT 這個利器。它會根據執行時的情況,實時生成更優化的機器指令。

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

0×09 對抗 GPU

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

那能否在演算法裡新增一些特徵,正好命中 GPU 的軟肋呢?

1.視訊記憶體瓶頸

大家聽過說「萊特幣」吧。不同於比特幣,萊特幣挖礦使用了 scrypt 演算法。這種演算法對記憶體依賴非常大,需要頻繁讀寫一個表。GPU 雖然每個執行緒都能獨立計算,但視訊記憶體只有一個,大家共享使用。這意味著,同時只有一個執行緒能操作視訊記憶體,其他有需要的只能等待了。這樣,就極大遏制了併發的優勢。

2.移植難度

山寨幣遍地開花的時候,還出現了一個叫 X11Coin 的幣,據稱能對抗 ASIC。它的原理很簡單,裡面摻雜了 11 種不同的加密演算法。這樣,製造出相應的 ASIC 複雜度大幅增加了。儘管這不是一個長久的對抗方案,但思路還是可以借鑑的。如果一件事過於複雜,很多攻擊者就望而生畏了,不如去做更容易到手的事。

3.其他想法

之所以 GPU 能大行其道,是因為目前的加密演算法,都是簡單的公式運算。這對 CPU 並沒太大的優勢。能否設計一個演算法,充分依賴 CPU 的優勢?CPU 有很多隱藏的強項,例如流水線。如果演算法中有大量的條件分支,也許 GPU 就不擅長了。

當然,這裡只是設想。自己創造加密演算法,是非常困難的,也不推薦這麼做。

0x0A 額外意義

除了能降低密碼破解速度,前端慢加密還有一些其他意義:

1.減少洩露風險

使用者輸入的明文密碼,在前端記憶體裡就已加密。離開瀏覽器,洩露風險就已結束。即使通訊被竊聽,或是伺服器上的惡意中介軟體,都無法拿到明文密碼。除非網頁本身有惡意程式碼,或是使用者系統存在惡意軟體。

2.無法私藏明文

儘管大部分網站都聲稱,不會儲存使用者的明文密碼。但這並沒有證據,也許私下裡仍在悄悄儲存。如果在前端加密,網站就無法拿到使用者的明文密碼了。也許正是這一點,很多網站不願意使用前端加密。

事實上,其實網站不願意也沒關係,我們可以自己做一個單機版的慢加密外掛。當選中網頁密碼框時,彈出我們外掛。在外掛裡輸入密碼後,開始慢加密計算。最後將結果填入頁面密碼框裡。這樣,所有的網站都可以使用了。當然,已註冊的賬號是不行的,得手動調整一下。

3.增加撞庫成本

「前端慢加密」需要消耗使用者的計算力,這個缺點有時也是件好事。

對於正常使用者來說,登入時多等一秒影響並不大。但對於頻繁登入的使用者來說,這就是一個障礙了。誰會頻繁登入?也許就是撞庫攻擊者。他們無法拖下這個網站的資料庫,於是就用線上登入的方式,不斷的測試弱口令賬號。如果通過 IP 來控制頻率,攻擊者可以找大量的代理 —— 網速有多快,就能試多快。

但使用了前端慢加密,攻擊者每試一個密碼,就得消耗大量的計算,於是將瓶頸卡在硬體上 —— 能算多快,才能試多快。所以,這裡有點類似 PoW(Proof-of-Work,工作量證明)的意義。關於 PoW,以後我們會詳細介紹。

0x0B 無法做到的

儘管「前端慢加密」有不少優勢,但也不是萬能的。上一節也提到,能減少風險,而不是消除風險。如果本地環境有問題,那任何密碼輸入都有風險。

下面我們來思考一個場景:某網站使用了「前端慢加密」,但沒有使用 HTTPS —— 這會導致鏈路被竊聽。回顧 0×05 小節,如果拿到「慢加密結果」,就可以直接登上賬號,即使不知道明文密碼。的確如此。但請仔細想一想,這不也降低損失了嗎?

本來不僅賬號被盜用,而且明文密碼也會洩露;而如今,只是賬號被盜用,明文密碼對方仍無法獲得。所以,前端慢加密的真正保護的是「密碼」而不是「賬號」。賬號被盜,密碼拿不到!

如果攻擊者不僅能竊聽,還能控制流量的話,就可以往頁面注入攻擊指令碼,從而獲得明文密碼。當然,這和電腦中毒、鍵盤偷窺一樣,都屬於「環境有問題」,不在本文討論範圍內。本文討論的是資料庫洩露的場景。

0x0C 多執行緒慢加密

使用者的配置越來越好,不少都是四核、八核處理器。能否利用多執行緒的優勢,將慢加密計算進行分解?如果每一步計算都依賴之前的結果,是無法進行拆解的。例如:

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

這是一個序列的計算。然而只有並行的問題,才能分解成多個小任務。不過,換一種方式的多執行緒也是可以的。例如我們使用 4 個執行緒:

# 執行緒 1
x1 = hash(password + "salt1")
for i = 0 ~ 2500
    x1 = hash(x1)
end

# 執行緒 2
x2 = hash(password + "salt2")
for i = 0 ~ 2500
    x2 = hash(x2)
end

# ...

最終將 4 個結果合併起來,再做一次加密,作為慢加密結果。但這樣會導致更容易破解嗎?留著給大家思考。

0x0D 總結

前端慢加密,就是讓每個使用者貢獻少量的計算資源,使加密變得更強勁。即使資料洩露,其中也凝聚了全網站使用者的算力,從而大幅增加破解成本。

0xFF 後記

前些年比特幣流行時,突發奇想用瀏覽器來挖礦。雖然沒做成,不過獲得了一些密碼學姿勢。近期重新進行了整理,並新增了一些新想法,於是寫篇詳細的文章分享一下。因為密碼學屬於傳統領域,所以結合當下流行的 Web 技術,才能更有新意。

如果你對演算法有疑惑,可以先仔細看 0×05 這節。

如果你是耐心看完本文的,希望能有收穫:  )

相關文章