Web的開放與便捷帶來了極高速的發展,但同時也帶來了至關多的隱患,特別是針對於核心代碼保護上,自做者從事Web前端相關開發的相關工做以來,並未聽聞到太多相關於此的方案,『前端代碼無祕密』這句話好似一個業界共識通常在前端領域傳播。但在平常的開發過程當中,咱們又會涉及以及須要至關強度的前端核心代碼的加密,特別是在於與後端的數據通訊上面(包括HTTP、HTTPS請求以及WebSocket的數據交換)。javascript
考慮一個場景,在視頻相關的產品中,咱們一般須要增長相關的安全邏輯防止被直接盜流或是盜播。特別是對於直播來講,咱們的直播視頻流文件一般會被劃分爲分片而後經過協商的算法生成對應的URL參數並逐次請求。分片一般以5至10秒一個間隔,若是將分片URL的獲取做爲接口徹底放置於後端,那麼不只會給後端帶來極大的壓力外還會帶來直播播放請求的延遲,所以咱們一般會將部分實現放置於前端以此來減小後端壓力並加強體驗。對於iOS或是Android來講,咱們能夠將相關的算法經過C/C++進行編寫,而後編譯爲dylib或是so並進行混淆以此來增長破解的複雜度,可是對於前端來講,並無相似的技術可使用。固然,自從asm.js及WebAssembly的全面推動後,咱們可使用其進一步加強咱們核心代碼的安全性,但因爲asm.js以及WebAssembly標準的開放,其安全強度也並不是想象中的那麼美好。html
本文首先適當回顧目前流行的前端核心代碼保護的相關技術思路及簡要的實現,後具體講述一種更爲安全可靠的前端核心代碼保護的思路(SecurityWorker)供你們借鑑以及改進。固然,做者並不是專業的前端安全從業者,對部分技術安全性的理解可能稍顯片面及不足,歡迎留言一塊兒探討。前端
在咱們的平常開發過程當中,對於JavaScript的混淆器咱們是不陌生的,咱們經常使用其進行代碼的壓縮以及混淆以此來減小代碼體積並增長人爲閱讀代碼的複雜度。常使用的項目包括:java
JavaScript混淆器的原理並不複雜,其核心是對目標代碼進行AST Transformation(抽象語法樹改寫),咱們依靠現有的JavaScript的AST Parser庫,能比較容易的實現本身的Javascript混淆器。如下咱們藉助 acorn 來實現一個if語句片斷的改寫。node
假設咱們存在這麼一個代碼片斷:c++
for(var i = 0; i < 100; i++){
if(i % 2 == 0){
console.log("foo");
}else{
console.log("bar");
}
}
複製代碼
咱們經過使用UglifyJS進行代碼的混淆,咱們可以獲得以下的結果:git
for(var i=0;i<100;i++)i%2==0?console.log("foo"):console.log("bar");
複製代碼
如今讓咱們嘗試編寫一個本身的混淆器對代碼片斷進行混淆達到UglifyJS的效果:github
const {Parser} = require("acorn")
const MyUglify = Parser.extend();
const codeStr = ` for(var i = 0; i < 100; i++){ if(i % 2 == 0){ console.log("foo"); }else{ console.log("bar"); } } `;
function transform(node){
const { type } = node;
switch(type){
case 'Program':
case 'BlockStatement':{
const { body } = node;
return body.map(transform).join('');
}
case 'ForStatement':{
const results = ['for', '('];
const { init, test, update, body } = node;
results.push(transform(init), ';');
results.push(transform(test), ';');
results.push(transform(update), ')');
results.push(transform(body));
return results.join('');
}
case 'VariableDeclaration': {
const results = [];
const { kind, declarations } = node;
results.push(kind, ' ', declarations.map(transform));
return results.join('');
}
case 'VariableDeclarator':{
const {id, init} = node;
return id.name + '=' + init.raw;
}
case 'UpdateExpression': {
const {argument, operator} = node;
return argument.name + operator;
}
case 'BinaryExpression': {
const {left, operator, right} = node;
return transform(left) + operator + transform(right);
}
case 'IfStatement': {
const results = [];
const { test, consequent, alternate } = node;
results.push(transform(test), '?');
results.push(transform(consequent), ":");
results.push(transform(alternate));
return results.join('');
}
case 'MemberExpression':{
const {object, property} = node;
return object.name + '.' + property.name;
}
case 'CallExpression': {
const results = [];
const { callee, arguments } = node;
results.push(transform(callee), '(');
results.push(arguments.map(transform).join(','), ')');
return results.join('');
}
case 'ExpressionStatement':{
return transform(node.expression);
}
case 'Literal':
return node.raw;
case 'Identifier':
return node.name;
default:
throw new Error('unimplemented operations');
}
}
const ast = MyUglify.parse(codeStr);
console.log(transform(ast)); // 與UglifyJS輸出一致
複製代碼
固然,咱們上面的實現只是一個簡單的舉例,實際上的混淆器實現會比當前的實現複雜得多,須要考慮很是多的語法上的細節,此處僅拋磚引玉供你們參考學習。web
從上面的實現咱們能夠看出,JavaScript混淆器只是將JavaScript代碼變化爲另外一種更不可讀的形式,以此來增長人爲分析的難度從而達到加強安全的目的。這種方式在好久之前具備很不錯的效果,可是隨着開發者工具愈來愈強大,實際上經過單步調試能夠很容易逆向出原始的Javascript的核心算法。固然,後續也有至關多的庫作了較多的改進,JavaScript Obfuscator Tool 是其中的表明項目,其增長了諸如反調試、變量前綴、變量混淆等功能加強安全性。但萬變不離其宗,因爲混淆後的代碼仍然是明文的,若是有足夠的耐心並藉助開發者工具咱們仍然能夠嘗試還原,所以安全性仍然大打折扣。算法
在Flash還大行其道的時期,爲了更好的方便引擎開發者使用C/C++來提高Flash遊戲相關引擎的性能,Adobe開源了 CrossBridge 這個技術。在這種過程當中,原有的C/C++代碼通過LLVM IR變爲Flash運行時所須要的目標代碼,無論是從效率提高上仍是從安全性上都有了很是大的提高。對於目前的開源的反編譯器來講,很難反編譯由CorssBridge編譯的C/C++代碼,而且因爲Flash運行時生產環境中禁用調試,所以也很難進行對應的單步調試。
使用Flash的C/C++擴展方式來保護咱們的前端核心代碼看起來是比較理想的方法,但Flash的移動端上已經沒有任何可被使用的空間,同時Adobe已經宣佈2020年再也不對Flash進行維護,所以咱們徹底沒有理由再使用這種方法來保護咱們前端的核心代碼。
固然,因爲Flash目前在PC上仍然有很大的佔有率,而且IE10如下的瀏覽器仍然有很多份額,咱們仍舊能夠把此做爲一種PC端的兼容方案考慮進來。
爲了解決Javascript的性能問題,Mozilla提出了一套新的面相底層的Javascript語法子集 -- asm.js,其從JIT友好的角度出發,使得Javascript的總體運行性能有了很大的提高。後續Mozilla與其餘廠商進行相關的標準化,產出了WebAssembly標準。
無論是asm.js或是WebAssembly,咱們均可以將其看做爲一個全新的VM,其餘語言經過相關的工具鏈產出此VM可執行的代碼。從安全性的角度來講,相比單純的Javascript混淆器而言,其強度大大的增長了,而相比於Flash的C/C++擴展方式來講,其是將來的發展方向,並現已被主流的瀏覽器實現。
能夠編寫生成WebAssembly的語言及工具鏈很是多,咱們使用C/C++及其Emscripten做爲示範編寫一個簡單的簽名模塊進行體驗。
#include <string>
#include <emscripten.h>
#include <emscripten/bind.h>
#include "md5.h"
#define SALTKEY "md5 salt key"
std::string sign(std::string str){
return md5(str + string(SALTKEY));
}
// 此處導出sign方法供Javascript外部環境使用
EMSCRIPTEN_BIND(my_module){
emscripten::function("sign", &sign);
}
複製代碼
接着,咱們使用emscripten編譯咱們的C++代碼,獲得對應的生成文件。
em++ -std=c++11 -Oz --bind \
-I ./md5 ./md5/md5.cpp ./sign.cpp \
-o ./sign.js
複製代碼
最後,咱們引入生成sign.js文件,而後進行調用。
<body>
<script src="./sign.js"></script>
<script> // output: 0b57e921e8f28593d1c8290abed09ab2 Module.sign("This is a test string"); </script>
</body>
複製代碼
目前看起來WebAssembly是目前最理想的前端核心代碼保護的方案了,咱們可使用C/C++編寫相關的代碼,使用Emscripten相關工具鏈編譯爲asm.js和wasm,根據不一樣的瀏覽器的支持狀況選擇使用asm.js仍是wasm。而且對於PC端IE10如下的瀏覽器,咱們還能夠經過CrossBridge複用其C/C++代碼,產出對應的Flash目標代碼,從而達到很是好的瀏覽器兼容性。
然而使用asm.js/wasm後對於前端核心代碼的保護就能夠高枕無憂了麼?因爲asm.js以及wasm的標準規範都是徹底公開的,所以對於asm.js/wasm標準實現良好反編譯器來講,徹底能夠儘量的產出閱讀性較強的代碼從而分析出其中的核心算法代碼。但幸運的是,目前做者還暫時沒有找到實現良好的asm.js/wasm反編譯器,所以我暫時認爲使用此種方法在保護前端核心代碼的安全性上已經可堪重用了。
做者在工做當中常常性會編寫前端核心相關的代碼,而且這些代碼大部分與通訊相關,例如AJAX的請求數據的加解密,WebSocket協議數據的加解密等。對於這部分工做,做者一般都會使用上面介紹的asm.js/wasm加CrossBridge技術方案進行解決。這套方案目前看來至關不錯,可是仍然存在幾個比較大的問題:
所以咱們花費兩週時間編寫一套基於asm.js/wasm更好的前端核心代碼保護方案:SecurityWorker。
SecurityWorker的目標至關簡單:可以儘量溫馨的編寫具備極強安全強度的核心算法模塊。其拆分下來實際上須要知足如下8點:
接下來咱們會逐步講解SecurityWorker如何達成這些目標並詳細介紹其原理,供你們參考改進。
如何在WebAssembly基礎上提高安全性?回想以前咱們的介紹,WebAssembly在安全性上一個比較脆弱的點在於WebAssembly標準規範的公開,若是咱們在WebAssembly之上再建立一個私有獨立的VM是否是能夠解決這個問題呢?答案是確定的,所以咱們首要解決的問題是如何在WebAssembly之上創建一個Javascript的獨立VM。這對於WebAssembly是垂手可得的,有很是多的項目提供了參考,例如基於SpiderMonkey編譯的 js.js 項目。但咱們並無考慮使用SpiderMonkey,由於其產出的wasm代碼達到了50M,在Web這樣代碼體積大小敏感的環境基本不具備實際使用價值。但好在ECMAScirpt相關的嵌入式引擎很是之多:
通過比較選擇,咱們選擇了duktape做爲咱們基礎的VM,咱們的執行流程變成了以下圖所示:
固然,從圖中咱們能夠看到整個過程實際上會有一個比較大的風險點,因爲咱們的代碼是經過字符串加密的方式嵌入到C/C++中進行編譯的,所以在執行過程當中,咱們是能在內存的某一個運行時期等待代碼解密完成後拿到核心代碼的,以下圖所示:
如何解決這個問題?咱們的解決思路是將Javascript變成另外一種表現形式,也就是咱們常見的opcode,例如假設咱們有這樣的代碼:
1 + 2;
複製代碼
咱們會將其轉變相似彙編指令的形式:
SWVM_PUSH_L 1 # 將1值壓入棧中
SWVM_PUSH_L 2 # 將2值壓入棧中
SWVM_ADD # 對值進行相加,並將結果壓入棧中
複製代碼
最後咱們將編譯獲得的opcode bytes按照uint8數組的方式嵌入到C/C++中,而後進行總體編譯,如圖所示:
整個過程當中,因爲咱們的opcode設計是私有不公開的,而且已經不存在明文的Javascript代碼了,所以安全性獲得了極大的提高。如此這樣咱們解決了目標中的#一、#二、#4。但Javascript已經被從新組織爲opcode了,那麼如何保證目標中的#8呢?解決方式很簡單,咱們在Javascript編譯爲opcode的關鍵步驟上附帶了相關的信息,使得代碼執行出錯後,可以根據相關信息進行準確的報錯。與此同時,咱們精簡了opcode的設計,使得生成的opcode體積小於原有的Javascript代碼。
duktape除了語言實現和部分標準庫外並不還有一些外圍的API,例如AJAX/WebSocket等,考慮到使用的便捷性以及更容易被前端開發者接收並使用,咱們爲duktape實現了部分的WebWorker環境的API,包括了Websocket/Console/Ajax等,並與Emscripten提供的Fetch/WebSocket等實現結合獲得了SecurityWorker VM。
那麼最後的問題是咱們如何減少最終生成的asm.js/wasm代碼的體積大小?在不進行任何處理的時候,咱們的生成代碼因爲包含了duktape以及諸多外圍API的實現,即便一個Hello World的代碼gzip後也會有340kb左右的大小。爲了解決這個問題,咱們編寫了SecurityWorker Loader,將生成代碼進行處理後與SecurityWorker Loader的實現一塊兒編譯獲得最終的文件。在代碼運行時,SecurityWorker Loader會對須要運行的代碼進行釋放而後再進行動態執行。如此一來,咱們將原有的代碼體積從原有gzip也會有340kb左右的大小下降到了180kb左右。
SecurityWorker解決了以前方案的許多問題,但其一樣不是最完美的方案,因爲咱們在WebAssembly上又建立了一個VM,所以當你的應用對於體積敏感或是要求極高的執行效率時,SecurityWorker就不知足你的要求了。固然SecurityWorker能夠應用多種優化手段在當前基礎上再大幅度的縮減體積大小以及提升執行效率,但因爲其已經達到咱們本身現有的需求和目標,所以目前暫時沒有提高的相關計劃。
咱們經過回顧目前主流的前端核心保護方案,並詳細介紹了基於以前方案作的提高方案SecurityWorker,相信你們對整個前端核心算法保護的技術方案已經有一個比較清晰的認識了。固然,對於安全的追求沒有終途,SecurityWorker也不是最終完美的方案,但願本文的相關介紹能讓更多人蔘與到WebAssembly及前端安全領域中來,讓Web變得更好。
方老溼,您學會了麼?