歡迎大家前往騰訊雲+社區,獲取更多騰訊海量技術實踐干貨哦~
本文分享了騰訊防水牆團隊關於機器對抗的動態化思路,希望能拋磚引玉,給現在正在做人機對抗的團隊一些啟發,幫助更多中小型公司的業務擺脫機器和爬蟲之痛。
0x00 前言
瀏覽器作為當今互聯網的一大流量入口,正在變得越來越強大。為了有更好的Web體驗,各類新的標准被制定並實施。PWA的出現,更是把移動端H5的體驗推向了另一個極致。越來越多業務使用H5作為主要入口的同時,也帶來了另一個問題:機器行為泛濫。只要有利益的地方就會有惡意,登錄注冊、投票領券等頁面很容易成為機器刷量的重災區,如今寫一個普通刷投票腳本的難度基本就跟寫一個“Hello World!”的難度差不多。在與機器對抗的歷程中,Web前端一直是非常薄弱的一環。瀏覽器毫無保留地把所有前端代碼拉取到本地並執行、所有前端代碼均透明可見,拿什么拯救前端代碼安全?
0x01 名詞解釋
代碼安全
本文中所提及的代碼安全,是指前端JavaScript代碼的安全。通常,如果一段JavaScript代碼只能在正常的瀏覽器中運行,無法或尚未在非正常瀏覽器的運行環境執行得到結果、無法被等價翻譯成其他編程語言的代碼,則認為這段代碼是安全的。
一段重要的JavaScript邏輯被置於其他環境以高於正常瀏覽器幾個數量級的效率運行並得到正確的結果,對於服務端及后面的業務來說,幾乎是一個災難。
數據保護
本文說的數據保護是指對HTTP/HTTPS協議上承載內容(如POST的body)的保護。HTTP協議是一個文本的協議,所有傳輸的內容從客戶端(即瀏覽器)的角度看都是可見且富有語義的,這意味着內容如果不加以保護,惡意用戶只需要理解內容中的各項參數,即可模擬相應的請求而無需閱讀或逆向前端JavaScript代碼的邏輯。注意,這里說的保護不是指TLS等傳輸過程的保護,而是指HTTP協議上層承載的具體數據內容的保護。
比如,正常一個查詢請求的URL形如https://example.com/query?from=shenzhen&destination=beijing,爬蟲開發者無需閱讀JavaScript便可知道參數要如何構造。而如果請求形如https://example.com/query?params=ZnJvbT1zaGVuemhlbiZkZXN0aW5hdGlvbj1iZWlqaW5n,惡意用戶在無法通過觀察立即知道參數構造方法的前提下,只能閱讀或需要先逆向JavaScript代碼,才能知道構造參數方法。這樣就達到了對數據進行保護的目的。
混淆
通過一些字符串替換規則或者抽象語法樹變換規則,將一段代碼等價替換成另一段可讀性很差的代碼,從而達到保護原有代碼安全。這個過程通常是不可逆的。
如:
function foo() { console.log('hello world!');
}
foo();
被變換成:
var a = 'console', b = 'log', c = 'hello', d = ' world!';function e() { window[a][b](c + d);
}
e();
此時代碼的可讀性被降低了,這是一種很簡單的混淆方式。
一些能被搜索引擎搜索到的文章會將代碼壓縮與混淆混為一談,類似Uglify的工具能把代碼壓縮成可讀性很低的代碼,如下圖:

但被瀏覽器強大的格式化功能格式化之后,各種邏輯仍然一覽無余。

