恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

AST反混淆JS还原实战:用Babel与vm还原字符串数组、控制流平坦化

  • 首页
  • 资讯中心
  • /
  • AST反混淆JS还原实战:用Babel与vm还原字符串数组、控制流平坦化

相关资讯

Unity战棋游戏开发:C#毕设源码中的网格寻路与AI骨架解析 2026/10/6 18:58:31
高阶OAM调制在5G毫米波中的工程实现与误码率优化 2026/10/6 18:58:31
南邮算法课项目实战:从跑通.zip到工程级交付 2026/10/6 18:58:31

最新资讯

Apache License 2.0核心条款解析:从专利保护到商业闭源合规
Oracle SQL Developer 21.4.3 启动与连接全指南
Agent-Reach:分布式智能体注册、发现与触达网关架构实践
Agent Skills实战指南:从原理到落地,让AI agent真正专业
SQL Developer 21.4.3 Windows x64 部署避坑指南
Agent-Reach:面向开发者的本地化智能体CLI协议栈

今日推荐

2026 AI 开发全家桶落地指南:TaoToken 统一 Key 打通 IDE 插件、Agent 与自动化代码审查全链路配置实测
MR25H40CDF+STM32F031C6工业级高可靠数据存储方案
MRAM+STM32工业断电数据保全实战指南

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

AST反混淆JS还原实战:用Babel与vm还原字符串数组、控制流平坦化

