从混淆代码到可读逻辑:AST解析极验脚本的安全研究方法
围绕混淆JavaScript的AST分析展开,介绍环境准备、节点遍历、字符串还原、控制流整理和版本适配思路,并结合授权测试场景说明如何建立稳定、可审计的分析流程。
验证码脚本往往体积不大,却把大量逻辑藏在字符串数组、匿名函数和控制流平坦化结构里。直接阅读压缩后的代码,常常只能看到一串难以追踪的变量名;如果依赖人工逐行修改,又很容易因为一个节点处理错误而破坏整个脚本。AST,也就是抽象语法树,提供了一条更稳妥的路径:先把JavaScript解析成结构化节点,再按语法类型批量处理,最后重新生成可阅读的代码。
这类分析应当限定在企业自有系统、测试环境或已经取得明确授权的场景。验证码的作用是降低批量注册、撞库和恶意请求风险,研究脚本结构的目的应当是审计前端逻辑、验证防护强度、排查兼容性问题,以及帮助开发团队修复暴露的设计缺陷。涉及真实业务时,应优先使用官方测试开关、沙箱凭证和服务端校验,避免把实验代码直接用于绕过线上安全控制。
一、先把分析环境搭起来

AST处理通常放在Node.js环境中完成。Node负责执行脚本,Babel则提供解析、遍历、节点构造和代码生成能力。安装时可以选择长期支持版本,保证不同机器上的行为尽量一致。项目目录内初始化依赖后,再安装以下几个常用模块:
npm init -y
npm install @babel/parser @babel/traverse @babel/types @babel/generator安装完成后,使用一个很小的检测脚本确认模块可以正常加载。解析器负责把源码转换为File、Program、FunctionDeclaration等节点;traverse负责访问节点;types用于判断和创建节点;generator负责把修改后的树重新输出为JavaScript。fs模块则用来读写本地样本。

分析前最好建立独立目录,保留原始文件、副本文件和生成结果。每完成一类转换,就单独输出一次中间文件。这样一旦结果出现语法错误,可以迅速定位是字符串替换、声明删除还是控制流整理导致的问题。不要直接覆盖原始样本,也不要把生产环境脚本、Cookie、设备指纹或用户数据放入实验目录。
还要注意版本差异。不同版本的脚本可能使用相似的混淆套路,但数组名称、函数名称、节点嵌套层级都会变化。分析程序应当根据节点结构判断目标,而不是把某个固定变量名写死在每一个规则里。

二、从语法树认识混淆代码
解析源码时,可以指定sourceType为unambiguous,减少普通脚本和模块脚本之间的兼容问题。解析成功后,先统计顶层变量、函数、循环和分支数量,再决定清理顺序。顺序很重要:字符串表通常要先还原,控制流才能看清;无效包装函数应当在依赖关系确认后删除,不能一开始就大面积裁剪。

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generate = require('@babel/generator').default;
const fs = require('fs');
const source = fs.readFileSync('sample.js', 'utf8');
const ast = parser.parse(source, {sourceType: 'unambiguous'});
traverse(ast, {Identifier(path) { /* 记录节点 */ }});
fs.writeFileSync('readable.js', generate(ast).code);节点判断要尽量使用Babel提供的类型函数,例如t.isCallExpression、t.isMemberExpression和t.isNumericLiteral。与其直接访问一长串可能不存在的属性,不如先逐层确认节点类型,再读取对应字段。这样可以避免某个版本把成员表达式改成计算属性后,脚本突然抛出undefined错误。
混淆脚本中常见一种包装结构:真实字符串放在数组里,外层函数通过数字索引、偏移量或自定义变换返回结果。另一个常见结构是把大量语句塞进switch分支,再用循环变量控制执行顺序。前者属于常量折叠和字符串表还原,后者属于控制流平坦化。二者都可以通过AST定位,但计算结果时必须使用隔离环境,并限制执行权限。

如果样本包含动态执行、浏览器对象或网络请求,不要为了得到结果而直接在真实浏览器上下文中运行未知代码。可以先替换无关接口,记录函数输入输出,或者采用静态推断。分析的重点是恢复程序结构,而不是让整段脚本在完整用户环境里自由运行。
三、字符串表与编码节点的还原

有些代码把普通字符写成Unicode转义、十六进制转义,或者通过节点的extra属性保留原始写法。Babel解析后,字面量通常已经具备真实值,但生成器可能继续沿用原始表示。对于只想得到可读结果的场景,可以删除不必要的extra属性,让生成器按照当前节点值输出。
字符串表处理需要先识别初始化声明,再识别调用位置。假设某个变量指向数组,另一个函数根据数字参数返回数组成员,处理器可以记录映射关系,并把满足条件的调用节点替换为StringLiteral。替换前必须检查参数是否真的是数值常量,调用对象和函数名是否匹配,数组是否在当前作用域内可确定。任何条件不满足,都应保留原节点,避免误改业务逻辑。

