Zelix KlassMaster 是 Java 字节码混淆器中最为强悍的混淆器,而令 ZKM 引以为傲的混淆就是它的控制流混淆。以下演示用的是 ASM 库,也就是 Java 字节码操作与分析框架。在正常 Java 方法中,控制流一般是树状或者块状的。比如原始逻辑长这样:字节码里虽然也是跳转,但结构还算清楚。控制流平坦化会把这种结构打散,搞进一个调度循环里。逻辑大概会变成这样:它的执行顺序被打散了,都变成一个个 case,通过 state 状态变量决定下一步跳到哪里。ZKM 不只用一种平坦化形式。它可能是 switch 调度,也可能是各种 if、goto、try-catch 混在一起。ZKM 会用这些东西混淆:反编译器会以为某些死代码能执行,会把简单逻辑还原成奇怪的嵌套判断,甚至直接反编译失败。逻辑可以这样写:循环是重点。因为你删掉一个假跳转之后,后面可能会暴露出新的死代码。删掉死代码之后,又可能出现新的常量分支。只跑一遍是不够的。ZKM 经常会塞一些假的异常处理器。
比如这样:或者:这种傻子 handler 是拿来污染控制流图的。拆法就是遍历 tryCatchBlocks,找到 handler 的 LabelNode 后面的真实指令。如果是经典的 GETSTATIC + ATHROW,就把 try-catch 记录和对应指令删掉。删的别太激进。正常代码里也可能有 catch 后重新抛出异常 的情况。如果可以确定是假的就删,不能确定就留着。ZKM 控制流里经常能看到这种结构:也可能是:这类分支很多是控制流垃圾。识别 模式 直接就:可以写成这样:这个是比较好处理的。常见假分支:或者:但是不能看到 iload + ifeq 就删。正常判断里一堆这种写法。比较靠谱的判断是看 ILOAD 执行前操作数栈是不是空的。如果操作数栈不是空的,它突然来个 iload + ifeq,这种就很像 ZKM 的假栈干扰。ASM 可以用 Analyzer 计算 Frame:ZKM 用 invokedynamic 生成某种状态变量,然后拿这个变量到处跳。大概长这样:可以先扫出这种状态变量:后面遇到 iload x / ifeq 的时候,如果 x 是这种状态变量,就可以考虑清掉。但要加副作用判断。因为有些分支包住的是真实代码,比如 PUTSTATIC、INVOKEVIRTUAL、ATHROW。这种删错了,程序直接寄。实际处理可以这样:这就比无脑删多了。对象分支也会出现类似模式:或者:处理思路和 int 分支差不多:它不只喜欢玩 int,也会拿 对象空值检查 当假边用。有时候 ZKM 会把一个简单 goto 拆成两个互相抵消的判断。类似:如果两个判断用的是同一个变量,而且 opcode 互为反向条件,那它其实就是绕了一圈。可以改成:判断函数:但是有个重要条件:两个跳转之间不能夹真实代码。如果中间有真实指令,不要改。当你删掉一堆假分支以后,会留下这种东西:但 x 后面已经没人读了。这时候不能直接删 istore x,因为栈上还有 invokedynamic 的返回值。直接删会导致操作数栈不平衡。正确做法是:代码:清理到后面,经常会出现这种分支:这是永远跳。或者:这是永远不跳。如果一定跳,就插入 GOTO target。
如果一定不跳,就直接删掉常量和条件跳转。控制流清完以后,方法里会残留大量不可达代码。比如:这时候就可以扫一遍。思路:这里不要乱删 LabelNode、FrameNode、LineNumberNode。尤其是 label,有些是 跳转目标,有些是 try-catch 边界,删错就损坏 class 文件。
登录后可查看完整内容
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291231.htm
[原创]ZKM控制流反混淆分析
197 浏览
1 回复
情形都分析的很到位了,感谢分享!