尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
AST解密Akamai混淆JS:从原理到Babel还原实战
拿到一份Akamai的混淆JS第一反应往往是——这玩意儿真是给人看的吗变量名全是_0x开头字符串被塞进一个巨大的数组里函数逻辑被拆得七零八落甚至还有一堆死代码和冗余分支。如果你在安全分析、性能诊断或者合规审查时遇到这类脚本最需要的不是硬着头皮读而是找到一条能快速把混淆代码还原到可读状态的路径。AST抽象语法树就是这条路径的核心工具。这篇文章我会完整梳理用AST技术解密Akamai混淆JS的实战流程从环境准备、AST基础到识别混淆特征、编写还原脚本再到实际样本的还原验证。整个过程会涉及Babel工具链的使用、字符串解密、控制流平坦化还原、标识符重命名等核心环节。文章面向已经会写基本JavaScript的读者你不需要提前精通编译原理只要跟着流程走一遍就能掌握一套可以复用到其他混淆样本上的解混淆方法。1. 先搞清楚Akamai到底在混淆什么1.1 一段典型的Akamai混淆代码长什么样Akamai Bot Manager这类产品会在浏览器端下发一段JavaScript用来做设备指纹采集、行为检测和挑战应答。为了保护这段逻辑不被轻易分析和复用它在下发前会做多层混淆处理。最直观的表现就是代码里几乎找不到任何有意义的单词所有标识符都被替换成类似_0x3f2a的十六进制风格名字字符串字面量被集中抽到一个大数组里运行时再通过下标取回。我拆过一份典型的样本开头长这样var _0x4b8a [\x72\x65\x64\x75\x63\x65, \x6c\x65\x6e\x67\x74\x68, \x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65, \x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74, ...]; function _0x1f2e(_0x3c9a, _0x5b7d) { var _0x4b8a _0x5b7d ? function() { return _0x3c9a.apply(this, arguments); } : function() { return _0x3c9a; }; return _0x4b8a; }再看函数体内的逻辑一个简单的字符串拼接会变成从数组里多次取值的操作中间还穿插着无意义的条件判断和永远不会执行的分支。如果只靠人肉翻一个几百行的脚本能看一整天。这类混淆的核心思路是在不改变程序语义的前提下让代码的结构变得极其难以理解。它不追求加密强度而是追求让分析成本无限升高。所以解混淆的本质也不是解密而是还原——把被伪装、被打散、被重命名的信息重新组织回我们可以理解的形式。1.2 为什么AST是解混淆的必经之路很多人第一反应是直接用正则去替换、去提取字符串。我最早也这么干过结果就是修了一个补丁又冒出一个新坑。原因在于混淆后的代码是高度结构化的正则只能处理文本层面的模式但它无法理解代码的嵌套关系、作用域和依赖顺序。AST则完全不一样。它会先把源码解析成一棵结构化的树每个节点都有明确的类型和层级关系。比如一个变量声明是VariableDeclaration一个函数调用是CallExpression一个字符串字面量是StringLiteral。我们可以在树的层面做精准的查找、替换、删除和重组而且可以保证操作后的代码在语法上依然是合法的JavaScript。Babel这个工具链把AST操作的门槛降到了极低。它提供了完善的解析器、遍历器和生成器我们只需要写几个插件函数就能批量处理各种混淆模式。这也是我在这篇文章里选择Babel做完整流程演示的原因——它成熟、稳定、资料多踩坑成本低。理解了AST这套思路后面碰到任何JS混淆变种你都可以用同样的方式来应对。2. 准备工作工具链与AST基础2.1 实操环境与依赖安装整个流程只需要Node.js环境和npm就能跑起来。我建议用一个干净的目录来实验避免跟你已有的工程依赖冲突。mkdir akamai-deobfuscator cd akamai-deobfuscator npm init -y npm install babel/parser babel/traverse babel/types babel/generator这四个包分别负责解析、遍历、类型判断与节点构造、代码生成。如果是第一次接触可能会疑惑为什么不用esprima或者acorn。说实话acorn的解析速度更快esprima也很经典但它们在做AST节点替换和重组时都远不如Babel全家桶来得方便。Babel的babel/traverse内置了完整的遍历机制你可以在进入某个节点时修改它也可以在离开时再改这对还原控制流平坦化这类需要多次遍历的场景非常关键。安装完成之后先准备一个入口脚本把解析和生成的骨架搭起来const parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; const fs require(fs); const code fs.readFileSync(input.js, utf-8); const ast parser.parse(code); // 这里后面会插入各种还原插件 const output generate(ast, { compact: false }).code; fs.writeFileSync(output.js, output);这样一个最小闭环就完成了。后面所有的解混淆逻辑都是在parse和generate之间操作这棵AST树。2.2 把混淆代码变成一棵AST树Babel解析出来的AST本质上是一个纯对象结构。每个节点都有type字段比如Program、FunctionDeclaration、MemberExpression还会有对应的子节点和属性。看一个最简单的例子。源码是var a hello;解析成AST后结构会变成Program body: [ VariableDeclaration declarations: [ VariableDeclarator id: Identifier (name: a) init: StringLiteral (value: hello) ] kind: var ]有了这棵树我们就可以精确地找到代码里的每一个字符串、每一个变量名、每一次函数调用。混淆脚本里常见的_0x1234[length]这种写法在AST里其实就是MemberExpression其中property是一个StringLiteral。我们只需要判断这个StringLiteral的内容是否可以被安全替换成标识符就能把它还原成_0x1234.length。不过AST也有一个门槛你刚开始操作时需要频繁地查看节点结构。我的习惯是先写一个简单的打印脚本把某个节点的JSON.stringify输出出来对照着看一遍它有哪些属性。这一步能帮你快速建立对AST的感觉后面的所有编写效率都会高很多。3. 核心流程四步还原法3.1 第一步精准识别混淆特征拿到代码后不要急着写脚本先做一次体检。我用Akamai样本的经验是重点看三个特征是否有一个巨大的字符串数组并且有一个专用的解码函数来访问它函数内部是否存在大量while/switch嵌套或者浓厚的if...else死代码分支标识符是否全部变成了_0x开头或者使用了十六进制转义的字符串这三个特征分别对应三种最常见的混淆手段字符串数组加密、控制流平坦化、标识符混淆。实际操作时Akamai往往会把它们叠加在一起还会在数组访问函数外面再套一层加密让你不能简单地通过替换下标为字面量来还原。识别这一步不要靠肉眼硬看可以写几个简单的统计脚本。比如统计所有Identifier节点里_0x开头占的比例统计所有StringLiteral节点里包含转义字符的数量。这些数字能直接告诉你混淆的力度和侧重点。3.2 第二步字符串解密还原字符串数组加密是Akamai混淆里最基础也最关键的一环。通常的模式是var _0xarr [\x68\x65\x6c\x6c\x6f, \x77\x6f\x72\x6c\x64]; function _0xget(_0xidx) { return _0xarr[_0xidx]; } var msg _0xget(0) _0xget(1);这个例子里_0xget(0)在运行时能取到hello_0xget(1)能取到world。但在静态分析时我们看不到msg的值。要还原这一类混淆思路是找到数组的定义找到访问数组的函数然后模拟这个函数的执行过程。这里有一个很关键的原则——只有当函数的入参全部是字面量时才适合做静态执行。如果某个调用的参数来自一个变量那我们就不能贸然替换否则会改变程序语义。在Babel里可以这样写一个基础插件const stringArrayName _0xarr; const decryptFunctionName _0xget; const stringArray {}; // 第一步收集数组内容 traverse(ast, { VariableDeclarator(path) { if (path.node.id.name stringArrayName) { // 这里需要判断 init 是 ArrayExpression然后遍历 elements 收集值 } } });收集完数组内容后再遍历所有CallExpression如果调用的是_0xget且参数是数字字面量就把它替换成对应的字符串字面量。这一步做完代码里的可读性会提升一大截字符串全部变成明文了。实际操作中Akamai会把数组拆成多个还会对下标做异或运算。比如_0xget(0x3f ^ 0x2a)这种写法。处理办法是递归地计算参数表达式如果参数表达式里全部是字面量和运算符就先用一个简单的表达式求值器算出结果再替换。这里要特别小心不要试图在浏览器里直接eval它而是自己写一个只支持常用运算符的计算函数避免引入执行风险。3.3 第三步控制流平坦化的还原字符串解密只是热身真正让代码难以阅读的是控制流平坦化。它的原理是把原来的if...else、while、for等分支结构全部拍平成一个while循环加switch分发的结构。从AST上看原来的块语句被拆散成一个大的状态机每次循环根据一个分发变量跳转到不同的case。我见过的最小化结构是这样的var _0xstate 3; while (true) { switch (_0xstate) { case 3: doSomething(); _0xstate 5; break; case 5: doAnotherThing(); _0xstate 0; break; case 0: return result; } }还原控制流平坦化是解混淆里最难的一步。我的实践方法是基于路径跟踪的状态机重组先找到while(true)节点并定位到它内部的switch。找到分发变量的初始赋值语句把它作为起始状态。遍历每个case记录该case的执行语句以及执行完毕后分发变量被修改成什么值。根据状态跳转关系把散落的语句块重新拼接成顺序执行的代码插入到原while节点所在的位置。这听起来简单但实际落地时有几个坑。Akamai会在每个case里插入大量不相关的死代码比如永远为true的条件判断、不会执行的break甚至会把分发变量的赋值藏在某个深层嵌套函数里。这些都需要在重组前做清洗。我在自己的还原脚本里会先用一个函数分析当前case的内容里是否包含对分发变量的赋值。如果存在多层嵌套就递归地深入查找。同时我会把识别出来的每个case的执行内容复制到一组BlockStatement里最后用这些BlockStatement替换掉原来的while节点。这个方案我实测了很多次最终处理效果都还挺稳定。3.4 第四步标识符重命名与美化输出前面的还原做完后代码语义已经清晰了很多但变量名还是_0x开头阅读起来依然难受。这一步做两件事将有意义的函数调用、变量声明在AST层面尝试还原成更直观的名称。比如某个函数内部调用了一段设备指纹检测逻辑如果能在上下文里识别出特征可以手动维护一张映射表把_0x3f2a改成getFingerprint。对剩余没有明确语义的标识符统一重命名为a、b、c这样连续的短名字减少视觉噪音。需要注意重命名必须保证同一作用域内不出现冲突。Babel的path.scope机制可以帮我们生成唯一的名字避免手动维护计数器。最后使用babel/generator输出代码时可以开启compact: false来美化格式这样代码会按照标准缩进输出可读性会大幅提升。我还会开具retainLines: false因为原始混淆代码的行信息基本没有利用价值保留反而会让格式混乱。4. 实操记录还原一份真实Akamai样本4.1 样本概览与混淆模式统计我找了一份比较典型的Akamai Bot Manager脚本作为测试样本文件大小约60KB格式化后大概3500行。用脚本统计了一下_0x开头的标识符占全部标识符的94%字符串数组一共有两个每个数组长度都超过了300。函数内部的控制流平坦化结构出现了7处其中最大的一个状态机包含了40多个case。这份样本还追加了一层自保护数组访问函数内部用了三异或运算而且数组本身在程序开头会被一个立即执行函数重新排列。也就是说你不能直接静态地按顺序读数组内容必须先找到那个洗牌函数模拟它执行之后才能得到真实的数组映射关系。4.2 核心还原脚本的完整实现整个还原脚本我分成四个模块分别对应上面的四步。因为篇幅有限这里重点展示字符串数组解密这个模块的核心逻辑。const parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; const t require(babel/types); function evaluateLiteral(node) { if (t.isNumericLiteral(node) || t.isStringLiteral(node) || t.isBooleanLiteral(node)) { return node.value; } if (t.isBinaryExpression(node)) { const left evaluateLiteral(node.left); const right evaluateLiteral(node.right); if (left undefined || right undefined) return undefined; switch (node.operator) { case : return left right; case -: return left - right; case *: return left * right; case ^: return left ^ right; default: return undefined; } } return undefined; } function collectArrayContent(path) { const arr {}; if (t.isArrayExpression(path.node.init)) { path.node.init.elements.forEach((el, idx) { if (t.isStringLiteral(el)) arr[idx] el.value; }); } return arr; } function decryptStringArray(ast) { let stringMap {}; // 第一步找出所有数组定义 traverse(ast, { VariableDeclarator(path) { if (t.isArrayExpression(path.node.init)) { const values collectArrayContent(path); stringMap[path.node.id.name] values; // 保留数组定义后面处理函数会用到 } } }); // 第二步解析数组访问函数的调用 traverse(ast, { CallExpression(path) { const callee path.node.callee; const args path.node.arguments; if (t.isIdentifier(callee) stringMap[callee.name]) { const idx evaluateLiteral(args[0]); if (idx ! undefined stringMap[callee.name][idx] ! undefined) { path.replaceWith(t.stringLiteral(stringMap[callee.name][idx])); } } } }); }这段代码的思路是先扫描所有数组变量把下标和字符串值的对应关系存到stringMap里再扫描所有函数调用如果被调用的函数名在stringMap里并且参数能求值为数字就直接替换成对应的字符串字面量。需要注意这个简化版本没有处理数组洗牌和多级解密函数的情况。实际样本里数组在定义之后还会被一个自执行函数重排所以单纯按照顺序去取下标是不对的。我的处理方法是先找到洗牌函数在AST层面模拟执行它对数组的重排然后再做字符串替换。具体来说就是先创建一个临时的数组副本遍历洗牌函数体内的赋值语句逐步更新副本内容最后用副本覆盖原始数组的元素顺序。4.3 还原效果与结果对比运行完整个脚本之后我看到的效果非常直观字符串明文率从几乎为0提升到接近100%所有十六进制转义的字符串都变成了可读内容。控制流平坦化的7处结构全部被消除替换成了顺序的if/else或者直接平铺的语句块。代码总行数从3500行下降到约1800行其中很大一部分是去除死代码和冗余函数后的结果。我把还原后的代码重新生成到一个新文件里打开看第一屏已经能清晰地看到函数名、字符串内容和基本的业务逻辑了。当然要达到完全看懂的水平还需要结合运行时调试来确认某些变量的值但相比直接用_0x堆砌的原始包这已经是从天书到能读懂的巨大跨越。5. 常见问题与避坑指南5.1 高频报错与排查思路报错1Property left of BinaryExpression expected node to be a Expression这个报错常见于尝试构建或替换表达式时左值或者右值传入了不完整的数据。排查思路是先打印当前节点看看是不是undefined确认无误后再构造。我自己的经验是遇到这类问题先检查遍历时对节点类型的判断很多情况下是因为把Statement当成Expression用了。报错2替换后生成代码语法错误这种问题通常是替换节点时破坏了语法结构。比如把一个Expression直接替换成了BlockStatement或者把StringLiteral替换进了本该是Identifier的位置。解决办法是在替换前用t.isXxx做一次类型校验也可以用babel/parser重新解析替换后的代码看能否通过语法解析。报错3字符串解密不生效最常见的原因是数组访问函数内部有多层封装或者参数里包含函数调用而不是纯字面量。排查思路是先确认数组是否被洗牌过再确认调用参数能否被evaluateLiteral求值。如果参数是_0x3f2a(0x10)这类嵌套调用需要递归处理。报错4死代码清理后变量未定义清理死代码时有些变量可能只在死代码分支里被声明一旦删除分支其他地方引用了该变量就会报错。所以清理前最好先做一次全量引用统计或者使用path.scope检查变量是否只有声明没有引用确认安全后再删除。5.2 几条真正值钱的实战教训不要一上来就写通用还原框架。我见过很多人想一步到位做一个全自动、能处理所有混淆的框架结果写到一半就卡住了。正确的做法是先针对手头样本做一个最小可用的脚本跑通之后再考虑抽象通用逻辑。混淆技术更新很快通用框架反而容易变成一块巨大的维护负担。先备份原始样本。每次操作前都保留一份原始文件因为还原脚本很可能会出错甚至把代码改坏。我在实践时会把原始样本按版本号归档这样出了问题可以快速对比是哪一步导致的。善用path.skip()和path.stop()控制遍历范围。遍历整棵AST的开销不小如果知道某个子树肯定不需要处理直接跳过能显著提升脚本执行速度。特别是大型样本遍历优化可以省下大量时间。尽量别在Node环境里执行混淆代码的逻辑。有些场景下直接在脚本里用eval执行混淆函数去取解密结果看起来省事但会带来安全风险。原因是你无法确定这段代码在运行时会访问什么全局变量或者触发什么隐藏副作用。更安全的做法是维护一个小型表达式求值器只支持自己需要的运算符。最后再分享一个我自己的习惯处理完一段混淆代码之后不要把还原结果直接扔掉我会把它压缩存档并且在旁边留下一份还原过程的技术笔记。因为同一家厂商的混淆脚本通常会有固定的代码生成器你这次总结出的模式很可能在下一次分析里派上大用场。每次做这种逆向还原实际上都是在和混淆器的开发者打交道你越了解他们的生成习惯你的还原效率就会越高。
RELATED

