-
-
[原创]ZKM控制流反混淆分析
-
发表于: 3天前 1024
-
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 文件。
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。