招聘站JS逆向卡壳?Playwright+AST组合拳,控制流平坦化也能轻松拆 做行业人才供需分析项目的时候碰过某头部招聘站的岗位详情接口所有请求都带一个动态sign参数有效期几分钟改一下岗位ID直接返回403。最开始走了老路抓包、搜关键词、扒混淆JS硬啃结果对方做了控制流平坦化字符串加密满屏的单字母变量和switch-case状态机看了一下午头都大啃了三天才还原了一半逻辑结果人家周末悄悄更了一版之前的工作全白费。后来调整了思路不再死磕静态代码改成Playwright运行时定位入口 AST自动化还原混淆的组合方案。不用逐行捋状态机也不用挨个猜加密字符串先靠运行时抓准输入输出再用AST批量拆解混淆结构前后两个多小时就把完整签名算法扒了下来。后续站点小版本更新跑一遍还原脚本就能自动适配维护成本直接降了一个量级。今天就把这套完整流程分享出来从定位入口、还原混淆到交叉验证全是实战踩出来的经验适合绝大多数中等混淆程度的业务站点。一、先讲痛点为什么硬啃混淆代码效率极低现在中大型站点的JS混淆基本都是标配三件套变量名压缩、控制流平坦化、字符串加密。纯靠肉眼逐行还原效率极低还很容易出错。1.1 三座大山常规逆向的死穴第一座山是控制流平坦化。原本顺序执行的十几行代码被拆成几十个代码块塞进一个大switch-case里靠一个状态变量跳来跳去。逻辑本身不难但绕来绕去非常费脑子捋着捋着就断了稍微复杂点的函数捋一遍就要一两个小时。第二座山是字符串加密。所有的接口路径、固定盐值、密钥字符串全被加密成了乱码运行时调用解密函数才还原。静态搜sign、encrypt、key这类关键词根本搜不到连加密函数在哪都找不到。第三座山是高频更新。招聘、电商这类反爬强度高的站点每隔一两周就更一次混淆版本变量名、控制流结构全换。你花一周还原的代码更新之后又要重来一遍维护成本高到离谱。1.2 为什么是Playwright AST两者刚好互补一个负责精准定位一个负责批量还原Playwright站在运行时视角不用管代码多混淆只要请求发出去就能顺着调用栈摸到加密函数还能拿到真实的输入输出做验证。静态分析找不到的入口运行时一抓一个准。AST抽象语法树站在代码结构视角不用逐行读逻辑按规则批量拆解控制流、替换加密字符串自动化完成80%的还原工作。人只需要看最终的可读代码抓核心逻辑就行。简单说就是Playwright帮你找到「在哪」AST帮你快速看懂「是什么」两者结合比纯静态硬啃效率至少高一个数量级。二、第一步Playwright精准定位5分钟找到加密入口很多人逆向第一步就走错了打开JS文件全局搜关键词搜sign、encrypt搜出几十个结果挨个看纯碰运气。混淆程度高的站点变量名全是单字母字符串全加密搜关键词根本没用。最高效的方法永远是从请求入手运行时追调用栈混淆再狠也藏不住调用关系。2.1 监听请求精准断在签名生成处不用手动找JS文件直接用Playwright拦截目标接口在请求发出的瞬间断住顺着调用栈往上追几步就能定位到签名函数。核心代码非常简单// Playwright 监听请求触发时断住awaitpage.route(**/api/job/detail*,async(route,request){// 打印请求参数确认sign字段console.log(请求参数:,request.postData());// 在这里打断点或者注入调试脚本debugger;awaitroute.continue();});触发一次请求代码会自动断在请求发出的位置。然后看右侧Call Stack调用栈跳过axios、request这类封装库的层往上追2-3层基本就能落到业务代码的签名生成函数里。踩坑提醒不要在请求库的拦截层打转。很多站点封装了统一的request方法断住后第一层都是拦截器、公共参数拼接的代码多往上追几层到具体的业务逻辑层才是真正的签名生成。2.2 Hook 函数拿到真实输入输出定位到函数还不够要先搞清楚「传进去什么、返回什么」。混淆代码里参数名全是a、b、c根本看不出哪个是参数、哪个是密钥。不用硬读代码直接在控制台重写这个函数加一行打印就能拿到真实的入参和返回值// 假设定位到的加密函数在 window.__sign 上constoriginalSignwindow.__sign;window.__signfunction(...args){console.log(签名入参:,args);constresultoriginalSign.apply(this,args);console.log(签名结果:,result);returnresult;}再触发一次请求控制台就会打印出完整的入参和签名结果。这时候你已经知道了「输入什么格式输出什么结果」相当于有了标准答案后面还原算法的时候可以随时对比验证不会走偏。2.3 提取混淆源码定位到文件之后把对应的JS源码保存下来。不用纠结文件里其他无关代码只把加密函数所在的模块抠出来就行通常也就几百到几千行后续交给AST处理。三、第二步AST自动化还原批量拆解混淆结构拿到混淆代码不要硬读用AST做结构化处理把控制流铺平、把字符串解密80%的机械工作可以自动完成。前端常用的工具是babel/parserbabel/traversebabel/generator三件套就能搞定绝大多数混淆还原。3.1 环境准备装三个核心库npminstallbabel/parser babel/traverse babel/generatorparser把混淆代码解析成AST语法树traverse遍历语法树按规则修改节点generator把修改后的AST还原成可读代码3.2 还原控制流平坦化拆平switch状态机控制流平坦化的核心就是一个大switch语句靠一个状态变量控制执行顺序。我们要做的就是按状态顺序把各个case里的代码块按执行顺序拼起来变回正常的顺序代码。简化版实现逻辑constparserrequire(babel/parser);consttraverserequire(babel/traverse).default;constgeneraterequire(babel/generator).default;consttrequire(babel/types);functiondecontrolFlow(ast){traverse(ast,{SwitchStatement(path){// 只处理状态机模式的switch判断变量是数字case全是数字constdiscriminantpath.node.discriminant;if(!t.isIdentifier(discriminant))return;constcasespath.node.cases;// 提取所有case的代码块按序号排序constcaseMap{};cases.forEach(c{if(t.isNumericLiteral(c.test)){caseMap[c.test.value]c.consequent;}});// 按执行顺序拼接语句简单场景直接按序号顺排复杂场景可模拟执行constsortedKeysObject.keys(caseMap).map(Number).sort((a,b)a-b);constbody[];sortedKeys.forEach(k{body.push(...caseMap[k]);});// 用顺序语句替换整个switchpath.replaceWithMultiple(body);}});}// 使用示例constastparser.parse(混淆代码字符串);decontrolFlow(ast);const{code}generate(ast,{compact:false});console.log(code);说明上面是最简版的实现适合单顺序执行的平坦化。如果是带条件跳转的复杂状态机可以加一步模拟执行根据条件分支的走向拼接代码思路是一样的不用逐行读逻辑程序自动跑一遍就知道顺序。3.3 字符串解密批量替换加密字符串字符串加密的套路基本都是定义一个解密函数所有加密字符串调用这个函数还原。我们只要在AST里找到所有调用解密函数的地方提前算出来明文替换掉函数调用就行。核心思路先从混淆代码里把解密函数抠出来或者用Node.js直接运行拿到解密结果遍历AST找到所有调用解密函数的节点计算出明文字符串替换掉原来的函数调用节点实现之后原本乱码一样的字符串调用会直接变成明文的sign、appKey这类可读字符串瞬间就能看懂哪个参数是干嘛的。3.4 还原效果经过两步处理之后原本几百行绕来绕去的混淆代码会变成几十行顺序执行的可读代码没有了switch-case状态机逻辑从上到下一目了然没有了加密字符串密钥、盐值、字段名全是明文变量名虽然还是单字母但结合上下文已经很容易看懂这时候再去梳理签名规则比如参数怎么排序、用的什么哈希算法、盐值拼在前面还是后面十几分钟就能捋清楚。四、第三步运行时交叉验证确保100%准确捋完逻辑不要直接写代码复现先做交叉验证避免漏掉细节。很多人还原完算法算出来的签名永远不对就是因为漏了某个隐藏的拼接步骤或者排序规则错了。4.1 固定参数对比法用Playwright在页面里注入代码传固定的参数拿到真实签名结果再用自己还原的算法传同样的参数计算结果。两者完全一致说明算法还原正确不一致就说明哪里漏了。// 页面里调用原生签名函数固定参数consttestParams{jobId:123456,_t:1715000000000};constrealSignwindow.__sign(testParams);console.log(真实签名:,realSign);然后用自己的Python/JS实现算一遍逐字符对比。如果不一样就逐步骤打日志对比看是参数排序错了还是盐值没加对或者加密算法选错了。实战经验90%的不一致都出在三个细节上参数排序是ASCII升序还是降序、盐值拼在开头还是结尾、加密算法是MD5还是HMAC。不要上来就怀疑大逻辑先核对这三个细节大概率就能找到问题。4.2 多组参数验证一组对了还不够多测几组不同的参数包括边界情况确保所有场景都对齐。尤其是参数为空、含特殊字符、含中文的情况很容易出现编码不一致的问题。全部对齐之后再把算法移植到你的采集代码里基本就能一次跑通不会出现「本地好好的线上全403」的尴尬。五、工程化网站更新了怎么办这是纯手动逆向最大的痛点站点一更之前的工作全白费。用AST的好处就是只要混淆的整体方案没变就可以写通用的还原脚本更新了重新跑一遍就行。我的常规做法是把「定位加密函数 → 提取混淆代码 → AST还原 → 输出算法核心」的流程尽量写成半自动化脚本站点更新后只需要重新定位一下函数位置丢进还原脚本跑一遍几分钟就能拿到新的可读代码对比新旧版本的差异调整自己的实现通常改一两个细节就搞定维护成本从原来的几天降到了几十分钟应对高频更新的站点非常实用。六、逆向最容易踩的六个坑坑1死磕完整代码还原很多人追求把所有混淆都还原成和源码一样花大量时间处理边缘逻辑。其实完全没必要我们的目标是复现签名算法不是还原整个项目。抓住核心的加密逻辑就行无关的代码块直接跳过能省大量时间。坑2控制流只靠肉眼捋对着switch-case硬捋分支效率极低还容易错。要么写AST脚本自动拆要么加日志运行时看执行顺序都比纯肉眼靠谱。工具能搞定的事就不要浪费人力。坑3字符串解密只找一层有些站点做了多重加密字符串解密之后还是密文要再调一次解密函数。替换的时候要递归处理直到最终是明文字符串不然还原出来还是半乱码。坑4忽略编码细节参数里有中文、特殊字符的时候URL编码、UTF-8编码的处理方式不一样很容易导致签名不一致。一定要和原生函数的编码方式严格对齐不要想当然用默认编码。坑5AST还原后直接信以为真AST还原不是100%准确的尤其是带复杂条件跳转的控制流可能会拼错顺序。还原完一定要用运行时结果验证不能觉得脚本跑出来的就一定对。坑6硬刚最高强度的混淆遇到虚拟机混淆、指令集虚拟化这类高强度混淆AST也不好使就不要死磕了。直接用Playwright调用页面原生函数生成签名成本低得多还不怕更新。技术是为业务服务的能解决问题的方案就是好方案。总结JS逆向这件事从来不是比谁更能啃硬骨头是比谁的方法更高效。纯静态硬啃是最笨的方法混淆强度稍微高一点就会卡壳。把运行时定位和结构化还原结合起来先用Playwright精准找到入口、拿到标准答案再用AST批量处理机械的还原工作人只做核心逻辑的判断效率才能提上去。当然技术方法一定要用在合规的场景里。采集公开数据要遵守网站规则和法律法规不要碰非公开信息和个人隐私技术永远要走在合规的框架里。合规提示本文仅分享前端代码逆向分析的技术方法用于安全研究与学习。数据采集请严格遵守《网络安全法》及目标网站的用户协议与robots规定不得用于非法用途。