Javascript的压缩与混淆

4,144 阅读16分钟

引言

网站的逻辑是通过javascript来实现的,但因为javascript可通过浏览器直接获取源码,通常情况下,网站所有者为保证web安全性,会对网站中的javascript文件经过处理,以防数据泄露和黑客攻击。

  • 代码压缩:去除代码中的空格、制表符、换行符等内容,将代码压缩至几行内容,降低代码可读性,同时也提高了网站加载速度,但是可通过一些代码格式化工具提高代码可读性。
  • 代码混淆:代码混淆是将代码转换成一种功能上等价,但是难于阅读和理解的形式。代码混淆可通过将代码中的各种元素改写成无意义的名字;重写代码中的部分逻辑,将其变成功能上等价,但是更难理解的形式;打乱代码的格式等方式混淆代码。但是代码混淆并不能真正阻止反向工程,只能增大其难度,因此,对于对安全性要求很高的场合,仅仅使用代码混淆并不能保证代码的安全性。

JavaScript Obfuscator

JavaScript Obfuscator是一个免费且高效的JavaScript混淆器,可将原始的JavaScript代码转换为新的表示形式,该表示形式将更难理解、复制、重用和修改,混淆后的代码将具有原始代码的功能。

JavaScript Obfuscator 官网

使用Node对JavaScript进行压缩与混淆的环境搭建

  • 安装好Node.js环境

  • 安装JavaScript Obfuscator库

    npm install --sace-dev javascript-obfuscator
  • 新建文件夹,使用npm init初始化工作空间
  • 新建js文件,使用require引入JavaScript Obfuscator,定义一个方法用来返回处理后的代码,code为原始代码,options为传入的JavaScript Obfuscator库选项值,最后使用控制台输出处理后的代码
    const code = ``
    const options = {}
    const obfuscator = require('javascript-obfuscator')
    function obfuscate(code, options) {
        return obfuscator.obfuscate(code, options).getObfuscatedCode()
    }
    console.log(obfuscate(code, options))

Javascript压缩

JavaScript Obfuscator提供了参数compact来完成对代码的压缩,默认为true

    const code = `
    let x = 'JavaScript Obfuscator'
    console.log(x)
    `
    const options = {
        compact: true
    }
    const obfuscator = require('javascript-obfuscator')
    function obfuscate(code, options) {
        return obfuscator.obfuscate(code, options).getObfuscatedCode()
    }
    console.log(obfuscate(code, options))

输出后的结果为:

    const _0x2381=['JavaScript\x20Obfuscator'];(function(_0x395104,_0x23815b){const _0x1d3967=function(_0xceb63e){while(--_0xceb63e){_0x395104['push'](_0x395104['shift']());}};_0x1d3967(++_0x23815b);}(_0x2381,0x154));const _0x1d39=function(_0x395104,_0x23815b){_0x395104=_0x395104-0x0;let _0x1d3967=_0x2381[_0x395104];return _0x1d3967;};let x=_0x1d39('0x0');console['log'](x);

Javascript混淆

  • 域名锁定

通过设置domainLock参数为相关的域名来让代码只能在特定的域名下运行

    const options = {
        domainLock: ['https://www.baidu.com/']
    }
  • 调试保护

通过设置debugProtection来禁用调试模式,进入无限Debug模式,使代码在调试过程中不断进入断点模式,无法顺畅运行

    const options = {
        debugProtection: true
    }

另外可通过设置debugProtectionInterval来指定Debug的间隔

  • 禁用控制台输出

通过设置diableConsoleOutput参数可禁用console.log输出功能,加大调试难度

    const options = {
        diableConsoleOutput: true
    }
  • 代码自我保护

通过设置selfDefending参数可开启代码的自我保护功能,混淆后的代码强制为一行显示,并且格式化和重命名将无法执行

    const options = {
        selfDefending: true
    }

