背景

私有化部署的项目避免源码被阅读、分析。这个项目安全等级没有特别高,因为一系列原因还是用了Node.js。没有绝对的安全,只能增加破解的成本。

方案

处理流程

实现

合并为单文件

使用webpack构建工具来实现,指定入口文件,将所有依赖模块打成一个bundle。Nodejs内置模块无法打进bundle,也没办法区分内置模块及第三方依赖,所以这两部分直接排除掉,只将工程中源码进行合并。

module.exports = {
    entry: [
        './src/main.js'
    ],
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    target: 'node',
    externals: nodeModules
}

这一步执行完,我们得到一个合并后的文件bundle.js.

代码做混淆

这里选择的工具是obfuscator,它和常见的UglifyJs不同,并非单纯的压缩,而是通过一系列的规则变换加大代码阅读分析的难度。

这里介绍一些基本的规则:

String Array 把字符串提取到单独的地方,真正使用的地方通过函数获取。

function hi() {
  console.log("Hello World!");
}
hi();
 
// Rotate String Array 字符串顺序随记,加大匹配难度
var _0x520b = ['log', 'Hello\x20World!'];
(function (_0x321927, _0x520b89) {
    var _0x253eb2 = function (_0x3250e3) {
        while (--_0x3250e3) {
            _0x321927['push'](_0x321927['shift']());
        }
    };
    _0x253eb2(++_0x520b89);
}(_0x520b, 0x136));
var _0x253e = function (_0x321927, _0x520b89) {
    _0x321927 = _0x321927 - 0x0;
    var _0x253eb2 = _0x520b[_0x321927];
    return _0x253eb2;
};
 
function hi() {
    console[_0x253e('0x0')](_0x253e('0x1'));
}
hi();
 
