Reversing Moon 4.0

2026-10-31

1

The source code has been published at: https://github.com/StylesFoundation/Moon

Moon Client is a Minecraft hacked client first released in 2018. Version 4.0 came out in November 2022, and it has never been cracked since. Version 4.0 introduced a private, modified JDK build and encrypted its classes so that only Moon's private JDK build can parse them. It also gives every user a different JAR, which lets them trace leakers.

Initial Research

After obtaining the JDK and the JAR, we looked through the files in the Moon JDK and found a 3 MB jcef.dll with no information at all. Some JDK distributions also ship a jcef.dll, but it is usually only 896 KB.

jcef.dll normally appears in JBR (JetBrains Runtime), with the description Java Chromium Embedded Framework, it is probably used for IntelliJ IDEA's built-in web preview. The jcef.dll in the Moon JDK, however, has no metadata whatsoever.

1

Scanning it with DiE shows that the DLL is protected with Themida/WinLicense virtualization and has a VM section named .skid.

1

The JAR also contains two DLL files and one SO file, these are the native obfuscation files.

1

Scanning them shows they are protected with VMProtect virtualization. The protection architecture is probably the same as Novoline's: there is a native DLL and an API DLL, and the VM section is named .UPX. Of course, this is definitely not UPX, because there is also a .UPX2; regular UPX doesn't have that, and is usually .UPX0.

1

Some of the classes in the JAR are recognized by Recaf as normal classes, but all the core classes and client classes are encrypted and Recaf cannot recognize them.

1

We tried dumping them with JVMTI (Java Virtual Machine Tool Interface), but the dumped class files were still encrypted, which means the encryption happens at a lower level.

Preliminary conclusion: the JDK is protected with Themida virtualization; the client uses native obfuscation + VMProtect virtualization + an authentication API DLL + VMProtect protection; the classes are encrypted and cannot be dumped with JVMTI.

Tracing the Native Code

1

The Moon client is started by a bootstrap launcher. Scanning it shows the launcher is also protected with Themida virtualization; its VM section was not renamed and is simply called .themida.

When launching the game, the launcher automatically adds -XX:+DisableAttachMechanism (which doesn't really do anything, since a Java agent can't dump anything at all) and -Duid= followed by your UID.

1

We soon traced it to jcef.dll and found that it reads this uid.

1

Looking at the surrounding code, we found the ClassFileParser::parse_stream function at RVA 0x1E8C30. It reads the uid property and converts the uid string to an integer.

It then reads the MagicNumber from the ClassBuffer and checks whether it is 0xCAFEBABE (a standard Java class). If it is, the class is loaded normally. If it isn't and the UID property is not empty, it calls GetClassName to get the class name, then calls CreateNewBuffer to create a new ClassBuffer, which goes into the parsing flow.

1

As pseudocode, this section is:

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);
    ...
}

Following the references further up from this method, we found code in DllMain that creates hooks with MinHook.

1 1

Here we also found a pattern, 40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B C3 48 83 C4 20 5B C3. Searching for it in jvm.dll shows that it is CreateNewBuffer.

However, the part that actually handles the hook is virtualized by Themida and can't be analyzed directly; it requires dynamic debugging.

1

Analysis shows that this function takes four parameters, className, classBuffer, classLength, uid, and returns a new buffer.

1

Testing shows that hooking this function also gives you the decrypted classes directly, so you can in fact decrypt the entire JAR just by hooking it. This is the simplest approach, but not the most elegant one. We'll cover how to do it at the end; for now we focus on the algorithm.

Encryption Algorithm

Tracing through the VM, after one of the VM exits execution reaches RVA 0xBA00, which looks like the ApplyKeystream of some cipher, and we found 4 constants:

0x61707865 0x3320646E 0x79622D32 0x6B206574

These are ChaCha20's "expand 32-byte k" constant (also called the sigma constant). Looking further, the ApplyKeystream function contains 64-byte block processing, 32-bit modular addition, XOR, and rotations.

1

We then found the rotation amounts 7, 9, 13, 18 in the method, which suggests the algorithm may be Salsa20, since ChaCha20's rotation amounts are normally 16, 12, 8, 7.

1

ChaCha20

1

Salsa20

The initialization and the number of loop rounds also match Salsa20 exactly. This is completely unmodified, stock Salsa20.

1

1

Key Derivation

While tracing, we found a CreateFileA call inside the VM. After hooking CreateFileA and running the decryption again, we saw that it was passed \\.\PhysicalDrive0.

After CreateFileA, it then calls DeviceIoControl to query STORAGE_DEVICE_DESCRIPTOR.

This is to read the disk's serial number string, meaning the HWID is used in the encryption. This is what the one-JAR-per-user scheme is for.

Continuing to trace after the serial number is read, we found a Base64 string that decodes to the concatenation serial + uid. Looking further, it builds a Base64 string of length 32; decoding it shows that it is the base64(serial + uid) mentioned above, repeated cyclically to fill 32 bytes. This is finally passed into the cryptographic routine, and that is the key generation logic.

Nonce Generation

Salsa20's nonce is 8 bytes. While looking for the nonce, we found that every class has a different nonce, and since this function is passed the ClassName, we can infer that nonce generation is probably related to the ClassName.

Hooking the decryption of com/moon/obfuscator/Bootstrap, we found that its nonce is =AXYyR3c. Searching further up, we found that the nonce is constructed as to_8bytes(reverse(base64(class_name))).

That is:

Base64ReverseTake the first 8 bytescom/moon/obfuscator/BootstrapY29tL21vb24vb2JmdXNjYXRvci9Cb290c3RyYXA==AXYyR3c092bC9icvRXYjNXdmJ2bv42bv12Lt92Y=AXYyR3c

For short names such as lua, it is repeated cyclically to fill 8 bytes:

Base64ReverseShorter than 8 bytes,repeat to fillluabHVhhVHbhVHbhVHb

Here class_name is the internal name, i.e. the one GetClassName returns in the Hook, in the form com/moon/obfuscator/Bootstrap.

Now that we have the algorithm and the key derivation, we can decrypt all the classes directly.

Decrypting via the Hook

This is a simpler way to decrypt, based on hooking the CreateNewBufferHook method, since it ultimately spits out the decrypted classes.

Since the Moon JDK doesn't restrict what code you run, all you need to do is compile a class yourself, run it with the Moon JDK, and load every class in the Moon JAR; the hook then dump out all of the plain-class.

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 Obfuscation

1

After decrypting the JAR and decompiling it with Recaf, we noticed that Moon's Java obfuscator is Zelix Klass Master™ 19. Unfortunately for Moon, we happen to have studied this obfuscator quite a bit.

1

See Obliterating Zelix Klass Master.

Authentication

1

A few useless methods

Clearly, because this client is overconfident in its custom JDK protection, it doesn't have strong authentication. The native obfuscation only covers a single init method, which was easily restored with JNI Trace, and its VMProtect appears to have no anti-debugging.

Like Novoline's, its native obfuscation splits out all the invokedynamic instructions, probably because it can't handle them.

1

One interesting detail is that the Raytrace Distance is only assigned after authentication passes. It was wrongly inlined during deobfuscation, so Raytrace was broken at first.

1

Comments

Loading comments…