爬蟲必須要瞭解的 JavaScript 混淆安全加固

作者:_楊溜溜

原文:

https://segmentfault.com/a/1190000019423501

前言

在安全攻防戰場中,前端代碼都是公開的,那麼對前端進行加密有意義嗎?可能大部分人的回答是,毫無意義,不要自創加密算法,直接用HTTPS吧。但事實上,即使不瞭解密碼學,也應知道是有意義的,因爲加密前解密後的環節,是不受保護的。HTTPS只能保護傳輸層,此外別無用處。

而加密環節又分:

本文主要列舉一些我見到的,我想到的一些加密方式,其實確切的說,應該叫混淆,不應該叫加密。

那麼,代碼混淆的具體原理是什麼?其實很簡單,就是去除代碼中儘可能多的有意義的信息,比如註釋、換行、空格、代碼負號、變量重命名、屬性重命名(允許的情況下)、無用代碼的移除等等。因爲代碼是公開的,我們必須承認沒有任何一種算法可以完全不被破解,所以,我們只能儘可能增加攻擊者閱讀代碼的成本。

語法樹AST混淆

在保證代碼原本的功能性的情況下,我們可以對代碼的AST按需進行變更,然後將變更後的AST在生成一份代碼進行輸出,達到混淆的目的,我們最常用的uglify-js就是這樣對代碼進行混淆的,當然uglify-js的混淆只是主要進行代碼壓縮,即我們下面講到的變量名混淆。

變量名混淆

將變量名混淆成閱讀比較難閱讀的字符,增加代碼閱讀難度,上面說的uglify-js進行的混淆,就是把變量混淆成了短名(主要是爲了進行代碼壓縮),而現在大部分安全方向的混淆,都會將其混淆成類16進制變量名,效果如下:

var test = 'hello';'hello';

混淆後:

var _0x7deb = 'hello';'hello';

注意事項:

eval語法,eval函數中可能使用了原來的變量名,如果不對其進行處理,可能會運行報錯,如下:

var test = 'hello';eval('console.log(test)');'hello';
eval('console.log(test)');

如果不對eval中的console.log(test)進行關聯的混淆,則會報錯。不過,如果eval語法超出了靜態分析的範疇,比如:

var test = 'hello';var variableName = 'test';eval('console.log(' + variableName + ')');'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';'hello';

混淆過後:

var _0x9d2b = ['hello'];var _0xb7de = function (_0x4c7513) {    var _0x96ade5 = _0x9d2b[_0x4c7513];    return _0x96ade5;};var test = _0xb7de(0);'hello'];

var _0xb7de = function (_0x4c7513{
    var _0x96ade5 = _0x9d2b[_0x4c7513];
    return _0x96ade5;
};

var test = _0xb7de(0);

當然,我們可以根據需求,將數組轉化爲二位數組、三維數組等,只需要在需要用到的地方獲取就可以。

常量混淆

將常量進行加密處理,上面的代碼中,雖然已經是混淆過後的代碼了,但是hello字符串還是以明文的形式出現在代碼中,可以利用JS中16進制編碼會直接解碼的特性將關鍵字的Unicode進行了16進制編碼。如下:

var test = 'hello';'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');'\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');'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;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(p1, 2);var k = _0x62fae._0x2374a(i, j);
    _0xeca4ffunction(_0x3c412, _0xae362{
        return _0x3c412 + _0xae362;
    },
    _0xe82aefunction(_0x63aec, _0x678ec{
        return _0x63aec * _0x678ec;
    },
    _0x2374afunction(_0x32487, _0x3a461{
        return _0x32487 || _0x3a461;
    }
};

var i = _0x62fae._0e8ca4f(12);
var j = _0x62fae._0xe82ae(p1, 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);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(name);_0x62fae._0x2374a(name, 0x8);
    _0xe82aefunction(_0x63aec, _0x678ec{
        return _0x63aec(_0x678ec);
    },
    _0xeca4ffunction(_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(name);
_0x62fae._0x2374a(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)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;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);return _0x534f6 + _0x85766; }
function _0x3f632(_0x534f6, _0x534f6return _0x534f6 - _0x534f6; }

// 動態判定函數
function _0x3fa24() {
    return true;
}

var c = _0x3fa24() ? : _0x513fa(12) : _0x3f632(12);

流程混淆

對執行流程進行混淆,又稱控制流扁平化,爲什麼要做混淆執行流程呢?因爲在代碼開發的過程中,爲了使代碼邏輯清晰,便於維護和擴展,會把代碼編寫的邏輯非常清晰。一段代碼從輸入,經過各種if/else分支,順序執行之後得到不同的結果,而我們需要將這些執行流程和判定流程進行混淆,讓攻擊者沒那麼容易摸清楚我們的執行邏輯。

控制流扁平化又分順序扁平化、條件扁平化,

順序扁平化

顧名思義,將按順序、自上而下執行的代碼,分解成數個分支進行執行,如下代碼:

(function () {    console.log(1);    console.log(2);    console.log(3);    console.log(4);    console.log(5);})();
    console.log(1);
    console.log(2);
    console.log(3);
    console.log(4);
    console.log(5);
})();

