破解 Zelix Klassmaster

破解 Zelix Klassmaster
简介
ZKM 是市面上最好的混淆器之一,但首先我们需要声明,这一切都不是出于恶意,我们只是想挑战一下自己。因此我们不会公开破解工具或我们的代码。但我们无法阻止你参考我们的做法并制作你自己的破解版本。这里展示的所有代码都只适用于我们手头的 ZKM 副本,你的版本可能会有所不同。
调研
说完这些,我们开始正题。首先是他们的官网。乍一看你可能会觉得网站有点过时,视觉风格仿佛来自 2000 年代,但别被外表骗了,这款混淆器可是市场顶尖。浏览网站时,头部有个功能介绍页面。
它拥有现代 Java 混淆器应有的所有标准功能,点击每个功能还能看到详细介绍。接下来我们来到“试用”页面。
这里需要填写表单来申请 ZKM 的评估版。最显眼的是这一条:
但幸运的是,我有自己的邮件服务器,有一个符合他们要求的邮箱。填写完表单后,我收到了 ZKM 的邮件。
邮件里有一些关于混淆器的信息,附带文档和支持邮箱。如果有疑问可以联系他们。我们需要的是下载链接。下载并解压 ZIP 文件后,得到了运行和破解 ZKM 所需的全部文件。
ZIP 文件
这里可以找到 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()V
和 com/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/
我没有公开全部控制流限制的破解方法,是希望大家自己去探索。我不会把所有细节都告诉你,希望你能从中学到东西。