nuym's blog

返回

破解 Zelix KlassmasterBlur image

破解 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;
    }
}
java

下面这个类负责解密这些加密日期,转为 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));
    }
}
java

下面这个类负责验证。

// 反混淆后
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)" : "");
        }
    }
}
java

解密后得到的三个日期 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!");
    }
}
java

这样就把假的过期时间硬编码进去了。现在我们的 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
plaintext

替换为:

R:
    iload i57
    iload i56
    pop2
    goto S
    goto T
plaintext

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

M:
  iload i42
  iload i41
  if_icmple N
  goto O
plaintext

替换为:

M:
    iload i42
    iload i41
    pop2
    goto N
    goto O
plaintext

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

修改许可证信息#

许可证信息存储在 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;
    }
java

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

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));
java

我们还可以通过修改 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
Author nuym
Published at 2025年7月26日
正在加载评论系统...