歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~css
本文分享了騰訊防水牆團隊關於機器對抗的動態化思路,但願能拋磚引玉,給如今正在作人機對抗的團隊一些啓發,幫助更多中小型公司的業務擺脫機器和爬蟲之痛。html
瀏覽器做爲當今互聯網的一大流量入口,正在變得愈來愈強大。爲了有更好的Web體驗,各種新的標準被制定並實施。PWA的出現,更是把移動端H5的體驗推向了另外一個極致。愈來愈多業務使用H5做爲主要入口的同時,也帶來了另外一個問題:機器行爲氾濫。只要有利益的地方就會有惡意,登陸註冊、投票領券等頁面很容易成爲機器刷量的重災區,現在寫一個普通刷投票腳本的難度基本就跟寫一個「Hello World!」的難度差很少。在與機器對抗的歷程中,Web前端一直是很是薄弱的一環。瀏覽器毫無保留地把全部前端代碼拉取到本地並執行、全部前端代碼均透明可見,拿什麼拯救前端代碼安全?前端
本文中所說起的代碼安全,是指前端JavaScript代碼的安全。一般,若是一段JavaScript代碼只能在正常的瀏覽器中運行,沒法或還沒有在非正常瀏覽器的運行環境執行獲得結果、沒法被等價翻譯成其餘編程語言的代碼,則認爲這段代碼是安全的。node
一段重要的JavaScript邏輯被置於其餘環境以高於正常瀏覽器幾個數量級的效率運行並獲得正確的結果,對於服務端及後面的業務來講,幾乎是一個災難。webpack
本文說的數據保護是指對HTTP/HTTPS協議上承載內容(如POST的body)的保護。HTTP協議是一個文本的協議,全部傳輸的內容從客戶端(即瀏覽器)的角度看都是可見且富有語義的,這意味着內容若是不加以保護,惡意用戶只須要理解內容中的各項參數,便可模擬相應的請求而無需閱讀或逆向前端JavaScript代碼的邏輯。注意,這裏說的保護不是指TLS等傳輸過程的保護,而是指HTTP協議上層承載的具體數據內容的保護。git
好比,正常一個查詢請求的URL形如https://example.com/query?from=shenzhen&destination=beijing
,爬蟲開發者無需閱讀JavaScript即可知道參數要如何構造。而若是請求形如https://example.com/query?params=ZnJvbT1zaGVuemhlbiZkZXN0aW5hdGlvbj1iZWlqaW5n
,惡意用戶在沒法經過觀察當即知道參數構造方法的前提下,只能閱讀或須要先逆向JavaScript代碼,才能知道構造參數方法。這樣就達到了對數據進行保護的目的。github
經過一些字符串替換規則或者抽象語法樹變換規則,將一段代碼等價替換成另外一段可讀性不好的代碼,從而達到保護原有代碼安全。這個過程一般是不可逆的。web
如:面試
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的工具能把代碼壓縮成可讀性很低的代碼,以下圖:
但被瀏覽器強大的格式化功能格式化以後,各類邏輯仍然盡收眼底。
代碼壓縮工具並不會對代碼起到太多的保護做用,其做用只是縮短變量名、刪減空格以及刪除未被使用的代碼,這些工具的目的是優化而非保護,只能「防君子而不防小人」。爲了進一步保護前端代碼,須要使用一些代碼混淆工具。
常規的數據保護方式是設計一個可逆變換函數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是很容易使用工具提取的。
所以,只有數據變換是很難很好保護數據的。那麼再對代碼進行混淆是否能夠有效保護數據呢?
目前能找到的公開混淆工具並很少,常見的有:
jscrambler做爲一款商業軟件,混淆效果好,但其付費計劃較爲昂貴。JavaScript-Obfuscator是目前比較熱門的一款開源混淆器,但混淆效果不盡人意。爲了驗證JavaScript-Obfuscator混淆效果,本文以字符串混淆爲例,編寫了一個簡單的腳本對通過JavaScript-Obfuscator混淆後的字符串進行自動化還原,代碼開源請戳:https://github.com/conanliu/de-js-obfuscator
有了這個工具,逆向出字符串的成本幾乎爲0。其實逆向字符串相對比較簡單,但這已是個開始,逆向出邏輯是早晚的事。普通強度的混淆能夠在一段時間內保護業務邏輯,一段時間之後,代碼便沒那麼安全了。以JavaScript-Obfuscator的混淆強度,「一段時間」一般不會超過一週。若是頁面承載的是一個高收益多惡意的業務,即便頁面的JavaScript代碼被JavaScript-Obfuscator混淆過,上線一週時間後,大部分關鍵邏輯也可能已經被逆向出來了。關鍵邏輯被逆向意味着刷量工具很快會被編寫出來,該業務將面臨被刷的風險。
對於一個正常的業務來講,JavaScript中數據保護相關的邏輯一個月變化一次已經至關頻繁了。若是爲了達到較好的抗破解須要在一週改變一次邏輯,這種對抗成本是很高的。那麼有沒有一種長效的機制,既能保證前端代碼的安全,而又不須要付出過量的成本呢?本文後面試圖從動態化的角度,探索一種新的人機對抗方式。
若是咱們有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都可能不一樣,此時須要有一個標識告知服務端 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文件的新鮮度,而且必定程度上能對重放攻擊進行彙集。
前端頁面性能是一個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
等方式通訊。
前端的打包工具備不少,如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的使用次數,能夠很容易標識並攔截重放的請求。
既然有了隨機的動態,是否還須要混淆?答案是確定的。雖然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);
}
複製代碼
本例中2三、reverse、13即是fx和fy的特徵,若是某次請求的js文件中包含reverse和13,則很大多是使用了bar變換,若是包含23,則很大可能使用了foo變換。經過這種特徵檢測,能夠輕鬆獲得請求的js中使用了何種變換組合。而檢測方法並不會很複雜,只須要一些簡單正則表達式便可。
本文分析了常規數據保護及混淆的短板,並從動態的角度,給出了一種對抗機器行爲的方式,同時在工程化上有一些思考。人機對抗之路艱辛且漫長,是在將來很長時間內都會存在的業務安全問題。但願動態化思路能給如今正在作人機對抗的團隊一些啓發,幫助更多中小型公司的業務擺脫機器和爬蟲之痛。另外,騰訊防水牆團隊在機器對抗上有較深的積累,若是想直接體驗成果能夠點擊此處:https://007.qq.com
問答
相關閱讀
此文已由做者受權騰訊雲+社區發佈,原文連接:https://cloud.tencent.com/developer/article/1145048?fromSource=waitui
歡迎你們前往騰訊雲+社區或關注雲加社區微信公衆號(QcloudCommunity),第一時間獲取更多海量技術實踐乾貨哦~