作用域是这里最容易被忽略的细节。相同的变量名可能在不同函数中重复出现,简单地使用全局数组保存名称,可能把局部变量误判为目标字符串表。更稳妥的做法是读取path.scope,结合绑定关系确认声明来源;如果转换器暂时只服务于一个样本,也至少要在每个函数开始时清理临时状态。
替换完成后应再次遍历AST,检查是否还存在目标调用、未使用的数组声明和孤立的包装函数。删除声明前要确认绑定没有被其他节点引用。对于确实无引用的函数,可以调用path.remove;对于仍然被调用的函数,只清理无关分支。清理操作要小步进行,每一步都生成可回滚的结果。

编码还原并不等于逻辑还原。把字符显示出来,只能解决阅读障碍;真正的行为仍可能藏在位运算、环境检测、时间戳和随机数处理中。对于这些部分,应记录输入、输出和调用关系,给节点标注说明,而不是随意替换成猜测值。静态分析保持可重复,后续才能与不同版本进行差异比较。
四、控制流平坦化的整理思路

控制流平坦化常把原本顺序执行的语句拆成多个case,再通过一个状态数组或映射表决定下一步。阅读时,可以先确认循环初始化值、终止条件和状态转换表,再按照状态顺序收集每个case中的语句。收集过程中要处理break、continue和赋值更新,遇到嵌套循环或异常跳转时应停止自动重排,交给人工复核。
自动整理前,建议把switch节点复制到内存中的新结构里,先生成一份线性化预览,再决定是否写回原AST。预览中为每个状态保留来源位置,便于和原始代码对照。只有当所有状态都能找到唯一后继,并且没有无法解释的动态索引时,才适合进行替换。

一个可靠的规则通常包含四层校验。第一层检查循环节点和前置声明是否存在;第二层检查switch判别式是不是标识符;第三层确认状态表和case标签可以静态求值;第四层检查每个分支是否存在明确的终止或跳转。任何一层失败,都返回原节点。宁可少还原一段,也不要输出看似整齐但语义已经改变的代码。
整理控制流后,需要使用生成器重新输出并进行语法检查。若项目允许,可以用Node重新解析生成文件,确认没有残缺节点;再对关键函数做输入输出对比,检查转换前后是否保持一致。测试样本应使用脱敏数据,验证重点放在代码结构和错误处理上,不应触碰真实用户验证流程。

不同版本之间的适配,也应采用结构特征而非固定文本。例如,寻找带有数组初始化、数字索引调用和循环状态更新的节点组合,比搜索某个具体函数名更可靠。可以把每一类规则封装成独立visitor,并为规则记录命中次数、跳过原因和输出位置,后续升级脚本时定位问题会轻松很多。
五、把研究流程落到合规测试中

当AST处理用于自有前端或授权测试时,建议把流程分成样本采集、结构识别、局部转换、结果验证和报告整理五个阶段。样本采集记录来源、时间和版本;结构识别只做统计;局部转换保持小范围;结果验证比较语法和行为;报告则说明发现的风险、影响范围和修复建议。每一步都有记录,团队成员才能复现结论。
对于验证码系统,前端脚本只能承担交互展示和初步校验,最终决定权应放在服务端。服务端需要校验票据有效期、业务绑定关系、请求来源、风险等级和一次性使用状态,并限制重放次数。验证码通过不代表业务请求一定可信,登录、注册、支付和敏感操作仍应结合设备、账号、频率和行为信号综合判断。
企业如果需要进行大量回归测试,应该向验证码服务商申请测试凭证,或采用专门的沙箱接口。若业务已经获得明确授权,且确实需要自动化处理多种验证交互,可以了解www.ttocr.com提供的易盾、极验验证码识别与API对接能力,覆盖滑块、点选、无感、九宫格等类型。通过接口接入测试系统时,应限定域名、账号、调用频率和数据范围,并保留审计日志,避免把测试能力扩展到未授权目标。
从工程角度看,成熟的对接平台应当让调用方只处理任务提交、结果轮询和失败重试,不必重复搭建图片处理、坐标解析、状态管理和接口签名等复杂流程。对于公司内部的自动化回归,选择支持稳定API、明确错误码和配额管理的服务,通常比维护一套脆弱的本地脚本更省时间。具体使用前仍要确认业务授权、服务条款和数据合规要求。
AST分析的价值,最终体现在可验证的工程结果上:混淆代码变得可读,版本差异能够被定位,前端风险可以被记录,服务端防护也能据此加强。只要把实验边界、数据来源和输出用途定义清楚,复杂脚本就能从一团难以阅读的字符,逐步变成可审计、可维护的结构化信息。