编程杂谈

难点使用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时才是正确的验证现在整个过程都清楚了,验证一下

编程杂谈

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

编程杂谈

scope作用域scope提供了一些属性和方法使用的例子consta=1000;letb=2000;letobj={name:'haha',add:function(a){a=400;b=300;lete=700;functiondemo(){letb=600;}demo();returna+a+b+1000+obj.name}}obj.add(100)获取’标识符’作用域path.scope.block()可以获取标识符作用域,返回Node对象visitor={Identifier(path){if(path.node.name==='e'){//e的作用域就是整个函数console.log(generator(path.scope.block).code)}}}traverse(ast,visitor)/*function(a){a=400;b=300;lete=700;functiondemo(){letb=600;}demo();returna+a+b+1000+obj.name;}*/但有时候作用域输出的范围与实际不符合,就需要获取父节点的作用visitor={FunctionDeclaration(path){//函数声明只有demo()满足要求console.log(generator(path.scope.block).code)}}traverse(ast,visitor)/*functiondemo(){letb=600;}*/demo()的作用域实际上应该是整个add()的范围,这时就与实际输出不符合,就需要获取父节点的作用域visitor={FunctionDeclaration(path){console.log(generator(path.scope.parent.block).code)}}traverse(ast,visitor)/*function(a){a=400;b=300;lete=700;functiondemo(){letb=600;}demo();returna+a+b+1000+obj.name;}*/path.scope.getBinding获取标识符对应的绑定对象(Binding)visitor={FunctionDeclaration(path){letbinding=path.scope.getBinding('a')console.log(binding)}}traverse(ast,visitor)/*Binding{identifier:Node{type:"Identifier",start:83,end:84,loc:SourceLocation,name:"a"},//标识符的Node对象scope:Scope{uid:1,path:NodePath,block:Node,labels:Map(0),inited:true,...},//对应标识符的scope对象path:NodePath{contexts:Array(0),state:Object,opts:Object,_traverseFlags:0,skipKeys:null,...},//对应标识符的Path对象kind:"param",//该标识符类型这里表示是一个参数constantViolations:Array(1)[NodePath],//存放修改了该标识符的节点constant:false,//是否是常量referencePaths:Array(2)[NodePath,NodePath],//引用了该标识符的NodePath数组referenced:true,//该标识符是否被引用references:2,//该标识符的引用次数hasDeoptedValue:false,//hasValue:false,value:null}*/referencePaths&constantViolations这两个是Binding对象里的属性/*Binding{...referencePaths:Array(2)[NodePath,NodePath],//引用了该标识符的NodePath数组,没有引用时为空数组constantViolations:Array(1)[NodePath],//存放修改了该标识符的节点,没有被修改时为空数组...}*/遍历作用域有两种方式遍历作用域path.scope.traverse({})binding.scope.traverse({}):推荐//path.scope.traverse({})visitor={FunctionExpression(path){letbinding=path.scope.traverse({Identifier(path){console.log('')}})}}traverse(ast,visitor)//binding.scope.traverse({})推荐visitor={FunctionExpression(path){letbinding=path.scope.getBinding('a')binding.scope.traverse({Identifier(path){console.log('')}})}}traverse(ast,visitor)把例子函数中的a=400改成a=60visitor={FunctionExpression(path){letbinding=path.scope.getBinding('a')binding.scope.traverse(path.scope.block,{//遍历a的作用域AssignmentExpression(path){if(path.node.left.name==='a'){path.node.right=type.valueToNode(60)}}})}}traverse(ast,visitor)标识符重命名binding.scope.rename()该方法,会重命名所有引用了该标识符的地方visitor={FunctionExpression(path){letbinding=path.scope.getBinding('b')binding&&binding.scope.rename('b','xx')}}traverse(ast,visitor)/*consta=1000;letb=2000;letobj={name:'haha',add:function(xx){//a-->xxxx=400;//函数的参数a-->xxb=300;lete=700;functiondemo(){letb=600;}demo();returnxx+xx+b+1000+obj.name;}};obj.add(100);*/随机生成标识符path.scope.generateUidIdentifier('_0xoO').name该方法会生成一个字符串且不会重复可以用来实现简单的标识符混淆,也可以用来反混淆,使变量看起来好分析visitor={Identifier(path){path.scope.rename(path.node.name,path.scope.generateUidIdentifier('_0xoO').name)}}traverse(ast,visitor)/*const_0xoO=1000;let_0xoO15=2000;let_0xoO18={name:'haha',add:function(_0xoO14){_0xoO14=400;_0xoO15=300;let_0xoO9=700;function_0xoO12(){let_0xoO11=600;}_0xoO12();return_0xoO14+_0xoO14+_0xoO15+1000+_0xoO18.name;}};_0xoO18.add(100);*/scope其他方法path.scope.hasBinding('a')返回值是bool值,作用是查询该作用域是否有该标识符的绑定该方法可以用path.scope.getBinding('a')代替,只是该方法的返回值是undefined和Binding{}visitor={Identifier(path){console.log(path.scope.hasBinding('a'))}}traverse(ast,visitor)/*true...*/path.scope.getAllBindings()获取该节点的所有绑定,返回的对象以标识符名为属性名,对应的属性是Binding{..}visitor={Identifier(path){console.log(path.scope.getAllBindings())}}traverse(ast,visitor)/*Object{a:Binding{..},b:Binding{..},obj:Binding{..}}*/path.scope.hasReference('a')查询当前节点是否有标识符a的引用,返回值是true/false

编程杂谈

Path对象Path&Node的区别Path是描述父子节点之间的链接,Path除了下面这些属性外,还包含添加、更新、移动、删除等方法Node对象是Path对象中的一个属性。path.node能取出当前遍历到的节点各种具体信息,不能使用Path对象的各种方法/*NodePath{contexts:Array(1)[TraversalContext],//上下文内容嵌套自己state:undefined,opts:Object{Identifier:Object,_exploded:true,_verified:true},_traverseFlags:0,skipKeys:null,parentPath:NodePath{contexts:Array(1),state:undefined,opts:Object,_traverseFlags:0,skipKeys:null,...},//父节点Path对象container:Node{type:"VariableDeclarator",start:4,end:162,loc:SourceLocation,id:Node,...},//容器里面是兄弟节点,有数组(有兄弟)和单个对象(没有兄弟)listKey:undefined,//有兄弟节点时,兄弟节点的容器的名字key:"id",//有兄弟节点时,代表兄弟容器索引,没有兄弟节点,代表对象属性名node:Node{type:"Identifier",start:4,end:7,loc:SourceLocation,name:"obj"},//Node对象type:"Identifier",parent:Node{type:"VariableDeclarator",start:4,end:162,loc:SourceLocation,id:Node,...},//父节点Node对象hub:undefined,data:null,context:TraversalContext{queue:Array(1),priorityQueue:Array(0),parentPath:NodePath,scope:Scope,state:undefined,...},scope:Scope{uid:0,path:NodePath,block:Node,labels:Map(0),inited:true,...},//作用域}*/Path对象中的方法获取当前节点为了获取到AST节点的属性值,需要先访问到该节点,然后用path.node.xxx(xxx相关属性)获取获取Node对象letvisitor={Identifier(path){console.log(path.node)//获取当前节点Node对象console.log(path.node.left)//获取Node节点具体属性值}}获取Path对象,但是有些属性值就没有必要包装operator、name…letvisitor={BinaryExpression(path){console.log(path.get('node'))//会把node包装成Path对象返回console.log(path.get('node.left'))//支持多级访问}}判断Path类型letvisitor={BinaryExpression(path){console.log(path.get('left').isIdentifier())console.log(path.get('right').isNumericLiteral({value:1000}))console.log(path.get('left').assertIdentifier())}}/*falsetrue报错*/节点转代码代码比较复杂,需要动态调试,就可以使用该方法,方便查看某一小段代码,就不需要到最后才全部再转换成代码,再看letvisitor={FunctionExpression(path){console.log(generator(path.node).code)console.log(path.toString())//因为Path对象,重写了toString()方法}}/*function(a,b){returna+b+1000;}function(a,b){returna*b+1000;}*/替换节点属性替换需要属性,需要符合实际逻辑,在允许的范围替换,不能随便替换,造成逻辑错误visitor={BinaryExpression(path){path.node.left=type.identifier(name="x")path.node.right=type.identifier(name="y")}}/*add:function(a,b){returnx+y;},mul:function(a,b){returnx+y;}*/替换节点replaceWith()使用一个节点替换一个节点,一对一的替换visitor={BinaryExpression(path){path.replaceWith(type.valueToNode('mimi'))}}/*add:function(a,b){return"mimi";},mul:function(a,b){return"mimi";}*/replaceWithMultiple()为多个节点替换一个节点两点特别说明:当替换的表达式替换后在一行时,最好使用type.expressionStatement()包裹一层新替换的节点traverse()也会去遍历,所以注意别陷入不合理的递归死循环遍历ReturnStatement()替换,替换里又有ReturnStatement(),陷入递归死循环,所以使用path.stop()停止visitor={ReturnStatement(path){path.replaceWithMultiple([type.expressionStatement(type.valueToNode('ZZ')),type.expressionStatement(type.valueToNode('XX')),type.returnStatement(type.valueToNode('YY')),])path.stop();}}traverse(ast,visitor)/*add:function(a,b){"ZZ";"XX";return"YY";},*/replaceInline(),接受一个参数,作用相对于是replaceWith()replaceWithMultiple()的综合当接受的参数是一个节点,就是replaceWith()当接受的参数是一个节点数组,就是replaceWithMultiple()添加有path.stop()的原因与上面一样visitor={StringLiteral(path){path.replaceInline(type.valueToNode('Hellemimi'))path.stop()},ReturnStatement(path){path.replaceInline([type.expressionStatement(type.valueToNode('ZZ')),type.expressionStatement(type.valueToNode('XX')),type.returnStatement(type.valueToNode('YY')),])path.stop()}}traverse(ast,visitor)replaceWithSourceString()接受字符串作为参数,然后解析到AST里面replaceWithSourceString()替换后的节点也会被解析,所以也需要path.stop()防止陷入递归死循环//把函数改为`闭包形式`visitor={ReturnStatement(path){letargumentPath=path.get('argument')argumentPath.replaceWithSourceString('function(){return'+argumentPath+'}'//字符串拼接会自动调用toString())path.stop()}}/*add:function(a,b){returnfunction(){returna+b+1000;};},*/删除节点visitor={EmptyStatement(path){//空语句也就是多余的分号path.remove()}}traverse(ast,visitor)插入节点插入兄弟节点insertAfter()当前节点之前,insertBefore()当前节点之后如果要操作ReturnStatement,可以先判断,直接return,注意该是用path.stop()是不行的visitor={ReturnStatement(path){path.insertAfter(type.valueToNode("XX"))path.insertBefore(type.valueToNode('MM'));}}traverse(ast,visitor)父级Path在Path对象中有两个属性parentPath、parent,其中parentPath是NodePath类型,是父级的Path对象,parent是父级的Node对象path.findParent&path.find参数都是一个函数,返回值是Path对象,没有找到返回null从当前节点,一直向上遍历,知道满足对应条件path.findParent()常用,查找范围不包含当前节点path.find()不常用,查找范围包含当前节点visitor={ReturnStatement(path){console.log(path.findParent((path)=>{returnpath.isObjectExpression()}))}}/*NodePath{...}*/path.getFunctionParent向上查找与当前节点最接近的父函数节点,返回值是Path对象visitor={ReturnStatement(path){console.log(path.getFunctionParent())}}traverse(ast,visitor)/*NodePath{...}NodePath{...}*/path.getStatementParent向上遍历找到父语句节点,如:return,if,whitch,while…visitor={ReturnStatement(path){console.log(path.getStatementParent())//会从当前节点开始//console.log(path.parentPath.getStatementParent())//取巧}}traverse(ast,visitor)其他方法替换path.parentPath.replaceWith()path.parentPath.replaceWithMultiple()path.parentPath.replaceInline()path.parentPath.replaceWithSourceString()删除path.parentPath.remove()同级节点了解同级节点,首要了解Path对象的这几个属性:container、listKey、keycontainer:有兄弟节点时,是兄弟节点数组,没有兄弟节点时就是当前节点Node对象listKey:有兄弟节点时,为容器名字,没有时是undefinedkey:有兄弟节点时,为当前节点,在容器中的索引,没有时,是当前节点属性名相关属性visitor={ReturnStatement(path){console.log(path.inList)//trueconsole.log(path.listKey)//bodyconsole.log(path.key)//0console.log(path.container)//[Node{...}]}}traverse(ast,visitor)获取兄弟节点path.getSibling参数是容器中节点的索引,visitor={ReturnStatement(path){console.log(path.getSibling(path.key))//Node{...}}}traverse(ast,visitor)网容器添加节点path.parentPath.unshiftContainer():在容器头部添加节点path.parentPath.pushContainer():在容器尾部添加节点添加之后的节点,也会再进行遍历,所以还是需要path.stop(),以防止进入递归死循环visitor={Property(path){path.parentPath.unshiftContainer(path.listKey,[type.objectProperty(key=type.identifier(name="unshift"),//第一个属性value=type.stringLiteral(value="OOO"))//第一个属性值)])path.parentPath.pushContainer(path.listKey,[type.objectProperty(key=type.identifier(name="push"),//第一个属性value=type.stringLiteral(value="MMM"))//第一个属性值)])path.stop()}}traverse(ast,visitor)