逆向 Moon 4.0

2026-10-31

1

源码已发布于:https://github.com/StylesFoundation/Moon

Moon Client 是一个自 2018 年发布的 Minecraft 外挂,在 2022 年 11 月发布了4.0版本,从这之后从未被破解。在 4.0 版本引入了一个私人魔改 JDK 构建并加密了 Class,只有 Moon 的私有 JDK 构建可以读取加密的 Class,并且实现了每个用户下载到的 Jar 都不一样,以此可以追踪泄露者。

初步研究

在获得 JDK 和 JAR 之后,观察 Moon JDK 里的文件,有一个 3MB 然后没有任何信息的 jcef.dll ,一些JDK发行版里也有这个jcef.dll但是通常大小只有896KB。

通常 jcef.dll 出现在 JBR(JetBrains Runtime) 里,信息为 Java Chromium Embedded Framework,可能是用于IntelliJ IDEA的内置网页预览。但是 Moon JDK 里的 jcef.dll 完全没有任何 Metadata。

1

使用 DiE 查壳之后发现这个 DLL 有 Themida/WinLicense 虚拟化保护,有一个叫.skid 的虚拟机 Section。

1

在 JAR 中可以看到也有两个 DLL 文件和一个 SO 文件,这是 Native 混淆文件。

1

查壳之后发现使用了 VMProtect 虚拟化保护,其保护架构可能和 Novoline 是一样的,都有一个 Native DLL 和一个 API DLL,并且虚拟机 Section 叫 .UPX,当然这绝对不是 upx,因为还有一个 .UPX2,正常的UPX没有一般是 .UPX0

1

JAR 里的 Class 部分可以被 Recaf 识别为正常 Class,但是所有核心 Class 和客户端 Class 都被加密了,无法使用 Recaf 识别。

1

尝试使用 JVMTI(Java Virtual Machine Tool Interface)进行 Dump,Dump 输出的 Class 文件仍然是加密的,说明他的加密位于更底层。

初步结论为,JDK 使用了 Themida 虚拟化保护,客户端使用了 Native 混淆+VMProtect 虚拟化保护+身份认证 DLL + VMProtect保护,Class 有加密无法且无法使用 JVMTI 进行转储。

追踪Native

1

Moon 客户端由一个 Bootstrap 启动器拉起,查壳之后发现启动器也是使用了 Themida 虚拟化保护,虚拟机 Section 没有重命名就叫 .themida。

拉起游戏时,启动器会自动添加-XX:+DisableAttachMechanism(实际上没什么用,JavaAgent 本来也 Dump 不出来东西),和-Duid=+你的UID参数。

1

很快,我们跟踪到了 jcef.dll,发现他有在读取这个uid

1

查看上下文代码,找到了ClassFileParser::parse_stream函数,RVA 0x1E8C30,可以发现他会取uid属性并将uid文本转成整数。

然后取ClassBuffer里的MagicNumber判断是不是0xCAFEBABE(标准Java Class),如果是就执行正常的 Class 加载操作,如果不是并且 UID 属性不为空的话就会调用 GetClassName 函数取出类名然后调用 CreateNewBuffer 函数创建一个新的 ClassBuffer 进入解析流程。

1

这一段转为伪代码为

cpp
const char* uid_property = Arguments::get_property("uid");
if (_byteswap_ulong(*(uint32_t*)buffer) != 0xCAFEBABE && uid_property)
{
    int64_t uid = atoll(uid_property);
    const char* name = GetClassName(buffer);
    int length = stream->end - stream->begin;
    uint8_t* new_buffer = CreateNewBuffer(name, stream->begin, stream->current, length, uid, thread);
    ...
}

在此方法继续向上跟踪引用,我们发现了在DllMain里有使用 MinHook 创建 Hook 的代码。

1 1

同时在此处发现了一个Pattern 40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B C3 48 83 C4 20 5B C3,在 jvm.dll 搜索,发现这是CreateNewBuffer。

然而真实 Hook 处理的部分被 Themida 虚拟化了,无法直接分析,需要动态调试。

1

经过分析,这个函数传入四个参数分别为className, classBuffer, classLength, uid,然后就会返回一个新的Buffer。

1

经过测试,Hook 这个函数,也可以直接获得解密之后的 Class,所以实际上你也可以直接通过 Hook 解密整个 JAR,这是最简单的方式但是不是最优雅的方式,这个方法的技巧我们放在最后写,我们目前着着重于算法的研究。

加密算法

