【探索】無形驗證碼 —— PoW 算力驗證


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

消耗時間還不簡單,休眠一下就可以了:

Sleep(1000)

這確實消耗了時間,但並沒有消耗 CPU。如果對方開了變速齒輪,這瞬間就能完成。

不過要消耗 CPU 也不難,寫一個大循環就可以了:

for i = 0 to 1000000000
end

但這和 Sleep 並無本質區別。對方究竟有沒有運行,我們從何得知?

所以,我們需要一個返回結果 —— 只有完整運行才有正確答案。

result = 0
for i = 0 to 1000000000
	result = result + i
end
return result

通過返回結果,我們就能校驗,對方是否完整運行了我們的程序。


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

有什么函數,是無法用公式推測的?也就是說,必須老老實實運行函數,才能得出結果。而找不到一種更快的算法,也能算出相同的結果。

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

MD5(X) == X

就無法用公式來解決了。要找出答案,只能一個個試過去,需要大量的時間。

但對於驗證者,是非常輕松的 —— 只需將收到的答案,計算一次就可判斷對錯。

Hashcash

當然,上面的那個例子太困難了,一時間根本找不到答案。但可以做一些改進,例如只要求結果前幾位是 0 就可以:

Hash(X) == "0000......"

這樣 0 的數量越少,滿足條件的 X 就越容易找到。

同時,為了防止答案重復使用,還可以再增加一個鹽值:

Hash(X, Salt) == "0000......"

鹽可以由驗證者提供,這樣就可以充當一個「問題」。符合條件的 X,則可以當做問題的「答案」,提交給驗證者鑒定。


這就是所謂的 PoW(Proof-of-Work),一種鑒定對方是否投入計算工作的機制。並且只需花費少量的資源,即可鑒定大量的工作。

使用散列函數實現的 PoW,就叫做 Hashcash。現實中比特幣使用了類似的原理,它使用了 SHA-256 作為散列函數。有權威的密碼學算法作為保障,因此只能暴力窮舉,而無法使用投機取巧的方法獲得結果,保障了挖礦工作的價值。

傳統應用

當然,Hashcash 早不是新鮮事,很久以前就用在反垃圾郵件中。

例如,用戶寫完郵件時,客戶端將「收件地址 + 郵件內容」作為 Salt,然后計算符合條件的答案:

Hash(X, Salt) == "000000..."

最后將找到的 X 附加在郵件中並發送。

服務端收到后,即可鑒定發送這封郵件,是否花費了計算工作。

對於正常用戶來說,額外的幾秒計算並不影響使用;但對於制造垃圾郵件的人,就大幅增加了成本。

傳統的限制策略,大多通過 IP、賬號,攻擊者可以用大量的馬甲和代理,來繞過這些限制;而使用了 PoW,就把瓶頸限制在硬件上 —— 計算有多快,操作才能多快。

Web 應用

同樣,Hashcash 也能用於 Web。例如論壇,增加機器發帖的計算成本。

在發帖時,計算:

Hash(X, 帖子內容) == "000000..."

提交時,帶上額外的參數 X。

后端即可判斷,用戶發這條帖子,是否付出了計算工作。

Web 改進

然而,網頁不同於客戶端。

例如寫郵件的例子:

郵件寫完后,客戶端可以隱藏在后台自動計算,然后再發送。但網頁就大不相同了。如果發帖時,網頁卡上好幾秒,將大幅降低用戶體驗。

因此,不能像郵件那樣,拿「帖子內容」、「標題」等這些用戶輸入的內容來計算。而是選擇一個始終固定的參數。


例如,可以參考傳統驗證碼的方式:

# 后端 - 生成隨機問題
ques = rand()
session["pow_ques"] = ques

echo(ques)

在頁面初始化時,后端生成一個隨機數,並下發到前端。

前端使用這個隨機數作為鹽值 —— 這樣頁面打開時,就可以開始計算了。

# 前端 - 挖礦
while Hash(X, ques) != "000000..."
	X = X + 1
end

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

正常情況下,用戶發帖前已完成計算。提交時,將答案 X 帶上。

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

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

# 后端 - 校驗
if hash(X, session["pow_ques"]) == "000000..."
	ok
else
	fail
end

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

目前也有不少 hashcash 的實際應用。例如 WordPress 的 hashcash 插件。甚至還有第三方的驗證服務 hashcash.io

Web 性能

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

好在如今有 asm.js,可接近原生性能,讓用戶的勞動不至於貶值;對於較老的瀏覽器,也可以使用 Flash 作后補。在上一篇文章 0x08 節 中已詳細講解。

