难点没有难点,只是有一个小小的坑Tip:例子:https://pan.bigdataboy.cn/s/g4JuP猿人学该题官网:https://match.yuanrenxue.com/match/16分析参数有一个m加密参数,控制台还一直输出直接跟栈进去,下断点看看看看btoa在哪定义,直接全部复制到本地window直接补上,看看u在什么定义把这部分,组合一下,复制到本地调用测试,发现f未定义,打断点,发现f其实是一串固定的字符串,直接复制下来继续执行,发现d未定义,这段代码还有个md5,也是没有扒下来发现d是一个函数,直接拔下来然后发现md5在一个匿名函数里,把这个匿名函数扒下来,改为自执行,执行测试,发现出值了,但这个值是不能用的继续分析是不是还缺少什么东西,走了错误的逻辑,所以才没有报错,首先考虑try..catch把n.g继续补上,但是这里要注意,变量名冲突验证通过
难点使用SoJsonV5加密混淆代码,各种反调,还有各种坑Tip:例子:https://pan.bigdataboy.cn/s/aZOsL猿人学该题官网:https://match.yuanrenxue.com/match/9分析流程题目已经说了是cookie了,那就跳过看值的步骤清空缓存,一打开控制台,就被断住,并且下一步一直在循环带,但是一直放行,就能正常加载,说明不是检测控制台调试,但是一格式化,重新加载,再放行就不行了,说明检测了代码格式化反调试sojson混淆后的代码,很恶心,各种混淆,反调,阻止你调试代码sojson检测代码格式,使用了正则匹配toString()方式,所以我们在所有的正则表达式出,打上断点,然后使用本地替换的方式来进行慢慢调试修改方式有两种,一种是修改检测代码块的位置,另一种是取反正则匹配的结果,看那种结果能正常执行然后保存,执行,下一步,正常执行,说明这个反调就过了当所有的正则都按照正常的流程执行后,会在这里一直debugger,但是为什么网站加载又不会被debugger,因为debugger只有当控制台打开时才会生效,直接把这两个debugger注释,然后再执行,发现来到另一个js文件,说明udc的反调就过掉了,并且debugger还在VM里面向上找栈,看看有什么信息然后设置一律不在此暂停继续执行,有时候能正常进入,有时候提示cookie失效,但至少说明已经可以执行了Hook现在反调已经过了,启用HookCookie大法,定位生成点,这就定位到生成点了分析加密逻辑//源代码for(varm=0x1;f[$b('\x30\x78\x31\x34\x31','\x72\x64\x71\x5d')+'\x68\x42'](m,5);m++){res=f[$b('\x30\x78\x65\x38','\x4c\x44\x70\x29')+'\x79\x67'](f[$b('\x30\x78\x61\x36','\x4f\x58\x71\x66')+'\x68\x43'](decrypt,'1650879458'),'\x72');}document[$b('\x30\x78\x35\x35','\x32\x6d\x25\x21')+$b('\x30\x78\x32\x39','\x31\x48\x56\x25')]=f[$b('\x30\x78\x37\x31','\x30\x46\x35\x73')+'\x79\x67'](f[$b('\x30\x78\x31\x61','\x56\x39\x5b\x42')+'\x79\x67'](f[$b('\x30\x78\x35\x32','\x6f\x41\x29\x28')+'\x7a\x58']('\x6d\x3d',f[$b('\x30\x78\x31\x65','\x54\x5e\x64\x6a')+'\x43\x77'](m,0x1)[$b('\x30\x78\x63\x38','\x30\x46\x35\x73')+$b('\x30\x78\x61\x34','\x6b\x46\x52\x77')+'\x6e\x67']()),res),f[$b('\x30\x78\x31\x37\x63','\x4f\x58\x71\x66')+'\x4c\x42']);//手动解混淆一下,就很清晰了,使用的第五次加密的结果,但是这里有个小疑问,难道每次加密的值一样???这就需要注意,如果不成功这些地方就需要注意for(varm=1;m<=5;m++){res=decrypt('1650879458')+'r';}document['cookie']='m='+(m-1)['toString']()+res+';path=/';代码整理然后把之前改写的udc.js放到Node去跑,然后补充缺少的环境,选择用const补环境,有个就是当代码里有重定义window之类的能报错,然后去删除,然后把定时器干掉,没有报错测试加密,然后呢console.log不来???原来是console.log被置空了整理流程1.访问https://match.yuanrenxue.com/match/9获取sessionid和时间戳,每次都写死在Js文件里的,需要重新请求2.然后传入时间错,获得加密的m3.带上m和seesion请求数据测试
难点代码混淆相比第五题简单一点Tip:例子:https://pan.bigdataboy.cn/s/3KXFv猿人学该题官网:https://match.yuanrenxue.com/match/6分析参数有一个m是加密值,q有点组合调用栈,进入看看怎么生成的,很简单的逻辑//简单分析一下t=Date.parse(newDate());varlist={"page":window.page,"m":r(t,window.o),"q":window.i+=window.o+'-'+t+"|",};window.o+=1;'t':每次请求重新生成'window.i+=window.o'-->'window.i=window.i+window.o''window.o'每次请求一次加一次进入r()函数,来到一个单独的Js文件,一眼就能分析出,最重要的加密函数是z()函数,window.o不能大于6分析代码把这个单独的Js复制下来在本地看看结构最上面这种混淆,直接去掉最后的('_')然后控制台就可以打印出源代码了直接运行,提示缺少window,我们直接补上,这里有个坑需要注意,在浏览器里,window是不能被重新赋值了搜索MessagetoolongforRSA,是此处提示错误,打上断点运行此处,发现堆栈很短,看来并不是加密过程中有什么值有问题,这时可能思路就断了这时可以折叠代码,看一下整个代码的执行流程,很容易发现此处异常,正常是false,本地总会缺少一点东西,能补的可以补上,那就直接改成false再次执行,得出结果,对比浏览器的加密结果一样然后出现{'0.0':'风控不通过,别匆忙,放慢速度'},只有第一页有数据出现这情况是它代码里有个全局变量,每次加密都会增加,该值与页面之间有关系,不对应就出现风控,如果用execjs执行就不行,execjs每次执行都是重新执行Js,不会累加//测试js代码window=globalwindow.o=1functionxx(){returnwindow.o++;}module.exports={//node_vm2需要导出xx}使用execjs使用node_vm2,会延续上下文使用node_vm2执行,正常获得值验证
难点代码混淆、cookie加密,还有有点难度Tip:例子:https://pan.bigdataboy.cn/s/5DvCq?password=phm69t猿人学该题官网:https://match.yuanrenxue.com/match/5分析参数mf疑似时间戳,但不一样,也是需要注意的[]m,RM4hZBv0dDon443M这两个确定是加密值分析‘m’‘f’通过调用栈,发现mf是直接赋值,都是通过window获取的可以先上下翻翻,看看有什么需要注意的地方,这里用Hook来定位window._$is(function(){'usestrict';varvalue_;Object.defineProperty(window,'_$is',{//修改_$ss为你需要查询的window属性get:function(){console.log('WindowHook捕获到->',value_);returnvalue_;},set:function(value){debugger;value_=value;console.log('WindowHook捕获到->',value_);returnvalue;},});})();最终定位此处_0x4e96b4['_$is']=_$yw;//_0x4e96b4就是window_$yw=_0x2d5f5b()["toString"]();//_0x2d5f5b()function_0x2d5f5b(){returnnew_0x35bb1d()['valueOf']();//_0x35bb1d是Date()对象}//综上分析_0x4e96b4['_$is']=newDate()['valueOf']()['toString']()继续通过Hook的方式,定位window.$_zw[23](function(){'usestrict';varvalue_;Object.defineProperty(window,'$_zw',{//修改_$ss为你需要查询的window属性get:function(){console.log('WindowHook捕获到->',value_);debugger;returnvalue_;},set:function(value){value_=value;console.log('WindowHook捕获到->',value_);debugger;returnvalue;},});})();最终定位此处//$_aiding['$_zw'][7]--->Datewindow.$_zw[23]=$_aiding['$_zw'][7].parse(new$_aiding['$_zw'][7]()//所以window.$_zw[23]=Date.parse(newDate())接着是两个加密的Cookie也是采用Hook的方式先定位赋值的地方(function(){'usestrict';varcookie_;Object.defineProperty(document,'cookie',{//修改_$ss为你需要查询的window属性get:function(){console.log('GetCookieHook捕获到-->',cookie_);debugger;returncookie_;},set:function(cookie_){cookie_=cookie_;console.log('SetCookieHook捕获到-->',cookie_);debugger;returncookie_;},});})();HookCookie加密m值_$Wa=_0x12eaf3();//_0x12eaf3function_0x12eaf3(){return_0x35bb1d[_$UH[0xff]](new_0x35bb1d());//_0x35bb1d是Date_$UH[0xff]是parse}//所以,但是还需要加密把加密函数扒下来_$Wa=Date.parse(newDate())HookCookie加密RM4hZBv0dDon443M值,就使用上面的Hook脚本就行继续使用Hookwindow方式,来Hook_$ss,定位到此处,发现是AES加密寻找AES的KeyModepadding值//CryptoJS.enc.Utf8.stringify(WordArray)-->WordArray.init对象转Utf-8_$UH[0x2e5]"ECB"_$UH[0x33c]+_$UH[0x33d]"Pkcs7"_0x4e96b4[_0xc77418('0x6','OCbs')]WordArray.init{words:Array(4),sigBytes:16}_$WwWordArray.init{words:Array(41),sigBytes:164}CryptoJS.enc.Utf8.stringify(_0x4e96b4[_0xc77418('0x6','OCbs')]);"MTY1MDE5ODgxNjUy"CryptoJS.enc.Utf8.stringify(_$Ww);"37cdb337788229c60faf0aaffb2d6e65,0df6d975c6b74ba95888b9ac8f0b335c,c333e16a0ae6031b2a1c7be404fdc3e8,61eed28470638021cb96fd084d2f1899,57b5c27e56210e05f2eea98b3bfb0cdb"经过几次的实验,发现这个Key是会变的,所以还需要分析Key是怎么来的,继续使用HookWindow脚本window._$qFWordArray.init{words:Array(4),sigBytes:16}_0x4e96b4[_0xc77418('0x6','OCbs')]WordArray.init{words:Array(4),sigBytes:16}Hook到此处,是Key生成的地方,原来Key是m值而来寻找原加密的值,继续使用HookWindow脚本来定位可以发现前四个是循环此处添加的,时间戳后面是三个零_$Wa=_0x12eaf3();_0x3d0f3f[_$Fe]='m='+_0x474032(_$Wa)+';\x20path=/';_0x4e96b4['_$pr']['push'](_0x474032(_$Wa));第五个,在这里,时间戳后面不是三个零try{_$yw=_0x2d5f5b()[_$UH[0x1f]]();_0x3d0f3f[_$Fe]='m='+_0x474032(_$yw)+';\x20path=/';_0x4e96b4['_$is']=_$yw;_0x4e96b4['_$pr']['push'](_0x474032(_$yw));}catch(_0x3c2e99){}整理一下流程参数f最先生成带三个0的时间戳参数m是加密时间戳数组的第五个未加密时的值时间戳数组前四个使用`Date.parse(newDate())`方式获取,结尾有三个零时间戳数组前第五个使用`newDate()['valueOf']()['toString']()`方式获取cookie的m就是参数的m加密而来MTY1MDE5ODgxNjUy-->由五个加密的时间戳再进行AES加密而来扒代码用手撕的方式吧定位到加密函数出_0x474032function_0x474032(_0x233f82,_0xe2ed33,_0x3229f9){return_0xe2ed33?_0x3229f9?v(_0xe2ed33,_0x233f82):y(_0xe2ed33,_0x233f82):_0x3229f9?_0x41873d(_0x233f82):_0x37614a(_0x233f82);}//美化一下格式function_0x474032(_0x233f82,_0xe2ed33,_0x3229f9){return_0xe2ed33?//undefined_0x3229f9?v(_0xe2ed33,_0x233f82):y(_0xe2ed33,_0x233f82):_0x3229f9?//undefined_0x41873d(_0x233f82):_0x37614a(_0x233f82);//所以最终执行的是`_0x37614a`}//所以只需要_0x37614a(时间戳)然后把_0x37614a()函数复制到本地,然后把所有缺少的函数补齐function_0x37614a(_0x32e7c1){return_0x499969(_0x41873d(_0x32e7c1));}这就是补完后的样子,然后运行,报错提示_$UH未定义打断点查看是个大数组,所以只需要这样补充就好_$UH={15:'charCodeAt',108:'length',276:'fromCharCode',}接着运行,报_0x1badc3[_$UH[31]]isnotafunction错,然后源文件该行断点,发现没有断住,说明没有执行这里,然后有个trycatch,断点看看接着就是报什么错,就到原文件看是什么,对比着来,这里有几个值需要注意,不然就算出值了也不能过,_0x4e96b4['_$tT']、_0x4e96b4['_$Jy'],需要该函数计算cookiem时才是正确的验证现在整个过程都清楚了,验证一下
例子//还原前vara,b,c;a=2,b=3,c=a+b;//还原后vara,b,c;a=2;b=3;c=a+b;思路获取SequenceExpression节点,获取其中expressions属性,遍历生成生成ExpressionStatement节点,再整体替换是否通用:是//逗号表达式还原a=2,b=3,c=a+b;traverse(ast,{SequenceExpression:{exit(path){letexpressions=path.node.expressions;letfinalexpression=expressions.pop();//获取最后一个,替换当前节点letstatement=path.getStatementParent();//向上遍历找到父节点expressions.map((v)=>{statement.insertBefore(type.ExpressionStatement(v))});path.replaceInline(//type.ExpressionStatement(finalexpression));}}})
例子//还原前((arr,nums)=>{while(--nums){arr["\x70\x75\x73\x68"](arr["\x73\x68\x69\x66\x74"]());}})(arr,arr["\x6c\x65\x6e\x67\x74\x68"]);//还原后((arr,nums)=>{while(--nums){arr["push"](arr["shift"]());}})(arr,arr["length"]);思路unicode与十六进制处理比较简单,只需要在ast转换成代码时,加上几个参数就行是否通用:是constjs_code=fs.readFileSync("./input/code.js",{encoding:"utf-8"})letast=parser.parse(js_code)//代码转化为ast//还原unicode与十六进制letdecode=generator(ast,{minified:true,jsescOption:{minimal:true}}).code//这里一键还原了unicode与十六进制,但是代码是的decode=generator(parser.parse(decode)).code//再转一次为了使代码格式化fs.writeFile("./output/code.js",decode,(err=>{}))
例子//还原前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)验证可以看到已经还原了)