开篇

start

你可能都没有听说过。 嘿嘿

如果你不知道他的强度,你可以尝试去使用反编译器去反编译 idea 的jar文件.

然后你会发现一堆狗屎调用,而且它会对每个函数的参数进行修改,可以说ZKM是市面上最好的java层混淆.

但是本篇文章主要是讲解定位代码,在网上都有很多关于ZKM的破解方法,但是不同版本方法名不一样,就无从下手了.

本文还会介绍了另外一种调试方法,而且不需要使用javaagent,且本文介绍的方法涉及的字节码操作较少,较为简单.

本文只透露大致方法,不会对每一个细节都深入描写,根据个人调试发现 ZKM15-ZKM23 试用版本的检测基本没有发生改变.

decomple

总结

这边提前总结一下

这一个方法,是在一次水群突然想到的操作,没想到最后居然成了.

总之,对于ZKM开发方个人还是建议可以把同验证相关的indy调用进行排除.

可以很大限度的隐藏很多的暗桩(虽然现在也很难找就是了)

关于ZKM的验证

ZKM 属于离线 许可证,即本地验证与一些其他的混淆器联网不同.

如果你想让试用版同正式版功能几乎一直,那么你就需要找到以下限制代码

  • 系统时间检测
  • ZIP时间检测
  • 混淆过程的时间检测
  • Flow流程控制1-2个限制
  • 一个奇怪的Flow检测(本文就不写了),如果你超过2了就会报错.

总之,ZKM的检测分为 加载阶段混淆阶段. 后者本人没有深入调试,具体流程也不得而知.

反Javaagent检测

这玩意写在了ZKM的 static 方法内,通过破坏加密后des字符串,导致解密失败(挺妙的)

我们可以直接对 RuntimeMXBean 类中的 getInputArguments 进行特征删除字符串.

此外还有一个我个人没有调试出来,其被检测到后会在Flow混淆后也会提示栈堆不平衡,这个检测非getInputArguments调用.

目前没定位出来,不会有任何提示捏.

思路方法

不难发现,对应的加密类型 例如: 字符串 整数型 长整数型 加密都通过indy来获得,且解密函数都是当前的类里

以下是 com.zelix.ZKM 类中的解密字符串函数:

1
2
3
4
5
6
7
8
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 ,以下是代码

1
2
3
4
5
6
7
8
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下面的代码,进行调试,方便查看调用情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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行后的 例子

1
2
3
4
5
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 ' 这就是特征点

具体的内部字节码分析,可以由你自己进行分析和修改,在你修改好后你就会发现加密的方法变多了(那肯定)

关于Flow混淆限制,我还修改了另外一个地方,这里就不写了,感兴趣的自己去探索探索.

最终效果

图片

img

img2

参考文章

ZKM-15 解除时间限制
ZKM-19 揭开谜团:Zelix Klassmasters Protection