算力驗證碼的嘗試

EtherDream · 2015/12/29 18:27html

0x00 前言


驗證碼的初衷是人機識別。不過大多時候,只是用來增長一些時間成本,下降頻率而已。前端

若是僅僅是爲了消耗時間,可否不用圖片,徹底用程序來實現?git

0x01 如何消耗時間


先來思考一個問題:寫一個能消耗對方時間的程序。github

消耗時間還不簡單,休眠一下就能夠了:算法

#!cpp
Sleep(1000)
複製代碼

這確實消耗了時間,但並無消耗 CPU。若是開了變速齒輪,瞬間就能完成。後端

要消耗 CPU 也不難,寫一個大循環就能夠了:瀏覽器

#!cpp
for i = 0 to 1000000000
end
複製代碼

不過這和 Sleep 並沒有本質區別。對方究竟有沒有運行,咱們從何得知?安全

因此,咱們須要一個返回結果 —— 只有完整運行纔有正確答案。性能優化

#!cpp
result = 0
for i = 0 to 1000000000
    result = result + i
end
return result
複製代碼

經過返回的結果,咱們就能判斷對方是否完整的運行了程序。session

不過上面這個問題,畢竟仍是 too simple。小學生都知道,用數列公式就能夠直接算出結果,根本不用花時間去跑循環。

那麼有什麼算法,是沒法用公式推測的?

顯然,單向散列函數就是。例如一個經典的問題:

#!cpp
MD5(X) == X
複製代碼

就沒法用公式來解決了。要找出答案,只能一個個窮舉過去,從而花費大量時間。

但對於驗證者,只需將收到的答案,計算一次就可判斷對錯,所以可輕易校驗。

這就是 PoW(Proof-of-Work),用來證實對方投入工做的方法。

固然,上面的例子太困難了,並且答案能夠重複使用,因此還需改進。例如:

#!cpp
MD5("問題" + X) == "0000......"
複製代碼

咱們只要求散列結果的前幾位是 0 就能夠。這樣位數越小,答案就越容易找到。同時增長一個鹽值,讓答案不能重複使用。

事實上,比特幣就用到了相似的方式,使用了 SHA-256 做爲散列函數。這樣只能窮舉,沒法用更快的方法投機取巧,體現了挖礦工做的價值。

用散列函數實現的 PoW,就叫 Hashcash

0x02 傳統應用


Hashcash 早不是新鮮事,曾經在反垃圾郵件中就已使用。

例如用戶寫完郵件時,客戶端將「收件地址 + 郵件內容」做爲 Salt,而後計算符合條件的答案:

#!cpp
Hash(X, Salt) == "000000..."
複製代碼

最後將找到的 X 附加在郵件中併發送。服務端收到後,便可鑑定發送這封郵件,是否花費了計算工做。

對於正經常使用戶來講,額外的幾秒並不影響使用;但對於製造垃圾郵件的人,就大幅增長了成本。

傳統策略,大多經過 IP、帳號等限制。攻擊者能夠用大量的馬甲和代理,來繞過這些限制。

而使用了 PoW,就把瓶頸限制在硬件上 —— 計算有多快,操做才能多快。

0x03 Web 應用


一樣的,Hashcash 也能用於 Web。例如論壇,可在發帖時計算:

#!cpp
Hash(X, 帖子內容) == "000000..."
複製代碼

不過,不一樣於郵件客戶端可在後臺自動計算,發帖時若是卡上好幾秒,將會大幅下降用戶體驗。

所以不能選擇 帖子內容、標題 等這些用戶輸入做爲鹽值。而是用傳統驗證碼的方式,後端下發一個隨機數。

前端使用這個隨機數做爲鹽值 —— 這樣頁面打開時,就能夠開始計算了。

#!cpp
# 後端 - 分配
session["pow_code"] = rand()

# 前端 - 挖礦
while Hash(X, pow_code) == "000000..."
    X = X + 1
end
複製代碼

咱們選擇一個適中的難度,例如 10 秒。經過多線程,還能夠更快的完成計算任務,同時不影響用戶體驗。

正常狀況下,用戶發帖前就已計算完成。提交時,將其附帶上。

若是提交時還未算出,則等待計算完成。(發帖太快,有灌水嫌疑)

#!cpp
# 前端 - 提交
wait X
submit(..., X)

# 後端 - 校驗
if Hash(X, session["pow_code"]) == "000000..."
    ok
else
    fail
end
複製代碼

這樣,就實現了一個「測試機器算力」的驗證碼。

目前已有提供 hashcash 第三方驗證的網站,例如 hashcash.io

0x04 Web 性能


固然在 Web 中使用,性能也是一大問題。若是 10 秒的腳本計算,用本地程序只需 1 秒,那攻擊者就可使用本地版的外掛了。

好在現在有 asm.js,可接近原生性能;對於較老的瀏覽器,也可使用 Flash 做後補。在上一篇文章 0x08 節 中已詳細講解。

若是算力實在不夠,也可使用後備方案 —— 傳統圖形驗證碼。

這樣,高性能用戶可享受更好的體驗,低性能用戶也能保障基本功能。

這也算是鼓勵你們使用現代瀏覽器吧:)

0x05 致命缺陷


不過,語言上的性能差距仍是有限的,外掛不會糾結於此,而是使用更強力的武器 —— GPU。

