index writing about bookmarks

Deobfuscating Minecraft Malware

Discovering “Cummyhook”

As someone who frequently plays Hypixel Skyblock & Minecraft in general, malware seems to become more & more common as you play the game, specifically malware attempting to steal Discord tokens, session IDs & more. I’ve noticed a majority of samples seem to target Skyblock players, primarily to steal their items. While looking around on YouTube, I came across something named “CummyHook,” apparently a “QOL” mod for Hypixel Skyblock, and after taking a better look in Recaf, it was obfuscated!

Understanding its obfuscation

This isn’t my first rodeo dealing with JVM obfuscation of all sorts. I started by opening it in Recaf, a very powerful & useful disassembler for JVM bytecode. Afterwards, I took a look at its packages and noticed the wrapped.github.deopped package, and there it was! Deopped Methods List When it comes to JVM obfuscation, it’s quite common for them to use string, variable & control flow obfuscation to make static analysis extremely annoying. I’ve noticed that they use multi-algorithm string encryption, via Blowfish, DES & a basic XOR cipher.

They also seemed to use some basic control-flow obfuscation which didn’t really matter as much, as I primarily use Recaf’s fallback decompiler which allows me to read the instructions directly rather than attempting to reconstruct the bytecode. Its obfuscation wasn’t exactly complicated; their implementations for DES & Blowfish were fairly basic. Their decryption routines are invoked statically throughout the code, the arguments being the encrypted string & key are pushed onto the operand stack via LDC instructions preceding the INVOKESTATIC instruction that calls the decryption routine.

// LDC "rfdoBVwxcyo9pP8DOsl6rYZf45H+aG3fC8HlOwfFWl..." // Encrypted String
// LDC "cBamw"                                       // Key String
// INVOKESTATIC wrapped/github/deopped/DeoppedConfig.lIlIl (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
// ASTORE_VAR ... // Store the decrypted result

The methods heavily utilize the mentioned decryption routines to initialize static string arrays, such as Deopped.llIlI etc.

Understanding what its payload is

So far, after taking a greater look at its obfuscation, I’ve been able to somewhat figure out an execution chain. To start with, wrapped.github.deopped.Deopped seems to have HTTP related utilities for exfiltrating data, wrapped.github.deopped.DeoppedConfig is the main mod class that hooks into Forge, and finally wrapped.github.deopped.DeoppedUtils is their code for data exfil via a Discord webhook. It starts by registering itself as a Minecraft Forge Mod, then as Forge’s pre-init stage occurs, it creates a new thread to avoid the game from freezing & similar during data collection & exfil. It starts by collecting:

  • IP Address
  • Geographical information such as Country, City etc.
  • ISP Information
  • Timezone

Afterwards, it’ll call Minecraft.getMinecraft().getSession(); to collect:

  • Minecraft Username
  • Your UUID
  • And finally your session token, which seems to be very common for Skyblock related malware.

Weirdly enough, it also grabs your saved servers, such as their names, IPs, etc. Afterwards, it’ll create an HTTP Client that bypasses any sort of authentication, for example via bypassing SSL certificate validation, and finally it’ll exfiltrate it to a Discord webhook formatted nicely in an Embed.

How I went about deobfuscating it

After becoming more familiar with its obfuscation, I opened IntelliJ IDEA and started writing a basic transformer for it. Based on analyzing its bytecode earlier, I’ve noticed some abnormalities that have made my job a lot easier, such as certain strings seemed to follow a pattern where, depending on their length, we could heuristically map them to either DES, Blowfish, or XOR.

We started by extracting strings, which was fairly easy due to their silly mistakes, and certain method signatures such as (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; made it very obvious it’s a method that takes 2 strings, such as the encrypted text & a key, and it also returns one string which is the decrypted result, which seems to be very common.

Afterwards, we started by extracting constants, specifically integer arrays which are used in decryption. We did it by using ASM to look for the static initializer method <clinit>, then iterating through the instructions within it. We looked for patterns indicating creation & population of an integer array, such as PUTSTATIC into a static field whose type descriptor starts with [I meaning int[], and subsequent instructions that push integer values onto the stack via BIPUSH before the array is stored.

And FINALLY! We extracted the strings & decrypted them. We iterated throughout the classes via ASM. We started by checking every method’s instructions for method calls, and then checking if the method’s owner and name match an entry in the knownDecryptor map:

enum class DecryptorType {
  BLOWFISH,
  DES,
  XOR,
  BASE64,
  UNKNOWN
}

And if the signature is expected, string, string -> string, it attempts to retrieve the two string arguments that should be on the operand stack just before the call. It does this by looking backwards from MethodInsnNode for the two preceding LdcInsnNode instructions that push String constants. The first one found going backwards is assumed to be the key, and the second one is the encrypted string. If both an encrypted string and key are extracted successfully, it’ll retrieve the DecryptorType from knownDecryptors and it’ll call the corresponding decryption method from the util.Crypto class, such as:

  fun decryptBlowfish(encrypted: String, key: String): String {
    try {
      val keySpec =
          SecretKeySpec(
              MessageDigest.getInstance("MD5").digest(key.toByteArray(StandardCharsets.UTF_8)),
              "Blowfish")
      val cipher = Cipher.getInstance("Blowfish")
      cipher.init(Cipher.DECRYPT_MODE, keySpec)
      val decoded = Base64.getDecoder().decode(encrypted.toByteArray(StandardCharsets.UTF_8))
      return String(cipher.doFinal(decoded), StandardCharsets.UTF_8)
    } catch (e: Exception) {
      return "DECRYPT_ERROR"
    }
  }

Although for Base64, it’ll just use the JVM’s standard java.util.Base64 decoder. If it’s unsure, it’ll attempt all known decryption methods. If decryption is successful, it’ll attempt to clean strings by removing null characters that are common in certain obfuscation. And finally, we’ll receive the decrypted strings.

Conclusion

Honestly, it wasn’t too difficult to figure out, probably some of the worst & most common obfuscation I’ve seen, that seems to be very common primarily in the Hypixel Skyblock community!? Overall, if you’re going to be a trashy person and infect others with malware, do it right :3

Pssst, If you’d like to take a greater look on how I did stuff, the repo is https://github.com/methodhandle/Discerpere :3