AST反混淆实战:某政务平台控制流平坦化破解,3小时还原AES加密逻辑

📅 2026/7/25 19:20:02 👁️ 阅读次数 📝 编程学习
AST反混淆实战:某政务平台控制流平坦化破解,3小时还原AES加密逻辑

上个月对接某地级市政务服务平台的公开数据共享接口,对方为了保障参数传输安全,前端对所有业务请求做了AES加密处理,加密逻辑打包在经过重度混淆的JS文件中。由于对方技术侧排期紧张,暂时提供不了详细的加密规则,只能我们自己逆向还原做联调。

拿到混淆文件的第一眼就知道是块硬骨头:核心加密函数被完整做了控制流平坦化加固,原本几十行的逻辑被拆成上百个分散的代码块,包裹在巨大的while-switch状态机里;所有字符串字面量全部加密存储,函数名、变量名全是无意义的十六进制命名。整个核心函数足足1400多行,肉眼根本理不清执行路径。

一开始试着用Chrome单步调试走了几轮,半小时过去了还在状态机里绕,照这个速度没个一两天啃不下来。索性沉下心基于Babel写AST自动化插件,从字符串解密、常量折叠到控制流还原,分层批量处理。从拿到混淆代码到完整还原AES逻辑、跑通官方测试用例,前后刚好3小时,比纯手动还原效率提升了5倍以上。

本文完整复盘整个反混淆过程,从混淆特征识别、AST插件编写到最终算法还原,分享定制化控制流平坦化的破解思路与实战踩坑细节。

一、混淆样本初判:政务场景的定制化加固

动手之前先做特征分析,判断混淆类型与强度,选性价比最高的突破路线。政务场景的混淆和普通电商、社交平台不一样,普遍是在通用混淆器基础上做了定制化加固,针对性更强。

1.1 典型混淆特征识别

打开目标JS文件,快速确认三重防护叠加:

  • 字符串全加密:顶部有加密字符串数组,配套自执行移位函数,所有字面量通过索引函数动态解密,代码里看不到任何可读常量
  • 控制流平坦化:核心加密函数外层是while(true)包裹的巨型switch语句,靠一个状态变量驱动执行流跳转,原始的顺序、分支、循环结构完全被打散
  • 定制化反调试:内置检测逻辑,检测到开发者工具、Node环境、格式化操作时,会主动改变解密密钥,输出错误结果

粗略统计,核心的加密函数原始逻辑大概80行左右,经过平坦化+死代码注入后膨胀到1400多行,代码膨胀比超过17:1,可读性基本为零。

1.2 整体对抗思路

面对定制化的控制流平坦化,不能直接套通用解混淆工具,大概率会失效。我们采用「分层突破、验证驱动」的思路,每处理一层就验证一次正确性,逐步逼近原始逻辑。

原始混淆JS文件

第一层:环境Mock + 字符串解密还原

第二层:常量折叠 + 死代码初步清理

第三层:识别平坦化分发器结构

构建状态转移图,区分顺序/分支/回环

第四层:代码块重组,恢复原始控制流

第五层:变量语义化 + 冗余逻辑清理

核心AES算法提取与联调验证

很多新手容易陷入一个误区:追求100%完美还原原始代码。实际上对于接口联调场景,我们只需要保证核心计算路径清晰、输入输出映射正确即可,没必要浪费时间在边缘分支和死代码的完美还原上。

二、工具链与前置准备

工欲善其事,必先利其器。AST反混淆的核心工具链是Babel全家桶,配合自定义插件完成针对性处理。

2.1 工具选型说明

  • @babel/parser:将JS代码解析为抽象语法树AST,支持最新ES语法,对混淆代码兼容性好
  • @babel/traverse:遍历AST节点,实现节点的查找、修改与替换,是反混淆的核心执行引擎
  • @babel/types:AST节点类型判断与构造工具,用来生成新的代码节点替换混淆结构
  • @babel/generator:将处理后的AST重新生成可读的JS代码
  • Node.js 18.x:脚本运行环境,配合vm模块执行解密逻辑