发布时间:2026/10/6 18:58:31
AST反混淆JS还原实战:用Babel与vm还原字符串数组、控制流平坦化 简介面向JavaScript逆向与爬虫工程师的AST反混淆还原工具二次开发版基于丁仔大佬的开源还原方案进行增强新增10余项功能并修复旧版作用域与兼容性问题可处理2022年4月前最新obfuscator.io混淆规则尤其补充了三元表达式转if-else能力。该版本在还原深度、异常兼容和脚本容错性上均有提升。压缩包共8个文件主体为5个JS脚本覆盖配置项定义、反混淆入口与示例调用等环节另有3个Markdown文档分别说明整体功能、本次更新和使用事项。包体仅54KB轻量即可直接运行适合处理混淆JS代码的逆向调试场景。已有4602人学习下载适合具备一定AST基础、希望快速搭建反混淆工作台的开发者。通过内置示例和文档读者能掌握从混淆规则识别到还原脚本调用的完整思路提升爬虫与JS逆向分析效率。1. AST反混淆JS还原工具它到底在还原什么在 js 逆向和反爬分析的工作里最磨人的不是算法本身而是你拿到一个只能跑不能读的 JS几万行挤成一行变量名清一色_0x4b2a1f字符串全是\x转义想正则替换根本找不到边界。AST 反混淆 JS 还原工具的思路是先把这段代码交给语法树解析器在 AST 节点上做逆向变换——还原字符串数组、压平控制流、剔除死代码最终输出结构正常、能调试的代码。这就是把一个黑匣子脚本变成可读源码的路径。它适合三类人刚接触 JS 逆向的新手、要做客户端脚本私域安全分析的一线工程师、以及需要审阅恶意样本的安全人员。2.0 这个版本号的含义在于反混淆不再是单个脚本而是变成了一条插件化还原管线你需要面对的是整套混淆手法的组合拳。2. 先拆开混淆看本质字符串数组、移位函数与控制流平坦化在AST里长什么样2.1 混淆不是加密一套等价变换的流水线很多人有个误区觉得混淆是加密反混淆是解密。实际上混淆完全没有加密那种单向性。混淆器做的是等价变换——它把你写的源码解析成 AST然后在树上做一系列保持语义的改写。加密后没有密钥就解不开但混淆后的代码你随时都能跑只是人读不懂。从 AST 视角看混淆器的工作可以拆成三类局部改写、结构改写、控制流改写。局部改写针对单个节点比如把admin变成\x61\x64\x6d\x69\x6e把数字10变成0xa结构改写是插入死代码、拆分表达式、把函数调用换成逗号表达式控制流改写最狠直接把顺序执行的语句改写成whileswitch分发器。这三类变换全部无损失、可逆反混淆要做的就是在 AST 上把它们逐层倒回去。理解这一点你就知道为什么反混淆工具不依赖猜——它依赖的结构信息全在树里。2.2 字符串数组与移位函数最常见的混淆也是还原的第一道菜在反爬 JS 脚本里最常看到的混淆手法就是把所有字符串字面量抽到一个数组里再通过下标访问。典型的混淆后形态长这样var _0x4b2a [ \x61\x64\x6d\x69\x6e, \x65\x31\x30\x61\x64\x63\x33\x39\x34\x39\x62\x61\x35\x39\x61\x62\x62\x65\x35\x36\x65\x30\x35\x37\x66\x32\x30\x66\x38\x38\x33\x65 ]; function _0xdecrypt(_0x1, _0x2) { return _0x4b2a[_0x2 - _0x1]; } var username _0xdecrypt(3, 4); var token _0x4b2a[1];这里_0xdecrypt(3, 4)实际返回的是_0x4b2a[1]。为什么因为混淆器先把数组下标做了位移再包一层函数。更复杂的变体会在数组定义后面挂一个自执行 IIFE执行时对数组做一次 shift、push 重新排序然后再通过另一个函数去访问。这就是移位函数名字的由来。还原这类混淆的思路很直接先算出解密函数的输出映射再把所有CallExpression替换成对应的字符串字面量。难点不在替换而在解密函数可能带状态——它不是纯函数内部改了数组顺序你必须在正确的执行时机去求值。2.3 控制流平坦化与死代码注入可读性崩塌的分水岭字符串数组还原后代码基本能看懂了但要是遇到控制流平坦化你依然两眼一抹黑。控制流平坦化的核心是把原来的顺序语句全部塞进switch的case里让执行顺序由状态变量控制function demo(a) { var state 0; var r; while (true) { switch (state) { case 0: if (a 100) { state 2; } else { state 1; } break; case 1: r a 1; state 3; break; case 2: r a * 2; state 3; break; case 3: return r; } } }这段代码读起来像一团乱麻但跑起来逻辑完全正常state就是指令指针while(true)是取指循环switch是分发器。还原它的本质是把 case 里的语句按状态变量的流转顺序重新拼回线性代码。至于死代码注入就是在这些 case 之间掺入永远不会执行的分支和变量专门干扰你手动跟读。这两样叠加是绝大多数人在 js 反爬实战里卡住的地方。2.4 正则替换为什么不行AST做反混淆的三个不可替代优势有人会问字符串数组用正则也能替换啊确实最简单的情况可以但正则方案有三个跨不过去的坎。我用一个对比表来说清楚维度正则方案AST 方案嵌套结构括号、模板串、嵌套数组一匹配就错树节点天然携带层级作用域信息不知道变量绑定在哪改名就撞车每个 Identifier 都能查到 Binding语义判断无法知道一个表达式是否带副作用可基于节点类型做纯度分析更实际的一点是正则做完替换之后你仍然拿不到一份排版好的代码。AST 方案到最后要用 generator 再生成一遍这一步顺带完成了 js 格式化缩进、换行、括号风格全可控。对后面要断点调试的人来说这一步省下的时间比前面所有还原加起来都多。3. 用Babel搭一套可复现的AST反混淆最小管线3.1 为什么选Babel系2.0还原工具的常规工程形态市面上标着 2.0、3.0 的还原工具解压开看基本都是个 Node 工程核心依赖跑不掉babel/parser、babel/traverse、babel/generator这三个包。原因很简单Babel 的解析器覆盖了几乎全部现代 JS 语法遇到特殊语法报错时错误信息清楚而且它本来就是干编译的遍历、替换、生成三个阶段拆得很干净。我自己用下来的体感是自己写 parser 完全是重复造轮子还造不稳。Babel 体系里改 AST 的 API 虽然多但文档和社区样本足够厚10 个混淆样本里 9 个的还原逻辑都能用 Babel 的遍历器直接表达。2.0 工具的工程形态就是把还原动作拆成一个个小插件按顺序塞进管线里跑而不是一个大函数全干完。3.2 底座脚本从parse到generate先把混淆代码格式化先搭最小底座。这一步什么都不还原只完成「解析 → 生成」等价于一个本地 js 格式化工具mkdir ast-recover cd ast-recover npm init -y npm install babel/parser babel/traverse babel/generator babel/typesconst parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; const fs require(fs); const code fs.readFileSync(obfuscated.js, utf-8); const ast parser.parse(code, { sourceType: script, plugins: [] }); // 后续的还原逻辑都写在这里遍历、替换、删除 const output generate(ast, { comments: false, compact: false, minified: false }).code; fs.writeFileSync(readable.js, output);这里有两个参数要注意。sourceType我用的是script因为混淆脚本大多以 IIFE 形式直接执行如果你看到代码里有import、export就要改成module或者用ambiguous让解析器自己去判断。plugins数组按需开比如代码里用了装饰器或者空值合并运算符需要对应加decorators、optionalChaining。生成端把compact和minified都设成false保证输出的是带缩进的完整代码这一步就是还原后的 js 格式化基础。3.3 第一类还原用path.evaluate()做常量折叠替换字符串访问点底座搭好后先处理最简单的情况所有参数都是字面量的表达式。Babel 的path.evaluate()可以直接做常量推导不用自己写递归求值const t require(babel/types); traverse(ast, { CallExpression(path) { const ev path.evaluate(); if (ev.confident typeof ev.value string) { path.replaceWith(t.stringLiteral(ev.value)); } } });这段代码的核心逻辑就一句话能静态求值就求值求出来是字符串就替换成字面量。evaluate()返回的confident字段表示推导结果是否可信value是推导出的值。这个 API 会沿着表达式树一路向上做常量折叠1 2、a b、_0x4b2a[1]这类只要能算出确定值的都会被折叠掉。但这里有个隐藏问题evaluate()一旦遇到函数调用只要参数是常量它也会尝试推导而实际上函数内部可能有副作用。所以第一版还原我建议加个白名单限制只对 callee 是 Identifier 的调用做替换成员表达式调用先跳过后面再处理。3.4 进阶还原把解密函数丢进vm执行拿到字符串数组真实顺序真正实战里的字符串数组几乎都带自执行移位函数。静态求值搞不定这种因为它依赖运行时状态。常见做法是把解密函数连同数组从 AST 里提取出来拼成一个小片段丢进 Node 的vm里执行const vm require(vm); function buildDecryptSnippet(ast) { // 从 AST 里定位三块内容 // 1. 目标数组变量声明var _0x4b2a [...] // 2. 解密函数声明function _0xdecrypt(...) {...} // 3. 自执行 IIFE负责对数组做 shift/push 重排 // 把这三个节点的源码用 generate 拼出来末尾追加 this.getStr _0xdecrypt; // 返回拼好的字符串 } const snippet buildDecryptSnippet(ast); const sandbox {}; vm.createContext(sandbox); vm.runInContext(snippet, sandbox, { timeout: 2000 }); const getStr sandbox.getStr;为什么要把数组、解密函数、自执行调用一起拼进去因为移位函数往往在 IIFE 里改变了数组顺序。你只拿数组不执行 IIFE后面的下标全是错位的只拿函数不拿数组函数没东西可操作。三个必须同上下文执行执行完再挂到沙箱全局对象上供外部调用。timeout: 2000是最低防线防止解密函数里有死循环。拿到getStr之后再遍历 AST 把访问点替换掉traverse(ast, { CallExpression(path) { const node path.node; if ( t.isIdentifier(node.callee, { name: _0xdecrypt }) node.arguments.length 1 t.isNumericLiteral(node.arguments[0]) ) { path.replaceWith(t.stringLiteral(getStr(node.arguments[0].value))); } } });注意这里我用的是「函数名 单参数」的匹配模式这是混淆器常见的一种简化形态。真实样本里参数可能是_0xdecrypt(3, 4)两个参数按 2.2 的例子需要算idx - key你就把参数都取出来用getStr对应的完整逻辑算。如果代码里引用了解密函数后还动态改了函数体这种模式匹配就会被绕过去那也是后面 5.1 要说的坑。3.5 输出参数调优compact、jsescOption与代码风格还原之后生成的代码风格也是要调的。默认的 generator 会把非 ASCII 字符转成\u转义序列中文字符串会变得很难看。我一般这么配const output generate(ast, { comments: false, compact: false, minified: false, jsescOption: { minimal: true }, retainLines: false }).code;jsescOption.minimal设为true后可读字符就直接原样输出不再转义中文文案和 URL 参数一眼就能看明白。retainLines我通常关掉因为还原过程里节点位置早就变了强行保留原行号反而让代码排版很怪。到这一步一个能处理字符串数组混淆的最小还原管线已经能跑了输出的代码从一行挤成几千个字符变成缩进清晰的 JS断点能打了变量名还乱着但至少能读。4. 按混淆场景逐个击破平坦化、执行顺序与死代码的实战策略4.1 控制流平坦化怎么压平定位dispatcher、重建执行顺序处理完字符串下一步通常是控制流平坦化。先要找到 dispatcher也就是那个while(true)switch(state)结构。识别特征有三个循环体是个 BlockStatement、里面恰好一个 SwitchStatement、switch 的 discriminant 是个标识符或成员表达式。找到之后还原的关键是顺着状态变量的赋值跳转重建顺序function readNextState(caseNode) { const body caseNode.consequent; for (let i body.length - 1; i 0; i--) { const stmt body[i]; if (t.isExpressionStatement(stmt) t.isAssignmentExpression(stmt.expression)) { const right stmt.expression.right; if (t.isNumericLiteral(right)) return right.value; return null; } } return null; } const cases switchStmt.cases; const order []; const visited new Set(); let cur 0; while (cur ! null !visited.has(cur)) { visited.add(cur); order.push(cur); cur readNextState(cases[cur]); }这段逻辑的意义是从 case 0 开始把每个 case 末尾给状态变量的赋值当作下一条指令的地址直到某个 case 末尾没有赋值说明是出口或者出现循环引用。拿到order后把 case 里的语句按这个顺序拼接再把外层的 while 删掉替换成顺序语句块平坦化就算压平了。但这里有个必须说清的边界这个简化版本只处理「顺序型」平坦化——每个 case 末尾只有一个常量赋值。真实混淆经常在 case 里嵌套 if-else不同分支给不同状态值那就需要把 case 拆成多个基本块重建控制流图。我自己的经验是能自动压平的比例大概六到七成剩下的是嵌套循环和状态变量依赖函数返回值的情况遇到这种就标记为「无法自动压平」别硬压。4.2 还原顺序不能乱先跑解密器再替换访问点最后删数组字符串数组还原的操作顺序是新手最常翻车的地方。正确顺序永远是三步第一步把解密器和数组提取出来执行拿到真实映射第二步遍历所有访问点把调用替换成字符串字面量第三步确认没有任何引用后再删掉数组声明和解密函数。很多人图省事替换完访问点立刻把数组定义删了结果后面还有一处访问点是用动态方式引用数组的运行时报ReferenceError。AST 工具的 delete 操作不像文本编辑器删掉就找不回来了动手之前先确认Binding的引用已经清零。我一般会用path.scope.getBinding(_0x4b2a).references看一眼引用数归零了再删。这类坑在 js 反爬实战里很典型还原结果在自己的测试环境跑通了换到真实环境就报错多半就是执行顺序上有残留。4.3 删死代码前先查副作用别把一个能跑的JS改崩掉死代码注入是混淆器最无赖的手段之一插一堆永假分支、未使用变量、不可能的循环来干扰阅读。但清理死代码是风险最高的还原操作一不小心就把有副作用的真代码当死代码删了。我判断能否删除的依据很简单先看表达式的纯度再查引用。function isSafeToRemove(path) { try { const ev path.evaluate(); if (ev.confident) return true; } catch (e) {} return false; }evaluate()求出来confident为 true 的才能考虑删。注意这里的隐藏语义如果表达式里有函数调用哪怕参数全是字面量evaluate 也经常返回confident: true但这个函数内部可能有副作用。所以还要加一层节点类型检查——CallExpression 一律视为有副作用除非 callee 是已知的纯函数MemberExpression 也要小心因为 getter 可能触发计算。最保守的策略是只删三类节点未引用的变量声明、逻辑上不可能到达的语句、以及Literal、BinaryExpression组成的纯表达式语句。宁可少删不可多删删崩了连还原前的代码都找不回来。4.4 遇到eval和new Function静态AST还原的边界与动态兜底静态 AST 还原有个明确的边界代码一旦把解密结果丢进eval或new FunctionAST 里只有字符串参数看不到这段代码的真实结构。这是 JS 语言本身给的死局任何还原工具都没法在纯静态层面破。常见的做法是动态兜底——在 Node 的vm里 hook 掉eval把执行时传入的代码片段捕获下来再单独还原const captured []; const sandbox { eval: function (code) { captured.push(code); return globalThis.eval(code); } }; vm.createContext(sandbox); vm.runInNewContext(sourceCode, sandbox, { timeout: 5000 });捕获到的代码片段可能是另一段被混淆的字符串那就形成一个「还原 → 执行 → 再捕获」的循环。我在实际样本里见过最深套到第四层的。处理动态跟静态不一样它需要跑真实代码所以样本可信度要先确认别拿不明来源的脚本直接在宿主环境裸跑。放到vm里、限时、无网络权限这三条是底线。5. 五个会让AST还原翻车的真实踩坑记录5.1 还原后运行结果不一致path.evaluate()的confident不总是可信现象用 3.3 的脚本还原一段代码字符串确实替换了但跑起来结果和原混淆代码不一致某些变量变成undefined。原因path.evaluate()对包含函数调用的表达式也会做常量推导它只判断参数是否可静态求值。如果被调函数内部依赖外部状态比如读全局变量、操作 DOM、修改了数组求出来的值在替换那一刻是错的或者替换改变了求值时机。解决给 CallExpression 的替换加白名单。只有 callee 是自己识别过的解密函数时才替换其余 CallExpression 一律不碰。识别解密函数不能只看名字要看它的函数体是否只做纯运算——没有外部变量访问、没有除赋值以外的副作用。5.2 字符串还原后变成乱码一次解码解不开的多层编码现象还原出来的字符串是一串\u0000、%0A、\x混着的乱码看着比混淆前还难读。原因混淆器把字符串先做了多层编码常见的是先 URL 编码再转十六进制或者反过来。你只用了一层解码逻辑自然剥不干净。这种玄学问题最坑因为每层编码都长得像最终结果。解决不要自己一层层猜。直接执行解密函数拿最终值拿出来的就是明文。静态解码只用来做交叉验证——如果 vm 跑出来的结果和静态解码对不上说明漏了一层处理优先相信 vm 的结果。5.3 控制流还原卡死循环哪些平坦化结构不能自动压平现象还原脚本跑到某个文件时长时间不结束或者输出的代码里还残留while(true)浏览器一执行就死循环。原因4.1的平铺算法要求每个 case 末状态变量赋值为常量。如果 case 里嵌了内层循环或者状态变量的新值是函数返回值、数组下标、计算结果这个简单的顺序重建就失效了。最常见的是状态赋值右侧是一个ParseInt(...)或_0x4b2a[1]。解决readNextState返回 null 时不要继续遍历直接放弃该处的自动压平保留原结构并打一条警告日志。血泪经验是宁可少还原不要硬还原压平算法一旦在错误的地方拼接代码出来的代码比混淆代码更没法调。5.4 sourceMappingURL没剥掉断点位置全错位的排查现象还原后的代码在浏览器里打断点命中的位置总是跳到莫名其妙的地方单步跟踪也全乱了。原因混淆脚本末尾可能带着//# sourceMappingURL...或者脚本内部有 sourcemap 相关注释。Babel 生成代码时默认会尝试处理源映射注释结果把还原前的行列信息和还原后的代码错位绑定了。解决在 parse 之前用正则把sourceMappingURL注释整体剥掉同时生成端显式关闭源映射相关配置。这一步要在进入还原流程前做否则后面所有行号都是脏的。5.5 还原变量名撞车作用域污染毁掉整份输出现象还原结果在控制台里直接报Identifier a has already been declared或者某个变量值被意外覆盖。原因还原插件往 AST 里插入临时变量时用了简单的名字拼接比如_0x加数字而混淆代码里本身就有一堆同名变量。Babel 的作用域系统在生成阶段会做重命名但如果你绕过作用域 API 直接replaceWith一段拼好的标识符代码它不会帮你做冲突检测。解决插入任何新标识符都用path.scope.generateUid()生成唯一名字不要手动拼。如果改的是现有变量名先path.scope.rename(oldName, newName)让 Babel 同步更新所有引用再动输出。6. 把还原工具往2.0打磨自检脚本、快照回归与增量插件6.1 用vm双跑做执行一致性自检还原工具做得再顺手没有验证环节就是盲改。我每次调整还原逻辑后都会跑一遍双执行对比把混淆前的原码和还原后的代码分别丢进两个vm沙箱执行采集console.log输出和退出状态然后做 diff。const vm require(vm); function runInSandbox(code) { const logs []; const sandbox { console: { log: (...args) logs.push(args.join( )) } }; vm.createContext(sandbox); try { vm.runInNewContext(code, sandbox, { timeout: 5000 }); } catch (e) { return { ok: false, logs, error: e.message }; } return { ok: true, logs }; } const before runInSandbox(originalCode); const after runInSandbox(recoveredCode); const pass before.ok after.ok before.logs.join(\n) after.logs.join(\n); console.log(pass ? E2E PASS : E2E FAIL);timeout: 5000防止还原后代码里藏了死循环console.log是主要的行为指纹。这套脚本要进工具的每次运行流程别改成手动跑否则几十个样本你根本没法逐个看。6.2 快照回归改一处还原逻辑不弄坏另一类样本样本文件主要混淆类型是否应通过sample_string_array.js字符串数组 移位是sample_flat_control.js控制流平坦化是sample_mixed.js上述两者组合是sample_not_obfuscated.js无混淆是原样通过把上面的 E2E 脚本对一批固定样本批量执行输出结果和 git 里保存的期望输出做 diff。任何一个还原插件的改动只有全量样本都通过才算安全。这比任何单元测试都实在因为 AST 还原的边界就是「改一个样式弄坏另一个」。6.3 增量式插件架构新混淆样式来了怎么加2.0 工具的核心变化是把所有还原逻辑拆成按序执行的独立插件字符串解旋、常量折叠、死代码剔除、控制流压平、变量重命名每个插件只干一件事。新遇到一种混淆样式时新增一个插件插到对应阶段不影响其他环节。坦白说这是我被翻车教育出来的习惯——早先我把所有逻辑写进一个 traverse 回调里换了个样本直接全盘崩掉想定位是哪一步出的问题都无从下手。拆成插件之后出问题能单独禁用、单独调参、单独验证。现在我的做法是每个插件接收(ast, config)config 里放开关和阈值比如「是否强制压平嵌套 case」「字符串替换是否执行 vm 模式」。这样同一个工具对不同来源样本可以快速切换策略。还原这条路没有终点只要混淆器还在出新手法工具就得跟着迭代。希望这套思路和踩坑记录帮到你至少让你下次打开那个 zip 包的时候知道里面的代码在干什么、自己该从哪一步改起。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号