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

Linux中國EtherDream2018-04-07 21:24:05

0x00 前言

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

0x01 暴力破解

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

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

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

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

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

0x02 慢加密

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

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

  1. for i = 0 ~ 10000

  2. x = md5(x)

  3. end

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

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

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

0x03 慢加密應用

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

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

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

0x04 慢加密缺點

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

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

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

0x05 前端加密

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

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

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

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

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

  1. back_fast_hash( front_slow_hash(password) )

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

0x06 對抗預先計算

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

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

  1. front_slow_hash(password + username)

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

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

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


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

  1. # 前端生成鹽值

  2. salt = rand()

  3. password = front_slow_hash(password + salt)

  4. # 提交時帶上鹽值

  5. submit(..., password, salt)

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

當然要注意的是,這個接口可以測試用戶是否存在,所以得有一定的控制。

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

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

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

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

0x07 強度策略

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

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

  • 強度固定

  • 強度可變

1.強度固定

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

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

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

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

2.強度可變

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

  1. # [註冊階段] 算力評估(線程 1 秒後中止)

  2. while

  3. x = hash(x)

  4. step = step + 1

  5. end

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

  1. # [登錄階段] 先獲得 step

  2. for i = 0 ~ step

  3. x = hash(x)

  4. end

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

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

3.動態調整方案

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

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

4.異想天開方案

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

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

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

相比 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.沙箱

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

  1. 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 無法做到的

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

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

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

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

0x0C 多線程慢加密

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

  1. for i = 0 ~ 10000

  2. x = hash(x)

  3. end

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

  1. # 線程 1

  2. x1 = hash(password + "salt1")

  3. for i = 0 ~ 2500

  4. x1 = hash(x1)

  5. end

  6. # 線程 2

  7. x2 = hash(password + "salt2")

  8. for i = 0 ~ 2500

  9. x2 = hash(x2)

  10. end

  11. # ...

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

0x0D 總結

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


0xFF 後記

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

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

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

來源:博客園 原文:http://www.cnblogs.com/index-html/p/4999897.html作者: EtherDream


閱讀原文

TAGS:明文密碼加密慢加密鹽值