经过对 VM 的追踪,在某个 Exit 之后走到了 RVA 0xBA00,疑似某个 Cipher 的ApplyKeystream,并且发现4个常量

0x61707865 0x3320646E 0x79622D32 0x6B206574

这是 ChaCha20 的expand 32-byte k常量(也叫 sigma 常量),继续看能在这个ApplyKeystream 函数里发现64字节块处理、32位模加法、异或和旋转。

1

随后在方法内发现7、9、13、18旋转量,说明算法可能是Salsa20,因为 ChaCha20 的旋转量一般是16、12、8、7

1

ChaCha20

1

Salsa20

然后在初始化循环轮数也与 Salsa20 完全吻合,这就是一个完全没有修改,原版 Salsa20

1

1

密钥派生

在追踪时发现VM里有CreateFileA调用,在Hook CreateFileA之后再次跑解密发现传入了\\.\PhysicalDrive0。

随后在 CreateFileA 之后又调用了 DeviceIoControl 查询 STORAGE_DEVICE_DESCRIPTOR

这是为了读取硬盘序列号字符串,也就是 HWID 参与了加密运算,这就是一人一JAR的用途。

读取序列号之后继续跟踪,发现一个 Base64 字符串,解码后为 serial + uid 拼接。继续观察,它构造了一个长度为 32 的 Base64 字符串,解码后发现这就是上面说的 base64(serial + uid) 循环补到 32 字节,最终传入了密码学方法,这就是 key 的生成逻辑。

Nonce 生成

Salsa20 的 Nonce 是 8 字节。在寻找 Nonce 的过程中发现每个 class 的 Nonce 都不一样,再加上这个函数传入了 ClassName,由此可以判断 Nonce 的生成可能和 ClassName 有关。

Hook 解密 com/moon/obfuscator/Bootstrap 时发现它的 Nonce 是 =AXYyR3c。继续往上找,发现 Nonce 的构造为to_8bytes(reverse(base64(class_name)))

也就是:

Base64反转截取前 8 字节com/moon/obfuscator/BootstrapY29tL21vb24vb2JmdXNjYXRvci9Cb290c3RyYXA==AXYyR3c092bC9icvRXYjNXdmJ2bv42bv12Lt92Y=AXYyR3c

对于短名字,比如 lua,则会循环补齐到 8 字节:

Base64反转不足 8 字节,循环补齐luabHVhhVHbhVHbhVHb

这里的class_name是 InternalName,也就是 Hook 里 GetClassName 拿到的那个,也就是com/moon/obfuscator/Bootstrap这种。

现在我们掌握了算法和密钥派生,可以直接解密所有 Class 了。

Hook解密法

这是一个更简单的解密方法,基于 Hook CreateNewBufferHook 方法,因为他最终会吐出解密之后的 Class。

由于 Moon JDK 并没有限制你运行任何代码,所以你只需要自己编译一个 Class 出来,用 Moon JDK 运行,然后加载 Moon Jar 里的所有类,Hook 就给所有明文吐出来了。

java
public static List<Class<?>> loadJarNoInit(File jar) throws IOException {
    List<Class<?>> classes = new ArrayList<>();
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    try (JarFile jarFile = new JarFile(jar)) {
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            String name = entry.getName();
            if (!name.endsWith(".class")) {
                continue;
            }
            String className = name
                    .substring(0, name.length() - ".class".length())
                    .replace('/', '.');
            try {
                Class<?> clazz = Class.forName(className, false, loader);
                classes.add(clazz);
            } catch (ClassNotFoundException | LinkageError ignored) {
            }
        }
    }
    return classes;
}

Java 混淆

1

解密 JAR 之后,使用 Recaf 反编译,我们注意到,Moon 的 Java 混淆为 Zelix Klass Master™ 19,不幸的是,我们刚好对此混淆器颇有研究。

1

参见Zelix Klass Master 混淆器研究

验证

1

几个意义不明的方法。

很显然由于这个外挂对自己的定制 JDK 保护过于自信,并没有一个很强的验证,Native 混淆只混淆了一个 init 方法,在使用 JNI Trace 之后就轻而易举的还原了,并且他的 VMProtect 疑似没有反调试。

其 Native 混淆和 Novoline 一样分离了所有 Invoke Dynamic,可能是无法处理。

1

其中有一个比较有趣的就是在过验证之后才会赋值 Raytrace Distance,在反混淆过程中被错误的内联了,所以最开始 Raytrace 出了问题。

1

评论

加载评论中…