之所以不直接用网上现成的通用解混淆工具,核心原因是政务场景的混淆做了定制化修改,解密函数加了环境检测,状态转移逻辑也做了变种,通用工具要么跑不起来,要么还原后逻辑错误。自己写插件虽然前期花点时间,但可控性强,遇到变种可以快速调整。

2.2 前置注意事项

做AST反混淆不需要精通编译原理,但必须掌握三个核心能力:

  1. 能识别常见的AST节点类型,知道WhileStatementSwitchStatementAssignmentExpression对应的代码结构
  2. 会用traverse遍历指定类型的节点,能在回调中拿到节点信息与父节点引用
  3. 会用types构造新的节点,替换掉旧的混淆节点

实际开发中大部分时间都在对照AST Explorer调节点属性,真正的核心逻辑代码量并不大。

三、分步实战:AST还原控制流平坦化

整个还原过程分六步推进,每一步都验证输出,确保没有引入逻辑错误。

3.1 第一步:字符串解密——绕过环境检测

字符串解密是所有反混淆的第一步,也是最基础的一步。但这次的解密函数做了定制化改造,不能直接抠出来用。

踩坑点:对方的解密函数内部做了环境检测,会检查windownavigator等浏览器对象是否存在。如果直接抠到Node.js里运行,会静默返回错误的解密结果,不会报错,但后续所有字符串都是错的,非常隐蔽。

解决方案是先做最小化浏览器环境Mock,给解密函数提供符合预期的上下文:

constvm=require('vm');// Mock最小浏览器环境constsandbox={window:{},navigator:{userAgent:'Mozilla/5.0'},document:{createElement:()=>({style:{}})}};vm.createContext(sandbox);// 注入抠出来的字符串数组、移位函数、解密主函数vm.runInContext(extractedDecryptCode,sandbox);constdecryptFunc=sandbox._0xdecrypt;

环境问题解决后,再遍历AST,将所有解密函数调用节点替换为实际的字符串字面量,最后移除不再被引用的解密函数与数组定义。

这一步完成后,代码里的方法名、属性名、常量字符串全部恢复可读,后续分析效率提升一个量级。耗时约40分钟。

3.2 第二步:常量折叠预处理

控制流还原之前,必须先做一轮常量折叠。原因很简单:很多状态变量的赋值不是直接写死常量,而是包了一层表达式运算,比如_state = 0x2a ^ 0x0f。如果不先计算,后续构建状态转移图时就识别不出目标状态,还原出来的逻辑全是错的。

我们写了一个简单的常量折叠插件:遍历所有二元运算表达式,如果左右两边都是字面量,就直接计算出结果,替换掉原表达式。
这一步处理完,状态变量的赋值基本都变成了直观的常量值,为下一步构建转移图扫清了障碍。

3.3 第三步:识别平坦化分发器结构

接下来要从AST中精准定位控制流平坦化的核心结构。标准的平坦化结构包含三个要素:外层while循环、内层switch分发器、独立的状态变量。

识别逻辑并不复杂:

  1. 遍历所有WhileStatement节点,判断循环体是否只包含一个SwitchStatement
  2. 检查switch的判别表达式是否为同一个状态变量
  3. 确认每个case块中都存在对状态变量的赋值,用于驱动下一次跳转

找到目标节点后,我们提取出三个关键信息:状态变量名、所有case分支对应的状态值、每个case块内的语句集合。
这次遇到的变种是:while循环的判断条件不是简单的true,而是一个布尔变量,本质上还是死循环,只是多了一层伪装,稍微调整识别逻辑就能匹配上。

3.4 第四步:构建状态转移图

这是整个还原过程最核心的一步。我们需要分析每个case块执行完之后,状态变量会被赋值为什么值,从而构建出完整的状态转移关系。

具体处理逻辑:

  1. 遍历每个case块的最后几条语句,查找对状态变量的赋值
  2. 如果是常量赋值,说明是无条件跳转,直接记录前驱后继关系
  3. 如果是条件判断内的赋值,说明是分支跳转,分别记录两个分支的目标状态
  4. 如果遇到break/return,说明是流程终止,标记为结束状态
  5. 如果出现状态回环,标记为循环结构

