资讯动态

猿人学第22题:初识 - 魔改标准算法 题解

发布时间:2026/10/13 3:13:55 来源:尧图企业网站定制
依旧是被简单标签的题目爆杀。先看题干此灵光由异化AES心法与逆练MD5口诀交织而成非天地正统乃是上古散修所创之非标准混元符印需逐帧逆推其变序与轮次方能复刻token真形再结合题目名称这道题应该是魔改了AES与MD5算法。而这句话需逐帧逆推其变序与轮次方能复刻token真形这给了我一个心理暗示难道我真的要定位算法的魔改点并一点点扣代码吗1先看一下题目里面有啥坏了。页面逻辑又进了混淆js文件。先请求一个页面看一下与之前我做过的题目类似也是每个页面连续两个请求第二个请求应该是使用了第一个请求的返回数据。展开看一下不错就是一个干净的getTime。看看响应也很干净Cookie除了sessionid没有别的东西除了那个Hm开头的Cookie这玩意貌似是百度的一个hm.js加的挺多网站也会有它不过目前我还没了解这玩意的作用注意到请求参数里面出现了now它的值跟之前getTime的响应一致。Cookie也没有别的东西。OK准备逆向token。打个XHR断点调用了两次$.ajax。第一次应该是getTime请求去he函数看看这个v很明显是jQuery的$。可以看到这个g对象就应当是传给$.ajax的那个success回调绑定w函数。顺带提一句这个$.ajax传参的这一长串逗号表达式大概结构是这样子的$.ajax((h{},...msuccess,gh,bg[m]w,// b是烟幕弹...Berror,xh,Sx[B]k,// S是烟幕弹...h));首先js中逗号表达式返回的是最后的值。比如vara5;a(a1,a2);console.log(a);// - 7!在本题目的混淆中这种几乎无处不在。观察到g h、x h赋值注意在js中这个赋值赋的是引用而不是拷贝所以g[m] wx[B] k这些操作都相当于修改了原来的h所以逗号表达式最后一个h是已经被修改过的 由逗号表达式的特点$.ajax接受的也只有它自己没有别的参数了。w函数第一行打个断点进去之后本题目的混淆手法之一现在就能看得很清楚了。注意回调函数传入的正是getTime的响应值这正是非常典型的控制流平坦化Control Flow Flattening。在解释它之前我们先回顾一下正常的代码vara32;if(a33){doSomeThing();}console.log(hello);while(a35){doOtherThing();a;}正常的代码会包含嵌套层级Nesting Level嵌套的块内部与块外部的代码并不是同级关系例如这个doOtherThing()与console.log(hello)。而控制流平坦化就是抹平这些代码间的嵌套层级使它们变成同级的再将原来的执行顺序打乱分散在不同的状态中// js没有goto?三元运算符也不能跟非表达式以下是伪代码 state1: var a 32; goto state6; // 去状态6 state2: console.log(hello); goto state4; state3: goto state114514; // 添加无意义的跳转或者单纯的多次跳转增加代码量 state4: a 35 ? (doOtherThing(), a 1); goto state405 : goto state1314; // 阴间在哪怕展平前代码是紧挨着的展平后可能也会差好几千行防静态分析 state5: doSomeThing(); goto state2; state6: a 33 ? goto state5 : goto state12; // if判断被拆成两个状态决定两个方向我们在学习c、java等编程语言时老师会告诉我们不要滥用goto因为过多的goto会降低代码可读性而降低代码可读性正是反爬的一大招数。js中没有goto怎么办答案是用一个单独的变量追踪状态用if...else/switch...case构成状态机每一个if分支/case作为一个独立的状态用for(;;)/while(true)驱动代码执行vars10;for(;;)switch(s){case0:s10;break;case1:// ”fall-through“故意往下一个状态走case2:a35?a(doOtherThing(),1):s-100;break;case3:s(doSomeThing(),45);break;...case10:vara(s1304,32);break;...case1314:a33?s-1311:s-2;break;...case...:return;// 假设这一段在一个函数中这代码可读性还是太好了。直接把switch...case换成嵌套if...elsevars10;vara;for(;;){if(s64){if(s32){if(s16){if(s8){if(s4){if(s2){if(s1){s10;}else{s1;}}else{if(a35){doOtherThing();a1;}else{s-100;}}}else{doSomeThing();s45;}}else{a32;s1304;}}else{...}}else{...}}else{if(a33){s-1311;}else{s-2;}}}而这么阴间的东西你会发现本题目里面到处都是而且题目的每个状态里面还会嵌套着一大堆嵌套的?三元运算式语句这个再单步调试不得跳转得连自己姓啥都不知道了。同时不知道是因为过于深的嵌套层级还是由一行代码格式化出来的代码行数过多是的22.js整个文件这一大坨都在一行里面在浏览器甚至有的代码没法高亮调试时也不会显示实时的值看前面这恐怖的缩进。言归正传既然单步调试不再可行那么就只能通过堆栈定位了。前面提到每点击一次按钮都会发送两次请求所以先定位到第二次请求发送之前这一块代码就是我们很熟悉的构造$.ajax参数可以看到此时这个W变量里面就有我们想要的token。按F8继续执行脚本可以观察到这个token跟抓包记录到的请求参数里面的token一致说明后续没有进行其他的什么魔改操作。新的问题是W由谁设置但是CtrlF搜索W 搜索到92条结果一个一个找不现实。再翻一下右侧的作用域可以发现右侧与W相同的变量还有一个O。而当前else块里面往上翻也就是我下图打断点的位置有一个l函数它有一堆参数并把返回值赋给了O。F8继续执行再点选一个新页面等断点在l前面停住后观察l执行前后O的值我们发现l大概率就是进行加密的函数。运气好的地方这个O变量没有与W相隔太远。运气也是实力的一部分尝试本地导出l。寻找l的函数定义在前面加一个window.Lconstfsrequire(fs)const{JSDOM}require(jsdom);constdomnewJSDOM(!DOCTYPE htmlhtmlbody/body/html,{url:https://match.yuanrenxue.cn/match/22,runScripts:outside-only,pretendToBeVisual:true});constscriptfs.readFileSync(./22.patched.js,utf8);dom.window.eval(script);console.log(dom.window.L);补充几点这个url设置的就是网页链接用于window.location相关的环境检测。由于本题目的跳转过多所以我借助ai才搞清了环境检测点正是window.location。不过实测中发现url仅保留origin也能正确算出token但是在实际分析中为了避免试错为了保险还是写全runScripts参数决定new JSDOM时是否加载内部的script标签。outside-only选项是不加载但是允许通过window.eval执行外部代码。pretendToBeVisual参数决定了页面上一些与可见性相关的api的值比如document.hidden在本选项设定为true时值为false反之则为true用于应对一些判断页面可见性的浅层环境检测。前面提到由于这份混淆的代码包含了页面的一部分渲染逻辑所以直接运行应该会报错。去真实页面看一下尝试在混淆代码的外层包上try...catch块再次运行成功导出window.L。需要注意的是l有7个参数第四个参数应该是一个页面逻辑相关的函数在l内部仅使用了一次而且调试发现在调用该函数之前token值就已经存在了所以直接删掉l内部的r()就可以再随便传个东西占位最后这个参数是应该是jQuery的$函数在l的返回值里面用到了它不过没有什么实质性的作用可以当作是环境检测的一部分在返回值里面把相应的值删掉或替换掉避免报错就可以了。elsereturnj{},Mj,Rpage,Jl,PM[R]J,Fj,Odh,Wn,IF[O]W,Uj,Kkw,Xtrim,Lo,Nval,qundefined,Gq||,te,neU[K]te,oej,aem,cewindow,vecuv,uece[ve],heoe[ae]ue,dej,petoken,yere,_ede[pe]ye,gej,menow,wef,bege[me]we,j经过测试token计算正确。第二个运气好的地方这个函数l的定义位于页面加载逻辑之前。如果位于之后那么就需要对函数l内部真正进行运算的函数进行进一步的提取。不过由于我之前被题干迷惑再借由ai的剖析我大致理解了本题目混淆文件的结构去除这层控制流扁平化的壳整体其实是一个类似于Browserify打包器的输出产物。调试过程中经常能见到一个位于闭包中的变量i里面是这样子的与webpack打包器不同bowserify打包器多了一个注册表这个i应当就是模块注册表了也是browserify的特征之一。混淆代码中那个很长的var h function是模块加载器。将原本的混淆代码保存下来自建一个新的html再引入它运行后的报错位置在真实界面中定位会发现调用了h(2)ai的解释是h(2)就是页面加载逻辑的所在模块所以如果要避免我之前说的问题可以在h(u)报错位置是h(u)运行时第一次加载界面此处u 2前面加一个导出h(12)h(12)返回的是crypto-js的魔改库再通过分析l函数里面的逻辑确定token的生成方式。这个过程耽误了我很长时间我甚至把那个AES函数的key等于666yuanrenxue66都分析出来了不过可惜的是简单题就有他简单的做法。而我的这种做法也依旧存在问题那就是假如题目的魔改发生在h(2)调用之中或之后呢这时以上方法通过h(12)拿到的可能并不是魔改后的而是标准算法中的原函数还有许许多多的环境检测问题怕的不是检测有多深而是检测到不一致后静默下发的假盐/假初始向量。总结一下虽然是简单难度但是我觉得这道题目牵扯到的知识点有必要日后反复回顾。待我日后技术精进我会再开一篇分析本题目的代码如何去扁平化解释我自己的发问。最后贴出解题代码法一constfsrequire(fs)const{JSDOM}require(jsdom);constdomnewJSDOM(!DOCTYPE htmlhtmlbody/body/html,{url:https://match.yuanrenxue.cn,runScripts:outside-only,pretendToBeVisual:true});constscriptfs.readFileSync(./22.js,utf8);dom.window.eval(script);letheaders{Sec-Ch-Ua:Chromium;v125, Not.A/Brand;v24,Accept:application/json, text/javascript, */*; q0.01,X-Requested-With:XMLHttpRequest,Sec-Ch-Ua-Mobile:?0,User-Agent:yuanrenxue,Sec-Ch-Ua-Platform:Windows,Sec-Fetch-Site:same-origin,Sec-Fetch-Mode:cors,Sec-Fetch-Dest:empty,Referer:https://match.yuanrenxue.cn/match/22,Accept-Encoding:gzip, deflate, br,Accept-Language:zh-CN,zh;q0.9,Priority:u1, i};(async(){lettasks[];lettimeawait(awaitfetch(https://match.yuanrenxue.cn/api/getTime,{headers:{Cookie:sessionid***,...headers}})).text();for(leti1;i6;i){tasks.push((async(){console.log(time);lettokendom.window.L(parseInt(time),/api/question/22,i,null,5,10).token;console.log(token);letdataawaitfetch(https://match.yuanrenxue.cn/api/question/22?page${i}pageSize10kwtoken${token}now${time},{headers:{Cookie:sessionid***,...headers}});return(awaitdata.json()).data.reduce((a,b)ab,0);})());}console.log((awaitPromise.all(tasks)).reduce((a,b)ab,0));})();法二constfsrequire(fs)const{JSDOM}require(jsdom);constdomnewJSDOM(!DOCTYPE htmlhtmlbody/body/html,{url:https://match.yuanrenxue.cn,runScripts:outside-only,pretendToBeVisual:true});constscriptfs.readFileSync(./22.js,utf8);dom.window.eval(script);constCryptoJSdom.window.CryptoJS;letkey666yuanrenxue66;letAESCryptoJS.AES;letMD5CryptoJS.MD5;letmodeCryptoJS.mode.ECB;letpaddingCryptoJS.pad.Pkcs7;letheaders{Sec-Ch-Ua:Chromium;v125, Not.A/Brand;v24,Accept:application/json, text/javascript, */*; q0.01,X-Requested-With:XMLHttpRequest,Sec-Ch-Ua-Mobile:?0,User-Agent:yuanrenxue,Sec-Ch-Ua-Platform:Windows,Sec-Fetch-Site:same-origin,Sec-Fetch-Mode:cors,Sec-Fetch-Dest:empty,Referer:https://match.yuanrenxue.cn/match/22,Accept-Encoding:gzip, deflate, br,Accept-Language:zh-CN,zh;q0.9,Priority:u1, i};(async(){lettasks[];lettimeawait(awaitfetch(https://match.yuanrenxue.cn/api/getTime,{headers:{Cookie:sessionid***,...headers}})).text();for(leti1;i6;i){tasks.push((async(){letrawDatatimei;lettokenMD5(AES.encrypt(rawData,key,{mode,padding}).toString()).toString();console.log(token);console.log(time);letdataawaitfetch(https://match.yuanrenxue.cn/api/question/22?page${i}pageSize10kwtoken${token}now${time},{headers:{Cookie:sessionid5LiN6K645YG355yL5Za1,...headers}});return(awaitdata.json()).data.reduce((a,b)ab,0);})());}console.log((awaitPromise.all(tasks)).reduce((a,b)ab,0));})();欢迎大家在评论区交流指出我的不足事后发言不要被题干迷惑了。这道题如果要真的如题干要求所说逆序推导那么个人觉得这道题的混淆已经大幅超出了简单题的范畴至少比我之前做过的几道中等题还要难。时刻应当记住我们要做的是尽量脱离浏览器让关键数据本地生成而过程即密文的计算是导出真实算法后用nodejs/python完整实现的高效版还是仅仅像这道题一样保留大量冗余代码仅导出包装关键加密函数的函数相对低效的执行这是真实场景中需要考虑效率时再想的。如果需要高效或者是混淆的程序代码经常发生变化但是魔改本身没有改变的时候那个时候就得考虑写babel插件、抚平状态机、进行白盒分析还原被深度混淆的魔改算法了。本题作为简单题我使用简单题的做法先拿到了答案但是在后期学习babel、AST的时候这道题完全可以当作是一个小boss了。 ↩︎

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