Skip to content

Client Patching

The Kraken API provides a set of JSON hooks describing key client classes, methods, and values. However, the API does not actually patch the injected-client.jar file code with things like the call stack value, client log field, mouse DLL, or agent information.

In this guide, we will go over the runtime patching process and how to use these hooks to actually patch out key fields and methods in the client.

Before going any further, it's worth noting that this document is purely for informational and educational purposes. It is to document my own understanding of the client patching process for future reference and should not be used to reverse-engineer, break, refactor, or otherwise modify the RuneLite client.

What is Patching?

Patching is simply the process of further modifying the RuneLite injected client at runtime so that it can be safely used with third party plugins. You can use any bytecode manipulation tool like ASM or ByteBuddy to patch the client.

By patching the client, we can remove certain checks, add additional code (like sending an event bus message for when a packet is sent), or even modify the client to add new functionality like exposing data that the RuneLite API doesn't expose.

JVM Agents: Static vs. Dynamic Attachment

When patching a Java application using tools like ByteBuddy, the modifications are typically applied via a Java Agent. A Java Agent is a special type of JAR file that utilizes the Instrumentation API to intercept and modify class definitions before or after they are loaded into the Java Virtual Machine (JVM).

There are two primary ways to introduce an agent into the JVM, and understanding the difference is critical for both stability and detection vectors.

1.Static Loading (premain)

Static loading occurs before the application even starts. By passing the -javaagent:path/to/your-agent.jar argument to the JVM at launch, the JVM invokes the agent's premain method before it executes the application's actual main method.

  • Safety & Stability: Because the agent is running before the game code initializes, it can intercept classes right as they are being loaded for the very first time. You don't have to worry about redefining a class that is already actively being used in memory.
  • Footprint: It is generally considered a "cleaner" approach because the instrumentation is woven into the standard startup sequence of the JVM.

2.Dynamic Attachment (agentmain)

Dynamic attachment allows an agent to be loaded into a JVM process that is already running. This is what happens under the hood when you call ByteBuddyAgent.install() from within your code. It uses the Java Attach API to reach out to the running JVM, connect to it, and trigger the agent's agentmain method.

  • Convenience: It allows you to patch the client on the fly without needing to modify the startup arguments of the RuneLite launcher.
  • Detection Risks: The Attach API leaves a significant footprint. It spins up specific attachment threads inside the JVM and alters the internal state in ways that are highly visible to runtime environment checks. Because game clients (including RuneLite and Jagex's underlying client) monitor their environment for unauthorized modifications, dynamic attachment is heavily scrutinized and easily flagged.

The Importance of ClassLoaders

When applying patches at runtime, knowing how to modify bytecode is only half the battle; knowing where that bytecode lives is just as important. This is where ClassLoaders come in. In Java, a ClassLoader is the subsystem responsible for finding and loading .class files into memory. Simple applications use a single, flat system ClassLoader. However, complex, modular applications like RuneLite use a highly segregated ClassLoader architecture.

RuneLite's Isolation Strategy

RuneLite does not load the vanilla/injected OSRS client or individual plugins into the root system classpath. Instead, it creates isolated, custom ClassLoaders. This is a great design for a plugin hub because it prevents Plugin A from accidentally crashing Plugin B due to a dependency conflict.

However, this isolation creates a common trap for developers writing patches.

The ClassLoader Trap

If you instruct ByteBuddy to patch a class like Client or the injected Mouse DLL, ByteBuddy needs to know which ClassLoader holds that class. If you don't explicitly specify the correct ClassLoader, ByteBuddy will default to the system ClassLoader or the ClassLoader of your patcher itself.

The result? ByteBuddy will successfully generate the patched bytecode and load it into memory—but it will load it into the wrong ClassLoader. The actual game client, running in its own isolated ClassLoader, will continue executing the original, unmodified code, completely ignoring your patch.

Targeting the Right Scope

To ensure your patches actually take effect, you must grab the specific ClassLoader that holds the target class (i.e. the RuneLite client's classloader).

java
@Inject
private Client client;

// ...

// We use RuneLite's classloader here because we are targeting injected-client classes!
Class<?> targetClass = client.getClass().getClassLoader().loadClass(securityHooks.getMouseHookDllClassName());

By calling client.getClass().getClassLoader(), we are actively reaching into the specific, isolated environment where the injected client is running. When the newly redefined bytecode is generated, it is pushed directly back into that exact same environment, ensuring the game picks up the modified instructions.

How to Patch

Below is a code snippet showing an example of how the Mouse DLL can be safely patched to return 0 (not injected click) every time. If you are unfamiliar with the mouse hook DLL or what it does, then check out this document on client detection which provides more information on the process.

java
@Override
public void apply(ClientPatchContext context) throws Exception {
    try {
        // HooksLoader comes from the Kraken API and contains up to date mappings for where the obfuscated
        // class/method is for the mouse DLL!
        SecurityHooks securityHooks = HooksLoader.getSecurityHooks();
        
        // Client in this instance is the RuneLite client.
        Class<?> targetClass = client.getClass().getClassLoader().loadClass(securityHooks.getMouseHookDllClassName());

        context.byteBuddy()
                .redefine(targetClass)
                // Replace the field read with our custom method call
                .visit(MemberSubstitution.strict()
                        .field(ElementMatchers.named("llimc"))
                        .onRead()
                        .replaceWith(MouseHookPatch.class.getMethod("provideZero"))
                        .on(ElementMatchers.named(securityHooks.getMouseHookDllMethodName())))
                .make()
                .load(targetClass.getClassLoader(), context.getClassReloadingStrategy());
    } catch (Exception e) {
        throw e;
    }
}

// ...
public static int provideZero() {
    return 0;
}

As you can see, patching is not a particularly complicated process with Bytebuddy. The HooksLoader class provides up to date mappings for the class and method names, and you can use the MemberSubstitution class to replace the field read with a custom method call.

Essentially, any time the llimc field is read,provideZero will be called instead which always returns the value 0 meaning that the client will see every click as a normal (non-injected) click.

Summary

To recap the client patching process:

  • The Goal: Modify the vanilla or injected client at runtime to safely interface with third-party plugins, bypass limitations, or expose hidden data.
  • The Tools: Bytecode manipulators like ASM or ByteBuddy allow us to edit compiled .class files directly in the JVM without needing source code.
  • The Mechanism: By using a Java Agent (either loaded statically via JVM arguments or attached dynamically at runtime), we gain access to the Instrumentation API, which allows us to redefine classes on the fly.
  • The Execution: Using up-to-date mappings (like those from the Kraken API), we can pinpoint obfuscated classes and inject hooks—such as using ByteBuddy's MemberSubstitution to replace a method call or variable read with our own custom logic.

Disclaimer reminder: Remember that runtime patching is a highly scrutinized process by game developers. Understanding these concepts is vital for client architecture and security research, but modifying live game environments carries heavy risks.