如果算力實在不夠,也可以使用后備方案 —— 傳統圖形驗證碼。

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

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

Hashcash 缺陷

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

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

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

  • 硬件瓶頸

  • 移植難度

  • CPU 算法

  • 以暴制暴

  • 自創加密

  • 串行模式

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

以暴制暴

如果我們也能在 Web 中調用顯卡計算,那 GPU 版的外掛就毫無優勢了。

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

當然,也可以通過一些 hack 的方式,例如曾有人嘗試用 WebGL 挖比特幣,但效率並不高。

如果未來 WebCL 成為標准,或許還能考慮。

自創加密

不要自創加密算法 —— 這似乎是一條真理。

在賬號安全里,這固然正確。但在對抗的場合下,就未必如此了。

經典的加密算法固然權威,但研究的人也多,破解的工具也多。

自創的算法,雖然在密碼學上很弱,但可以把邏輯實現的很長很復雜,並且加以混淆,就可以在「隱蔽性」上占優勢了。

這樣,要將其移植到 GPU 上,就困難了。

同時還可以制作模板,定期變幻代碼。在攻擊者破解之前,邏輯又變化了。

在對抗中,隨機應變的弱算法,顯然比一成不變的強算法更好。

串行模式

Hashcash 的原理,決定了它是可以並行計算的。有什么樣的算法,是無法並行計算的?

如果每次計算都依賴上次結果,就無法並行了。例如 slowhash:

function slowhash(x)
	for i = 0 to 1000000000
		x = hash(x)
	end
	return x
end

這種串行的計算,自然是無法拆分的。

但這能用到 PoW 上嗎?顯然不行!

因為 PoW 雖然計算困難,但得 容易鑒定。而這種方式,鑒定時也得重復算一遍,成本太大了。


但發揮一下想象:如果能夠設計得當,這還是可以嘗試的 —— 我們可以使用 UGC 的模式,讓用戶來貢獻算力!

首先,需要一個訪問量較大的網站,在其中悄悄放置一個腳本:

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

submit(Q, A)

我們利用在線的用戶,來生成問題和答案!!

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

當后端題庫有一定的積累時,就可以使用驗證碼的模式了。

用戶訪問時,后端從題庫中抽取一個問題,安排給前端計算:

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

# 前端 - 計算問題
A = slowhash(Q)

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

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

# 后端 - 鑒定
Q = session["pow_ques"]
if A == db[Q]
	ok
else
	fail
end

使用預先計算的方式,避免了耗時的鑒定工作。同時,把讓用戶來出題,可大幅節省硬件成本。


回顧之前的 hashcash,因為散列結果是不確定的,題解時間有一定的隨機性。

但 slowhash 這種方式,只是重復執行散列函數 N 次,所以解題的時間更固定。

演示

出於簡單,這里演示一個 hashcash-md5 版的:

https://github.com/EtherDream/proof-of-work-hashcash

如果想看算力速度,可以查看這里

看起來好像不慢,不過對比 GPU 的速度 就相形見絀了。

所以 hashcash 如果使用經典算法,簡直就是不堪一擊的。或許自己寫的「隱蔽式加密」算法,反而更能對抗一些。

至於 slowhash 那種串行模式的 PoW,涉及到較多策略和數據積累,本文就不演示了,下回再討論。


(2017/03/01 補充)現在 WebGL2 出來了,着色器支持整數以及位運算,因此非常適合 PoW 計算:

Demo:https://www.etherdream.com/FunnyScript/glminer/glminer.html

用我的筆記本核顯,每秒都能計算 2700 萬次 SHA256 算法。換成好點的顯卡例如泰坦,每秒可以 5 億以上的 hash 計算~ (注意 SHA256 比 MD5 慢多了)


總結

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

驗證方式 驗證對象 用戶體驗 攔截假人 實際意義
傳統驗證 圖像識別 人腦 有交互 部分攔截 杜絕假人
算力驗證 問題解答 電腦 無感知 無法攔截 減少濫用

論效果,當然還是傳統驗證碼更好;論體驗,計算對於用戶是透明的,無需任何交互。

簡單來說,傳統驗證碼是防止機器「不能使用」;而 PoW 防止的則是「不能濫用」。


當然上述提到發帖那個案例,並不恰當。即使用上 PoW,限制每 5 秒發一帖,那么一分鍾仍有 12 貼 —— 這速度顯然還是有點太快。更適合 PoW 的場合,應該類似找回密碼、或者接收短信等功能,比較容易在短時間內被大量濫用,用於增加攻擊者刷接口的成本。


免責聲明!

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



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