这里又遇到一个定制化变种:部分case块里没有直接赋值状态变量,而是调用了一个内部子函数来修改状态。静态分析看不到子函数内部逻辑,导致转移图断链。
解决方案是结合动态调试:在每个case入口打印状态值,跑一遍真实加密流程,把实际的状态转移序列记录下来,补全静态分析缺失的分支。

3.5 第五步:代码块重组与平坦化消除

有了完整的状态转移图,接下来就是按执行顺序把分散的代码块重新拼接起来,恢复成正常的顺序执行与分支结构。

处理规则:

  • 顺序执行:A状态无条件跳转到B状态,直接将两个代码块按顺序拼接
  • 条件分支:一个状态分出两个后继状态,构造if-else语句包裹对应代码块
  • 循环结构:状态转移出现回环,识别为循环,构造对应的while语句
  • 终止状态:遇到return或break,直接保留原始语句

这一步完成后,原来1400多行的while-switch结构被还原成了120多行的正常代码,代码量直接缩减90%以上。虽然变量名还比较简陋,但核心逻辑已经完全可读。耗时约1.5小时。

3.6 第六步:死代码清理与变量重命名

控制流还原之后,代码里还残留很多无用的变量赋值、永远走不到的死分支、冗余的中间变量。这一步做收尾清理:

  1. 移除对状态变量的所有赋值与引用
  2. 消除只赋值不使用的冗余变量
  3. 合并连续的变量声明,简化表达式
  4. 根据代码语义做半语义化重命名,比如把_0xa1b2改成sboxroundKey这类直观名称

全部处理完成后,用@babel/generator生成最终代码,再用Prettier做格式化,就得到了可读性良好的还原代码。

四、核心AES加密逻辑定位与还原

反混淆只是手段,最终目标是还原加密算法,完成接口联调。

4.1 快速定位加密函数

还原后的代码结构非常清晰,通过三个典型特征很快就锁定了AES加密逻辑:

  • 存在固定的16轮循环结构,轮次与AES-128完全对应
  • 代码中定义了一张256字节的S盒查表数组,数值和标准AES S盒一致
  • 有明显的字节替换、行移位、列混合、轮密钥加四段式运算结构

顺着调用链往上追溯,很快找到了对外暴露的加密入口函数,确认整个加密采用AES-128-CBC模式。

4.2 加密算法完整拆解

经过梳理,整个参数加密的完整流程如下:

业务请求参数集合

按键名字典序排序

按 key=value& 格式拼接成明文字符串

拼接机构编码与时间戳盐值

派生16字节AES密钥

生成随机16字节IV向量

AES-128-CBC 加密 + PKCS7填充

IV拼接到密文头部

自定义Base64编码输出

最终加密参数字符串

几个政务场景特有的细节:

  • 密钥不是固定值,由对接方的机构编码+官方分配的固定盐值通过一次SHA256截断派生而来
  • IV是随机生成的,每次加密都不同,最终拼接到密文前16字节一起传输,这也是相同参数每次密文都不一样的原因
  • 最终编码不是标准Base64,字符表做了少量替换,去掉了URL特殊字符,适合作为GET参数传输
  • 参数排序时会自动过滤空值和签名字段,顺序必须严格按ASCII码升序

4.3 联调一致性验证

算法还原的金标准永远是官方测试用例。我们从对方接口文档里取了3组官方测试样本,包含不同参数组合、中文参数等场景,用还原后的算法逐一加密。

初期遇到两个小偏差:一是中文参数的编码处理,对方默认用UTF-8编码,我们一开始误按GBK处理了;二是Base64字符表有两个字符顺序不对。调整之后,3组样本的输出密文和官方样本完全一致,逐字节无偏差,接口联调一次通过。

五、踩坑实录与效率对比

整个过程看似顺畅,实际踩了不少定制化混淆的坑,很多问题都是通用混淆样本里遇不到的。

5.1 印象最深的几个坑

坑一:解密函数的环境检测陷阱
最开始抠出解密函数直接在Node里跑,没报错但解密出来的字符串全是乱的,以为是抠错了函数,反复核对了好几遍。后来单步调试才发现,函数内部检测了window对象,不存在就会静默切换到错误密钥,返回的是假解密结果,非常有迷惑性。最后Mock了最小浏览器环境才解决。

