算力驗證碼的嘗試

wyzsk發表於2020-08-19
作者: EtherDream · 2015/12/29 18:27

0x00 前言


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

如果僅僅是為了消耗時間,能否不用圖片,完全用程式來實現?

0x01 如何消耗時間


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

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

#!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

透過返回的結果,我們就能判斷對方是否完整的執行了程式。

不過上面這個問題,畢竟還是 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 實現,並對演算法做了一定最佳化。

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

如果想看詳細的算力速度,可以檢視這個 Demo:

http://www.etherdream.com/FunnyScript/hashcash/js/test.html

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

至於序列模式的 PoW,涉及到很多策略和資料積累,本文就演示了,下回單獨討論。

0x0A 總結


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

驗證方式 驗證物件 使用者體驗 攔截假人
傳統驗證 影像識別 人腦 需要互動 部分攔截
算力驗證 問題解答 電腦 無感知 無法攔截

論效果,當然還是傳統的圖形驗證更好,但這是以犧牲使用者體驗為代價的。

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

但是算力驗證則不同。硬體的發展,也會帶動瀏覽器的算力提升,最終只需將問題難度調高即可。

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

所以算力驗證,結合傳統防禦方案,才能出發揮價值。

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

相關文章