Hashcash 的本質就是跑 hash,這是 GPU 最擅長的。例如著名的 oclHashcat,和 CPU 徹底不在一個數量級。

對抗硬件的並行計算,大體有以下方案和思路:

  • 硬件瓶頸
  • 移植難度
  • CPU 算法
  • 以暴制暴
  • 代碼混淆
  • 串行模式

前 3 個在上一篇文章 0x09 節 提到了,下面討論一些不一樣的。

0x06 以暴制暴


若是咱們也能在 Web 中調用顯卡計算,那 GPU 版的外掛就毫無優點了。

不過,這個想法彷佛有些遙遠。儘管目前主流瀏覽器都支持 WebGL,但都只侷限於渲染加速上,並未提供通用計算接口。

固然,也能夠經過一些 hack 的方式,例如曾有人嘗試用 WebGL 挖比特幣,但效率並不高。

若是將來 WebCL 成爲標準,或許還能考慮。

0x07 代碼混淆


上回討論慢加密時,曾提到爲何要性能優化。由於本身創造加密算法是不推薦的,因此得優化現有的算法。

不過,相比帳號安全,驗證碼的要求則低得多,並且隨時能夠更換算法,所以不妨本身來創造一個。

自創的加密算法,強度顯然沒有保障。但咱們能夠從「隱蔽性」上着手 —— 將代碼混淆到難以讀懂,這時,考驗對方的則是逆向能力了。

這和以前寫的《對抗假人 —— 先後端結合的 WAF》有點相似。不過,若是混淆能作到足夠好,還須要 PoW 機制嗎?

有勝於無。由於瀏覽器指紋、用戶行爲等信息,都是能夠經過沙盒模擬的。而工做量計算,必須消耗硬件資源,才能得出結果。

所以,使用了 PoW 就能增長攻擊者一些硬件成本。

0x08 串行模式


Hashcash 的原理,決定了它是能夠並行計算的。有什麼樣的算法,是沒法並行計算的?

若是每次計算都依賴上次結果,就沒法並行了。例如以前討論的 slowhash:

#!cpp
function slowhash(x)
    for i = 0 to 1000000000
        x = hash(x)
    end
    return x
end
複製代碼

這種串行的計算,天然是沒法拆分的。但能用到 PoW 上嗎?

顯然不行!由於 PoW 雖然計算困難,但得 容易鑑定。而這種方式,鑑定時也得重複算一遍,成本太大了。

但在現實中,只要設計得當,仍是能夠嘗試的 —— 咱們使用相似 UGC 的模式,讓用戶來貢獻算力!

首先須要一個訪問量較大的網站,在其中悄悄放置一個腳本。利用在線的用戶,來生成問題和答案。

#!cpp
# 隱蔽的腳本
Q = rand()
A = slowhash(Q)

submit(Q, A)
複製代碼

固然,這項工做必須足夠隱蔽,防止被好奇的用戶發現,提交錯誤的答案。

當後端題庫有必定的積累時,就可使用驗證碼的模式了。用戶訪問時,後端從題庫中隨機抽取一個問題,安排給前端計算:

#!cpp
# 後端 - 分配問題
Q = select_key_from_db()
session["pow_ques"] = Q

# 前端 - 計算問題
A = slowhash(Q)
複製代碼

用戶提交時,後端無需任何計算,直接經過查表,便可判斷答案是否正確:

#!cpp
# 前端 - 提交
submit(..., A)

# 後端 - 鑑定
Q = session["pow_ques"]
if A == db[Q]
    ok
else
    fail
end
複製代碼

使用預先計算的方式,避免了繁重的鑑定工做。同時,把計算交給用戶來完成,可大幅節省硬件成本。

固然,這種模式還有不少須要考慮的地方,這裏只是介紹下基本思路,之後再詳細討論。

相比 hashcash 題解時間有必定的隨機性,slowhash 的時間是固定的,所以難度更可控。

0x09 演示


由於 Hashcash 比較簡單,因此這裏演示一個 md5 版的,使用 asm.js 和 flash 實現,並對算法作了必定優化。

github.com/EtherDream/…

若是想看詳細的算力速度,能夠查看這個 Demo:

www.etherdream.com/FunnyScript…

看起來好像不慢,不過對比 GPU 的速度 就相形見絀了。因此,使用經典算法的 Hashcash,簡直就是不堪一擊的。

至於串行模式的 PoW,涉及到不少策略和數據積累,本文就演示了,下回單獨討論。

0x0A 總結


最後來對比下,算力驗證和傳統圖形驗證的區別。

驗證方式 驗證對象 用戶體驗 攔截假人
傳統驗證 圖像識別 人腦 須要交互 部分攔截
算力驗證 問題解答 電腦 無感知 沒法攔截

論效果,固然仍是傳統的圖形驗證更好,但這是以犧牲用戶體驗爲代價的。

硬件在不斷的發展,識圖軟件會愈來愈強大。而人腦始終是有限的,優點會愈來愈小,最終致使驗證碼愈來愈複雜。

可是算力驗證則不一樣。硬件的發展,也會帶動瀏覽器的算力提高,最終只需將問題難度調高便可。

固然,安全防護涉及的領域越多越好。每個方案都不是無敵的,都只是爲了增長一些攻擊成本而已。

因此算力驗證,結合傳統防護方案,才能出發揮價值。

相關文章
相關標籤/搜索