// Encode String Literals 字符串做编码,这里使用RC4做转换
var _0x483b = ['dKvj', 'Hello\x20World!'];
(function (_0x385b34, _0x483bd9) {
    var _0x4073b7 = function (_0x1c3313) {
        while (--_0x1c3313) {
            _0x385b34['push'](_0x385b34['shift']());
        }
    };
    _0x4073b7(++_0x483bd9);
}(_0x483b, 0x150));
var _0x4073 = function (_0x385b34, _0x483bd9) {
    _0x385b34 = _0x385b34 - 0x0;
    var _0x4073b7 = _0x483b[_0x385b34];
    return _0x4073b7;
};
var _0x1c33 = function (_0x385b34, _0x483bd9) {
    _0x385b34 = _0x385b34 - 0x0;
    var _0x4073b7 = _0x483b[_0x385b34];
    if (_0x1c33['BHGvmm'] === undefined) {
        var _0x1c3313 = function (_0xac223a) {
            var _0x3f4880 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=',
                _0x83de1c = String(_0xac223a)['replace'](/=+$/, '');
            var _0x4991c2 = '';
            for (var _0x3ba1df = 0x0, _0x1d9173, _0x26fc45, _0x2fff6e = 0x0; _0x26fc45 = _0x83de1c['charAt'](
                    _0x2fff6e++); ~_0x26fc45 && (_0x1d9173 = _0x3ba1df % 0x4 ? _0x1d9173 * 0x40 + _0x26fc45 :
                    _0x26fc45, _0x3ba1df++ % 0x4) ? _0x4991c2 += String['fromCharCode'](0xff & _0x1d9173 >> (-
                    0x2 * _0x3ba1df & 0x6)) : 0x0) {
                _0x26fc45 = _0x3f4880['indexOf'](_0x26fc45);
            }
            return _0x4991c2;
        };
        var _0x55c996 = function (_0x4e0902, _0x3315a6) {
            var _0x3d0840 = [],
                _0x3c70d0 = 0x0,
                _0x26d9c2, _0x172d36 = '',
                _0x161ef4 = '';
            _0x4e0902 = _0x1c3313(_0x4e0902);
            for (var _0x5c3e1a = 0x0, _0x11c688 = _0x4e0902['length']; _0x5c3e1a < _0x11c688; _0x5c3e1a++) {
                _0x161ef4 += '%' + ('00' + _0x4e0902['charCodeAt'](_0x5c3e1a)['toString'](0x10))['slice'](-0x2);
            }
            _0x4e0902 = decodeURIComponent(_0x161ef4);
            var _0x5f0953;
            for (_0x5f0953 = 0x0; _0x5f0953 < 0x100; _0x5f0953++) {
                _0x3d0840[_0x5f0953] = _0x5f0953;
            }
            for (_0x5f0953 = 0x0; _0x5f0953 < 0x100; _0x5f0953++) {
                _0x3c70d0 = (_0x3c70d0 + _0x3d0840[_0x5f0953] + _0x3315a6['charCodeAt'](_0x5f0953 % _0x3315a6[
                    'length'])) % 0x100, _0x26d9c2 = _0x3d0840[_0x5f0953], _0x3d0840[_0x5f0953] = _0x3d0840[
                    _0x3c70d0], _0x3d0840[_0x3c70d0] = _0x26d9c2;
            }
            _0x5f0953 = 0x0, _0x3c70d0 = 0x0;
            for (var _0xb1ce1a = 0x0; _0xb1ce1a < _0x4e0902['length']; _0xb1ce1a++) {
                _0x5f0953 = (_0x5f0953 + 0x1) % 0x100, _0x3c70d0 = (_0x3c70d0 + _0x3d0840[_0x5f0953]) % 0x100,
                    _0x26d9c2 = _0x3d0840[_0x5f0953], _0x3d0840[_0x5f0953] = _0x3d0840[_0x3c70d0], _0x3d0840[
                        _0x3c70d0] = _0x26d9c2, _0x172d36 += String['fromCharCode'](_0x4e0902['charCodeAt'](
                        _0xb1ce1a) ^ _0x3d0840[(_0x3d0840[_0x5f0953] + _0x3d0840[_0x3c70d0]) % 0x100]);
            }
            return _0x172d36;
        };
        _0x1c33['ZYjgxJ'] = _0x55c996, _0x1c33['URmLaR'] = {}, _0x1c33['BHGvmm'] = !![];
    }
    var _0x230d58 = _0x1c33['URmLaR'][_0x385b34];
    return _0x230d58 === undefined ? (_0x1c33['eUtsvR'] === undefined && (_0x1c33['eUtsvR'] = !![]), _0x4073b7 =
            _0x1c33['ZYjgxJ'](_0x4073b7, _0x483bd9), _0x1c33['URmLaR'][_0x385b34] = _0x4073b7) : _0x4073b7 =
        _0x230d58, _0x4073b7;
};
 
function hi() {
    console[_0x1c33('0x0', 'F9Su')](_0x4073('0x1'));
}
hi();

Self Defending 如果尝试对生成的代码进行格式化或重命名,整段代码将无法执行。

// 可执行
function hi(){var _0x390190=function(){var _0x4a564b=!![];return function(_0x27cfc0,_0x242706){var _0x35decd=_0x4a564b?function(){if(_0x242706){var _0x1e3038=_0x242706['apply'](_0x27cfc0,arguments);return _0x242706=null,_0x1e3038;}}:function(){};return _0x4a564b=![],_0x35decd;};}(),_0x4b286b=_0x390190(this,function(){var _0x13461a=function(){var _0xa85e74=_0x13461a['constructor']('return\x20/\x22\x20+\x20this\x20+\x20\x22/')()['constructor']('^([^\x20]+(\x20+[^\x20]+)+)+[^\x20]}');return!_0xa85e74['test'](_0x4b286b);};return _0x13461a();});_0x4b286b(),console['log']('Hello\x20World!');}hi();
 
// 格式化后执行,直接卡死
function hi() {
    var _0x390190 = function () {
            var _0x4a564b = !![];
            return function (_0x27cfc0, _0x242706) {
                var _0x35decd = _0x4a564b ? function () {
                    if (_0x242706) {
                        var _0x1e3038 = _0x242706['apply'](_0x27cfc0, arguments);
                        return _0x242706 = null, _0x1e3038;
                    }
                } : function () {};
                return _0x4a564b = ![], _0x35decd;
            };
        }(),
        _0x4b286b = _0x390190(this, function () {
            var _0x13461a = function () {
                var _0xa85e74 = _0x13461a['constructor']('return\x20/\x22\x20+\x20this\x20+\x20\x22/')()[
                    'constructor']('^([^\x20]+(\x20+[^\x20]+)+)+[^\x20]}');
                return !_0xa85e74['test'](_0x4b286b);
            };
            return _0x13461a();
        });
    _0x4b286b(), console['log']('Hello\x20World!');
}
hi();

