2043 字
10 分钟

破解 Zelix Klassmaster

破解 Zelix Klassmaster#

example

简介#

ZKM 是市面上最好的混淆器之一,但首先我们需要声明,这一切都不是出于恶意,我们只是想挑战一下自己。因此我们不会公开破解工具或我们的代码。但我们无法阻止你参考我们的做法并制作你自己的破解版本。这里展示的所有代码都只适用于我们手头的 ZKM 副本,你的版本可能会有所不同。

调研#

说完这些,我们开始正题。首先是他们的官网。乍一看你可能会觉得网站有点过时,视觉风格仿佛来自 2000 年代,但别被外表骗了,这款混淆器可是市场顶尖。浏览网站时,头部有个功能介绍页面。

features_page

它拥有现代 Java 混淆器应有的所有标准功能,点击每个功能还能看到详细介绍。接下来我们来到“试用”页面。

try_it_page

这里需要填写表单来申请 ZKM 的评估版。最显眼的是这一条:

no_free_email

但幸运的是,我有自己的邮件服务器,有一个符合他们要求的邮箱。填写完表单后,我收到了 ZKM 的邮件。

zkm_email

邮件里有一些关于混淆器的信息,附带文档和支持邮箱。如果有疑问可以联系他们。我们需要的是下载链接。下载并解压 ZIP 文件后,得到了运行和破解 ZKM 所需的全部文件。

ZIP 文件#

zip_contence

这里可以找到 ZKM 的 jar 包和相关文件。如果想破解它,首先要搞清楚认证机制。我们可以断网运行 ZKM,发现它依然能认证并混淆文件,说明认证完全离线完成,所以某处一定有判断 30 天试用期和评估版的逻辑。幸运的是,我有一个很酷的工具——ZKM 完全反混淆器。由于 ZKM 是用自家混淆器保护的,我们可以窥探代码,找到认证方法并移除每个类只能混淆少量方法的限制。不过,原始类名无法恢复,需要自己分析。破解前,先理解认证机制。

认证机制内部原理#

既然 ZKM 是离线应用,如果我们把系统时间调到过去会怎样?(提示:你不能这么做)。ZKM 开发者早就考虑到了,加入了防止这种操作的检测。

下面这个类包含三个加密日期,作为字符串存储在私有静态字段中,通过三个实例方法返回。

// 反混淆后
public class tl extends t7 {
private static final String d;
private static final String a;
private static final String e;
static {
d = "WbGOSe0eeA";
a = "Vbgm0ee0A";
e = "V4QIX66fY66PE";
}
public String j() {
return e;
}
public String R() {
return a;
}
public String l() {
return d;
}
}

下面这个类负责解密这些加密日期,转为 long 类型。

// 反混淆后
public final class emp {
public static long L(String string) {
char[] cArray = string.toCharArray();
char[] cArray2 = new char[cArray.length];
for (int i = 0; i < cArray.length; ++i) {
char c = cArray[i];
if (c >= '0' && c <= '9') {
if (c > '0') {
c = (char) (58 - c + 48);
}
} else if (c >= 'A' && c <= 'J') {
c = (char) (c - 17);
} else if (c >= 'K' && c <= 'T') {
c = (char) (c - 27);
} else if (c >= 'U' && c <= 'Z') {
c = (char) (c - 37);
} else if (c >= 'a' && c <= 'd') {
c = (char) (c - 43);
} else if (c >= 'e' && c <= 'n') {
c = (char) (c - 53);
} else if (c >= 'o' && c <= 'x') {
c = (char) (c - 63);
}
cArray2[i] = c;
}
return Long.parseLong(new String(cArray2));
}
}

下面这个类负责验证。