坑二:状态变量间接赋值导致转移图断裂
一开始构建的状态转移图总有几个分支连不上,还原后的代码逻辑缺块。排查后发现,有几个case没有直接赋值状态变量,而是调用了一个内部函数间接修改。静态分析看不到函数内部逻辑,自然就断了链。最后靠动态调试打日志,记录实际状态流转序列,才补全了完整的转移图。

坑三:switch穿透逻辑漏处理
有两个case分支是没有break的穿透逻辑,初始版本的识别逻辑漏掉了这种情况,导致代码块拼接错误,还原后的加密结果始终不对。后来对照动态执行的中间值逐轮比对,才发现少执行了一段代码。这种定制化的混淆手段不常见,但遇到了就很容易卡壳。

坑四:IV拼在密文头部,默认按固定IV解密失败
刚还原完算法的时候,以为IV是固定值,结果解密官方样本全是乱码。折腾了很久才反应过来,对方把随机IV放在了密文的前16字节,后面才是真正的密文。这个设计本身是标准做法,但如果惯性思维以为IV是固定的,就很容易栽跟头。

5.2 效率对比与耗时拆解

我们事后做了一次横向对比,统计了纯手动还原和AST自动化还原的耗时差异:

环节纯手动还原预估AST自动化还原效率提升
字符串解密2小时40分钟3倍
控制流梳理还原10小时1.5小时6.7倍
算法定位与验证3小时1小时3倍
总计约15小时约3小时5倍

可以看到,效率提升最明显的就是控制流还原环节,这也是AST自动化最能发挥价值的地方。纯手动梳理状态机非常消耗精力,还容易出错,而机器处理这种结构化的重复工作天生擅长。

六、方法论总结

回头看整个过程,控制流平坦化的破解并没有什么黑科技,本质就是逆向混淆的构造过程:对方把线性代码拆成状态机,我们就把状态机拼回线性代码。

6.1 通用破解流程

总结下来,面对任何控制流平坦化保护,都可以遵循这个通用流程:

  1. 前置清理:先做字符串解密、常量折叠、简单死代码移除,降低后续分析难度
  2. 结构识别:定位while-switch分发器与状态变量,确认平坦化结构
  3. 构建转移图:分析每个代码块的后继关系,区分顺序、分支、回环
  4. 代码重组:按转移关系拼接代码块,恢复原始控制结构
  5. 收尾优化:清理冗余变量,格式化输出,语义化重命名
  6. 逻辑验证:用已知样本验证还原后的代码逻辑正确性

6.2 几点实战心得

第一,不要上来就硬啃。先做结构化分析,选对工具和路线,比闷头逐行读代码效率高得多。
第二,自动化为主,人工为辅。能批量处理的就写插件处理,把精力花在工具搞不定的复杂分支和逻辑验证上。全手动还原效率太低,全自动化又容易出错,两者结合性价比最高。
第三,边还原边验证。每处理完一层就做一次基础验证,不要一口气处理完再排查问题。字符串解密完先验一下常量对不对,控制流还原完先跑一下简单用例,有问题早发现早调整。
第四,不要追求完美还原。核心计算路径还原清楚就够了,异常分支、边缘逻辑没必要浪费时间。目标是解决实际问题,不是做代码考古。

合规声明

本文所述技术仅用于合法的政务数据对接、接口联调、安全研究与自有系统建设场景。任何技术都有其适用边界,读者在实际应用中请严格遵守《网络安全法》《数据安全法》《政务数据共享开放条例》等相关法律法规,在授权范围内开展技术工作,不得用于非法破解、越权访问、盗取政务数据等违规场景。技术本身是中性的,合理使用、守住边界,是每个技术从业者的基本职业操守。

前端混淆与反混淆的对抗始终在螺旋升级,通用混淆器的破解方案越来越成熟,定制化加固会越来越多。但底层的思路是相通的:理解混淆的原理,找到结构化的规律,用工程化的手段批量解决问题。相比于死磕某一个具体样本,更重要的是建立起一套可复用的反混淆工作流,遇到新的混淆变种能快速调整适配,这才是AST反混淆真正的价值所在。