代碼壓縮工具並不會對代碼起到太多的保護作用,其作用只是縮短變量名、刪減空格以及刪除未被使用的代碼,這些工具的目的是優化而非保護,只能“防君子而不防小人”。為了進一步保護前端代碼,需要使用一些代碼混淆工具。
0x02 常規化方案及缺陷
1. 可逆變換保護數據
常規的數據保護方式是設計一個可逆變換函數f對數據進行變換,瀏覽器端提交給服務端的數據 d 經過該可逆變換函數 f 處理后得到變換后的數據 d′
d′=f(d)
d′ 提交到服務端后使用反函數 f−1 即可得到原數據d。
d=f−1(d)
如果惡意用戶不知道f的運算步驟,則無法構造出合法的d′。其中 f 可以是一種數據處理算法,也可以是一種加密算法。但是,由於 f 的運算步驟是固定的,且算法最終執行在瀏覽器中,即使 f 是一種私有的數據處理算法,也終究會被逆向得到其運算步驟。如今nodejs已經相當成熟,在未使用任何混淆工具對 f 進行保護的前提下,惡意用戶可直接從前端JavaScript代碼中截取出核心邏輯,不需要太多成本便可編寫出能在nodejs上運行的破解工具。
特別的,如果函數 f 中含有額外的參數 p ,運算步驟形如 d′=f(d,p) ,此時變換 p 並不能增加 f 的安全性。惡意用戶在截取變換函數f的同時可以一並得到參數p,並且在未經混淆的代碼里,p是很容易使用工具提取的。
因此,只有數據變換是很難很好保護數據的。那么再對代碼進行混淆是否可以有效保護數據呢?
2. 對代碼進行混淆保護
目前能找到的公開混淆工具並不多,常見的有:
- jscrambler(商業)
- JavaScript-Obfuscator(開源)
jscrambler作為一款商業軟件,混淆效果好,但其付費計划較為昂貴。JavaScript-Obfuscator是目前比較熱門的一款開源混淆器,但混淆效果不盡人意。為了驗證JavaScript-Obfuscator混淆效果,本文以字符串混淆為例,編寫了一個簡單的腳本對經過JavaScript-Obfuscator混淆后的字符串進行自動化還原,代碼開源請戳:https://github.com/conanliu/de-js-obfuscator
有了這個工具,逆向出字符串的成本幾乎為0。其實逆向字符串相對比較簡單,但這已經是個開始,逆向出邏輯是遲早的事。普通強度的混淆可以在一段時間內保護業務邏輯,一段時間以后,代碼便沒那么安全了。以JavaScript-Obfuscator的混淆強度,「一段時間」通常不會超過一周。如果頁面承載的是一個高收益多惡意的業務,即使頁面的JavaScript代碼被JavaScript-Obfuscator混淆過,上線一周時間后,大部分關鍵邏輯也可能已經被逆向出來了。關鍵邏輯被逆向意味着刷量工具很快會被編寫出來,該業務將面臨被刷的風險。
對於一個正常的業務來說,JavaScript中數據保護相關的邏輯一個月變化一次已經相當頻繁了。如果為了達到較好的抗破解需要在一周改變一次邏輯,這種對抗成本是很高的。那么有沒有一種長效的機制,既能保證前端代碼的安全,而又不需要付出過量的成本呢?本文后面試圖從動態化的角度,探索一種新的人機對抗方式。
0x03 動態化方案介紹
如果我們有5個數據變換函數 f1,f2,f3,f4,f5,針對每次請求,我們隨機挑選2個變換函數 fx 和 fy,並隨機挑選一個分隔符 s ,真實數據 d 被隨機拆分成 d1和 d2 ,最終數據為
d′=combine(fx(d1),s,fy(d2))
d′提交到服務端后,服務端進行切分操作得到一個二元組
(d′1,d′2)=split(d′,s)
再用fx和fy的反函數處理d′1和d′2,最終得到原始數據:
d1=f−1x(d′1)
d2=f−1y(d′2)
d=d1+d2
雖然單次破解的難度仍為t≈1week,但由於每次請求對應的算法組合均不同,單次破解后並不適用與后面的請求。因此理論上需要逆向並腳本化該邏輯的時間代價是指數級增長的,最終惡意用戶因為逆向成本太高,而轉向了使用起來更簡單的模擬器。至於模擬器的對抗不在本文的討論中。但可以明確的是,模擬器的對抗比自動腳本的對抗要容易一些。同時由於執行模擬器比執行自動腳本需要更多的資源,這也無形中增加的惡意的作惡成本,最終導致惡意在投入和產出中失衡。
該動態化方案雖然聽起來可行,但在實際工程化中會遇到很多問題:
- 如何標識某次請求的函數組合?
- 如何權衡頁面性能?
- 如何解決js編譯速度太慢的問題?
- 是否需要混淆?
接下來將針對以上問題,探索如何在工程上一一解決。
0x04 工程化問題探索
1. 如何標識某次請求的函數組合?
經過隨機組合后用戶每次得到的js均可能不同,此時需要有一個標識告知服務端 fx 和 fy 分別是哪兩個。一種可行的方案是將 x 和 y 的內容連同變換函數 fx 和 fy一起,直接明文編碼到js中,提交數據時將x和y跟隨d′一起提交。但這種標識容易被某些正則規則直接從js文件中提取,惡意用戶可遍歷出所有變換函數及其對應的邏輯,再根據匹配出的標識進行組合。
更嚴謹的做法是編譯js文件時生成一個簽名串signature,將該signature作為變量編譯到js文件中。最后signature連同生成的數據d′一起提交到服務端,服務端使用f−1sig(signature)得到解密d′的關鍵參數,進而對d′進行解密。簽名的生成算法可表示成如下形式:
signature=fsig(x,y,s,random,timestamp)
其中x為第一個變換函數的標識,y為第二個變換函數的標識,s為分隔符,random為一個隨機數,timestamp為生成簽名的時間戳。設計隨機數的目的是讓每次生成的簽名均不同,而時間戳可以感知簽名對應js文件的新鮮度,並且一定程度上能對重放攻擊進行聚集。
2. 如何權衡頁面性能?
前端頁面性能是一個Web應用必然會關注的問題,一種通用而有效的性能優化方式是合理地為頁面中的資源文件設置緩存。通常對於一個模塊化良好且使用成熟打包工具打包的項目,入口html的緩存策略會被配置為Cache-Control: no-cache,而js/css/image等資源文件會設置一個比較長的緩存時間。但負責數據保護的js文件如果含有動態生成的邏輯,該js文件將不能再使用緩存,否則一旦緩存時間控制不當,將會引發各類數據解密失敗的問題。
正常情況下在人機對抗的場景中,頁面並不需要對所有的請求均做人機驗證,也就是說,負責人機驗證的JavaScript代碼並不會被正常用戶訪問多次,所以在人機驗證的環節,部分基於緩存的優化是可以省略的。理想情況下,用戶在一段時間內僅會訪問一次人機驗證的邏輯。此時要做好的是保證用戶首次加載的體驗,而二次訪問的體驗可以暫且不予考慮。
建議的方案是將數據保護相關的邏輯從整個工程的JavaScript代碼中剝離出來,直接inline編譯到html頁面中,或者編譯到一個獨立的js文件中,為該js文件單獨設置Cache-Control: no-cache的response頭部。該js與其他js之間可以使用全局變量、postMessage等方式通信。
3. 如何解決js編譯速度太慢的問題?
前端的打包工具有很多,如gulp、webpack、Rollup等,這些工具各有長處,也有很多針對編譯過程的優化,但目前都無法在需要毫秒級響應的場景完成一次打包,因此編譯打包需要異步完成。那么如何生成出足夠數量的js滿足正常訪問和對抗場景的需求?比較簡單的方案是循環跑編譯腳本,編譯好一個替換一次,短時間內用戶可能會訪問到同一個js,隨着舊js被新編譯出來的js替換,一段時間內用戶訪問的js可以認為是隨機的,此時js的變換間隔取決於編譯速度。
除了簡單方案外,這里介紹一種更靈活的方案,即將編譯產物緩存並提供隨機訪問。首先,把安全相關的js文件從靜態服務中剝離出來,由一個后端的web server輸出js內容。該server上維護着一個長度一定的數組,構建工具編譯好一個js文件后,將該文件的內容發送給web server,web server將接收到的內容順序填充到數組中;當有用戶頁面時,瀏覽器向web server請求該js內容,web server從數組中隨機挑選一個,返回給瀏覽器。
這種服務方式有很多好處,除了可以保證安全js的隨機性,還能將signature的生成放到web server中完成。構建工具在編譯js時將編譯的元信息發送給web server,此時並不生成出signature。用戶需要請求該js時再根據元信息實時生成一個signature,填充到js文件內容中。這樣生成的signature每次都是獨立的,通過檢測signature的使用次數,可以很容易標識並攔截重放的請求。

