总结不同的混淆,可以搭配使用,有增加代码量、使执行流程混乱、使眼睛看起来花…,但它们都是给逆向者增加分析难度完整代码:https://pan.bigdataboy.cn/s/G2Msx验证源代码未混淆Date.prototype.format=function(formatStr){varstr=formatStr;str=str.replace(/yyyy|YYYY/,this.getFullYear());str=str.replace(/MM/,this.getMonth()+1>9?(this.getMonth()+1).toString():'0'+(this.getMonth()+1));//encryptAsciistr=str.replace(/dd|DD/,this.getDate()>9?this.getDate().toString():'0'+this.getDate());//encryptreturnstr;};console.log(newDate().format('yyyy-MM-dd'));混淆后vararr=["cHJvdG90eXBl","Zm9ybWF0","cmVwbGFjZQ==","Z2V0RnVsbFllYXI=","Z2V0TW9udGg=","dG9TdHJpbmc=","MA==","Z2V0RGF0ZQ==","Y29uc29sZQ==","bG9n","eXl5eS1NTS1kZA==","RGF0ZQ=="];((arr,nums)=>{while(--nums){arr["\x70\x75\x73\x68"](arr["\x73\x68\x69\x66\x74"]());}})(arr,arr["\x6c\x65\x6e\x67\x74\x68"]);window[atob(arr[0])][atob(arr[1])][atob(arr[2])]=function(OOOOOO){var_array="6|14|4|13|15|5|8|0|12|1|16|7|10|3|2|9|11".split("|"),_index=0;while(!![]){switch(+_array[_index++]){case0:function_xxx5(a,b){returna+b;}continue;case1:function_xxx3(a,b){returna^b;}continue;case2:eval(String.fromCharCode(79,79,79,79,79,111,32,61,32,79,79,79,79,79,111,91,97,116,111,98,40,97,114,114,91,51,93,41,93,40,47,77,77,47,44,32,95,120,120,120,40,95,120,120,120,50,40,116,104,105,115,91,97,116,111,98,40,97,114,114,91,53,93,41,93,40,41,44,32,95,120,120,120,51,40,54,48,56,56,54,51,44,32,54,48,56,56,54,50,41,41,44,32,95,120,120,120,52,40,54,57,53,55,54,48,44,32,54,57,53,55,54,57,41,41,32,63,32,95,120,120,120,53,40,116,104,105,115,91,97,116,111,98,40,97,114,114,91,53,93,41,93,40,41,44,32,95,120,120,120,54,40,49,56,53,56,57,55,44,32,49,56,53,56,57,54,41,41,91,97,116,111,98,40,97,114,114,91,54,93,41,93,40,41,32,58,32,95,120,120,120,55,40,97,116,111,98,40,97,114,114,91,55,93,41,44,32,95,120,120,120,56,40,116,104,105,115,91,97,116,111,98,40,97,114,114,91,53,93,41,93,40,41,44,32,95,120,120,120,57,40,53,51,49,48,54,54,44,32,53,51,49,48,54,55,41,41,41,41,59));continue;case3:OOOOOo=OOOOOo[atob(arr[3])](/yyyy|YYYY/,this[atob(arr[4])]());continue;case4:function_xxx10(a,b){returna>b;}continue;case5:function_xxx7(a,b){returna+b;}continue;case6:function_xxx12(a,b){returna+b;}continue;case7:function_xxx(a,b){returna>b;}continue;case8:function_xxx6(a,b){returna^b;}continue;case9:eval(atob("T09PT09vID0gT09PT09vW2F0b2IoYXJyWzNdKV0oL2RkfERELywgX3h4eDEwKHRoaXNbYXRvYihhcnJbOF0pXSgpLCBfeHh4MTEoMjAxOTEyLCAyMDE5MDUpKSA/IHRoaXNbYXRvYihhcnJbOF0pXSgpW2F0b2IoYXJyWzZdKV0oKSA6IF94eHgxMihhdG9iKGFycls3XSksIHRoaXNbYXRvYihhcnJbOF0pXSgpKSk7"));continue;case10:varOOOOOo=OOOOOO;continue;case11://encryptreturnOOOOOo;continue;case12:function_xxx4(a,b){returna^b;}continue;case13:function_xxx9(a,b){returna^b;}continue;case14:function_xxx11(a,b){returna^b;}continue;case15:function_xxx8(a,b){returna+b;}continue;case16:function_xxx2(a,b){returna+b;}continue;}}};window[atob(arr[9])][atob(arr[10])](newwindow[atob(arr[0])]()[atob(arr[2])](atob(arr[11])));
特别说明:本文生成节点时,函数的使用对于Js来说是错误的(不能指定参数给值),这样写是方便看节点属性思路流程平坦化有好几种形式,结果都是打乱代码执行流程,原理都一样,while-switch、for-switch当然阿里极验的平坦流混淆,更复杂,这里只是最简单的,分发器还能看见while(!![]){//一个死循环switch(...){//每次循环改变switch的判断,而走不同的分支case"0":...case"1":...}}实现获取代码块,映射代码块的执行循序{index:i,value:代码块节点},然后打乱执行顺序,构造分发器,构建死循环,再构建switch分支,替换原来的代码块//流程平坦流混淆traverse(ast,{FunctionExpression(path){letblockStatement=path.node.body;//映射语句执行顺序letstatement=blockStatement.body.map((v,i)=>{return{index:i,value:v}})//流程打乱语句for(leti=1;i<statement.length;i++){constj=Math.floor(Math.random()*(i+1));[statement[i],statement[j]]=[statement[j],statement[i]];}//构建分发器,创建swichCase数组letdispenserArr=[];//流程分发数组letcases=[];//构建cases节点statement.map((v,i)=>{dispenserArr[v.index]=i;letswitchCase=type.switchCase(test=type.numericLiteral(value=i),consequent=[v.value,type.continueStatement()])cases.push(switchCase);})//生成_array和_index标识符,利用BableAPI保证不重名letarray=path.scope.generateUidIdentifier('array')letindex=path.scope.generateUidIdentifier('index')//生成var_array='0|2|2'.spiit('|'),index=0;节点letdispenserStr=dispenserArr.join('|');letdispenser=type.variableDeclaration(kind='var',declarations=[type.variableDeclarator(id=array,init=type.callExpression(callee=type.memberExpression(object=type.stringLiteral(value=dispenserStr),property=type.identifier(name='split')),_arguments=[type.stringLiteral(value='|')]),),type.variableDeclarator(id=index,init=type.numericLiteral(value=0))])//生成while-swiich节点letwhileSta=type.whileStatement(test=type.unaryExpression(//while循环条件节点operator='!',argument=type.unaryExpression(operator='!',argument=type.arrayExpression(elements=[]))),body=type.blockStatement(//swiich节点body=[type.switchStatement(discriminant=type.unaryExpression(//构建switch判断部分switch(+array[index++])operator='+',argument=type.memberExpression(object=array,property=type.updateExpression(operator='++',argument=index),computed=true),),cases=cases)//case节点]))//用分发器和while循环来替换原有的节点path.get('body').replaceWith(type.blockStatement(body=[dispenser,whileSta]))}})
思路这种加密与逐行加密区别不大,只是去掉了字符串加密函数,改用charCodeAt将字符串转到ASCII码,把解密函数换成String.fromCharCode,最后使用eval执行//代码逐行ASCII码混淆traverse(ast,{FunctionExpression(path){letblockStatement=path.node.bodyletstatement=blockStatement.body.map((v)=>{if(type.isReturnStatement(v)){returnv;//返回节点处理}v.leadingComments&&v.leadingComments[0].value.replace('','')=='encryptAscii'&&deletev.leadingComments;//删加密标识注释的兼容处理//判断是否是注释行if(!(v.trailingComments&&v.trailingComments[0].value.replace('','')=='encryptAscii')){returnv}deletev.trailingComments;//删除注释//遍历生成eval(String.fromCharCode())这种节点letcode=generator(v).code;letcodeAscaii=[].map.call(code,(v)=>{returntype.numericLiteral(v.charCodeAt(0))});//生成节点returntype.expressionStatement(expression=type.callExpression(callee=type.identifier('eval'),_arguments=[type.callExpression(callee=type.memberExpression(object=type.identifier(name='String'),property=type.identifier(name='fromCharCode')),_arguments=codeAscaii)]))})path.get('body').replaceWith(type.blockStatement(statement))//替换}})
特别说明:本文生成节点时,函数的使用对于Js来说是错误的(不能指定参数给值),这样写是方便看节点属性思路遍历FunctionExpression函数节点,然后把其中的body逐行加密,然后使用时解密,使用eval执行,但是大范围使用,特征太明显,所以可以在源代码中写上需要加密行的标上注释,就可以指定关键行,使用这种混淆方式letencFun=(str)=>{returnBuffer.from(str,'utf-8').toString('base64');}traverse(ast,{FunctionExpression(path){letblockStatement=path.node.bodyletbody=blockStatement.body.map((v)=>{if(type.isReturnStatement(v)){returnv;//返回节点处理}//遍历生成eval(stob('xxx'))这种节点letcode=generator(v).code;letcipherText=encFun(code);returntype.expressionStatement(expression=type.callExpression(callee=type.identifier(name='eval'),_arguments=[type.callExpression(callee=type.identifier('atob'),_arguments=[type.stringLiteral(cipherText)])]))})path.get('body').replaceWith(type.blockStatement(body=body))//替换}})指定行指定行就是判断这一行是否有指定的注释,然后再把注释删掉//指定行加密letencFun=(str)=>{returnBuffer.from(str,'utf-8').toString('base64');}traverse(ast,{FunctionExpression(path){letblockStatement=path.node.bodyletstatement=blockStatement.body.map((v)=>{if(type.isReturnStatement(v)){returnv;//返回节点处理}v.leadingComments&&v.leadingComments[0].value.replace('','')=='encrypt'&&deletev.leadingComments;//删加密标识注释的兼容处理//判断是否是注释行if(!(v.trailingComments&&v.trailingComments[0].value.replace('','')=='encrypt')){returnv}deletev.trailingComments;//删除注释//遍历生成eval(stob('xxx'))这种节点letcode=generator(v).code;letcipherText=encFun(code);returntype.expressionStatement(expression=type.callExpression(callee=type.identifier(name='eval'),_arguments=[type.callExpression(callee=type.identifier('atob'),_arguments=[type.stringLiteral(cipherText)])]))})path.get('body').replaceWith(type.blockStatement(statement))//替换}})
特别说明:本文生成节点时,函数的使用对于Js来说是错误的(不能指定参数给值),这样写是方便看节点属性二项式转函数花指令花指令用来更好的隐藏源代码的真实意图,还能增加代码量,增加分析难度类型一//源代码varnum=a+b;//变成functionxxx(c,d){returnc+d;}varnum=xxx(a,b)类型二//源代码varnum=add(a);//变成functionxxx(c,v){returnc(v);}varnum=xxx(add,v)类型一思路获取二项式的符号和左右部分,然后生成函数节点,把函数节点添加到当前代码块的最前面,然后把原始的二项式替换为调用表达式//二项式转花指令traverse(ast,{BinaryExpression(path){letoperator=path.node.operator;letleft_=path.node.left;//加个下滑线防止冲突letright_=path.node.right;//构造函数节点letfunName=path.scope.generateUidIdentifier('xxx');leta=type.identifier(name='a');letb=type.identifier(name='b');letfunc=type.functionDeclaration(id=funName,params=[a,b],body=type.blockStatement(body=[type.returnStatement(argument=type.binaryExpression(operator=operator,left=a,right=b))]))//把生成的函数节点添加到当前代码的最前面letblockStatement=path.findParent(function(p){returnp.isBlockStatement()})blockStatement.node.body.unshift(func);//替换原节点为调用表达式path.replaceWith(type.callExpression(callee=funName,_arguments=[left_,oright_]))}})类型二思路获取调用表达式的函数和参数,然后生成函数节点,把函数节点添加到当前代码块的最前面,然后把原始的调用表达式替换为新的调用表达式,只处理一层的fun('aa')不处理fun.fun('aa')例子functionadd(a,b){returna+b;}console.log(add(2,3))代码//调用表达式转花指令traverse(ast,{CallExpression(path){letcallee_=path.node.callee;letarguments_=path.node.arguments;if(!type.isIdentifier(callee_)){//只处理一层的`fun('aa')`不处理`fun.fun('aa')`return}//构造函数节点letfunName=path.scope.generateUidIdentifier('fff');letf=type.identifier(name='f')//第一个参数,是函数letparams_=[];//参数列表for(letarginarguments_){params_.push(name=path.scope.generateUidIdentifier('arg'))}letfunc=type.functionDeclaration(id=funName,params=[f].concat(params_),body=type.blockStatement(body=[type.returnStatement(type.callExpression(callee=f,_arguments=params_))]))//把生成的函数节点添加到当前代码的最前面letblockStatement=path.findParent(function(p){returnp.isBlockStatement()||p.isProgram()//兼容处理如何函数需要添加到最外面层})blockStatement.node.body.unshift(func);//替换原节点为调用表达式arguments_.unshift(callee_)path.replaceWith(type.callExpression(callee=funName,_arguments=arguments_))path.skip()}})
特别说明:本文生成节点时,函数的使用对于Js来说是错误的(不能指定参数给值),这样写是方便看节点属性思路标识符一般都是有语义的,把标识符无语义化,那么分析难度就会再次增加使用scope.getOwnBinding()例如:在Program节点下,可以获取全局的标识符,而函数内部的标识符名获取不到,要获取局部的的标识符,可以遍历局部节点()的标识符,这样就获取的是函数局部的标识符。标识符混淆letrenameOwnBinding=function(path){letownBindingObj={},//存放binding对象,重名时需要globalBindingObj={},//i=0;//先获取标识符作用域path.traverse({Identifier(p){letname=p.node.name;letbinding=p.scope.getOwnBinding(name)//当前标识符当前节点的绑定binding?(ownBindingObj[name]=binding):(globalBindingObj[name]=1)}})for(letoldNameinownBindingObj){do{varnewName='_0xsdc2d'+i++;}while(globalBindingObj[newName]);//防止与全局标识符混淆变量名重复ownBindingObj[oldName].scope.rename(oldName,newName);}}traverse(ast,{'Program|FunctionExpression|FunctionDeclaration'(path){//酌情增加节点renameOwnBinding(path);}})在进行以再次加密一遍标识符随机生成上面的标识符混淆,还是能看着不是太混乱,我们在继续混乱一点letgeneratorIdentifier=function(decNum){letflag=['O','o','0']letretval=[]while(decNum>0){retval.push(decNum&3)decNum=parseInt(decNum/3)}letIdentifier=retval.reverse().map(function(v){returnflag[v]}).join('')Identifier.length<6?(Identifier=('OOOOOO'+Identifier).substr(-6)):Identifier[0]=='0'&&(Identifier='O'+Identifier)returnIdentifier}//修改上面的生成标识符的地方//varnewName='_0xsdc2d'+i++;varnewName=generatorIdentifier(i++);
特别说明:本文生成节点时,函数的使用对于Js来说是错误的(不能指定参数给值),这样写是方便看节点属性说明【AST混淆】一、常量&标识符的混淆之后,虽然字符串已经加密了,但是依旧在原来的位置,数组混淆,就是要把这些字符串提取到数组中,然后字符串原先的位置改用数组下标的方式去访问数值成员,字符串还能提取到多个数组,不同的数组处于不同的作用域。//混淆前atob("RGF0ZQ==")//混淆后letarr=["RGF0ZQ=="]atob([0]])使用例子window[atob("RGF0ZQ==")][atob("cHJvdG90eXBl")][atob("Zm9ybWF0")]=function(formatSTr){varstr=formatSTr;varweek=[atob("5pel"),atob("5LiA"),atob("5LqM"),atob("5LiJ"),atob("5Zub"),atob("5LqU"),atob("5YWt")];str=str[atob("cmVwbGFjZQ==")](/yyyy|YYYY/,this[atob("Z2V0RnVsbFllYXI=")]());str=str[atob("cmVwbGFjZQ==")](/MM/,this[atob("Z2V0TW9udGg=")]()+1>9?(this[atob("Z2V0TW9udGg=")]()+1)[atob("dG9TdHJpbmc=")]():atob("MA==")+(this[atob("Z2V0TW9udGg=")]()+1));str=str[atob("cmVwbGFjZQ==")](/dd|DD/,this[atob("Z2V0RGF0ZQ==")]()>9?this[atob("Z2V0RGF0ZQ==")]()[atob("dG9TdHJpbmc=")]():atob("MA==")+this[atob("Z2V0RGF0ZQ==")]());returnstr;};window[atob("Y29uc29sZQ==")][atob("bG9n")](newwindow[atob("RGF0ZQ==")]()[atob("Zm9ybWF0")](atob("eXl5eS1NTS1kZA==")));数组混淆letbigArr=[];//存字符串大数组traverse(ast,{StringLiteral(path){letvalue=path.node.value;//获取值letbigArrIndex=bigArr.indexOf(value);//判断字符串是否再大数据letindex=bigArrIndex;if(bigArrIndex==-1){//添加字符串到大数据letlength=bigArr.push(value);index=length-1;}path.replaceWith(//字符串替换成调用表达式type.memberExpression(object=type.identifier('arr'),property=type.numericLiteral(value=index),computed=true))}})//把大数组组合成节点添加到代码头部bigArr=bigArr.map((v)=>{returntype.stringLiteral(v)})bigArr=type.variableDeclarator(id=type.identifier('arr'),init=type.arrayExpression(bigArr))bigArr=type.variableDeclaration(kind='var',declarations=[bigArr])//大数组添加到头部ast.program.body.unshift(bigArr)实现数组乱序上面实现的数组混淆,数组元素与索引之间是一一对应的,那如果数组元素与索引之间还需要函数进行转换,运行时,才把大数据还原到正确顺序,那样难度就会再上升。乱序函数&还原函数//乱序函数((arr,nums)=>{while(--nums){//简单的倒序arr.unshift(arr.pop())}})(this.bigArr,this.bigArr.length)//还原函数((arr,nums)=>{while(--nums){//再倒回来arr.push(arr.shift())}})(arr,arr.length)添加到代码头部还原函数要在乱序数组之后this.bigArr=type.variableDeclarator(id=type.identifier('arr'),init=type.arrayExpression(this.bigArr))this.bigArr=type.variableDeclaration(kind='var',declarations=[this.bigArr])this.codeAst&&this.ast.program.body.unshift(this.codeAst)//把解密函数添加到头部this.ast.program.body.unshift(this.bigArr)//大数组添加到头部完整代码arrayEncrypt=function(ast){//数组混淆,把字符串提取到一个大数组letbigArr=[];//存字符串大数组traverse(ast,{StringLiteral(path){letvalue=path.node.value;//获取值letbigArrIndex=bigArr.indexOf(value);//判断字符串是否再大数据letindex=bigArrIndex;if(bigArrIndex==-1){//添加字符串到大数据letlength=bigArr.push(value);index=length-1;}path.replaceWith(//字符串替换成调用表达式type.memberExpression(object=type.identifier('arr'),property=type.numericLiteral(value=index),computed=true))}})//把大数组组合成节点添加到代码头部bigArr=bigArr.map((v)=>{returntype.stringLiteral(v)})this.bigArr=bigArr;//为数组乱序做准备this.ast=ast;//把数组和顺序还原函数添加到头部做准备returnthis}arrayEncrypt.prototype.arrayShuffle=function(){//打乱大数组函数((arr,nums)=>{while(--nums){arr.unshift(arr.pop())}})(this.bigArr,this.bigArr.length)//读取解密函数//letcode=fs.readFileSync('./utils/confountArray.js',{encoding:"utf-8"})//解密函数太长可以单独放在一个文件里letcode='((arr,nums)=>{while(--nums){arr.push(arr.shift())}})(arr,arr.length)'this.codeAst=parser.parse(code);returnthis}arrayEncrypt.prototype.unshiftArrayDeclaration=function(){//合并大数据&还原函数this.bigArr=type.variableDeclarator(id=type.identifier('arr'),init=type.arrayExpression(this.bigArr))this.bigArr=type.variableDeclaration(kind='var',declarations=[this.bigArr])this.codeAst&&this.ast.program.body.unshift(this.codeAst)//把解密函数添加到头部this.ast.program.body.unshift(this.bigArr)//大数组添加到头部returnthis}//打乱数组newarrayEncrypt(ast).arrayShuffle().unshiftArrayDeclaration()//不打乱数组//newarrayEncrypt(ast).unshiftArrayDeclaration()实现十六进制字符串上面的数组混淆,其中的还原函数的函数没有办法提取到大数组中,但其中的push、shift这些方法都可以转换成字符串,简单的编码成十六进制字符串arrayEncrypt.prototype.stringToHex=function(){lethexEnc=function(code){for(varhexStr=[],i=0,s;i<code.length;i++){s=code.charCodeAt(i).toString(16);hexStr+='\\x'+s;//bable会自动处理转义字符,所以生成时,反斜杠会多,最后生成代码时要替换掉}returnhexStr;}traverse(this.ast,{MemberExpression(path){if(type.isIdentifier(path.node.property)){letname=path.node.property.name;path.node.property=type.stringLiteral(hexEnc(name))}path.node.computed=true;}});returnthis}newarrayEncrypt(ast).arrayShuffle().unshiftArrayDeclaration().stringToHex()//newarrayEncrypt(ast).unshiftArrayDeclaration().stringToHex()//ast转化为代码letcode=generator(ast).codecode=code.replace(/\\\\x/g,'\\x')//替换掉十六进制多余的转义字符
特别说明:本文生成节点时,函数的使用对于Js来说是错误的(不能指定参数给值),这样写是方便看节点属性使用例子Date.prototype.format=function(formatSTr){varstr=formatSTr;str=str.replace(/yyyy|YYYY/,this.getFullYear())str=str.replace(/MM/,(this.getMonth()+1)>9?(this.getMonth()+1).toString():'0'+(this.getMonth()+1));str=str.replace(/dd|DD/,this.getDate()>9?this.getDate().toString():'0'+this.getDate())returnstr}console.log(newDate().format('yyyy-MM-dd'))实现数值常量的加密NumberLiteral节点:value->BinaryExpression节点:cipherNum^key代码中的数值常量可以遍历NumberLiteral,获取其中的value属性得到,然后随机生成一个Key,把Key和Value进行异或,得到加密后的cipherNum,即cipherNum=value^key,这样就可以用BinaryExpression节点等价的替换NumberLiteral节点/*加密数值常量:1-->343333^343332cipherNum=value^keyvalue=cipherNum^key*/traverse(ast,{NumericLiteral(path){letvalue=path.node.value;letkey=parseInt(Math.random()*(999999-100000),10)letcipherNum=value^key;path.replaceWith(type.binaryExpression('^',left=type.numericLiteral(value=cipherNum),right=type.numericLiteral(value=key)))//替换节点里也有NumericLiteral节点,会造成死循环,因此需要加入path.skip()path.skip()}})实现字符串常量的加密字符串常量的加密,是使用一个加密函数,对字符串进行加密,在使用时,又解密成原始字符串先遍历StringLiteral节点,获取value属性,然后对value进行加密,把StringLiteral节点替换成CallExpression节点(调用表达式)例子这里的例子,需要改变一下,把调用方法使用之前的文章,改成字符串调用的方式window["Date"]["prototype"]["format"]=function(formatSTr){varstr=formatSTr;str=str["replace"](/yyyy|YYYY/,this["getFullYear"]());str=str["replace"](/MM/,this["getMonth"]()+1>9?(this["getMonth"]()+1)["toString"]():'0'+(this["getMonth"]()+1));str=str["replace"](/dd|DD/,this["getDate"]()>9?this["getDate"]()["toString"]():'0'+this["getDate"]());returnstr;};window["console"]["log"](newwindow["Date"]()["format"]('yyyy-MM-dd'));加密对字符串进行base64编码,然后浏览器在运行时,调用atob再解码encFun=(str)=>{returnBuffer.from(str,'utf-8').toString('base64');}traverse(ast,{StringLiteral(path){letstr=path.node.value;letencStr=encFun(path.node.value);path.replaceWith(type.callExpression(callee=type.identifier(value='atob'),//atob()不支持中文解码_arguments=[type.stringLiteral(value=encStr)]))//替换节点里也有stringLiteral节点,会造成死循环,因此需要加入path.skip()path.skip();}})