编程杂谈

例子//还原前eval(atob("c3RyID0gc3RyWyJyZXBsYWNlIl0oL2RkfERELywgdGhpc1siZ2V0RGF0ZSJdKCkgPiA5ID8gdGhpc1siZ2V0RGF0ZSJdKClbInRvU3RyaW5nIl0oKSA6ICcwJyArIHRoaXNbImdldERhdGUiXSgpKTs="));eval(String.fromCharCode(115,116,114,32,61,32,115,116,114,91,34,114,101,112,108,97,99,101,34,93,40,47,121,121,121,121,124,89,89,89,89,47,44,32,116,104,105,115,91,34,103,101,116,70,117,108,108,89,101,97,114,34,93,40,41,41,59));//还原后str=str["replace"](/dd|DD/,this["getDate"]()>9?this["getDate"]()["toString"]():'0'+this["getDate"]());;str=str["replace"](/yyyy|YYYY/,this["getFullYear"]());;还原这两种可以同时处理,因为都是eval执行,所以遍历调用表达式,判断eval,是就取出eval的参数,判断节点类型,如果是StringLiteral说明就是一串代码字符串,直接替换,如果是其他类型,直接使用node的eval执行,然后替换节点。是否通用:是特别说明:如果遇到解密字符串的是自写的函数,需要先找到解密函数//nodeatobvars=newBuffer.from("待解码的字符","base64").toString("binary")//nodebtoavars=newBuffer.from("待编码的字符","binary").toString("base64")//还原代码functionatob(code){returnnewBuffer.from(code,"base64").toString("binary")}//eval还原traverse(ast,{CallExpression(path){//判断特征if(path.node.callee.name!=='eval')return;letarguments=path.node.arguments//eval的参数letcode=generator(arguments[0]).codeif(type.isStringLiteral(arguments)){path.replaceWith(type.identifier(code))}else{path.replaceWith(type.identifier(eval(code)))//node的eval}}})

编程杂谈

