Reversing Novoline

2026-11-05

1 1

Novoline is one of the oldest Minecraft hacked clients, with development first starting in 20191. Around 2020, because AlphaAntiLeak was total bullshit2, it was cracked by Chinese, after which its authentication was strengthened several times.

In 2021, due to some conflict I don't know the details of, the original Novoline developers gast and tasteful left Novoline to strike out on their own and founded novoline@intent, which later became novoline.lol.

From then on there were two different Novolines, novoline.wtf and novoline.lol, developed by two different teams. The two sides were irreconcilable, each claiming to be the legitimate Novoline (similar to North and South Korea), and they have completely different protection and obfuscation systems. Unfortunately, both Novolines were discontinued in 2023.

In this article, we will reverse both versions of Novoline.

Novoline.wtf 3.0

Novoline.wtf uses a protection system called AkrienAntiLeak, also abbreviated as AAL, but it is not Alpha Anti Leak.

Interestingly, this AkrienAntiLeak Launcher uses the AlphaAntiLeak icon (or one resembling AAL's), perhaps as a tribute to AAL.

1

Initial Investigation

Novoline also has a custom JVM. Checking it for packers shows that jvm.dll is protected with VMProtect virtualization.

1

In the builds folder there is a very large .tar.gz file, which we initially suspected was the client file. On closer inspection, however, we found that it is neither a standard GZip file nor a standard Zip file.

1

We also found a file named AkrienGuard.jar in the libraries folder, but when we tried to reverse it with Recaf, we found that the classes in it are encrypted.

1

The most obvious sign is that the original CAFEBABE header has been changed to FFFFFFFF (-1)

Since Novoline has been discontinued for so long, we can't fully launch the client and dump the encrypted classes with JVMTI (Java Virtual Machine Tool Interface). Of course, I already tried this before it was shut down, and they couldn't be dumped back then either.

Reversing the JVM

The first step, naturally, is to find the class parsing function, but we couldn't find its signature directly in memory. So the parsing function was most likely virtualized, or it may have been heavily modified so that its signature no longer matches.

To test our hypothesis, we tried to locate it by searching for key data. As we all know, the MagicNumber of a normal class is CAFEBABE, while in these modified classes it is FFFFFFFF. This means we can try to locate the function by searching for one that contains both CAFEBABE and FFFFFFFF.

And indeed, we found it.

1

1

We found that the function at RVA 0x1DF140 meets exactly these conditions, and after analysis we can confirm that it is the class parsing function.

Analyzing the Encryption Algorithm

We found that when the buffer is determined to start with FFFFFFFF, execution jumps to the region at RVA 0xCF2007 in the .akr0 section.

1

1

We also noticed two constants, 0x5DEECE66D and 0xFFFFFFFFFFFF.

1

These are exactly the constants used by the LCG (Linear Congruential Generator) algorithm. LCG is a very common PRNG (Pseudo-Random Number Generator) algorithm.

In the RVA 0xCF2007 region we also found constants matching this algorithm, as well as the constant 0x38AC7C2D4A9D, which is already an internal state value.

alt text

We also found that a fixed offset of 0x80 is added to the generated bytes.

From this, we can reconstruct one part of the computation as:

java
long state = 0x38AC7C2D4A9DL;
final long M = 0x5DEECE66DL;
final long MASK = (1L << 48) - 1;
byte[] K = new byte[1024];
byte[] KOff = new byte[1024];
for (int i = 0; i < 1024; i++) {
    state = (state * M + 11) & MASK;
    int b = (int) (state >>> 40);
    K[i] = (byte) b;
    KOff[i] = (byte) ((b + 0x80) & 0xFF);
}

1

Continuing further down, we found that it also initializes another array.

1

This part has code mutation, but you can still see that ECX increments from 0 toward 1024 and R9 advances 4 bytes per iteration.

1

1

asm
CF2101  mov edx, [rbp+rsi*4+50h]
CF2105  dec r9d
CF210F  movsxd rax, ecx
CF2261  lea rcx, [rbp+50h]
CF226A  lea rcx, [rcx+rax*4]
CF2273  mov eax, [rcx]
CF227A  mov [rbp+rsi*4+50h], eax
CF2283  dec rsi
CF228E  mov [rcx], edx

The previous loop initialized an array of 1024 integers; next comes a loop with a decreasing index that swaps elements.

This looks very much like a standard Fisher–Yates Shuffle algorithm.

1

Following RVA 0xCF2172 further down, we also find an implementation similar to Java Random's nextInt.

1

The generation logic of this table can be reconstructed as:

java
int[] P = new int[1024];
for (int i = 0; i < 1024; i++) {
    P[i] = i;
}
for (int bound = 1024; bound > 1; bound--) {
    int j = nextInt(bound);
    int t = P[bound - 1];
    P[bound - 1] = P[j];
    P[j] = t;
}

1

Further analysis shows that after initializing this table, it uses the table to select the key and the output position, decrypting the data in 1024-byte blocks.

1

java
out[chunk + P[i]] = (byte) (data[chunk + i] ^ KOff[P[i]]);

1

Blocks shorter than 1024 bytes are decrypted in order.

java
out[chunk + i] = (byte) (data[chunk + i] ^ KOff[P[i]]);

1

Finally, each byte of the previous step's output is XORed with 6.

java
for (int i = 0; i < out.length; i++) {
    out[i] ^= 0x06;
}

Analyzing the Class Structure Encryption

Continuing the analysis, we found that different data in Novoline's classes are encrypted differently, but the underlying idea is always the same: basically, they all call nextInt on a Random with the same state to get a key and then XOR with it.

Constant Pool:

java
final long M = 0x5DEECE66DL;
final long MASK = (1L << 48) - 1;
long[] state = { (seed ^ M) & MASK };

byte[] outCp = new byte[encryptedCp.length];
for (int i = 0; i < encryptedCp.length; i++) {
    state[0] = (state[0] * M + 11) & MASK;
    int r = (int) (state[0] >>> 16);
    outCp[i] = (byte) (encryptedCp[i] ^ (r & 255));
}

After repeated analysis and experimentation, we confirmed the handling of the rest to be as follows:

Data Handling
access_flags No-op
this_class XOR next_int() & 0xFFFF
super_class XOR next_int() & 0xFFFF
interfaces_count XOR next_int() & 0xFFFF
interfaces[i] Each XORed with next_int() & 0xFFFF
methods_count XOR next_int() & 0xFFFF
methods[i].access_flags No-op
methods[i].name_index XOR next_int() & 0xFFFF
methods[i].descriptor_index XOR next_int() & 0xFFFF
methods[i].attributes_count No-op
method_key of each method One separate next_int() call
attribute_name_index, attribute_length of method attributes No-op
method.max_stack XOR method_key & 0xFFFF
method.max_locals XOR method_key & 0xFFFF
method.code_length XOR method_key
method.code[bci] Handled separately
method.exception_table_length, exception_table[] None
method.attributes_count, nested attributes None
info of other method attributes None
fields_count XOR next_int() & 0xFFFF
fields[i].access_flags None
fields[i].name_index XOR next_int() & 0xFFFF
fields[i].descriptor_index XOR next_int() & 0xFFFF
fields[i].attributes_count XOR next_int() & 0xFFFF
attribute_name_index, attribute_length, info of field attributes None
Class-level attributes_count Shifted right by 1 bit
attribute_name_index, attribute_length, info of class attributes None

bci handling:

java
byte[] insts = new byte[encryptedInsts.length];
for (int i = 0; i < encryptedInsts.length; i++) {
    insts[i] = (byte) (encryptedInsts[i] ^ 0x8C);
}

byte key = (byte) (methodKey & 0xFF);
for (int step : new int[]{2, 4, 6}) {
    for (int bci = 0; bci < insts.length; bci += step) {
        insts[bci] ^= key;
    }
}

Decrypting the Client File

Having analyzed the entire class encryption scheme, we can finally decrypt the classes in AkrienGuard.jar and read the code.

1

It turns out that AkrienGuard.jar is actually a tool that decrypts files + loads classes, and the file decryption scheme is also a simple XOR.

After decrypting the Novoline client file, we found that it also uses the same class encryption.

1

Decrypting it the same way gives us a normal JAR file.

1

Java Obfuscation

1

It uses ZKM obfuscation, but for some reason the ZKM watermark was erased, so the version can't be determined, though the version doesn't really matter. It can be removed easily with our deobfuscation approach.

Clearly, like Moon, Novoline was overconfident in its custom JVM protection: the Java layer doesn't even have any native obfuscation.

1

What's baffling, though, is that its code has Native annotations; native obfuscation just never actually got applied.

Client Authentication

1

Authentication is mainly based on several native methods provided by AkrienAntiLeak and the native methods in net.skidunion.API (provided by the novo-api DLL). The main reason Novoline/Akrien was never cracked is the custom JVM protection; once the classes are decrypted, it turns out that its checking logic is pretty stupid.

The native method layout of com.moon.security.API is identical to that of net.skidunion.API. I don't know whether it's a tribute or they use the same authentication system; my guess is that it isn't the same authentication system, and it's more likely a tribute.

1

Novoline

1

Moon

The method most likely to be the authentication is authorize in ru.fals3r.aal.API. Looking at the code that references it, the authentication logic is just ThreadLocalRandom.current().nextLong, followed by checking whether what authorize returns is the fucking md5Hex(String.valueOf(r ^ 11L)).

1

Frankly, this is wild enough to rattle all of Europe. Although this authorize may be backed by VMProtect protection, this Java check is perhaps a little too stupid: all you need to do is polyfill the authorize method so that it returns this value, and its authentication passes.

java
public static String authorize(long l) {
    return DigestUtils.md5Hex((String)String.valueOf(l ^ 0xBL));
}

Another check verifies the return value of getHost in net.skidunion.API: it checks getHost() + /token, verifying that the hashCode of this URL equals "https://protection.novoline.wtf/api/v1/token".hashCode(). This is meant to guard against fake servers, for the authentication that follows.

1

The login request assembles a hardcoded User-Agent, whose meaning we don't know, and then obtains the delsyToken returned by the server. This token is used to log in to the Novoline Online Service (NOS).

1

This request can actually be skipped entirely, going straight to the init method below; we probably don't need a delsyToken anyway. Since we are going to disable NOS next, we can simply patch it to go straight to the initialization method below.

1

The initialization method takes the parameters (Ljava/lang/String;ILjava/lang/String;)V. The first parameter is username|<|>|uid, which the initialization method splits on |<|>|. The second is an int, and the third is a signature issued by the server, so we also need to patch the signature check.

1

Of course, this check just computes an MD5 from the HWID. We don't even need to care how it's implemented; just get rid of it.

Next comes the NOS connection.

1

This is actually quite easy to patch: just patch out the method that sends the request. It doesn't check whether the connection succeeded or anything like that; all it needs is for an initialization to complete.

java
public CompletableFuture AdvancedAntiSkid1iIIIIil(String string, long l) {
    return CompletableFuture.completedFuture(new AdvancedAntiSkid1Ili1111("1"));
}

Now that we have patched every authentication point, launching it with a regular JDK to test it shows that it has been cracked.

1

Interestingly, if you extract anime_pictures.pngs from the JAR, you'll find that it contains a big collection of anime character pictures.

1

Novoline.lol 6.99

Novoline.lol was originally sold on the Intent Store, so it once used the Intent Store's authentication system.

After version 5.x it was no longer sold on the Intent Store but sold independently instead, using an private authentication system. Apart from that, it has no class encryption or similar protection, but it does use native obfuscation with VMProtect.

1

Obfuscation

1

Novoline.lol's Java obfuscation is Zelix Klass Master + some custom obfuscation + native obfuscation. This native obfuscator is probably the same one Moon uses, since it also splits out Invoke Dynamic.

For the native obfuscation, since some methods are protected with VMP, we used JNI tracing combined with dynamic debugging to recover the logic of the VMP-protected methods. Methods without VMP are easier to recover: just use JNI tracing together with the decompiled pseudocode to analyze the control flow.

1

It has three custom obfuscations. One moves numbers into a separate class called num_pool. Of course, this doesn't do much: you can easily inline them and run some constant-folding transformers from open-source deobfuscators to decrypt it.

1

Another stuffs the strings of a bunch of different classes into a single class dedicated to holding these strings, which are then encrypted with ZKM's string obfuscation. Honestly, this is pretty useless; you could even say it throws away the advantage that Initialization Order provides. It actually makes decryption easier: just run this class's static block on its own in a sandbox, and everything gets decrypted.

1 1

The third turns every reference into a wrapper and rewrites the original references to point to a wrapper class; for non-static methods, this is passed in first as an argument. Honestly, a lot of this is mixed with junk code from the ZKM obfuscation, so cleaning it up completely may actually be quite hard.

But all in all, it doesn't get in the way of analysis; it's just a bit annoying.

Authentication

The authentication logic is in the net.skidunion.am class. In the previous step, using JNI tracing and dynamic debugging, we had already recovered nearly all of this class's native code into Java logic. Before authentication starts, there is a long stretch of code that checks the environment, so let's inspect it.

When an environment check fails, it reports to http://huesos.space/auth/post, perhaps to track crackers. Of course, this domain is dead now.

Environment Checks

1

Before login starts, it first checks the hashes and file sizes of rt.jar, cacerts and jvm.dll. This is why JDK 8u231 is required: every Novoline installation tutorial video tells you to download JDK 8u231.

1

An image from a 2023 Novoline installation tutorial video

1

After checking the JRE hashes, it also checks for Xbootclasspath and javaagent arguments, and checks whether the disableattachmechanism argument is present. In other words, you must have the disableattachmechanism argument and no debugging arguments to proceed to the next step.

1

This matches the fact that all the tutorials at the time also had you add the disableattachmechanism argument, although I think it doesn't do a damn thing.

1

It then checks whether the caller of this method is a native method; if it isn't, it won't be allowed to run either.

1

The next step checks whether the Attach Listener and Signal Dispatcher threads exist; if they do, it won't be allowed to run either.

Watermark

Before authentication starts, there is a very baffling piece of code.

1

It reads something called assets/minecraft/textures/gui/presets/drought.png™. This image is originally used for the custom world presets, but here it is read and compared against the sha256hex of the HWID.

1

Because Novoline has a one-JAR-per-user mechanism, when you download the JAR with your token, this image is replaced with the sha256hex of your HWID, so the JAR is effectively bound to your HWID. Notably, Novoline.wtf does not have this mechanism.

1

Vanilla Minecraft's reference to this image

Then I wondered: does Novoline handle this at all? Did they cut this preset?

Well! I was overthinking it. In Novoline, opening this custom world preset screen simply crashes the game.

1

Authentication Logic

1

Novoline's authentication goes over HTTP, with two endpoints, http://novoline.lol/flow/auth/token and http://novoline.lol/flow/auth/login?uid=. Requests are sent with Apache HttpClient.

The first step obtains the authentication initialization parameters via http://novoline.lol/flow/auth/token. The parameters sent are:

  • HWID
  • User-Agent (NovoProtect/1.0 + version number)
  • Contents of drought.png
  • ZonedDateTime.now().getZone().getId() (the local time zone, e.g. Asia/Shanghai)

The response consists of:

  • Signature
  • PublicKey (used for subsequent signature verification)
  • DelsyToken (who the fuck is Delsy?)

The second step logs in via http://novoline.lol/flow/auth/login?uid= + your UID. The parameters sent are:

  • HWID
  • User-Agent (NovoProtect/1.0 + version number)
  • Contents of drought.png
  • ZonedDateTime.now().getZone().getId()
  • DelsyToken

It then responds with a blob, which goes into verification.

1

First, the response is verified with DSA; the DSA key is the one from the response in the first step. If this verification fails, it shows File corrupted! It may have been tampered with or cracked. The program won't work anymore. This string was obviously copied from VMProtect; why they wrote it like this, I'm still puzzled.

The response is then decrypted with Blowfish. The Blowfish key is Base64(SHA1("chinkXch0ng")), passed via the two properties java.net.preferIPv4Stack and java.skidders (evidently gast still holds a grudge against Chinese!).

1 1 1

After decoding, the response is split (.split("-")) into five pieces of data:

  • encryptedUsername
  • UID
  • encryptedHWID
  • encryptedDelsyToken
  • encryptedTimestamp

These encrypted fields are decrypted with a derived AES key and IV. The derivation is:

  • Key: md5(HWID+DelsyToken)
  • IV: md5(DelsyToken.substring(2))

After decryption, it verifies that this data matches what you submitted (UID, HWID and token), then checks whether the decrypted timestamp differs from the local timestamp by more than 5 seconds. If it does, it shows Your computer time is not synchronized, or its off by some seconds..

That said, the timestamp seemingly has nothing to do with the time zone, since timestamps are universal. Perhaps that's there to detect Chinese users, following Anthropic PBC's example. Once all these checks pass, the username is decrypted and client initialization completes.

If your HWID doesn't match, you simply get an empty response.

1

HWID Generation

1

The HWID is generated as PROCESSOR_IDENTIFIER+volume:vsn+USERDOMAIN, then encrypted with a fixed AES/CBC/PKCS5PADDING key:

  • Key: WryHYmrnvanSJr6v
  • IV: kpzb989Cb5876Ggw

We found a Novoline installation tutorial video and successfully decrypted the HWID shown in it. Of course, this isn't much use; it's just pretty funny.

1

1

If you want, you can press F12 to open the console and paste this code to try it yourself.

JavaScript
async function novolineDecryptHWID(b64) {
  const s2b = s => Uint8Array.from(s, c => c.charCodeAt(0));
  const ct = s2b(atob(b64.replace(/\s+/g, "")));
  const key = await crypto.subtle.importKey("raw", s2b("WryHYmrnvanSJr6v"), { name: "AES-CBC" }, false, ["decrypt"]);
  const pt = await crypto.subtle.decrypt({ name: "AES-CBC", iv: s2b("kpzb989Cb5876Ggw") }, key, ct);
  return new TextDecoder("latin1").decode(pt);
}

await novolineDecryptHWID("w/mfdZppleehDaAcIg9EVQl2JLoKi3wiYY1FytEqLwct4zpiyrQl+8zjjgTDygyel6MqbyjZlF9QqlczcLj0RK7rYuaiCVeQpV+Ai6486Is=") 

Removing the Authentication

Clearly, once the native code is decrypted, removing the authentication is quite simple: just delete all the authentication request logic and go straight to init. And since all the data is filled in correctly and init runs to completion, the hidden checks won't trigger either.

1

Of course, since the authentication has been removed, drought.png is no longer needed either, so it can simply be replaced with the original drought.png. With that, we've made the first Novoline that can open the custom world preset screen normally.

1

Then I noticed that my ReleaseType had become Dev™. After some digging, it turns out that any UID from 1 to 5 enables Dev mode.

1

The LongJump bypass values are only assigned after authentication passes. I first cracked Novoline 6.97 in 2023, but because I hadn't decrypted this, LongJump didn't work, so I never released it.

java
this.q = new double[]{0.42f, 0.54, 0.13, 1.8, 0.028, 0.084, 1.004, 1.18, 0.333125};

After filling these values back in, LongJump works properly.

Novoline 112321

We also reversed Novoline 112321, mainly to find out how the Intent Store's authentication works.

1

As soon as I started decompiling, I was baffled: this store.intent.API also has the same fucking getHost, getHWID, initialize and other shit, exactly the fucking same as Novoline.wtf and Moon. Could it be that the authentication for all these hacked clients is outsourced to skidunion? But after looking into it, this thing is actually never called.

The real authentication is already much the same as in 6.99, except that the endpoints are https://intent.store/product/38/protection/token and https://intent.store/product/38/protection/login?uid=, and its token verification logic is similar to 6.99's.

Conclusion

We are also working on restoring both Novolines to source code and upgrading them to 1.8.9 (Novoline is on 1.8.8), but since things at university have been busy lately, it may take a while. We've already made a lot of progress, such as removing the obfuscation and nearly finishing most of the remapping work. If you'd like to get involved or help, feel free to contact us anytime.

1

Download

The cracked Novoline has been released at: https://github.com/StylesFoundation/Novoline-unlicensed/releases/tag/1

It includes cracks of Novoline.wtf 3.0 and Novoline.lol 6.99. Although they can't bypass Hypixel now, you should be able to play on some nostalgia servers such as rewound, or on servers with shitty anticheats.

We also removed the natives, since they are no longer needed to run it. And the cracked Novoline.wtf doesn't need a custom JDK either; it launches with a regular JDK 17.

1

In case you want to record videos on servers like rewound and would like to use your own username and UID, you can find the Custom.txt file in the JAR's root directory, edit it, and put it back into the JAR. The first line is the username and the second line is the UID. This works for both Novolines.

1

  1. The oldest Novoline video I could find: https://www.youtube.com/watch?v=Wiri8fUD9X4 ↩

  2. https://systemfailu.re/2020/07/20/alpha-anti-leak-in-depth-analysis/ ↩

Select any text to annotate it

Comments

Loading comments…