输出结果如下:

    const _0x3d23=['log','JavaScript\x20Obfuscator','compile','^([^\x20]+(\x20+[^\x20]+)+)+[^\x20]}','test','constructor','return\x20/\x22\x20+\x20this\x20+\x20\x22/'];(function(_0x1c63ad,_0x3d23a7){const _0x206b13=function(_0x38a220){while(--_0x38a220){_0x1c63ad['push'](_0x1c63ad['shift']());}};const _0xbcdb3d=function(){const _0x2cb417={'data':{'key':'cookie','value':'timeout'},'setCookie':function(_0x1e6bac,_0x233f4c,_0x7f906c,_0x9eb8af){_0x9eb8af=_0x9eb8af||{};let _0x7bddbf=_0x233f4c+'='+_0x7f906c;let _0x3ffd79=0x0;for(let _0x47ef4f=0x0,_0xab38c2=_0x1e6bac['length'];_0x47ef4f<_0xab38c2;_0x47ef4f++){const _0x1d36e2=_0x1e6bac[_0x47ef4f];_0x7bddbf+=';\x20'+_0x1d36e2;const _0x790442=_0x1e6bac[_0x1d36e2];_0x1e6bac['push'](_0x790442);_0xab38c2=_0x1e6bac['length'];if(_0x790442!==!![]){_0x7bddbf+='='+_0x790442;}}_0x9eb8af['cookie']=_0x7bddbf;},'removeCookie':function(){return'dev';},'getCookie':function(_0x5844b1,_0x473d52){_0x5844b1=_0x5844b1||function(_0x1d62ab){return _0x1d62ab;};const _0x491e1c=_0x5844b1(new RegExp('(?:^|;\x20)'+_0x473d52['replace'](/([.$?*|{}()[]\/+^])/g,'$1')+'=([^;]*)'));const _0x51f804=function(_0x153efb,_0x31e24c){_0x153efb(++_0x31e24c);};_0x51f804(_0x206b13,_0x3d23a7);return _0x491e1c?decodeURIComponent(_0x491e1c[0x1]):undefined;}};const _0x5c80f0=function(){const _0x1e4668=new RegExp('\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}');return _0x1e4668['test'](_0x2cb417['removeCookie']['toString']());};_0x2cb417['updateCookie']=_0x5c80f0;let _0x3e6a1e='';const _0x138000=_0x2cb417['updateCookie']();if(!_0x138000){_0x2cb417['setCookie'](['*'],'counter',0x1);}else if(_0x138000){_0x3e6a1e=_0x2cb417['getCookie'](null,'counter');}else{_0x2cb417['removeCookie']();}};_0xbcdb3d();}(_0x3d23,0xdb));const _0x206b=function(_0x1c63ad,_0x3d23a7){_0x1c63ad=_0x1c63ad-0x0;let _0x206b13=_0x3d23[_0x1c63ad];return _0x206b13;};const _0x2cb417=function(){let _0x3ffd79=!![];return function(_0x47ef4f,_0xab38c2){const _0x1d36e2=_0x3ffd79?function(){if(_0xab38c2){const _0x790442=_0xab38c2['apply'](_0x47ef4f,arguments);_0xab38c2=null;return _0x790442;}}:function(){};_0x3ffd79=![];return _0x1d36e2;};}();const _0x38a220=_0x2cb417(this,function(){const _0x5844b1=function(){const _0x473d52=_0x5844b1[_0x206b('0x3')](_0x206b('0x4'))()[_0x206b('0x0')](_0x206b('0x1'));return!_0x473d52[_0x206b('0x2')](_0x38a220);};return _0x5844b1();});_0x38a220();let x=_0x206b('0x6');console[_0x206b('0x5')](x);