说明第二种平坦流相比于第一种难度都差不多,只是分发器结构不同,难点也是在于没有break的case流程分支构造分析结构可以通过观察,发现类似于一下的结构vari=0,arr=[...];while(!![]){switch(syZ[i++]){case1:...varmP=arr.push//获取pushbreak;case2:...varr8V=[...]//部分分发器break;case3:...mP.apply(arr,r8V);//更新分发器break;case4:...//没有breakcase5:...break还原思路1、通过特征判断,确定平坦流代码2、获取分发器初始数组(shufferVar)3、遍历while-case映射出case值和case代码块({‘case值’:代码块}),并获取出相应的变量名4、通过初始分发器数组,遍历映射的case,组建完整的分发器5、还原1、判断特征平坦流都在函数节点里初始分发器数组+whilwtraverse(ast,{'FunctionDeclaration|FunctionExpression'(path){//第一裂平坦流都在函数里path.traverse({VariableDeclaration(p){//通过特征定位定位分发器位置varrbD=0,BFH=[]letdeclarations=p.node.declarations;if(declarations.length!==2||!type.isNumericLiteral(declarations[0].init,{value:0})||!type.isArrayExpression(declarations[1].init))return;letwhilePath=p.getSibling(p.key+1)if(!type.isWhileStatement(whilePath))return;//声明语句后不是while节点}})}})2、获取分发器初始数组先通过定位while节点,获取前兄弟节点,判断是否是初始分发器数组//获取同级的初始分发器数组letshufferVar=declarations[1].id.name;letshufferArr={value:declarations[1].init.elements};//初始分发器数组console.info('初始分发器:',{name:shufferVar,lenght:shufferArr.value.length})3、映射映射为了方面获取case代码块,映射过程,可以通过apply的特征,记录其他分发器数组的变量名//映射case代码块,并记录更新分发器变量名let_case={},shufferVarName={arrName:{}};whilePath.node.body.body[0].cases.map((v)=>{//映射case和代码块letswitchCase=v.consequent;//获取cases里的代码块数组if(type.isExpressionStatement(switchCase.slice(-2)[0])){letn=switchCase.slice(-2)[0]if(n.expression.callee//形式:mP.apply(syZ,r8V);&&n.expression.callee.property.name==='apply'&&n.expression.arguments[0].name===shufferVar){shufferVarName.arrName[n.expression.arguments[1].name]=true//记录另一部分分发器变量名switchCase.splice(-2,1)//删除apply}}_case[v.test.value]=switchCase})4、组建完整分发器每次取从tmpShufferArr删除头部值并获得,来进行过一遍流程分支,过的过程中动态添加tmpShufferArr值,来获得完整的分发器lettmpShufferArr=shufferArr.value.concat()//深复制shufferArr=[]//置空while(tmpShufferArr.length!==0){//这样写每次更新临时分发器时,能增加循环letindex=tmpShufferArr.shift().value//每次从临时分发器头部获取流程值letconsequent=_case[index];shufferArr.push(index)//从临时分发器获得的流程值才是真实的letn=consequent.slice(-1)[0]//获取最后一个代码块if(!type.isBreakStatement(n)&&!type.isReturnStatement(n)){//为了处理:结尾不是break和return的case,下一次直接进入下一个case代码块tmpShufferArr.unshift({value:index+1})//更新临时分发器}letb=consequent.slice(-2)[0]//再获取倒数第二个,判断是否是分发器if(type.isVariableDeclaration(b)&&b.declarations[0]&&shufferVarName.arrName[b.declarations[0].id.name]){//更新临时分发器tmpShufferArr.push(...b.declarations[0].init.elements)_case[index].splice(-2,1)//删除分发器}if(type.isVariableDeclaration(b)&&b.declarations[0]&&b.declarations[0].init&&b.declarations[0].init.object&&b.declarations[0].init.object.name===shufferVar//形式:varxoL=KDW.p;&&b.declarations[0].init.property.name==='p'){_case[index].splice(-2,1)//删除push声明}}console.info('完整分发器:',JSON.stringify(shufferArr))还原遍历分发器,获取case代码块,添加到父节点的body里,并删除breakp.remove()//删除分发器whilePath.remove()//删除while节点letparentPath=whilePath.parentshufferArr.map((v)=>{if(type.isBreakStatement(_case[v].slice(-1)[0])){_case[v].pop()//删除break}parentPath.body.push(..._case[v])//添加到path节点})验证

编程杂谈

流程平坦化它使用了两种平坦流,这里还原第一种分析结构每次执行case时,才更新控制器的值,这种平坦流还是挺常见的,但该Js做了一点处理varsCS=16;//分发器初始值while(!![]){switch(sCS){case1:...sCS+=22;break;case2:...sCS+=3;//该分支没有break,直接进入下一个casecase3:...sCS+=12;break;case4:...sCS+=2;break;case5:...sCS+=12;break;...}还原思路1、通过特征判断,确定平坦流代码2、获取分发器初始值(shufferVar)3、遍历while-case映射出case值和case代码块({'case值':代码块})4、再次遍历while-case,构造分支流程(自己说的词),表示流程的前后分支关系,当然没有break的需要特殊处理一下,形如:[[2,3],[1,2],[3,4]]5、通过分支流程的前后关系,构造出分发器数组6、遍历分发器数组(shufferArr),根据数组元素顺序获取映射的代码块,同时判断break,并删除break和更新流程节点,添加到父节点的body里面,添加前删除分发器节点和while节点1、判断特征平坦流都在函数节点里面,声明节点+whilw节点traverse(ast,{'FunctionDeclaration|FunctionExpression'(path){path.traverse({//只处理分发器是单个声明的VariableDeclaration(p){//通过特征定位定位分发器位置varrbD=0;letdeclarations=p.node.declarations;if(!(declarations.length===1&&type.isNumericLiteral(declarations[0].init)))return;letwhilePath=p.getSibling(p.key+1)if(!type.isWhileStatement(whilePath))return;//声明语句后不是while节点}})}})2、获取分发器判断完成,获取分发器变量名和初始值,变量名用来更新分发器值时判断//获取分发器letshufferVar={name:declarations[0].id.name,value:declarations[0].init.value}//分发器起始值console.info('分发器其起始值:',shufferVar)3、映射case映射为了方面获取case代码块//映射case和代码块let_case={}whilePath.node.body.body[0].cases.map((v)=>{letswitchCase=v.consequent;//获取cases里的代码块数组_case[v.test.value]=switchCase})4、构造分支流程构造分支流程的思路是,既然进入到该case,那么分发器现在的值就是该case的值,那么下一步的流程更新代码,就在该case代码块里面,也就能构造出前后分支的关系数组了//构造分支流程letshufferFlow=[],//分支流程数组flag=false;//没有break标识whilePath.node.body.body[0].cases.map((v)=>{letn=v.consequent.slice(-1)[0]//获取最后一个代码块大多数是break,不是break的话,该cases执行完就跳到下一个caseif(type.isBreakStatement(n)){letcaseNum=v.test.valueletnextNode=v.consequent.slice(-2)[0]if(nextNode.expression.left.name===shufferVar.name){//变量名与分发器起始值一样更新letnextNum=caseNum;eval(`nextNum//下一次流程${nextNode.expression.operator}${nextNode.expression.right.value}`)flag&&(nextNum-=1)&&(flag=false)//特殊处理就是下一次节点-1shufferFlow.push([caseNum,nextNum])}}else{if(!type.isReturnStatement(n)){//不是return执行完直接到下一个cases,但下一个case需要单独处理letcaseNum=v.test.valueletnextNode=caseNum+1flag=true//标识记录shufferFlow.push([caseNum,nextNode])}}})5、还原分发器上一步构造出来的前后分支关系,顺序是乱的,没有还原出代码的前后执行关系,所以在还原分发器之前,还需要通过分发器初始值,还原出代码的前后执行关系。得到代码的前后执行流程后,还原出分发器时,这里取巧的使用了函数,它原本是用来数组聚合的//构造前后分支关系letshufferFlow=[],//前后分支关系flag=false;//没有break标识whilePath.node.body.body[0].cases.map((v)=>{letn=v.consequent.slice(-1)[0]//获取最后一个代码块大多数是break,不是break的话,该cases执行完就跳到下一个caseif(type.isBreakStatement(n)){letcaseNum=v.test.valueletnextNode=v.consequent.slice(-2)[0]if(nextNode.expression.left.name===shufferVar.name){//变量名与分发器起始值一样更新letnextNum=caseNum;eval(`nextNum//下一次流程${nextNode.expression.operator}${nextNode.expression.right.value}`)flag&&(nextNum-=1)&&(flag=false)//特殊处理就是下一次节点-1shufferFlow.push([caseNum,nextNum])}}else{if(!type.isReturnStatement(n)){//不是return执行完直接到下一个cases,但下一个case需要单独处理letcaseNum=v.test.valueletnextNode=caseNum+1flag=true//标识记录shufferFlow.push([caseNum,nextNode])}}})console.log('前后分支关系:',JSON.stringify(shufferFlow),shufferFlow.length)//通过起始节点流程还原letshufferFlow_=[]shufferFlow.map((v1,i1)=>{if(shufferVar.value===v1[0]){shufferFlow_.push(v1)//寻找起头for(leti=0;i<shufferFlow.length;i++){for(letj=0;j<shufferFlow.length;j++){if(shufferFlow_[i][1]===shufferFlow[j][0]){shufferFlow_.push(shufferFlow[j]);break}}}}})console.log('分支执行流程:',JSON.stringify(shufferFlow_))//构造分发器letshufferArr=[];shufferFlow_.reduce((k1,k2)=>{k1&&shufferArr.push(...k1)k2[1]>0&&shufferArr.push(k2[1])})console.log('分发器数组:',JSON.stringify(shufferArr),shufferArr.length)6、整体还原获取完整分发器后,只需要把每个case里面的代码,按照分发器提取,添加到父节点的body里面就好,这个过程使用判断删除一点代码就好了p.remove()//删除分发器初始值whilePath.remove()//删除while节点letparentPath=whilePath.parentshufferArr.map((v)=>{if(type.isBreakStatement(_case[v].slice(-1)[0])){_case[v].pop()//删除break_case[v].pop()//删除流程更新节点}parentPath.body.push(..._case[v])//添加到path节点})console.log('父节点body数:',parentPath.body.length)验证可以看到已经还原了)

编程杂谈

总结不同的混淆,可以搭配使用,有增加代码量、使执行流程混乱、使眼睛看起来花…,但它们都是给逆向者增加分析难度完整代码: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]))}})

编程杂谈

特别说明:本文生成节点时,函数的使用对于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()}})