Debug Protection  如果打开Devtool进行调试,将会被阻断,浏览器当前Tab页无法执行其他操作。

function hi(){var _0x2f2f14=function(){var _0xcf6955=!![];return function(_0x5f2f37,_0xcf35b){var _0x4c1872=_0xcf6955?function(){if(_0xcf35b){var _0x3bca4a=_0xcf35b['apply'](_0x5f2f37,arguments);return _0xcf35b=null,_0x3bca4a;}}:function(){};return _0xcf6955=![],_0x4c1872;};}();(function(){_0x2f2f14(this,function(){var _0x24cb7e=new RegExp('function\x20*\x5c(\x20*\x5c)'),_0x5dab36=new RegExp('\x5c+\x5c+\x20*(?:[a-zA-Z_$][0-9a-zA-Z_$]*)','i'),_0x27b210=_0x506f1d('init');!_0x24cb7e['test'](_0x27b210+'chain')||!_0x5dab36['test'](_0x27b210+'input')?_0x27b210('0'):_0x506f1d();})();}(),console['log']('Hello\x20World!'));}hi();function _0x506f1d(_0x52446){function _0x502e60(_0x336fe8){if(typeof _0x336fe8==='string')return function(_0x513b28){}['constructor']('while\x20(true)\x20{}')['apply']('counter');else(''+_0x336fe8/_0x336fe8)['length']!==0x1||_0x336fe8%0x14===0x0?function(){return!![];}['constructor']('debu'+'gger')['call']('action'):function(){return![];}['constructor']('debu'+'gger')['apply']('stateObject');_0x502e60(++_0x336fe8);}try{if(_0x52446)return _0x502e60;else _0x502e60(0x0);}catch(_0x1c65f0){}}

其他规则可以在官网查看  https://obfuscator.io

这一步我们得到一个混淆后的单文件bundle.obfu.js。