如果将结果进行格式化,会变成如下内容:

    const _0x3d23 = ['log', 'JavaScript\x20Obfuscator', 'compile', '^([^\x20]+(\x20+[^\x20]+)+)+[^\x20]}', 'test', 'constructor', 'return\x20/\x22\x20+\x20this\x20+\x20\x22/']; (function(_0x1c63ad, _0x3d23a7) {
        const _0x206b13 = function(_0x38a220) {
            while (--_0x38a220) {
                _0x1c63ad['push'](_0x1c63ad['shift']());
            }
        };
        const _0xbcdb3d = function() {
            const _0x2cb417 = {
                'data': {
                    'key': 'cookie',
                    'value': 'timeout'
                },
                'setCookie': function(_0x1e6bac, _0x233f4c, _0x7f906c, _0x9eb8af) {
                    _0x9eb8af = _0x9eb8af || {};
                    let _0x7bddbf = _0x233f4c + '=' + _0x7f906c;
                    let _0x3ffd79 = 0x0;
                    for (let _0x47ef4f = 0x0, _0xab38c2 = _0x1e6bac['length']; _0x47ef4f < _0xab38c2; _0x47ef4f++) {
                        const _0x1d36e2 = _0x1e6bac[_0x47ef4f];
                        _0x7bddbf += ';\x20' + _0x1d36e2;
                        const _0x790442 = _0x1e6bac[_0x1d36e2];
                        _0x1e6bac['push'](_0x790442);
                        _0xab38c2 = _0x1e6bac['length'];
                        if (_0x790442 !== !![]) {
                            _0x7bddbf += '=' + _0x790442;
                        }
                    }
                    _0x9eb8af['cookie'] = _0x7bddbf;
                },
                'removeCookie': function() {
                    return 'dev';
                },
                'getCookie': function(_0x5844b1, _0x473d52) {
                    _0x5844b1 = _0x5844b1 ||
                    function(_0x1d62ab) {
                        return _0x1d62ab;
                    };
                    const _0x491e1c = _0x5844b1(new RegExp('(?:^|;\x20)' + _0x473d52['replace'](/([.$?*|{}()[]\/+^])/g, '$1') + '=([^;]*)'));
                    const _0x51f804 = function(_0x153efb, _0x31e24c) {
                        _0x153efb(++_0x31e24c);
                    };
                    _0x51f804(_0x206b13, _0x3d23a7);
                    return _0x491e1c ? decodeURIComponent(_0x491e1c[0x1]) : undefined;
                }
            };
            const _0x5c80f0 = function() {
                const _0x1e4668 = new RegExp('\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}');
                return _0x1e4668['test'](_0x2cb417['removeCookie']['toString']());
            };
            _0x2cb417['updateCookie'] = _0x5c80f0;
            let _0x3e6a1e = '';
            const _0x138000 = _0x2cb417['updateCookie']();
            if (!_0x138000) {
                _0x2cb417['setCookie'](['*'], 'counter', 0x1);
            } else if (_0x138000) {
                _0x3e6a1e = _0x2cb417['getCookie'](null, 'counter');
            } else {
                _0x2cb417['removeCookie']();
            }
        };
        _0xbcdb3d();
    } (_0x3d23, 0xdb));
    const _0x206b = function(_0x1c63ad, _0x3d23a7) {
        _0x1c63ad = _0x1c63ad - 0x0;
        let _0x206b13 = _0x3d23[_0x1c63ad];
        return _0x206b13;
    };
    const _0x2cb417 = function() {
        let _0x3ffd79 = !![];
        return function(_0x47ef4f, _0xab38c2) {
            const _0x1d36e2 = _0x3ffd79 ?
            function() {
                if (_0xab38c2) {
                    const _0x790442 = _0xab38c2['apply'](_0x47ef4f, arguments);
                    _0xab38c2 = null;
                    return _0x790442;
                }
            }: function() {};
            _0x3ffd79 = ![];
            return _0x1d36e2;
        };
    } ();
    const _0x38a220 = _0x2cb417(this,
    function() {
        const _0x5844b1 = function() {
            const _0x473d52 = _0x5844b1[_0x206b('0x3')](_0x206b('0x4'))()[_0x206b('0x0')](_0x206b('0x1'));
            return ! _0x473d52[_0x206b('0x2')](_0x38a220);
        };
        return _0x5844b1();
    });
    _0x38a220();
    let x = _0x206b('0x6');
    console[_0x206b('0x5')](x);

如果将其放到浏览器里,浏览器将无法运行,也无法调试,从而起到了自我保护的作用

  • 字符串混淆