// 反混淆后
public final class ew extends tl {
public static String r(
String string, // V4QIX66fY66PE
String string2, // WbGOSe0eeA
String string3, // Vbgm0ee0A
Object object
) {
try {
String string4 = object.getClass().getName();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd MMM yyyy");
TimeZone timeZone = TimeZone.getDefault();
simpleDateFormat.setTimeZone(timeZone);
long l = emp.L(string); // 1668344144454L Sun Nov 13 08:55:44 VET 2022
long l2 = emp.L(string2); // 2764800000L Sun Feb 01 20:00:00 VET 1970
long l3 = emp.L(string3); // 172800000L Fri Jan 02 20:00:00 VET 1970
Date date = new Date(l);
String string5 = simpleDateFormat.format(date);
long l4 = System.currentTimeMillis();
// 检查 l3 * 4 * 4 是否等于 l2
if (l3 * 4 L * 4 L != l2){
throw new h_(string5 + (b0.gF ? " (01)" : ""));
}
// 检查当前时间是否大于 l
if (l4 > l) {
throw new h_(string5 + (b0.gF ? " (02)" : ""));
}
// 检查当前时间是否小于 l - l2
if (l4 < l - l2) {
throw new h_(string5 + (b0.gF ? " (03)" : ""));
}
J = string;
long l5 = l4 + l3;
eru eru2 = new eru(System.getProperty("java.class.path"), c2.b);
Object[] objectArray = eru2.y();
// 遍历 classpath 下的文件
for (int i = 0; i < objectArray.length; ++i) {
if (objectArray[i] instanceof File) {
long l6 = ((File) objectArray[i]).lastModified();
// 检查文件最后修改时间是否小于等于 l5
if (l6 <= l5) continue;
throw new h_(string5 + (b0.gF ? " (04)" : ""));
}
File file = new File(((ZipFile) objectArray[i]).getName());
if (!file.exists()) continue;
long l7 = file.lastModified();
// 检查文件最后修改时间是否大于 l5
if (l7 > l5) {
throw new h_(string5 + (b0.gF ? " (05)" : ""));
}
if (!file.getName().endsWith("ZKM.jar")) continue;
e8c e8c2 = null;
try {
e8c2 = new e8c(file);
ZipEntry zipEntry = e8c2.getEntry(string4.replace('.', '/') + ".class");
if (zipEntry == null) continue;
long l8 = zipEntry.getTime();
// 检查 entry 时间是否大于 l5
if (l8 > l5) {
throw new h_(string5 + (b0.gF ? " (06)" : ""));
}
// 检查当前时间是否大于 entry 时间加 l2
if (l8 != -1 L && l4 > l8 + l2){
throw new h_(string5 + (b0.gF ? " (07)" : ""));
}
((ZipFile) e8c2).close();
continue;
} catch (IOException iOException) {
continue;
} finally {
if (e8c2 != null) {
try {
((ZipFile) e8c2).close();
} catch (IOException iOException) {
}
}
}
}
eru2.F();
return string5;
} catch (NumberFormatException numberFormatException) {
throw new h_(b0.gF ? " (08)" : "");
}
}
}

解密后得到的三个日期 long,可以转为 java/util/Date 对象。分别是:

  • 1970年1月2日 20:00:00 VET(初始日期,用于计算时间)
  • 1970年2月1日 20:00:00 VET(结束日期,初始日期后一个月)
  • 2022年11月13日 08:55:44 VET(过期日期)

现在我们明白了认证机制,可以开始破解了。简单来说,我们让 ZKM 认为许可证在很久以后才会过期,实际上就是“永不过期”。由于有很多校验,最好的办法是分析校验逻辑并修改 com/zelix/emp:L(Ljava/lang/String;)J 方法,让它返回我们自定义日期的 long 值。

我们把原方法替换成这样:

// 反混淆后
public final class emp {
public static long L(final Object[] var0) {
final LocalDate ld = LocalDate.of(2099, 12, 31);
final Date d = Date.from(ld.atStartOfDay(ZoneId.systemDefault()).toInstant());
switch ((String) var0[0]) {
case "V4QIX66fY66PE":
case "WbGOSe0eeA":
return d.getTime();
case "Vbgm0ee0A":
return d.getTime() / 4L / 4L;
}
throw new RuntimeException("Error decrypting expiration date!");
}
}

这样就把假的过期时间硬编码进去了。现在我们的 ZKM 副本就“永不过期”了,可以继续移除控制流限制。

移除控制流限制#

控制流限制是 ZKM 为防止评估版一次性混淆太多方法而设置的。我们可以通过修改 com/zelix/emp:UY()Vcom/zelix/emp:qh()V 来移除。

简单来说,我们把 com/zelix/emp:UY()V 里的如下指令:

iload i57
iload i56
if_icmple S
goto T

替换为:

R:
iload i57
iload i56
pop2
goto S
goto T

com/zelix/emp:qh()V 里的如下指令:

M:
iload i42
iload i41
if_icmple N
goto O

替换为:

M:
iload i42
iload i41
pop2
goto N
goto O

这样控制流限制就被绕过了,可以随意混淆任意数量的方法。我们还改了其他地方,但这里不公开。

修改许可证信息#

许可证信息存储在 com/zelix/tl 类的 j 字段中。

public tl() {
String[] stringArray = new String[]{
"License No.: ",
"none ",
"License Type: ",
"30 day evaluation expiring ",
"Licensee: ",
"(name) - (company) ",
" ",
"(email)",
"Not used ",
"Not used "
};
this.j = stringArray;
}

这是许可证信息,在评估版中可以随意更改。我们把它改成了帮助过我们的人。

instructions.add(new VarInsnNode(Opcodes.ALOAD,0));
instructions.add(new FieldInsnNode(Opcodes.GETFIELD,className,"j","[Ljava/lang/String;"));
instructions.add(new LdcInsnNode(5));
instructions.add(new LdcInsnNode("Thnks_CJ, dramatically & accessmodifier364"));
instructions.add(new InsnNode(Opcodes.AASTORE));

我们还可以通过修改 com/zelix/ew:k 字段去除“Evaluation”水印。

总结#

现在我们已经破解了 ZKM,可以无限制地混淆自己的项目。但这只是出于学习目的,我不支持盗版。如果你想用 ZKM,请到官网购买:https://zelix.com/

我没有公开全部控制流限制的破解方法,是希望大家自己去探索。我不会把所有细节都告诉你,希望你能从中学到东西。

原文链接: https://thnkscj.github.io/posts/cracking-zkm/

破解 Zelix Klassmaster
https://www.nuym.cn/posts/crack-zkm/
作者
nuym
发布于
2025-07-26
许可协议
CC BY-NC-SA 4.0