开篇
本篇文章主要是讲解定位代码,在网上都有很多关于ZKM的破解方法,但是不同版本方法名不一样,就无从下手了.
本文还会介绍了另外一种调试方法,而且不需要使用javaagent,且本文介绍的方法涉及的字节码操作较少,较为简单.
本文只透露大致方法,不会对每一个细节都深入描写,根据个人调试发现 ZKM15-ZKM25 试用版本的检测基本没有发生改变.

神必操作
一次水群突然想到的操作,没想到最后居然成了。 (此类操作似乎和旧版本的 jnic 混淆cracked版本同理)
关于ZKM的验证
ZKM 属于离线 许可证,即本地验证与一些其他的混淆器联网不同.
如果你想让试用版同正式版功能几乎一直,那么你就需要找到以下限制代码
- 系统时间检测
- ZIP时间检测
- 混淆过程的时间检测
- Flow流程控制1-2个限制
- 一个奇怪的Flow检测(本文就不写了),如果你超过2了就会报错.
总之,ZKM的检测分为 加载阶段 和 混淆阶段. 后者本人没有深入调试,具体流程也不得而知.
反Javaagent检测
这玩意写在了ZKM的数值初始化 static 方法内,要绕过这个检测我们可以通过破坏加密后des字符串让他解密失败
实现检测不到正确的检测 虚拟机 参数,如下
它通常存在于 每个类中的static的这个初始化获取类的Key值的这个类中的 某个 方法里。
static { v0 = var1_1 = u22.a(-8425532214920198678L, -5751268713453691846L, MethodHandles.lookup().lookupClass()).a(155055646340681L) ^ ZKM.a ^ 133041716194123L;}这里我们就是 u22 ,下面是它的检测是写在 private static void a0(目前看来应该名称都是叫a0)
switch (v0) { case 0: { var0 = Class.forName(new String(var1_9)); continue block16; } case 1: { v0 = 2; v1 = "I$8\u0012tn\u0015\u0007S\u000e66k\u0000ZLg\u000b\u0018XQo`|e\u001f?!\fF\u0018;\u0004l\u0003'\u0014e"; continue block19; } case 2: { var3_2 = ((List)Class.forName(var4_1).getMethod(var4_1, new Class[]{Class.class}).invoke(null, new Object[]{var0})).get(0); continue block17; } case 3: { var5_3 = var4_1; v0 = 4; v1 = "\u000e\u001d*\u00168w\u0013"; continue block19; } case 4: { var6_4 = var4_1; v0 = 5; v1 = "\u000e\u001d<\u00064h\u0010\u001eD"; continue block19; } case 5: { var7_5 = var4_1; v0 = 6; v1 = "\u000e$)\u00164v\u0018\u0000V\u001a13r\u0011"; continue block19; } case 6: { var8_6 = var4_1; v0 = 7; v1 = "\u000e$)\u00164v\u0004\b@Ha"; continue block19; } case 7: { var9_7 = var4_1; v0 = 8; v1 = "\u000e//\u0005;c\u0013\fZTa"; continue block19; }}var10_8 = var4_1;v5 = ((Collection)var0.getMethod(var5_3, new Class[0]).invoke(var3_2, new Object[0])).iterator();要想解决这个调试我们可以直接,通过字节码修改:
if (classNode.name.contains("com/zelix/u22")) { if (methodNode.name.equals("a0")) { // 把他们的字符改为空白符,他们就无法解密了 // 这里保证了这整个类有的字符串全是要检测的参数的密文 for (AbstractInsnNode insnNode : methodNode.instructions.toArray()) { if (insnNode instanceof LdcInsnNode && ((LdcInsnNode) insnNode).cst instanceof String) ((LdcInsnNode) insnNode).cst = "132"; } }}(造成堆栈不平衡的问题一般都是运行到某个方法触发异常,导致没法把堆栈添加到list中)
这里其中的一个可能会导致出现这个问题的是:
System.setProperty("java.class.path", "ZKM.jar");我们必须得让这个 class path 为 ZKM.jar。
此外还有几个我调没有出来,其被检测到后会在Flow混淆后也会提示栈堆不平衡的神秘问题。
目前没定位出来,不会有任何提示捏.
思路方法
不难发现,对应的加密类型 例如: 字符串 整数型 长整数型 加密都通过indy来获得,且解密函数都是当前的类里
以下是 com.zelix.ZKM 类中的解密字符串函数:
private static Object b(MethodHandles.Lookup lookup, MutableCallSite mutableCallSite, String string, Object[] objectArray) { int n = (Integer)objectArray[0]; long l = (Long)objectArray[1]; String string2 = ZKM.b(n, l); MethodHandle methodHandle = MethodHandles.constant(String.class, string2); mutableCallSite.setTarget(MethodHandles.dropArguments(methodHandle, 0, new Class[]{Integer.TYPE, Long.TYPE})); return string2;}以及发现调用都为 invokedynamic , 有 method 也包括 field ,以下是代码
public static Object a(MethodHandles.Lookup lookup, MutableCallSite mutableCallSite, String string, MethodType methodType, Object[] objectArray) { int n = objectArray.length - 2; long l = (Long)objectArray[n]; long l2 = (Long)objectArray[++n]; MethodHandle methodHandle = rjj.a(lookup, mutableCallSite, string, methodType, l, l2); mutableCallSite.setTarget(MethodHandles.explicitCastArguments(methodHandle, methodType)); return methodHandle.asSpreader(Object[].class, objectArray.length).invoke(objectArray);}关于上面的函数 rjj.a("l", (Object)v32, (long)-2994513164395422785L, (long)var1_1); 其中字符串 l 为上面的string参数,剩余则存进了 objectArray 中
其中第五行,为设置绑定目标代码, 我们可以通过hook下面的代码,进行调试,方便查看调用情况
public static MethodHandle a(MethodHandles.Lookup lookup, MutableCallSite mutableCallSite, String string, MethodType methodType, long l, long l2) { int n = string.charAt(0) ^ (int)l2 & 7; MethodHandle methodHandle = null; Field field = null; Method method = null; try { if (n == 108 || n == 113 || n == 112 || n == 115) { field = rjj.c(l, l2); Class<?> clazz = field.getDeclaringClass(); String string2 = field.getName(); Class<?> clazz2 = field.getType(); methodHandle = n == 108 ? lookup.findGetter(clazz, string2, clazz2) : (n == 113 ? lookup.findSetter(clazz, string2, clazz2) : (n == 112 ? lookup.findStaticGetter(clazz, string2, clazz2) : lookup.findStaticSetter(clazz, string2, clazz2))); } else { method = rjj.d(l, l2); Class<?> clazz = method.getDeclaringClass(); String string3 = method.getName(); MethodType methodType2 = MethodType.methodType(method.getReturnType(), method.getParameterTypes()); methodHandle = n == 114 ? lookup.findVirtual(clazz, string3, methodType2) : (n == 104 ? lookup.findStatic(clazz, string3, methodType2) : lookup.findSpecial(clazz, string3, methodType2, clazz)); } return MethodHandles.dropArguments(methodHandle, methodType.parameterCount() - 2, new Class[]{Long.TYPE, Long.TYPE}); } catch (Exception e) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(exception.getClass().getName()).append(" : ").append(field != null ? field.toString() : (method != null ? method.toString() : " null ")).append(" : ").append(exception.toString()); throw new RuntimeException(stringBuilder.toString()); }}有了上面的方法,我们分别对每个函数进行 hook ,
我们则可以对ZKM的各种调用以及 字符串 数值 进行拦截或者修改,可以通过 StackTracer 来获取得到caller className methodName直接分析方法的调用情况.
相对 arthas 一步一步调试方便了许多,也不需要ognl表达式来调用,我们可以直接从输出的log本文直接查询,就能快速定位了。
时间检测
ZKM 是通过加密long值成一段成一段字符串.
有了上面的操作,我们要找到时间的字符串,在此基础上对调用数据进行修改,即可达到通过时间检测.
这边假设你已经对indy的函数进行hook,通过输出我们可以找到3个连续输出均由数字字母组成的字符串
那个字符串就是时间加密后的字符串,我们可以在invoke后对数据返回进行一个修改
返回一个正确的时间即可(正确的时间,经过多次的测试,时间必须控制在试用的时间范围内)
我对 System.currentTimeMillis 进行拦截修改,这是我的一个写在 上方代码块的第16行后的 例子
if (methodName.equals("currentTimeMillis")) { method = FakeTime.class.getMethod("getFakeTime"); clazz = method.getDeclaringClass(); methodName = method.getName();}还有一些关于时间的过检测,这边就不写出来了,让大伙自己探索一下
Flow混淆限制
经过个人测试,ZKM_log中的提示的 MESSAGE: Obfuscating control flow in only one or two methods in each class
其中的方法包括 <init> <clinit> 和 其他方法,但是 <init> 和 <clinit> 并不会在ZKM_log.txt中输出实际上可能会1-3的方法
而我们的具体定位方法是用的字符串hook,也许你注意到每一个混淆的方法都会输出一则语句 Obfuscated flow in method ' 这就是特征点,按照这个
调用栈去找,100%能定位到位置
具体的内部字节码分析,请自己进行修改,可以重点关注 iconst_2 和 一些跳转,修改后即可去除flow的限制
关于Flow混淆限制,还有别的比较神必暗桩,这里就不写了,感兴趣的自己去探索探索.
最终效果
图片


参考文章
部分信息可能已经过时