Web前端慢加密

wyzsk發表於2020-08-19
作者: EtherDream · 2015/11/27 10:15

0x00 前言


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

0x01 暴力破解


密碼破解,就是把加密後的密碼還原成明文密碼。似乎有不少方法,但最終都得走一條路:暴力窮舉。

也許你會說還可以查表,瞬間就出結果。雖然查表不用窮舉,但表的製造過程仍然需要。查表只是將窮舉提前了而已。

只要演算法不可逆,那隻能窮舉。

窮舉的原理很簡單。只要知道密文是用什麼演算法加密的,我們也用相同的演算法,把常用的片語跑一遍。若有結果和密文一樣,那就猜中了。

窮舉的速度有多快?這和加密演算法有關。加密一次有多快,猜一次也這麼快。

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

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

0x02 慢加密


如果能提高加密時間,顯然也能增加破解時間。

如果加密一次提高到 10 毫秒,那麼攻擊者每秒只能猜 100 個,破解速度就慢了一萬倍。

怎樣才能讓加密變慢?最簡單的,就是對加密後的結果再加密,重複多次。

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

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

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

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

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

0x03 慢加密應用


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

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

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

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

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

即使資料洩露,也能保障「密碼」這最後一道隱私。

0x04 慢加密的缺點


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

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

所以,效能和安全總是難以兼得。

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

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

0x05 前端加密

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

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

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

p1

過去,提交的是明文密碼;現在,提交的則是明文密碼的慢加密結果。無論是註冊,還是登陸。

而服務端,無需任何改動。將收到的慢加密結果當做原來的明文密碼就行。以前是怎麼儲存的,現在還是怎麼儲存。

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

p2

事實上,慢加密結果這個中間值,是不可能破解出來的!

因為它是一個雜湊值 ———— 毫無規律的隨機串,例如 32 位十六進位制字串,而字典都是有意義的片語,幾乎不可能跑到它!

除非位元組逐個窮舉。但這有 16^32 種組合,是個天文數字。

所以「慢加密結果」是無法透過資料庫裡洩露的密文「逆推」出來的。

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

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

back_fast_hash( front_slow_hash(password) )

然後對比密文,即可判斷有沒有猜中。這樣就可以用跑字典來破解。

但是有 front_slow_hash 這個障礙,破解速度就大幅降低了。

0x06 對抗預先計算


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

攻擊者可以用這套演算法,把常用片語的「慢加密結果」提前算出來,製作成一個「新字典」。將來拖庫後,就可以直接跑這個新字典了。

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

front_slow_hash(password + username)

這樣,即使相同的密碼,對於不同的使用者,「慢加密結果」也不一樣了。

也許你會說,這個鹽值不合理,因為使用者名稱是公開的。攻擊者可以對某個重要人物的賬號,單獨為他建立一個字典。

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

因為這是在前端。使用者還沒登入,那返回誰的鹽值?登陸前就能獲得賬號的鹽值,這不還是公開的嗎。

所以,前端加密的鹽值無法隱藏,只能公開。

當然,即使公開,單獨提供一個鹽值引數,也比使用者名稱要好。因為使用者名稱永遠不變,而獨立的鹽值可以定期更換。


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

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

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

後端將使用者的鹽值也儲存起來。

登入時,輸完使用者名稱,就可以開始查詢使用者對應的鹽值:

p3

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

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

p4

前端在加密當前密碼時,同時開啟一個新執行緒,計算新鹽值和新密碼。提交時,將它們全都帶上。

如果「當前密碼」驗證成功,則用「新密碼」和「新鹽值」覆蓋舊的。

這樣更換鹽值,還是隻用到前端的算力。

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

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

0x07 強度策略


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

現實中,使用者的算力是不均衡的。有人用的是神級配置,也有的是古董機。這樣,加密強度就很難設定。

如果古董機使用者登入會卡上幾十秒,那肯定是不行的。對於這種情況,只有以下選擇:

  • 強度固定
  • 強度可變

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 一個腦洞大開的方案,前提是網站有足夠大的訪問量。

如果當前有很多線上使用者,它們不就是一堆免費的計算節點嗎?計算量大的問題,扔給他們來解決。

p5

不過這樣做也有一些疑慮,萬一正好推送給了壞人怎麼辦?

顯然,不能把太多的敏感資料放出去。節點只管計算,完全不用知道、也不能知道這個任務的最終目的。

但是,如果遇到惡作劇節點,故意把資料算錯怎麼辦?

所以不能只推送給一個節點。多選幾個,最終結果一致才算正確。這樣風險機率就降低了。

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

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

0x08 效能最佳化


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 這個利器。它會根據執行時的情況,實時生成更最佳化的機器指令。

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

0x09 對抗 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 慢加密能否平行計算?


使用者的配置越來越好,不少都是四核、八核處理器。能否利用多執行緒的優勢,將慢加密計算進行分解?

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

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

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

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

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

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

# ...

最終將 4 個結果合併起來,再做一次加密,作為慢加密結果。

但這樣會導致強度變低嗎?留著給大家思考。

0x0C 總結


前端慢加密,就是讓每個使用者貢獻少量的計算資源,使加密變得更強勁。

即使資料洩露,其中也凝聚了全網站使用者的算力,從而大幅增加破解成本。

0xFF 後記


前些年比特幣流行時,突發奇想用瀏覽器來挖礦。雖然沒做成,不過獲得了一些密碼學姿勢。

近期重新進行了整理,並新增了一些新想法,於是寫篇詳細的文章分享一下。

因為密碼學屬於傳統領域,所以結合當下流行 Web 技術,才能更有新意。

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

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

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章