4. 是否需要混淆?
既然有了隨機的動態,是否還需要混淆?答案是肯定的。雖然fx和fy每次都不一樣,但兩個不同的變化函數,一定會有其獨有的特征。例如fx和fy在JavaScript中的算法實現如下:
function foo(x) {
x = String.fromCharCode.apply(null, x.split('').map(i => i.charCodeAt(0) + 23); return btoa(x)
}function bar(y) {
y = String.fromCharCode.apply(null, y.split('').reverse().map(i => i.charCodeAt(0) + 13); return btoa(y);
}
本例中23、reverse、13便是fx和fy的特征,如果某次請求的js文件中包含reverse和13,則很大可能是使用了bar變換,如果包含23,則很大可能使用了foo變換。通過這種特征檢測,可以輕松得到請求的js中使用了何種變換組合。而檢測方法並不會很復雜,只需要一些簡單正則表達式即可。
0x05 總結
本文分析了常規數據保護及混淆的短板,並從動態的角度,給出了一種對抗機器行為的方式,同時在工程化上有一些思考。人機對抗之路艱辛且漫長,是在未來很長時間內都會存在的業務安全問題。希望動態化思路能給現在正在做人機對抗的團隊一些啟發,幫助更多中小型公司的業務擺脫機器和爬蟲之痛。另外,騰訊防水牆團隊在機器對抗上有較深的積累,如果想直接體驗成果可以點擊此處:https://007.qq.com
問答
騰訊雲域名安全認證問題?
相關閱讀
騰訊安全平台部專家研究員胡育輝:千億黑產背后的破局之道
全景解析騰訊雲安全:從八大領域輸出全鏈路智慧安全能力
手機黑卡,這個仇我是記下了
**此文已由作者授權騰訊雲+社區發布,原文鏈接:https://cloud.tencent.com/developer/article/1145048?fromSource=waitui **
歡迎大家前往騰訊雲+社區或關注雲加社區微信公眾號(QcloudCommunity),第一時間獲取更多海量技術實踐干貨哦~