流程圖如下:

640?wx_fmt=jpeg
控制流扁平化3

混淆過後代碼如下:

(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;    }}());
    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;
    }
}());

混淆過後的流程圖如下:

640?wx_fmt=jpeg
控制流扁平化4

流程看起來了。

條件扁平化

條件扁平化的作用是把所有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;}
    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;
}

如上代碼,流程圖是這樣的

640?wx_fmt=jpeg
控制流扁平化1

控制流扁平化後代碼如下:

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;        }    }}
    var R, L, s, k;
    var next = 0;
    for(;;) {
        switch(next) {
        case 0: k = 0; s = 1; next = 1break;
        case 1if (k < w) next = 2else next = 6break;
        case 2if (x[k] == 1) next = 3else next = 4break;
        case 3: R = (s * y) % n; next = 5break;
        case 4: R = s; next = 5break;
        case 5: s = R * R % n; L = R; k++; next = 1break;
        case 6return L;
        }
    }
}

混淆後的流程圖如下:

640?wx_fmt=jpeg
控制流扁平化2

直觀的感覺就是代碼變了,所有的代碼都擠到了一層當中,這樣做的好處在於在讓攻擊者無法直觀,或通過靜態分析的方法判斷哪些代碼先執行哪些後執行,必須要通過動態運行才能記錄執行順序,從而加重了分析的負擔。

需要注意的是,在我們的流程中,無論是順序流程還是條件流程,如果出現了塊作用域的變量聲明(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;        }    }}
    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 = 1break;
        case (2 * a + b): if (k < w) next = 2else next = 6break;
        case (2 * b - a): if (x[k] == 1) next = 3else next = 4break;
        case (3 * a + b + c): R = (s * y) % n; next = 5break;
        case (2 * b + c): R = s; next = 5break;
        case (2 * c + b): s = R * R % n; L = R; k++; next = 1break;
        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))();'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゚]) (゚Θ゚)) ('_');/*´∇`*/ ['_']; 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') {        // 代碼被篡改!    }}
    // 篡改校驗
    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();    }})();
    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();    }})();
    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();    }}());
    '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參數進行加密,並埋下陷阱,在解碼時插入無用代碼,干擾顯示,大量換行、註釋、字符串等大量特殊字符,導致顯示卡頓。

結束

大概我想到的混淆就包括這些,單個特性使用的話,混淆效果一般,各個特性組合起來用的話,最終效果很明顯,當然這個看個人需求,畢竟混淆是個雙刃劍,在增加了閱讀難度的同時,也增大了腳本的體積,降低了代碼的運行效率。

參考文獻

代碼混淆之道——控制流扁平與不透明謂詞理論篇(https://www.cnblogs.com/ichunqiu/p/7383045.html)

END


往期文章回顧


拿不到offer全額退款 | 第四期人工智能 NLP / CV 課 培訓招生

數說那些年我們一起經歷的高考


640?wx_fmt=png

有多少個人在看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章