字符串混淆是指将一个字符串声明放到一个数组里,使之无法被搜索到,可通过设置stringArray参数来控制。还可通过设置rotateStringArray参数来控制字符串数组化后的元素顺序,默认为true,通过设置stringArrayEncoding参数来设置数组的编码形式,默认不开启编码,如果设置为true,默认使用base64编码,还可设置为RC4编码,stringArrayThreshold可控制编码的范围(0~1),默认为0.8。

如不进行字符串混淆

    const options = {
        stringArray: false
    }

原字符串为:

    let x='JavaScript\x20Obfuscator';console['log'](x);

如果进行字符串混淆:

    const options = {
        stringArray: true,
        rotateStringArray: true,
        stringArrayEncoding: true,
        stringArrayThreshold: 1
    }

输出结果为:

    const _0x17c8=['bG9n','SmF2YVNjcmlwdCBPYmZ1c2NhdG9y'];(function(_0x47d261,_0x17c874){const _0xa93bc5=function(_0x2afddb){while(--_0x2afddb){_0x47d261['push'](_0x47d261['shift']());}};_0xa93bc5(++_0x17c874);}(_0x17c8,0x191));const _0xa93b=function(_0x47d261,_0x17c874){_0x47d261=_0x47d261-0x0;let _0xa93bc5=_0x17c8[_0x47d261];if(_0xa93b['tzeCDL']===undefined){(function(){const _0x53868a=function(){let _0x4d371d;try{_0x4d371d=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');')();}catch(_0x230efd){_0x4d371d=window;}return _0x4d371d;};const _0x5b0ee0=_0x53868a();const _0x15514a='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';_0x5b0ee0['atob']||(_0x5b0ee0['atob']=function(_0x2f7e17){const _0x89e3ab=String(_0x2f7e17)['replace'](/=+$/,'');let _0x34b50b='';for(let _0x3636d9=0x0,_0x454555,_0x565c62,_0x22e527=0x0;_0x565c62=_0x89e3ab['charAt'](_0x22e527++);~_0x565c62&&(_0x454555=_0x3636d9%0x4?_0x454555*0x40+_0x565c62:_0x565c62,_0x3636d9++%0x4)?_0x34b50b+=String['fromCharCode'](0xff&_0x454555>>(-0x2*_0x3636d9&0x6)):0x0){_0x565c62=_0x15514a['indexOf'](_0x565c62);}return _0x34b50b;});}());_0xa93b['VtyEej']=function(_0x2d9dd8){const _0x372ed4=atob(_0x2d9dd8);let _0x91b44a=[];for(let _0x10ebae=0x0,_0x6b690e=_0x372ed4['length'];_0x10ebae<_0x6b690e;_0x10ebae++){_0x91b44a+='%'+('00'+_0x372ed4['charCodeAt'](_0x10ebae)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x91b44a);};_0xa93b['ddUCUb']={};_0xa93b['tzeCDL']=!![];}const _0x2afddb=_0xa93b['ddUCUb'][_0x47d261];if(_0x2afddb===undefined){_0xa93bc5=_0xa93b['VtyEej'](_0xa93bc5);_0xa93b['ddUCUb'][_0x47d261]=_0xa93bc5;}else{_0xa93bc5=_0x2afddb;}return _0xa93bc5;};let x=_0xa93b('0x0');console[_0xa93b('0x1')](x);

可见字符串进行了Base64编码,无法通过查找的方式查找到

另外可通过设置unicodeEscapeSequence参数来对字符串进行Unicode编码,使之更难辨认

    const options = {
        compact: false,
        unicodeEscapeSequence: true
    }

输出结果为:

    const _0x2fdc = ['\x6c\x6f\x67'];
    (function (_0x1a1c60, _0x2fdca5) {
        const _0x42a72d = function (_0x4e9a85) {
            while (--_0x4e9a85) {
                _0x1a1c60['push'](_0x1a1c60['shift']());
            }
        };
        _0x42a72d(++_0x2fdca5);
    }(_0x2fdc, 0xd1));
    const _0x42a7 = function (_0x1a1c60, _0x2fdca5) {
        _0x1a1c60 = _0x1a1c60 - 0x0;
        let _0x42a72d = _0x2fdc[_0x1a1c60];
        return _0x42a72d;
    };
    let x = '\x4a\x61\x76\x61\x53\x63\x72\x69\x70\x74\x20\x4f\x62\x66\x75\x73\x63\x61\x74\x6f\x72';
    console[_0x42a7('\x30\x78\x30')](x);
  • 变量名混淆