相关推荐

Linux rsync远程同步带密码认证:SSH与daemon模式实战

Linux rsync远程同步带密码认证:SSH与daemon模式实战

1. 弄懂rsync远程同步与密码认证的核心矛盾刚接触Linux运维那会儿,我最怕的就是跨服务器传文件。用scp吧,小文件还行,一旦遇到几十GB的日志或者上万个小文件,那进度条能让人等到怀疑人生。后来老同事甩给我一条rsync命令&#xff…

📅 2026/9/16 21:09:48
RISC-V端侧AI推理:RVV 1.0手写算子与Titan引擎优化实战

RISC-V端侧AI推理:RVV 1.0手写算子与Titan引擎优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/16 21:09:48
MeteorSeed代码生成框架:提升CRUD开发效率的利器

MeteorSeed代码生成框架:提升CRUD开发效率的利器

1. MeteorSeed项目概述MeteorSeed是一个面向开发者的轻量级代码生成框架,它通过解析数据库结构自动生成基础CRUD代码,显著提升后台管理系统的开发效率。我在实际项目中使用这个工具已经两年多,它帮我节省了至少40%的重复编码时间。这个工具特…

📅 2026/9/16 21:04:48
MORE NEWS

更多资讯

📰

零基础Python调用豆包大模型API实战教程

