先说我第一次点开猿人学这道题的感受网页源码打开满屏var _0xabc1 [\x6a\x6f\x69\x6e, \x69\x6e\x64\x65\x78\x4f\x66]这种玩意儿所有变量名都变成_0x开头的十六进制缩写函数名也全是_0x2f3c这种随机串当时直接看吐了。这就是典型的JS混淆 源码乱码组合也是反爬虫领域最常见、最恶心的一层壳。但说实话混淆并不是无解的。只要你搞清楚它是在哪个环节做的、用了什么策略、代码执行前还原成什么样就能一步步把源码“擦干净”还原成逻辑清晰、能直接阅读和调试的形式。这篇文章我就用猿人学这道经典题目当引子完整拆解一次 JS 混淆源码乱码的定位思路、还原流程和实操避坑经验。内容围绕js逆向实战展开适合有一定 JS 基础、想入门前端代码保护与解混淆的读者纯新手也不用慌我会把涉及的基础概念也讲清楚。猿人学平台的题目其实很有代表性它把“源码乱码”分成两层一是字面意义上看不懂编码转义、进制替换二是结构意义上理不清控制流扁平化、代码自执行、字符串数组化。它本质上在考你两件事第一能不能快速定位加密入口第二能不能把混淆还原到可读可调试的状态。这篇文章不会只讲这一道题的答案而是讲一套可以复用的方法论拿去哪道题都通用。1. 项目思路拆解混淆码不是“乱”而是有规律的“变”1.1 先弄明白服务端为什么要做 JS 混淆很多初学者遇到源码乱码第一反应是“这代码被人故意写坏了”或者“是不是文件损坏了”。其实不是混淆是一个非常成熟的代码保护手段核心目的有四个防止直接阅读源码保护前端加密算法、接口签名逻辑、风控参数生成规则不暴露防止简单复制代码给脚本小子和爬虫新手设门槛增加代码体积和复杂度拖慢逆向者的静态分析速度隐藏关键字符串比如接口地址、加密密钥、请求头参数名都藏在字符数组里从反爬角度想前端代码里往往藏着签名算法、加密参数、请求头生成规则这些一旦被读出来爬虫就能伪造正常的请求。所以混淆的根本目的不是让代码报错而是“让代码能正常运行但你读不懂”。这就意味着任何混淆代码在浏览器里执行前必然会被解密成可读形式否则 JS 引擎也跑不了。这个“执行前的可读瞬间”就是我们逆向的破绽。理解这一点特别重要。很多人在源码乱码上硬刚试图去读那一堆_0x变量名这是错误路线。正确路线是弄清楚它“怎么解回来的”然后让代码自己解给你看或者用工具批量还原。1.2 源码乱码的常见形态先认出这是什么“病症”在动手还原之前你得先能识别出眼前的乱码是哪一种混淆策略。不同策略的还原方法差别很大。常见的有这么几类混淆类型特征表现还原难度变量名混淆_0x1a2b、_0x3f8c这种随机标识符低重命名即可字符串数组化所有字符串集中在数组里通过下标引用中需要还原解密函数字符串编码\x61\x62\x63、\u0061这种进制转义低解码即可控制流扁平化switch-case 大循环包裹原始逻辑高需要还原执行顺序死代码注入大量不会执行的无意义代码混淆视线中需要动态分析剔除自执行函数包裹关键逻辑都包在 IIFE 里参数传一堆东西低定位内层函数即可eval 加密明文代码被 base64/16进制编码后 eval 执行高需要 hook eval猿人学这道题比较典型它把字符串数组化、进制转义、变量名混淆、控制流扁平化全揉在一起了。一句话概括就是“套娃”外层是一个大自执行函数里面定义了一个字符数组一段移位函数然后一堆_0x开头的变量和函数中间还夹着巨型 switch-case。你直接读源码确实就是天书。1.3 为什么我不推荐第一眼就硬啃源码这里我得说句经验之谈任何混淆代码先跑起来再分析。你盯着静态源码看一小时不如在浏览器里断点调试十分钟。因为混淆代码虽然看着乱但它的执行逻辑和原代码是一致的。在开发者工具里下断点、看调用栈、查看变量值能非常直观地看到“解密后”的真实内容。我自己复盘时总结过一条原则静态分析负责“猜方向”动态调试负责“定事实”。先通过静态扫一眼判断混淆类型和加密函数大体位置然后立刻切到动态调试去确认关键参数和调用链。两头结合效率比单看一种高一倍不止。后续所有还原操作也要围绕“先让代码能执行”这个前提来做。2. 核心还原技术与工具链AST 解混淆是主力2.1 为什么工具选 AST 而不是正则替换字符串数组化这类混淆很多人第一反应是写正则替换比如把所有_0xabc1[1]替换成实际字符串。这个思路方向对但实操很容易翻车原因有三混淆代码是动态解密数组下标往往不是常量可能是一个表达式计算结果同一个字符串可能被多次调用直接替换可能漏掉上下文相关的逻辑控制流扁平化、死代码注入这种结构性混淆正则根本没法处理必须用 ASTAST抽象语法树把代码解析成树形结构每个节点都能被遍历、识别、修改最后再生成回代码。你可以针对不同类型节点写还原逻辑精准修改指定的表达式、语句或变量名而且不会破坏代码结构。这也是现在js在线ast解混淆、Babel 插件生态以及各类 deobfuscator 库的底层原理。2.2 我常用的工具清单与选型逻辑先列一份我自己顺手又稳定的工具链都是实际项目里试过的AST 在线解析网站适合一眼看出代码结构的问题比如 astexplorer.net左侧粘贴混淆代码右侧看 JSON 树。推荐配合代码格式化使用。Babel 工具链babel/core、babel/parser、babel/traverse、babel/generator。做解混淆脚本的核心依赖写一段 Node 脚本就能批量处理。js-deobfuscator 等开源还原库能自动处理一部分常见混淆适合批量、初步清理。注意不是所有混淆都能一次还原很多时候要手写插件配合。自研还原插件针对题目特化的还原步骤比如“字符串解密函数还原”、“控制流平坦化还原”核心还是 Babel 插件形式。Chrome DevTools格式化、断点、监视、控制台直接执行永远是最直接的工具特别是在定位入口阶段。选型逻辑其实很简单能用现成工具的先跑现成工具解决不了再手写。没有人会每一题都从零写一个完整解混淆器基本都是“先自动化清理 手动微调”的组合。2.3 用 AST 还原“字符串数组 移位函数”的核心思路前面说过字符串数组混淆会把代码里的明文串抽到一个数组里然后定义一个移位函数打乱数组顺序再提供一个解密函数按下标读取。还原这种混淆的最关键一步就是找到解密函数的实现然后模拟执行把引用点替换为真实字符串。具体步骤是从 AST 里定位数组定义节点通常是var _0xxxx [...]找到数组移位函数特征把数组元素 push 到末尾或从开头 splice循环改变数组顺序找到解密函数特征接收下标和第二个参数内部调移位函数返回字符串在 Node 里模拟执行这段逻辑得到解密后的字符串映射表遍历 AST 里所有调用解密函数的节点替换成真实字符串这个思路看起来简单实际坑在第二步。移位函数往往也是混淆过的参数藏在另一个数组里而且可能在文件加载时被立即执行。你需要先在运行时拿到真实的数组顺序再反向推导解密过程。这时候结合浏览器调试直接在 console 里调用解密函数是最快的验证方法。3. 实操过程从源码乱码到干净代码的全流程3.1 第一步格式化让代码“能看”拿到乱码源码的第一步永远是格式化。在浏览器开发者工具里点“Pretty-print”按钮在代码左上角{}图标能瞬间把压缩成一行或几行的代码展开成正常的缩进结构。这个操作对后续所有分析都至关重要否则你连函数边界都分不清。格式化之后你可以看到大致的结构自执行函数、数组定义、函数声明、一个调用入口。这时候先不要细读而是快速标记几个关键信息有多少个大函数有没有明显的解密函数有没有eval、Function这种动态执行点这些标记是后续深入分析的索引。3.2 第二步动态调试定位入口与解密函数这一步是整个逆向过程中最核心、也最考验经验的部分。打开 DevTools 的 Sources 面板在可疑入口处下断点刷新页面看执行流程停在哪里。我的习惯是先搜索接口 URL 或关键请求参数名直接定位到发起请求的代码附近在该位置向上寻找加密函数的调用栈找到真正的“签名入口”在入口处下断点观察参数传递过程逐步进入内部函数一旦发现某个函数内部返回的字符串内容是明文那八成就是解密函数以猿人学这道题为例我发现所有_0x数组元素在浏览器里都已经是明文字符串了说明解密早已在加载阶段完成。我直接在 console 里执行数组名直接看到了[join, indexOf, split]这些原始字符串这就是“运行时真实值”比静态猜快多了。3.3 第三步用 Babel 脚本自动还原字符串数组动态调试确认了数组和解密函数的规则后就可以写脚本批量还原了。下面这个脚本是最简版的字符串数组还原插件核心逻辑是“找到解密函数名遍历调用点替换成字符串”。const parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; const types require(babel/types); const code 你的混淆代码字符串; const ast parser.parse(code, { sourceType: script, }); // 在真实的混淆代码里这里需要根据动态分析得到解密函数名 const decodeFuncName _0x2f3c; traverse(ast, { CallExpression(path) { // 第一个例子解密函数传入数字下标直接替换 const { callee } path.node; if ( types.isIdentifier(callee) callee.name decodeFuncName path.node.arguments.length 2 types.isNumericLiteral(path.node.arguments[0]) ) { const idx path.node.arguments[0].value; const realString runDecode(idx); // 这里替换成你在 console 里调用的真实解密逻辑 path.replaceWith(types.stringLiteral(realString)); } }, }); const output generate(ast, { compact: true }).code; console.log(output);这段代码只是骨架实际使用时runDecode需要你在浏览器中直接调用原页面的解密函数获取真实字符串。更进阶的做法是把解密逻辑用 JS 重写在 Node 里执行。这里我建议不要试图复制整个混淆解密函数太重了直接在浏览器 console 里执行原函数然后复制输出结果生成一张下标和字符串的映射表也就是“硬编码映射”。这个办法在扣代码场景下非常稳能少踩很多环境坑。3.4 第四步控制流平坦化还原的两种实战打法字符串数组还原完了代码里能看到明文字符串了但大型 switch-case 执行流程还是让人头大。控制流平坦化会把你正常的 if-else、for 循环打散成一个大循环 状态机每次循环从状态值判断下一个要执行的分支。还原思路有两种第一种是动态插桩法。在 switch 的 dispatch 位置记录每次 state 的跳转顺序把所有执行路径打出来再根据路径还原成顺序代码。适合状态分支不太多的情况。实际操作可以在 DevTools 里给switch入口条件断点然后手动执行几次记录 state 变化顺序。第二种是静态 AST 还原法。分析 switch 的每个 case 块把每个 case 结尾给下一个 state 赋的值找到然后构建一个有向图再遍历图把分支节点按执行序重新拼成顺序语句。这个思路对代码结构还原最彻底但脚本复杂度高。我一般是混用先动态跑一遍拿执行顺序再用 AST 做结构化还原能省一半时间。3.5 第五步补环境在 Node 里跑通还原后的代码还原得再漂亮最后也得验证结果对不对。很多人卡在这一步因为前端代码里有大量浏览器环境变量比如window、document、navigatorNode 里根本没有。补环境的思路是“缺什么补什么”把用到但缺失的浏览器对象在 Node 的 global 里模拟出来。对一个 JS 逆向项目来说最常用的补环境工具是 jsdom它可以模拟一大套浏览器 API。你也可以手动补几个关键对象例如global.window global; global.navigator { userAgent: Mozilla/5.0 ..., platform: Win32, }; global.document { cookie: , getElementById: () null, // 按需补充 };补环境的原则只有一个够用就行不要追求完整模拟浏览器。你只需要让目标代码不出错能跑出正确签名就够了。补多了反而可能踩到各种奇奇怪怪的兼容问题。3.6 第六步验证还原结果确认签名一致还原后的代码能跑通不代表还原正确最终验证标准是结果对比。我会同时跑两遍一遍在浏览器 console 里调用原混淆代码的加密函数记录输出一遍在 Node 里调用还原后的函数对比签名、请求头、加密串是否一致。这里有个小技巧在浏览器中直接调用加密函数时把函数 toString 出来看看它引用了哪些全局变量和外部函数可以快速判断还原时哪些依赖没补全。很多 Node 跑不通的情况用这个办法定位特别快。对比一致后整个还原工作才算真正收工。4. 常见问题与排查技巧实录4.1 典型问题速查表我把实操中高频踩坑点整理成了表格方便你对照排查问题现象可能原因解决思路格式化后仍有大量\xNN字符字符串编码没还原完用 AST 遍历 StringLiteral 解码或用自写 replace 解码字符串数组能打印但下标越界移位函数没执行数组顺序错在浏览器里先跑完整加载流程再打印数组还原脚本跑完代码报语法错误AST 遍历范围太大改坏了结构缩小遍历范围只针对指定函数或指定解密函数节点控制台执行原解密函数 is not defined函数在闭包内部全局不可访问在函数内部下断点用 local 变量访问或修改作用域Node 补充环境后报navigator is not defined浏览器环境缺失按需补全局对象跑一步补一个还原后签名与原接口不一致字符串替换错或环境导致计算差异对比浏览器与 Node 环境中的关键中间变量网页无限 debugger 或反调试混淆里内置了 Debugger 陷阱在 DevTools 里禁用断点或用猴子补丁替换debugger指令4.2 浏览器控制台就是最值钱的“调试机器”很多新手喜欢一上来就写还原脚本其实这是绕远路。浏览器控制台里有现成的解密结果你完全可以直接获取。比如面对一个字符串数组混淆你在控制台输入数组名回车所有明文字符串都在眼前。为什么还要费劲去还原我见过最离谱的是有人硬抄混淆代码到本地 Node 里跑结果报错一堆最后发现是数组移位函数里使用了浏览器 API。这种完全没必要。正确做法是浏览器里取真实值、本地做还原替换。浏览器负责提供“运行时事实”本地脚本负责“批量整理”。4.3 循环遍历死循环了怎么办写 AST 还原脚本时最常见的一个坑是遍历过程中替换了节点又被后续遍历再次处理导致无限循环或者重复替换。比如你写了path.replaceWith之后遍历器可能继续访问新生成的节点又匹配到同一个条件于是死循环。我的规避办法是每次替换前先检查当前节点有没有被打过标记。可以用 Node 对象上的自定义属性标记或者在替换时判断是否已经被替换成了字符串类型。另一种办法是使用path.skip()跳过子节点遍历或者把替换和遍历分成两遍第一遍收集所有需要替换的节点路径第二遍统一替换。4.4 AST 工具在线版和本地版的取舍在线 AST 解析工具适合看结构和快速实验但它不适合处理大文件和复杂插件逻辑。我一般在分析初期用在线工具看树结构真正跑还原时用本地 Node Babel。毕竟本地可以写各种自定义插件批量跑完还能对比输出。在线工具相当于“显微镜”本地脚本相当于“流水线”职责不同别指望一个工具解决所有问题。4.5 字符串数组还原时最容易踩的“隐藏坑”字符串数组混淆里经常会有二次加密数组元素本身不是明文而是十六进制转义或 base64 编码真正的解密在读取时才会发生。如果你只还原了第一次的引号包裹没执行解码逻辑替换出的字符串还是乱的。所以在还原时要注意观察解密函数内层是否还有decodeURIComponent、atob、parseInt这类二次处理。排查方法也很简单直接看解密函数的返回值。如果返回的字符串看起来还是%E4%BD%A0%E5%A5%BD这种 URL 编码或者乱码说明还有一层解码没还原。4.6 不要迷信“一键还原工具”网上有些工具号称一键还原 OB 混淆试过之后你会发现能处理的只是最简单的那一层。真正实战里的混淆都是叠加混合的字符串数组还原了控制流扁平化还在死代码还在自执行函数嵌套还在。工具能帮你清理最快的那一层但理解原理、能写脚本插件才是核心能力。我的建议是把开源 deobfuscator 当成“清理第一层”的加速器后面的结构性还原必须自己动手。5. 从题目到实战这套思路能用到哪5.1 不只猿人学所有反爬 JS 都能用这套流程猿人学这道题只是一个非常有代表性的样例它的混淆层次很全特别适合练手。实际业务里遇到的混淆只会更复杂但还原思路完全一致格式化、动态调试定位入口、模拟执行解密函数、AST 批量替换、补环境验证结果。这套流程放在任何反爬 JS 面前都能直接迁移。我在实际项目中还遇到过 vite 打包混淆、webpack 模块化混淆、wasm 加密叠加 JS 混淆等组合拳。本质逻辑没有区别只是在上层包了更多壳需要一层一层剥。剥壳的顺序一般是从外到内从简单到复杂先处理编码转换再处理字符串数组最后处理控制流。5.2 建议的练习路径与进阶方向如果你刚接触这一块我推荐的练习路径是先找简单的 eval 加密题目练习定位和 hook理解“动态执行”这个核心再练字符串数组混淆手写一个 Babel 还原插件熟悉 AST 操作然后挑战控制流平坦化先动态插桩还原再尝试静态图还原最后把还原结果对接进 Node 脚本学会补环境跑通完整流程每一步都要有实际代码产出不能光看教程。我自己是刷完猿人学整条 JS 混淆题目列表才算真正把 AST 解混淆玩熟。这个方向进阶到后面还需要学 V8 引擎原理、JS 字节码、甚至 wasm 逆向但那是另一个层次的事了先把眼前这层“乱码壳”剥干净再说。5.3 关于“源码乱码”的边界认知最后想提醒一句混淆不是加密它只是提高阅读门槛。服务端做混淆的目的不是让代码无法被理解而是让理解成本高到让大部分人不愿意下手。所以我们做解混淆本质上是在做“成本对抗”我们用工具和自动化手段把成本降下来。对于写爬虫脚本的人来说掌握了 JS 混淆还原相当于打开了一扇门你不再被表象的“乱码”吓住而是知道这背后一定有规律可循。对于做前端安全的人来说知道混淆能被还原就会在设计保护方案时多考虑多层结合而不是指望靠单纯混淆一劳永逸。双方在这个领域的攻防对抗也推动了解混淆工具链和前端代码保护方案不断升级。6. 经验心得几个能让你少走半年弯路的建议最后分享几个我在大量解混淆实战中沉淀下来的习惯这些不是教程里会写的东西但对提升效率帮助非常大。第一先跑通再还原永远不要拿静态代码硬刚。任何混淆代码只要浏览器能正常运行它内部的所有解密结果在内存里都是明文。利用好浏览器控制台你就能拿到所有真实数据。第二步才是写脚本批量处理。第二写还原脚本时先用小规模样例验证。粘贴整个几千行的混淆代码到 Babel 里跑挂了根本不知道错在哪。先截取一小段包含解密函数和调用的代码跑通逻辑后再扩大范围。我所有还原插件都是这么一点点“喂”大的。第三保存每一阶段的还原结果。我习惯给文件名加阶段后缀origin.js、format.js、string-recovered.js、controlflow-recovered.js、final.js。这样每次改坏都能回退到上一步也方便对比中间产物是否合理。第四善用 git 做代码版本管理。还原脚本和中间产物都提交到本地仓库每次修改记录清楚。操作复杂度越高版本管理越重要这是从“玩玩”进阶到“工程化”的关键差异。第五心态放平接受“还原之后还是有手动工作”这个事实。AST 能解决结构性还原但代码的可读性优化依然要花时间比如给变量名重命名为有意义的名称、补上注释、理清函数调用关系。不要指望一键得到完美代码。我做猿人学这些混淆题目的最大收获不是背下了某个工具或某段脚本而是建立起了一套“从乱码到可读”的通用思维模型。以后再遇到任何莫名其妙的 JS 代码第一步想到的不再是“这什么东西”而是“先跑起来再拆开看”。这个思维转换比任何工具都值钱。