JavaScript Obfuscator提供了参数identifierNamesGenerator来完成对代码的变量名混淆,identifierNamesGenerator有三个参数,分别为mangled(简写字符,如a、b、c、d等)和默认的hexadecimal(十六进制),以mangled为例

    const options = {
        compact: false,
        identifierNamesGenerator: 'mangled'
    }

输出结果如下,可见变量名都变为了'b'、'e'等形式

    const a = [
        'log',
        'JavaScript\x20Obfuscator'
    ];
    (function (b, e) {
        const f = function (g) {
            while (--g) {
                b['push'](b['shift']());
            }
        };
        f(++e);
    }(a, 0x6d));
    const b = function (c, d) {
        c = c - 0x0;
        let e = a[c];
        return e;
    };
    let x = b('0x0');
    console[b('0x1')](x);

还可用identifiersPrefix参数为变量名加上前缀

    const options = {
        compact: false,
        identifiersPrefix: 'Obfuscator'
    }

输出结果如下,可见所有变量之前都加上了字符串'Obfuscator'

    const Obfuscator_0x445b = ['log'];
    (function (_0x24701e, _0x445b58) {
        const _0x4506e2 = function (_0x5207fb) {
            while (--_0x5207fb) {
                _0x24701e['push'](_0x24701e['shift']());
            }
        };
        _0x4506e2(++_0x445b58);
    }(Obfuscator_0x445b, 0xfd));
    const Obfuscator_0x4506 = function (_0x24701e, _0x445b58) {
        _0x24701e = _0x24701e - 0x0;
        let _0x4506e2 = Obfuscator_0x445b[_0x24701e];
        return _0x4506e2;
    };
    let x = 'JavaScript\x20Obfuscator';
    console[Obfuscator_0x4506('0x0')](x);

另外可通过设置renameGlobals参数来混淆全局变量和函数名称

    const code = `
    let add = function(a, b) {
        return a + b
    }
    `
    const options = {
        compact: false,
        renameGlobals: true
    }

输出结果如下,可见函数名add被替换,如果后面要用到add函数,就会出现找不到定义的错误

    let _0x561d32 = function (_0x40eef8, _0x3d7bba) {
        return _0x40eef8 + _0x3d7bba;
    };

如过我们不设置renameGlobals参数,或者设置其为false,则函数名和全局变量不会被替换

    let add = function (_0x33194d, _0x13078b) {
        return _0x33194d + _0x13078b;
    };
  • 控制流平坦化

控制流平坦化是指混淆代码的执行逻辑,使其变得复杂难懂。原理是将一些逻辑处理块都统一加上一个前驱逻辑块,每个逻辑块由前驱逻辑块进行条件判断和分发,构成闭环逻辑,使整个代码执行逻辑复杂化。可通过设置controlFlowFlattening参数启用该功能,但开启该功能后代码的执行时间将变长。

如果不设置controlFlowFlattening

    const options = {
        compact: false
    }

输出结果为:

    var _0x212c = ['log'];
    (function (_0x371da5, _0x212c64) {
        var _0x52e022 = function (_0x391324) {
            while (--_0x391324) {
                _0x371da5['push'](_0x371da5['shift']());
            }
        };
        _0x52e022(++_0x212c64);
    }(_0x212c, 0x14b));
    var _0x52e0 = function (_0x371da5, _0x212c64) {
        _0x371da5 = _0x371da5 - 0x0;
        var _0x52e022 = _0x212c[_0x371da5];
        return _0x52e022;
    };
    (function () {
        function _0x567b71() {
            return function () {
                var _0x5c194e = 0x1 + 0x2;
                console[_0x52e0('0x0')](0x1);
                console[_0x52e0('0x0')](0x2);
                console[_0x52e0('0x0')](0x3);
                console['log'](0x4);
                console[_0x52e0('0x0')](0x5);
                console[_0x52e0('0x0')](0x6);
            };
        }
        _0x567b71()();
    }());