很多朋友一听到“大模型开发”就心里发怵,总觉得这是算法工程师才能碰的东西,自己连代码都没写过几行,学这个是不是自讨苦吃。实际上,现在是做AI应用最好的时候,因为底层的模型能力都被封装成API了,你不需要…

📰

S7-200与组态王实现单容液位控制系统:从PLC程序到画面组态全解析

来了。既然标题里都说了“手把手把程序逻辑和画面组态揉碎了讲”,那咱们就不兜圈子,直接进入正题。单容液位控制在工控领域里算是入门级的经典对象,但恰恰是这种“经典”,才最考验基本功。很多刚入行的朋友上来就盯着PID参数怎么调…

📰

BCELoss与BCEWithLogitsLoss:二分类损失函数详解

二分类任务是深度学习里最常见也最容易被轻视的场景,而BCELoss与BCEWithLogitsLoss正是PyTorch中处理这类任务的两大核心损失函数。我见过太多人在这两个函数上踩坑:有的忘记加Sigmoid导致loss直接NaN,有的在多标签任务里误用了多分类的Cross…

📰

shadcn-svelte Table 组件详解:从基础响应式表格到 Data Table 实战

shadcn-svelte Table 组件详解:从基础响应式表格到 Data Table 实战 【免费下载链接】shadcn-svelte shadcn/ui, but for Svelte. ✨ 项目地址: https://gitcode.com/GitHub_Trending/sh/shadcn-svelte shadcn-svelte 是 shadcn/ui 在 Svelte 生态的官方移植…

📰

Colibri:纯C实现的MoE边缘推理引擎

1. Colibri不是蜂鸟,是前沿模型推理引擎的代号你搜“colibri”,首页跳出的可能是宠物鸟饲养指南、某款蓝牙耳机型号,或是巴西某家咖啡馆的官网——但最近半年,在AI基础设施工程师的私密讨论组、GitHub issue评论区和深夜编译日志里…

📰

PHP反序列化漏洞实战:字符串逃逸与session注入绕过过滤

这道题我在BUUCTF上刷的时候卡了挺久,不是因为反序列化本身多难,而是入口藏得比较深,加上filter会把关键词替换成空字符串,导致序列化数据长度对不上,直接unserialize就炸。后来把源码审计思路捋顺之后发现&#xff0c…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