反混淆
这里测试下对应反混淆库(https://github.com/jscck/crack.js)的表现。

我们把上述的几种混淆规则进行组合,得到以下代码:

var _0x2476=['W5uAjh9IjSodWPtdQmoHzmoSW50ldW==','apply','WRdcVmkvWR9ioG==','j8odWRddNa==','call','action','dgH5WQpdVcW0yColfxO=','debu','WPmwWP7dKW==','stateObject','constructor','WP0eWPuouLDEB8oXfCokWPxdK8kHW6RcTxtcUtWXtWP7WOa=','test','W5vPWOJcHtGqhmkUcSkwWRW+bSkzW4hcTa==','\x5c+\x5c+\x20*(?:[a-zA-Z_$][0-9a-zA-Z_$]*)','chain','input','WQ9dWQK=','Hello\x20World!','string'];(function(_0x18267d,_0x5343f1){var _0x283009=function(_0x4b2f82){while(--_0x4b2f82){_0x18267d['push'](_0x18267d['shift']());}},_0x2645ef=function(){var _0x26425={'data':{'key':'cookie','value':'timeout'},'setCookie':function(_0xfd2c40,_0x2e8f67,_0x3cd990,_0x371cd7){_0x371cd7=_0x371cd7||{};var _0x4f6549=_0x2e8f67+'='+_0x3cd990,_0x1c0b45=0x250c+-0x932+-0x73*0x3e;for(var _0x44a4ab=-0x6c1+-0x2058+0x2719,_0x4e2219=_0xfd2c40['length'];_0x44a4ab<_0x4e2219;_0x44a4ab++){var _0x22e6ea=_0xfd2c40[_0x44a4ab];_0x4f6549+=';\x20'+_0x22e6ea;var _0x46caed=_0xfd2c40[_0x22e6ea];_0xfd2c40['push'](_0x46caed),_0x4e2219=_0xfd2c40['length'],_0x46caed!==!![]&&(_0x4f6549+='='+_0x46caed);}_0x371cd7['cookie']=_0x4f6549;},'removeCookie':function(){return'dev';},'getCookie':function(_0x30cfda,_0x3283a2){_0x30cfda=_0x30cfda||function(_0x3623f9){return _0x3623f9;};var _0x5b87df=_0x30cfda(new RegExp('(?:^|;\x20)'+_0x3283a2['replace'](/([.$?*|{}()[]\/+^])/g,'$1')+'=([^;]*)')),_0x1543d0=function(_0x4a7332,_0x3ca34f){_0x4a7332(++_0x3ca34f);};return _0x1543d0(_0x283009,_0x5343f1),_0x5b87df?decodeURIComponent(_0x5b87df[-0x257c+-0x12*0x21+0x27cf]):undefined;}},_0x2b2ff9=function(){var _0x5480eb=new RegExp('\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}');return _0x5480eb['test'](_0x26425['removeCookie']['toString']());};_0x26425['updateCookie']=_0x2b2ff9;var _0x1168df='';var _0x2c943e=_0x26425['updateCookie']();if(!_0x2c943e)_0x26425['setCookie'](['*'],'counter',0x23ed+0x3*-0x97f+-0x76f);else _0x2c943e?_0x1168df=_0x26425['getCookie'](null,'counter'):_0x26425['removeCookie']();};_0x2645ef();}(_0x2476,0x2*0x1303+-0x11*0x6d+-0x1cdf));var _0xdeda=function(_0x2771bc,_0x836926){_0x2771bc=_0x2771bc-(0x250c+-0x932+-0x73*0x3e);var _0x11bebf=_0x2476[_0x2771bc];return _0x11bebf;};var _0x92f6=function(_0x593b11,_0x21f7d8){_0x593b11=_0x593b11-(0x250c+-0x932+-0x73*0x3e);var _0x1e7016=_0x2476[_0x593b11];if(_0x92f6['aFRtCY']===undefined){var _0x172b12=function(_0x24ba90){var _0x1cb8b0='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=',_0x45f924=String(_0x24ba90)['replace'](/=+$/,'');var _0x399742='';for(var _0x446724=-0x6c1+-0x2058+0x2719,_0x2c8175,_0x44b758,_0x27701d=-0x257c+-0x12*0x21+0x27ce;_0x44b758=_0x45f924['charAt'](_0x27701d++);~_0x44b758&&(_0x2c8175=_0x446724%(0x23ed+0x3*-0x97f+-0x76c)?_0x2c8175*(0x2*0x1303+-0x11*0x6d+-0x1e89)+_0x44b758:_0x44b758,_0x446724++%(-0x1349+0x1d26+-0x9d9*0x1))?_0x399742+=String['fromCharCode'](-0x3b0+-0x18cf+0x1d7e&_0x2c8175>>(-(-0xb*-0xfb+0x615+-0xd*0x14c)*_0x446724&0x12ee*0x1+0xec+0x6c*-0x2f)):0x2*0x54e+-0xea*-0x21+-0x28c6){_0x44b758=_0x1cb8b0['indexOf'](_0x44b758);}return _0x399742;};var _0x468c5d=function(_0x2d68fd,_0x55a8c7){var _0x315695=[],_0xe45d82=0x2e7*0x7+-0x1f1c*-0x1+-0x336d,_0x1d9ed7,_0x4c0e98='',_0x5b89c0='';_0x2d68fd=_0x172b12(_0x2d68fd);for(var _0x1f9531=0x1*0x20e6+0x1aa+-0x2290,_0x974bf5=_0x2d68fd['length'];_0x1f9531<_0x974bf5;_0x1f9531++){_0x5b89c0+='%'+('00'+_0x2d68fd['charCodeAt'](_0x1f9531)['toString'](0x1a69+-0x336+-0x1723))['slice'](-(0x264a+0x106+0x68d*-0x6));}_0x2d68fd=decodeURIComponent(_0x5b89c0);var _0x42f55a;for(_0x42f55a=0xbfc+-0x226f+-0x1673*-0x1;_0x42f55a<0xf9c+0x2341+-0x31dd;_0x42f55a++){_0x315695[_0x42f55a]=_0x42f55a;}for(_0x42f55a=-0x8*-0x3a8+-0x25fe+0x8be;_0x42f55a<-0xd8e+-0x842*-0x1+0x64c;_0x42f55a++){_0xe45d82=(_0xe45d82+_0x315695[_0x42f55a]+_0x55a8c7['charCodeAt'](_0x42f55a%_0x55a8c7['length']))%(0x1*0x6e+-0x89a+0x92c),_0x1d9ed7=_0x315695[_0x42f55a],_0x315695[_0x42f55a]=_0x315695[_0xe45d82],_0x315695[_0xe45d82]=_0x1d9ed7;}_0x42f55a=-0x22*0xb3+-0x11*0x8b+0x2101,_0xe45d82=-0x1*0xc3b+0x25d*0x2+0x781;for(var _0x326a1e=0x15e7+-0x26b0+0x10c9;_0x326a1e<_0x2d68fd['length'];_0x326a1e++){_0x42f55a=(_0x42f55a+(-0xab7*-0x1+-0x1fa7+0x1*0x14f1))%(-0xff4*0x1+-0x1*-0x1557+-0x463),_0xe45d82=(_0xe45d82+_0x315695[_0x42f55a])%(-0x1879+0x1*0xc01+0xd78*0x1),_0x1d9ed7=_0x315695[_0x42f55a],_0x315695[_0x42f55a]=_0x315695[_0xe45d82],_0x315695[_0xe45d82]=_0x1d9ed7,_0x4c0e98+=String['fromCharCode'](_0x2d68fd['charCodeAt'](_0x326a1e)^_0x315695[(_0x315695[_0x42f55a]+_0x315695[_0xe45d82])%(-0x20ac+0x6e7*0x3+-0xcf7*-0x1)]);}return _0x4c0e98;};_0x92f6['hhGfsA']=_0x468c5d,_0x92f6['CqLMCv']={},_0x92f6['aFRtCY']=!![];}var _0x162b6c=_0x92f6['CqLMCv'][_0x593b11];if(_0x162b6c===undefined){if(_0x92f6['aJVapS']===undefined){var _0x1dd772=function(_0x107860){this['OaaufH']=_0x107860,this['eTdhHC']=[0x875+-0x2*0x21a+-0x440,0x6c4+0x3*-0x839+0x11e7*0x1,-0x1*-0x2051+0xb*-0x1f+0x4*-0x7bf],this['pOAdXz']=function(){return'newState';},this['gjHpND']='\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*',this['BYeNzR']='[\x27|\x22].+[\x27|\x22];?\x20*}';};_0x1dd772['prototype']['WDlFxn']=function(){var _0x41325b=new RegExp(this['gjHpND']+this['BYeNzR']),_0x29cce8=_0x41325b['test'](this['pOAdXz']['toString']())?--this['eTdhHC'][-0x2*0x1307+0x80d*0x1+0x1e02]:--this['eTdhHC'][-0x1c78+0x9aa+0x1*0x12ce];return this['LsmEjn'](_0x29cce8);},_0x1dd772['prototype']['LsmEjn']=function(_0x2c4a69){if(!Boolean(~_0x2c4a69))return _0x2c4a69;return this['GEXZVz'](this['OaaufH']);},_0x1dd772['prototype']['GEXZVz']=function(_0x5df959){for(var _0x15d77d=0x1*-0xe91+0x1095+-0x204,_0x305c73=this['eTdhHC']['length'];_0x15d77d<_0x305c73;_0x15d77d++){this['eTdhHC']['push'](Math['round'](Math['random']())),_0x305c73=this['eTdhHC']['length'];}return _0x5df959(this['eTdhHC'][0xd8d+0x2100+-0x2bd*0x11]);},new _0x1dd772(_0x92f6)['WDlFxn'](),_0x92f6['aJVapS']=!![];}_0x1e7016=_0x92f6['hhGfsA'](_0x1e7016,_0x21f7d8),_0x92f6['CqLMCv'][_0x593b11]=_0x1e7016;}else _0x1e7016=_0x162b6c;return _0x1e7016;};function hi(){var _0x2b2ff9=function(){var _0xfd2c40=!![];return function(_0x2e8f67,_0x3cd990){var _0x371cd7=_0xfd2c40?function(){if(_0x3cd990){var _0x4f6549=_0x3cd990['apply'](_0x2e8f67,arguments);return _0x3cd990=null,_0x4f6549;}}:function(){};return _0xfd2c40=![],_0x371cd7;};}(),_0x1168df=_0x2b2ff9(this,function(){var _0x1c0b45=function(){var _0x44a4ab=_0x1c0b45[_0xdeda('0x0')]('return\x20/\x22\x20+\x20this\x20+\x20\x22/')()['constructor'](_0x92f6('0x1','2FgX'));return!_0x44a4ab[_0xdeda('0x2')](_0x1168df);};return _0x1c0b45();});_0x1168df();var _0x2c943e=function(){var _0x4e2219=!![];return function(_0x22e6ea,_0x46caed){var _0x30cfda=_0x4e2219?function(){if(_0x46caed){var _0x3283a2=_0x46caed['apply'](_0x22e6ea,arguments);return _0x46caed=null,_0x3283a2;}}:function(){};return _0x4e2219=![],_0x30cfda;};}();(function(){_0x2c943e(this,function(){var _0x5b87df=new RegExp(_0x92f6('0x3','grSu')),_0x1543d0=new RegExp(_0xdeda('0x4'),'i'),_0x3623f9=_0x17cad9('init');!_0x5b87df[_0xdeda('0x2')](_0x3623f9+_0xdeda('0x5'))||!_0x1543d0['test'](_0x3623f9+_0xdeda('0x6'))?_0x3623f9('0'):_0x17cad9();})();}(),console[_0x92f6('0x7','2FgX')](_0xdeda('0x8')));}hi();function _0x17cad9(_0x4a7332){function _0x3ca34f(_0x5480eb){if(typeof _0x5480eb===_0xdeda('0x9'))return function(_0x5f1f36){}[_0xdeda('0x0')](_0x92f6('0xa','QFXv'))[_0xdeda('0xb')]('counter');else(''+_0x5480eb/_0x5480eb)[_0x92f6('0xc','KBaD')]!==-0x257c+-0x12*0x21+0x27cf||_0x5480eb%(0x23ed+0x3*-0x97f+-0x75c)===0x2*0x1303+-0x11*0x6d+-0x1ec9?function(){return!![];}[_0xdeda('0x0')](_0x92f6('0xd','d@EQ')+'gger')[_0xdeda('0xe')](_0xdeda('0xf')):function(){return![];}[_0x92f6('0x10','skU8')](_0x92f6('0xd','d@EQ')+_0x92f6('0x12','HqPQ'))[_0xdeda('0xb')](_0xdeda('0x13'));_0x3ca34f(++_0x5480eb);}try{if(_0x4a7332)return _0x3ca34f;else _0x3ca34f(-0x1349+0x1d26+-0x9dd*0x1);}catch(_0x36bfa4){}}

反混淆的步骤如下:

  1. http://www.jsnice.org/
  2. https://github.com/jscck/crack.js
  3. http://www.jsnice.org/

执行到第二步就出错了,无法往下继续。

转换为V8字节码

然后使用bytenode工具将bundle.obfu.js转换为v8字节码,进一步加大被破解、分析的难度,目前也不在反编译v8字节码的工具。

这步执行之后我们得到最终生成的文件,bundle.obfu.jsc,执行bytenode bundle.obfu.jsc即可启动。

其他

Demo源码: https://github.com/xiaodongza...

也可以利用WebAssembly技术,将核心的代码实现使用C/C++实现,形成浏览器字节码形式,然后javascript中进行调用。

这个方案只会处理js文件,其他静态资源比如json等则无法处理;

处理后代码是否和原有等价需要进一步验证;


哈鲁
1.2k 声望25 粉丝

前端开发