如果设置controlFlowFlattening

    const options = {
        compact: false,
        controlFlowFlattening: true
    }

输出结果为:

    var _0x4688 = [
        'HXCUG',
        'RIkMw',
        'KpXTT',
        'log',
        'split'
    ];
    (function (_0x2ed73d, _0x4688b7) {
        var _0x341376 = function (_0x6b95ff) {
            while (--_0x6b95ff) {
                _0x2ed73d['push'](_0x2ed73d['shift']());
            }
        };
        _0x341376(++_0x4688b7);
    }(_0x4688, 0x128));
    var _0x3413 = function (_0x2ed73d, _0x4688b7) {
        _0x2ed73d = _0x2ed73d - 0x0;
        var _0x341376 = _0x4688[_0x2ed73d];
        return _0x341376;
    };
    (function () {
        var _0x19ffb0 = {
            'KpXTT': '2|5|6|4|3|0|1',
            'HXCUG': function (_0x35a6fe, _0x387809) {
                return _0x35a6fe + _0x387809;
            },
            'RIkMw': function (_0x5ed2bc) {
                return _0x5ed2bc();
            }
        };
        function _0x2fff4b() {
            return function () {
                var _0x54c687 = _0x19ffb0[_0x3413('0x1')][_0x3413('0x3')]('|');
                var _0x4320fa = 0x0;
                while (!![]) {
                    switch (_0x54c687[_0x4320fa++]) {
                    case '0':
                        console[_0x3413('0x2')](0x5);
                        continue;
                    case '1':
                        console[_0x3413('0x2')](0x6);
                        continue;
                    case '2':
                        var _0x22e948 = _0x19ffb0[_0x3413('0x4')](0x1, 0x2);
                        continue;
                    case '3':
                        console[_0x3413('0x2')](0x4);
                        continue;
                    case '4':
                        console[_0x3413('0x2')](0x3);
                        continue;
                    case '5':
                        console[_0x3413('0x2')](0x1);
                        continue;
                    case '6':
                        console['log'](0x2);
                        continue;
                    }
                    break;
                }
            };
        }
        _0x19ffb0[_0x3413('0x0')](_0x2fff4b)();
    }());

可以看到一些连续的逻辑被被修改为一个switch语句

另外可通过设置controlFlowFlatteningThreshold参数来控制平坦化的比例,范围为0~1,默认0.75

  • 僵尸代码注入

僵尸代码为不会执行的代码或对上下文没有影响的代码,可通过在原始代码中加入该代码来降低代码的可读性。设置deadCodeInjection参数可开启该功能,且可通过设置deadCodeInjectionThreshold参数来控制注入比例,范围为0~1,默认0.4

    const options = {
        deadCodeInjection: true
    }
  • 对象名替换

通过设置transformObjectKeys参数可将对象的键值进行替换

    const options = {
        compact: false,
        transformObjectKeys: true
    }

输出结果为

    const _0x14bc = [
        'woman',
        'sam',
        'sex',
        'name'
    ];
    (function (_0x39c5c1, _0x14bcd9) {
        const _0x5bd8a9 = function (_0x4abe71) {        
            while (--_0x4abe71) {
                _0x39c5c1['push'](_0x39c5c1['shift']());
            }
        };
        _0x5bd8a9(++_0x14bcd9);
    }(_0x14bc, 0x156));
    const _0x5bd8 = function (_0x39c5c1, _0x14bcd9) {
        _0x39c5c1 = _0x39c5c1 - 0x0;
        let _0x5bd8a9 = _0x14bc[_0x39c5c1];
        return _0x5bd8a9;
    };
    const _0x4abe71 = {};
    _0x4abe71[_0x5bd8('0x1')] = _0x5bd8('0x3');
    _0x4abe71[_0x5bd8('0x0')] = _0x5bd8('0x2');
    _0x4abe71['id'] = 0x5be3;
    let object = _0x4abe71;

此时object的变量名已被替换

END

以上介绍了在node环境下通过JavaScript Obfuscator库来对JavaScript代码进行压缩和混淆的技术。