控制流平坦化与 AST 还原
本节目标
- 理解「控制流平坦化」把顺序逻辑打乱成「状态机」
- 看懂
while(switch)这种典型平坦化结构 - 知道用 AST 把平坦化「反平」回正常顺序
控制流平坦化(Control Flow Flattening)把一个「顺序执行」的函数,改成「一个状态变量 + 一个 switch 分发」的死循环,执行顺序靠状态推进,读起来像一团乱麻。
// 运行环境:Node.js 16+(无需依赖)
// 运行:node jsr-l12.cjs
// 原始顺序逻辑:
function orig(a, b) { const s = a + b; const d = s * 2; return d }
// 平坦化后(等价,但难读):
function flattened(a, b) {
let state = 0
let s, d
while (true) {
switch (state) {
case 0: s = a + b; state = 1; break
case 1: d = s * 2; state = 2; break
case 2: return d
}
}
}
console.log(orig(1, 2), flattened(1, 2)) // 都是 6AST 还原思路(详见 ast-pro 课程):用 @babel/parser 解析 → 识别 while(true){switch(state){...}} 模式 → 按 state 的跳转顺序把 case 里的语句「串」回原本的顺序 → 用 @babel/generator 重新生成干净代码。这比人肉读快得多。
名词解释
控制流平坦化(Control Flow Flattening):一种代码混淆技术,把「自然顺序执行」的代码,伪装成「状态机 + switch 分发」的循环。原本第 1、2、3 步的顺序,变成「state=0 做第1步→state=1 做第2步→...」。读起来极乱,但「计算语义完全不变」。它是混淆里较高级的一种。
反平坦化(De-fl_attention):把平坦化的代码「还原」回正常顺序。最可靠的做法是用 AST 工具解析出 switch 每个 case 的语句和 state 跳转关系,按跳转顺序重排,再生成新代码。它把「人肉苦读」变成「脚本自动重构」,是 AST 实战最有成就感的场景之一。
课后练习
练习 1:平坦化后的代码和原代码,运行结果一样吗?为什么逆向还要还原它?
答案:运行结果完全一样(混淆不改语义,只改结构)。还原是为了「人能读懂、能提取算法」——你不还原也能断点跟,但跟着 switch 跳来跳去极慢,且难把算法抄出来复现。还原是为了效率和可维护性。
练习 2:用 AST 反平坦化,关键要提取哪两类信息?
答案:① 每个
case里「实际执行的语句」;②state变量「从哪个 case 跳到哪个 case」的转移关系。有了这两样,按转移顺序把语句串起来,就是原始顺序。这正是 babel 遍历 + 路径替换能做的事。
总结
控制流平坦化是混淆里的「狠角色」,但本质是「纸老虎穿了迷彩」。我的观点:人肉读平坦化代码是性价比最低的事——你跟着 switch(state) 跳几十次,脑子就乱了,还容易漏语句。正确做法永远是交给 AST:解析 → 抓 case 与 state 跳转 → 重排生成。这也是为什么我强烈建议先把 ast-pro 课程吃透——反平坦化就是它最漂亮的应用。一句话:平坦化改的是「执行顺序的写法」,没改「每条语句本身」,AST 还原就是「按跳转把语句摆回原顺序」,逻辑零损失。别跟状态机搏斗,让脚本替你摆。