在安全攻防戰場中,前端代碼都是公開的,那麼對前端進行加密有意義嗎?可能大部分人的回答是,毫無心義
,不要自創加密算法,直接用HTTPS吧。但事實上,即便不瞭解密碼學,也應知道是有意義
的,由於加密前
和解密後
的環節,是不受保護的。HTTPS只能保護傳輸層,此外別無用處。html
而加密環節又分:前端
本文主要列舉一些我見到的,我想到的一些加密方式,其實確切的說,應該叫混淆,不該該叫加密。git
那麼,代碼混淆的具體原理是什麼?其實很簡單,就是去除代碼中儘量多的有意義的信息,好比註釋、換行、空格、代碼負號、變量重命名、屬性重命名(容許的狀況下)、無用代碼的移除等等。由於代碼是公開的,咱們必須認可沒有任何一種算法能夠徹底不被破解,因此,咱們只能儘量增長攻擊者閱讀代碼的成本。github
原文地址算法
在保證代碼本來的功能性的狀況下,咱們能夠對代碼的AST按需進行變動,而後將變動後的AST在生成一份代碼進行輸出,達到混淆的目的,咱們最經常使用的uglify-js就是這樣對代碼進行混淆的,固然uglify-js
的混淆只是主要進行代碼壓縮,即咱們下面講到的變量名混淆。npm
將變量名混淆成閱讀比較難閱讀的字符,增長代碼閱讀難度,上面說的uglify-js
進行的混淆,就是把變量混淆成了短名(主要是爲了進行代碼壓縮),而如今大部分安全方向的混淆,都會將其混淆成類16進制變量名,效果以下:數組
var test = 'hello';
混淆後:瀏覽器
var _0x7deb = 'hello';
注意事項:安全
eval語法,eval函數中可能使用了原來的變量名,若是不對其進行處理,可能會運行報錯,以下:函數
var test = 'hello'; eval('console.log(test)');
若是不對eval中的console.log(test)進行關聯的混淆,則會報錯。不過,若是eval語法超出了靜態分析的範疇,好比:
var test = 'hello'; var variableName = 'test'; eval('console.log(' + variableName + ')');
這種咋辦呢,可能要進行遍歷AST找到其運行結果,而後在進行混淆,不過貌似成本比較高。
全局變量的編碼,若是代碼是做爲SDK進行輸出的,咱們須要保存全局變量名的不變,好比:
<script> var $ = function(id) { return document.getElementById(id); }; </script>
$
變量是放在全局下的,混淆事後以下:
<script> var _0x6482fa = function(id) { return document.getElementById(id); }; </script>
那麼若是依賴這一段代碼的模塊,使用$('id')
調用天然會報錯,由於這個全局變量已經被混淆了。
將JS中的常量提取到數組中,調用的時候用數組下標的方式調用,這樣的話直接讀懂基本不可能了,要麼反AST處理下,要麼一步一步調試,工做量大增。
以上面的代碼爲例:
var test = 'hello';
混淆事後:
var _0x9d2b = ['hello']; var _0xb7de = function (_0x4c7513) { var _0x96ade5 = _0x9d2b[_0x4c7513]; return _0x96ade5; }; var test = _0xb7de(0);
固然,咱們能夠根據需求,將數組轉化爲二位數組、三維數組等,只須要在須要用到的地方獲取就能夠。
將常量進行加密處理,上面的代碼中,雖然已是混淆事後的代碼了,可是hello
字符串仍是以明文的形式出如今代碼中,能夠利用JS中16進制編碼會直接解碼的特性將關鍵字的Unicode進行了16進制編碼。以下:
var test = 'hello';
結合常量提取獲得混淆結果:
var _0x9d2b = ['\x68\x65\x6c\x6c\x6f']; var _0xb7de = function (_0x4c7513) { _0x4c7513 = _0x4c7513 - 0x0; var _0x96ade5 = _0x9d2b[_0x4c7513]; return _0x96ade5; }; var test = _0xb7de('0x0');
固然,除了JS特性自帶的Unicode自動解析之外,也能夠自定義一些加解密算法,好比對常量進行base64編碼,或者其餘的什麼rc4等等,只須要使用的時候解密就OK,好比上面的代碼用base64編碼後:
var _0x9d2b = ['aGVsbG8=']; // base64編碼後的字符串 var _0xaf421 = function (_0xab132) { // base64解碼函數 var _0x75aed = function(_0x2cf82) { // TODO: 解碼 }; return _0x75aed(_0xab132); } var _0xb7de = function (_0x4c7513) { _0x4c7513 = _0x4c7513 - 0x0; var _0x96ade5 = _0xaf421(_0x9d2b[_0x4c7513]); return _0x96ade5; }; var test = _0xb7de('0x0');
將全部的邏輯運算符、二元運算符都變成函數,目的也是增長代碼閱讀難度,讓其沒法直接經過靜態分析獲得結果。以下:
var i = 1 + 2; var j = i * 2; var k = j || i;
混淆後:
var _0x62fae = { _0xeca4f: function(_0x3c412, _0xae362) { return _0x3c412 + _0xae362; }, _0xe82ae: function(_0x63aec, _0x678ec) { return _0x63aec * _0x678ec; }, _0x2374a: function(_0x32487, _0x3a461) { return _0x32487 || _0x3a461; } }; var i = _0x62fae._0e8ca4f(1, 2); var j = _0x62fae._0xe82ae(i, 2); var k = _0x62fae._0x2374a(i, j);
固然除了邏輯運算符和二元運算符之外,還能夠將函數調用、靜態字符串進行相似的混淆,以下:
var fun1 = function(name) { console.log('hello, ' + name); }; var fun2 = function(name, age) { console.log(name + ' is ' + age + ' years old'); } var name = 'xiao.ming'; fun1(name); fun2(name, 8);
var _0x62fae = { _0xe82ae: function(_0x63aec, _0x678ec) { return _0x63aec(_0x678ec); }, _0xeca4f: function(_0x92352, _0x3c412, _0xae362) { return _0x92352(_0x3c412, _0xae362) }, _0x2374a: 'xiao.ming', _0x5482a: 'hello, ', _0x837ce: ' is ', _0x3226e: ' years old' }; var fun1 = function(name) { console.log(_0x62fae._0x5482a + name); }; var fun2 = function(name, age) { console.log(name + _0x62fae._0x837ce + age + _0x62fae._0x3226e); } var name = _0x62fae._0x2374a; _0x62fae._0xe82ae(fun1, name); _0x62fae._0xeca4f(fun2, name, 0x8);
上面的例子中,fun1和fun2內的字符串相加也會被混淆走,靜態字符串也會被前面提到的字符串提取
抽取到數組中(我就是懶,這部分代碼就不寫了)。
須要注意的是,咱們每次遇到相同的運算符,需不須要從新生成函數進行替換,這就按我的需求了。
將咱們經常使用的語法混淆成咱們不經常使用的語法,前提是不改變代碼的功能。例如for換成do/while,以下:
for (i = 0; i < n; i++) { // TODO: do something } var i = 0; do { if (i >= n) break; // TODO: do something i++; } while (true)
將靜態執行代碼添加動態判斷,運行時動態決定運算符,干擾靜態分析。
以下:
var c = 1 + 2;
混淆事後:
function _0x513fa(_0x534f6, _0x85766) { return _0x534f6 + _0x85766; } function _0x3f632(_0x534f6, _0x534f6) { return _0x534f6 - _0x534f6; } // 動態斷定函數 function _0x3fa24() { return true; } var c = _0x3fa24() ? _0x513fa(1, 2) : _0x3f632(1, 2);
對執行流程進行混淆,又稱控制流扁平化,爲何要作混淆執行流程呢?由於在代碼開發的過程當中,爲了使代碼邏輯清晰,便於維護和擴展,會把代碼編寫的邏輯很是清晰。一段代碼從輸入,通過各類if/else分支,順序執行以後獲得不一樣的結果,而咱們須要將這些執行流程和斷定流程進行混淆,讓攻擊者沒那麼容易摸清楚咱們的執行邏輯。
控制流扁平化又分順序扁平化、條件扁平化,
顧名思義,將按順序、自上而下執行的代碼,分解成數個分支進行執行,以下代碼:
(function () { console.log(1); console.log(2); console.log(3); console.log(4); console.log(5); })();
流程圖以下:
混淆事後代碼以下:
(function () { var flow = '3|4|0|1|2'.split('|'), index = 0; while (!![]) { switch (flow[index++]) { case '0': console.log(3); continue; case '1': console.log(4); continue; case '2': console.log(5); continue; case '3': console.log(1); continue; case '4': console.log(2); continue; } break; } }());
混淆事後的流程圖以下:
流程看起來扁
了。
條件扁平化的做用是把全部if/else分支的流程,所有扁平到一個流程中,在流程圖中擁有相同的入口和出口。
以下面的代碼:
function modexp(y, x, w, n) { var R, L; var k = 0; var s = 1; while(k < w) { if (x[k] == 1) { R = (s * y) % n; } else { R = s; } s = R * R % n; L = R; k++; } return L; }
如上代碼,流程圖是這樣的
控制流扁平化後代碼以下:
function modexp(y, x, w, n) { var R, L, s, k; var next = 0; for(;;) { switch(next) { case 0: k = 0; s = 1; next = 1; break; case 1: if (k < w) next = 2; else next = 6; break; case 2: if (x[k] == 1) next = 3; else next = 4; break; case 3: R = (s * y) % n; next = 5; break; case 4: R = s; next = 5; break; case 5: s = R * R % n; L = R; k++; next = 1; break; case 6: return L; } } }
混淆後的流程圖以下:
直觀的感受就是代碼變扁
了,全部的代碼都擠到了一層當中,這樣作的好處在於在讓攻擊者沒法直觀,或經過靜態分析的方法判斷哪些代碼先執行哪些後執行,必需要經過動態運行才能記錄執行順序,從而加劇了分析的負擔。
須要注意的是,在咱們的流程中,不管是順序流程仍是條件流程,若是出現了塊做用域的變量聲明(const/let),那麼上面的流程扁平化將會出現錯誤,由於switch/case內部爲塊做用域,表達式被分到case內部以後,其餘case沒法取到const/let的變量聲明,天然會報錯。
上面的switch/case的判斷是經過數字(也就是謂詞)的形式判斷的,並且是透明的,能夠看到的,爲了更加的混淆視聽,能夠將case判斷設定爲表達式,讓其沒法直接判斷,好比利用上面代碼,改成不透明謂詞:
function modexp(y, x, w, n) { var a = 0, b = 1, c = 2 * b + a; var R, L, s, k; var next = 0; for(;;) { switch(next) { case (a * b): k = 0; s = 1; next = 1; break; case (2 * a + b): if (k < w) next = 2; else next = 6; break; case (2 * b - a): if (x[k] == 1) next = 3; else next = 4; break; case (3 * a + b + c): R = (s * y) % n; next = 5; break; case (2 * b + c): R = s; next = 5; break; case (2 * c + b): s = R * R % n; L = R; k++; next = 1; break; case (4 * c - 2 * b): return L; } } }
謂詞用a、b、c三個變量組成,甚至能夠把這三個變量隱藏到全局中定義,或者隱藏在某個數組中,讓攻擊者不能那麼輕易找到。
將腳本進行編碼,運行時 解碼 再 eval 執行如:
eval (…………………………..……………. ……………. !@#$%^&* ……………. .…………………………..……………. )
可是實際上這樣意義並不大,由於攻擊者只須要把alert或者console.log就原形畢露了
改進方案:利用Function / (function(){}).constructor
將代碼當作字符串傳入,而後執行,以下:
var code = 'console.log("hellow")'; (new Function(code))();
如上代碼,能夠對code進行加密混淆,例如aaencode,原理也是如此,咱們舉個例子
alert("Hello, JavaScript");
利用aaencode混淆事後,代碼以下:
゚ω゚ノ= /`m´)ノ ~┻━┻ //*´∇`*/ ['_']; o=(゚ー゚) =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);(゚Д゚)={゚Θ゚: '_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o -(゚Θ゚)] ,゚Д゚ノ:((゚ー゚==3) +'_')[゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] = ((゚Д゚)+'_') [ (゚ー゚)+(゚ー゚)-(゚Θ゚) ];(゚Д゚) ['o'] = ((゚Д゚)+'_') [゚Θ゚];(゚o゚)=(゚Д゚) ['c']+(゚Д゚) ['o']+(゚ω゚ノ +'_')[゚Θ゚]+ ((゚ω゚ノ==3) +'_') [゚ー゚] + ((゚Д゚) +'_') [(゚ー゚)+(゚ー゚)]+ ((゚ー゚==3) +'_') [゚Θ゚]+((゚ー゚==3) +'_') [(゚ー゚) - (゚Θ゚)]+(゚Д゚) ['c']+((゚Д゚)+'_') [(゚ー゚)+(゚ー゚)]+ (゚Д゚) ['o']+((゚ー゚==3) +'_') [゚Θ゚];(゚Д゚) ['_'] =(o^_^o) [゚o゚] [゚o゚];(゚ε゚)=((゚ー゚==3) +'_') [゚Θ゚]+ (゚Д゚) .゚Д゚ノ+((゚Д゚)+'_') [(゚ー゚) + (゚ー゚)]+((゚ー゚==3) +'_') [o^_^o -゚Θ゚]+((゚ー゚==3) +'_') [゚Θ゚]+ (゚ω゚ノ +'_') [゚Θ゚]; (゚ー゚)+=(゚Θ゚); (゚Д゚)[゚ε゚]='\\'; (゚Д゚).゚Θ゚ノ=(゚Д゚+ ゚ー゚)[o^_^o -(゚Θ゚)];(o゚ー゚o)=(゚ω゚ノ +'_')[c^_^o];(゚Д゚) [゚o゚]='\"';(゚Д゚) ['_'] ( (゚Д゚) ['_'] (゚ε゚+(゚Д゚)[゚o゚]+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚Θ゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚o゚]) (゚Θ゚)) ('_');
這段代碼看起來很奇怪,不像是JavaScript代碼,可是實際上這段代碼是用一些看似表情的符號,聲明瞭一個16位的數組(用來表示16進制位置),而後將code當作字符串遍歷,把每一個代碼符號經過string.charCodeAt
取這個16位的數組下標,拼接成代碼。大概的意思就是把代碼當作字符串,而後使用這些符號的拼接代替這一段代碼(能夠看到代碼裏有不少加號),最後,經過(new Function(code))('_')
執行。
仔細觀察上面這一段代碼,把代碼最後的('_')
去掉,在運行,你會直接看到源代碼,而後Function.constructor
存在(゚Д゚)
變量中,感興趣的同窗能夠自行查看。
除了aaencode,jjencode原理也是差很少,就不作解釋了,其餘更霸氣的jsfuck,這些都是對代碼進行加密的,這裏就不詳細介紹了。
因爲JavaScript自帶debugger
語法,咱們能夠利用死循環性的debugger
,當頁面打開調試面板的時候,無限進入調試狀態。
在代碼開始執行的時候,使用setInterval
定時觸發咱們的反調試函數。
在代碼生成階段,隨機在部分函數體中注入咱們的反調試函數,當代碼執行到特定邏輯的時候,若是調試面板在打開狀態,則無限進入調試狀態。
因爲咱們的代碼可能已經反調試了,攻擊者能夠會將代碼拷貝到本身本地,而後修改,調試,執行,這個時候就須要添加一些檢測進行斷定,若是不是正常的環境執行,那讓代碼自行失敗。
在代碼生成的時候,爲函數生成一份Hash,在代碼執行以前,經過函數 toString 方法,檢測代碼是否被篡改
function module() { // 篡改校驗 if (Hash(module.toString()) != 'JkYxnHlxHbqKowiuy') { // 代碼被篡改! } }
檢查當前腳本的執行環境,例如當前的URL是否在容許的白名單內、當前環境是否正常的瀏覽器。
若是爲Nodejs環境,若是出現異常環境,甚至咱們能夠啓動木馬,長期跟蹤。
插入一些永遠不會發生的代碼,讓攻擊者在分析代碼的時候被這些無用的廢代碼混淆視聽,增長閱讀難度。
與廢代碼相對立的就是有用的代碼,這些有用的代碼表明着被執行代碼的邏輯,這個時候咱們能夠收集這些邏輯,增長一段斷定來決定執行真邏輯仍是假邏輯,以下:
(function(){ if (true) { var foo = function () { console.log('abc'); }; var bar = function () { console.log('def'); }; var baz = function () { console.log('ghi'); }; var bark = function () { console.log('jkl'); }; var hawk = function () { console.log('mno'); }; foo(); bar(); baz(); bark(); hawk(); } })();
能夠看到,全部的console.log都是咱們的執行邏輯,這個時候能夠收集全部的console.log,而後製造假斷定來執行真邏輯代碼,收集邏輯注入後以下:
(function(){ if (true) { var foo = function () { if ('aDas' === 'aDas') { console.log('abc'); } else { console.log('def'); } }; var bar = function () { if ('Mfoi' !== 'daGs') { console.log('ghi'); } else { console.log('def'); } }; var baz = function () { if ('yuHo' === 'yuHo') { console.log('ghi'); } else { console.log('abc'); } }; var bark = function () { if ('qu2o' === 'qu2o') { console.log('jkl'); } else { console.log('mno'); } }; var hawk = function () { if ('qCuo' !== 'qcuo') { console.log('jkl'); } else { console.log('mno'); } }; foo(); bar(); baz(); bark(); hawk(); } })();
斷定邏輯中生成了一些字符串,在沒有使用字符串提取的狀況下,這是能夠經過代碼靜態分析來獲得真實的執行邏輯的,或者咱們可使用上文講到的動態執行來決定執行真邏輯,能夠看一下使用字符串提取和變量名編碼後的效果,以下:
var _0x6f5a = [ 'abc', 'def', 'caela', 'hmexe', 'ghi', 'aaeem', 'maxex', 'mno', 'jkl', 'ladel', 'xchem', 'axdci', 'acaeh', 'log' ]; (function (_0x22c909, _0x4b3429) { var _0x1d4bab = function (_0x2e4228) { while (--_0x2e4228) { _0x22c909['push'](_0x22c909['shift']()); } }; _0x1d4bab(++_0x4b3429); }(_0x6f5a, 0x13f)); var _0x2386 = function (_0x5db522, _0x143eaa) { _0x5db522 = _0x5db522 - 0x0; var _0x50b579 = _0x6f5a[_0x5db522]; return _0x50b579; }; (function () { if (!![]) { var _0x38d12d = function () { if (_0x2386('0x0') !== _0x2386('0x1')) { console[_0x2386('0x2')](_0x2386('0x3')); } else { console[_0x2386('0x2')](_0x2386('0x4')); } }; var _0x128337 = function () { if (_0x2386('0x5') !== _0x2386('0x6')) { console[_0x2386('0x2')](_0x2386('0x4')); } else { console[_0x2386('0x2')](_0x2386('0x7')); } }; var _0x55d92e = function () { if (_0x2386('0x8') !== _0x2386('0x8')) { console[_0x2386('0x2')](_0x2386('0x3')); } else { console[_0x2386('0x2')](_0x2386('0x7')); } }; var _0x3402dc = function () { if (_0x2386('0x9') !== _0x2386('0x9')) { console[_0x2386('0x2')](_0x2386('0xa')); } else { console[_0x2386('0x2')](_0x2386('0xb')); } }; var _0x28cfaa = function () { if (_0x2386('0xc') === _0x2386('0xd')) { console[_0x2386('0x2')](_0x2386('0xb')); } else { console[_0x2386('0x2')](_0x2386('0xa')); } }; _0x38d12d(); _0x128337(); _0x55d92e(); _0x3402dc(); _0x28cfaa(); } }());
除了注入執行邏輯之外,還能夠埋入一個隱蔽的陷阱,在一個永不到達
且沒法靜態分析
的分支裏,引用該函數,正經常使用戶不會執行,而 AST 遍歷求值時,則會觸發陷阱!陷阱能幹啥呢?
在代碼用eval包裹,而後對eval參數進行加密,並埋下陷阱,在解碼時插入無用代碼,干擾顯示,大量換行、註釋、字符串等大量特殊字符,致使顯示卡頓。
大概我想到的混淆就包括這些,單個特性使用的話,混淆效果通常,各個特性組合起來用的話,最終效果很明顯,固然這個看我的需求,畢竟混淆是個雙刃劍,在增長了閱讀難度的同時,也增大了腳本的體積,下降了代碼的運行效率。