Skip to content

Third Party Client Detection

RuneLite has some tricks up its sleeve to help it accurately detect third party clients! In this guide we are going to cover some of the clever ways that RuneLite uses to detect and share third party client information with OSRS. This is one current advantage to using the native C++ client over the Java client as there are far fewer client detection methods currently in place.

As always, this guide is purely for education purposes and is not meant for reverse engineering, hacking, botting, or breaking any of RuneLite or Jagex's terms of service. This guide is for my own personal knowledge and reference. It is not intended to be a comprehensive list of all third party client detection methods, and more methods will almost certainly come up in the future that aren't covered in this guide.

Mouse Hook DLL

The mouse hook DLL is a more recent addition to RuneLite's arsenal of detection systems. RuneLite ships with a DLL file which registers a low-level mouse hook called: LLMHF_INJECTED (information here). The DLL file is called: rlicn_{os.arch}.dll and is packaged as a resource within a JAR file loaded as part of the RuneLite bootstrap process. The {os.arch} will be something like x86 or x64 representing your CPU architecture.

This detection mechanism is only applicable to Windows machines and is designed to detect when a mouse click is injected into the game process, i.e. AHK scripts or mouse clicks done over a Desktop sharing program like TeamViewer, Chrome RDP, or ParSec.

The MSLLHOOKSTRUCT Structure

When a Windows application wants to monitor mouse activity globally (across the entire operating system, not just within its own window), it uses the SetWindowsHookEx API to install a Low-Level Mouse Hook (WH_MOUSE_LL).

Every time a mouse event occurs (a movement, a click, or a scroll), Windows intercepts the event and passes a pointer to an MSLLHOOKSTRUCT to the hook procedure before the event reaches the target window.

In C++, the struct looks like this:

c
typedef struct tagMSLLHOOKSTRUCT {
    POINT     pt;          // The x/y coordinates of the cursor
    DWORD     mouseData;   // Wheel scroll data or specific X-button data
    DWORD     flags;       // Event-injected flags (This is the important part)
    DWORD     time;        // The timestamp of the message
    ULONG_PTR dwExtraInfo; // Additional info associated with the message
} MSLLHOOKSTRUCT, *PMSLLHOOKSTRUCT, *LPMSLLHOOKSTRUCT;

This structure acts as the single source of truth for the OS regarding a mouse event.

The LLMHF_INJECTED Flag

The flags field is a DWORD (a 32-bit unsigned integer) that contains bitwise flags describing the origin of the event. When a human physically clicks a standard USB mouse, the hardware interrupt travels through the kernel driver to the OS. Windows processes this event normally, and the event-injected bits within the flags field remain 0. If a program uses a user-space Windows API like SendInput(), mouse_event(), or high-level wrappers like Java's Robot class to simulate a click, Windows automatically flips bit 0 of the flags field to 1.

This specific bit is known as LLMHF_INJECTED (0x00000001). Typically, the easiest way to circumvent this process is to patch the client at runtime to always hardcode a 0 value for the llimc value in the client class. This means that regardless irrespective the origin of the click event, whenever the field is accessed it will always return 0. You can see an example of this in the patching document.

JVM Agents & Platform Information

Before a user logs into the game with RuneLite, Jagex will construct and populate a buffer containing lots of information about the user's platform. This data and buffer construction all takes place in a class called PlatformInfo which is part of the vanilla client.

The following table details the information included in the login packet which is built in part by PlatformInfo.write(). It's important to note that the data is written in this specific order, and is invaluable in helping to identify which obfuscated fields map to which actual data.

i.e. cw was written last in the PlatformInfo.write() method so even though PlatformInfo's field order are changed we know that cw corresponds to the agent value. Thus, to spoof the agent value, we can simply hardcode the value of cw to whatever we want via client patching.

FieldExample
osType1
osVersion12
vendor2
javaMajor11
javaMinor1
javaPatch25
maxMemory4096
cpuCores32
systemMemory16365
signerssun.nio.ch.FileDispatcher
Unknown_3(unknown)
Unknown_4(unknown)
processjava.exe
parentProcessjava.exe
Unknown_7(unknown)
Unknown_8(unknown)
Unknown_9(unknown)
Unknown_10(unknown)
Unknown_11(unknown)
Unknown_12(unknown)
Unknown_13(unknown)
clientName304+411+client4122yq\nclient19537qj\nnrc.RuneLite297start\nnrc.RuneLite274main\nnrcpk.KrakenLoader+21main
agentJvmAgent.jar

This information is written to a Buffer (in the same way packets are) and sent over the network to Jagex servers. There are 2 key pieces of information here that we care about for client detection:

  • clientName
  • agent

The client name very closely resembles the call stack trace that RuneLite generates (more information here) and the agent contains the name of a JAR file being used as a JVM agent. More information on JVM agents can be found here.

It should be obvious that we definitely don't want RuneLite or Jagex being aware that an agent is in use modifying classes at runtime. Most clients will patch the field value for agents so that when it is read a hardcoded value of: "" for no agents present.

Care has to be taken in how this field is patched. The PlatformInfo.write() method accepts a Buffer writer as an argument and within the buffer writer object a fixed size byte array is provisioned based on the data being written. This means that if you directly modify the client name (callstack) or agent fields within the write() method you will almost certainly either:

  • Have a byte array undersized where OSRS servers discard the packet, and you see: "RuneScape has been updated, please restart client"
  • Have a byte array too small to hold all the modified data you are sending and you will get a java.lang.ArrayIndexOutOfBoundsException

Neither outcome is desirable, so make sure that when you are patching the clientName or agent fields you are patching them at the class level (globally) for all methods in the PlatformInfo class. This way, when the write() method is called it will always provision the correct sized byte array for the modified data you are sending.

Code Signing & Assertions Check

This check answers a simple question for Jagex: "Is this the real, untampered RuneLite client jar, or did someone rebuild it?" It does this without shipping a hash of the bytecode. Instead it leans on a feature built into Java itself, jar signing, and folds the answer into the same PlatformInfo login buffer we covered above (it is the signers field in that table). As a bonus, it also quietly leaks whether you started the client with Java assertions enabled, which is a dead giveaway that you are running from an IDE or a development launch.

As with the rest of this guide, obfuscated names like bq, cp.az, and lw.wj are specific to one client revision (1.12.38 here) and will change on every update. Match on behaviour, not on names.

Background: what "signers" even means

When Jagex (or RuneLite) builds the official client jar, they run jarsigner over it. That embeds two files in the jar: a .SF signature file and a .RSA certificate block (you can see these yourself, look inside client-<version>.jar under META-INF/ and you will find RL.SF and RL.RSA). The certificate is essentially a tamper-evident seal, only the holder of RuneLite's private key can produce it.

Java exposes this at runtime through a single method on every Class object:

java
Object[] signers = SomeClass.class.getSigners();
  • If the class was loaded from a properly signed jar, getSigners() returns the certificate chain that signed it. That array is identical for every user on the planet running the official jar, because everyone is running the exact same signed bytes.
  • If the class was loaded from an unsigned jar (for example, one you rebuilt from decompiled sources, or classes loaded loose from a build/ directory), getSigners() returns null.

So "did you tamper with the client?" collapses into "does this class still carry RuneLite's signature?". That is the entire idea behind this check.

How the fingerprint is built

The client does not send the certificate itself. It sends a small integer derived from it. Here is the actual obfuscated routine that builds the value (it runs early, right as the client wires up its three main cache archives, well before the login screen appears):

java
// Obfuscated (1.12.38). This same body appears in two places, cp.az(...) and pw.ao(...).
vt vt2 = eb.pt;   // eb.pt is the singleton PlatformInfo instance
vt2.bq = (Arrays.hashCode(oe.cz.xr.getClass().getSigners()) >> 2) * 721999872
       + -1694189056
       + (lw.wj - 1) * 1337221120;
vt2.cp = client.ri();   // the call stack fingerprint, covered in the next section

Deobfuscated, it reads like this:

java
// What it actually means
Client client        = oe.cz;              // the live client instance
Callbacks callbacks  = client.xr;          // the RuneLite "Callbacks" hook object...
Class<?> hookClass   = callbacks.getClass(); // ...whose real class is net.runelite.client.callback.Hooks

int signerHash       = Arrays.hashCode(hookClass.getSigners()); // 0 if the jar is unsigned
boolean assertionsOff = lw.wj;             // JVM synthetic $assertionsDisabled flag

platformInfo.signers = (signerHash >> 2) * MAGIC_A
                     + MAGIC_B
                     + (assertionsOff ? 0 : -1) * MAGIC_C;

Let us unpack the three moving parts, because each one is doing real work:

  1. oe.cz.xr.getClass().getSigners()oe.cz is the client instance. Its xr field is the net.runelite.api.hooks.Callbacks object, which at runtime is RuneLite's net.runelite.client.callback.Hooks class (this class lives in the signed client.jar). So the client is asking "is the class that implements my RuneLite hooks still signed?". This is a smart choice of victim class: Hooks is the bridge that makes RuneLite work at all, so it has to be present and has to come from the client jar.

  2. Arrays.hashCode(...) >> 2 — the signer array is turned into a single int with Arrays.hashCode, then shifted right by two bits. For the official signed jar this is a fixed, known number. For an unsigned jar getSigners() is null, and Arrays.hashCode(null) is 0, so this whole term becomes 0.

  3. lw.wj — this is not a real field anyone wrote. It is the JVM's synthetic $assertionsDisabled flag that the compiler adds to lw (some obfuscated Widget class). It is computed once per class like this:

    java
    // Compiler-generated, decompiled
    static { wj = !lw.class.desiredAssertionStatus(); }

    So lw.wj is true (1) in normal launches (assertions off) and false (0) when the JVM is started with -ea / -enableassertions. The (lw.wj - 1) term is therefore 0 normally and -1 under -ea. That is how a purely development-only flag sneaks into your login fingerprint.

There is also an implicit check hiding here: if the RuneLite hooks were never installed, client.xr would be null and oe.cz.xr.getClass() would throw a NullPointerException during login. In other words, "you must have RuneLite's hooks wired up" is enforced for free, just by how the value is read.

How it lands on the wire

The value is stored in the PlatformInfo bq field and written out inside PlatformInfo.write() as a short (two bytes):

java
// Inside the obfuscated PlatformInfo.write() (vt.az)
xy2.bv(this.bq * -799972875, -1542382992);   // bv() writes the low 16 bits, big-endian

The * -799972875 is just the obfuscator's matching "decode" multiplier (it is the modular inverse of the 1749910109 the constructor multiplies by), a standard trick to make static analysis harder. The practical result is that only the low 16 bits of the computed number reach Jagex.

Because of the specific magic constants, those 16 bits always land on a fixed lattice. Working the unsigned-jar case all the way through (signerHash = 0) gives clean, memorable numbers:

Launch conditiongetSigners()AssertionsValue sent (signers short)
Rebuilt / unsigned jar, normal launchnull → hash 0off0x0200 (512)
Rebuilt / unsigned jar, -ea launchnull → hash 0on (-ea)0x2200 (8704)
Official signed jar, normal launchRuneLite certoffa specific fixed value determined by RuneLite's signing certificate

Jagex knows what the official signed jar produces. Anything else, an unsigned rebuild sending 0x0200, or a value shifted by the -ea term, stands out immediately against the known-good number.

Legitimate vs. illegitimate values

  • Legitimate: you are running the untouched, officially signed client.jar that RuneLite's own launcher downloaded and verified, and you did not pass -ea. getSigners() returns RuneLite's certificate, the assertions term is zero, and the short matches what every other real user sends.
  • Illegitimate: you decompiled the client, changed something, and rebuilt it. Your rebuilt jar is unsigned, getSigners() returns null, and you send 0x0200 (or 0x2200 from an IDE run with assertions on). Either way the number no longer matches the official jar's fingerprint.

This is exactly why the "cleanest" approach to a RuneLite-based client is to never modify the client jar on disk at all, and instead patch behaviour at runtime (see the patching guide). Runtime patching with a tool like ByteBuddy redefine keeps the class name, fields, and, critically, the loaded jar's signature intact, so getSigners() still returns RuneLite's certificate.

How to stay consistent with a real client

You mitigate this check by making sure all three inputs match a normal player, not by faking the number:

  1. Run the official signed jars, unmodified. Let RuneLite's own launcher fetch and verify injected-client.jar / client.jar, then add your own code on top (extra jars on the classpath, runtime patches). Do not rebuild, re-sign, or hand-edit the client jar, the moment you do, getSigners() stops matching. If your tooling verifies the client hash before launch (as a safety gate), that also guarantees you are running the same signed bytes as everyone else.

  2. Never ship -ea to end users. Java assertions are a development convenience. If your run configuration or launcher passes -enableassertions/-ea, lw.wj flips and your signers short shifts by the assertions term (0x02000x2200 in the unsigned example). Only enable assertions when testing locally and never on an account you care about.

  3. Make sure RuneLite's hooks are actually installed. Remember the implicit null check, if Callbacks (client.xr) is missing you will NullPointerException during login. A correctly loaded RuneLite client already satisfies this, but a broken custom bootstrap that skips hook installation will fail here.

  4. If you must patch it, patch the field, not the jar. In the rare case you cannot run a signed jar (for example a fully rebuilt client), the only safe option is to spoof the bq field read the same way the agent and call stack are handled: substitute every read of the signers field across the whole PlatformInfo class so the buffer allocates the correct size (see the warning in the JVM Agents & Platform Information section about undersized/oversized buffers). You would need to know the exact value a genuine signed client emits for that revision, and pin it. For most projects this is unnecessary overkill, running the real signed jar is simpler and safer.

⚠️ Reminder: the honest, low-risk path is to keep the official signed client bytes untouched and do everything additive at runtime. Faking getSigners() is only ever needed when you have already given up jar integrity, at which point you are fighting an uphill battle against a value only Jagex knows for certain.

Random.dat

The random.dat file is one of Jagex’s oldest client tracking mechanisms. Even though you are playing on RuneLite, RuneLite is simply running the underlying vanilla Old School RuneScape client, which is what actually generates and reads this file. If you look in your computer's home directory (usually C:\Users\<YourUsername>\ on Windows or ~ on macOS/Linux), you will likely find it sitting there.

I.e., RuneLite doesn't have anything to do with the random.dat file in your root dir it's purely a Jagex mechanism.

What is inside random.dat?

Despite its ambiguous name, the file is not a dump of random game data or cache files. It is a tiny binary file traditionally only 24 bytes in size—that contains a randomly generated Unique Identifier (UID).

When you load the OSRS client for the very first time, the game checks your home directory. If random.dat does not exist, the client generates a new, completely unique cryptographic string and saves it to that file. Every time you launch the client or log in after that, the client reads this UID and sends it to Jagex's servers during the login handshake.

Jagex uses random.dat as a persistent digital fingerprint for your machine. Because it is stored outside the standard game cache, it often survives game updates, client installs, and cache clearing. Jagex can use the UID as a trust metric. If an account is suddenly accessed from a new IP address, but the random.dat UID matches the one normally used by the account owner, the system might treat the login as a slightly higher risk as Jagex is able to tell It's still your account from the same computer.

random.dat files are unique per character not per account.

Why "Nuking" the File Fails

Many proxy setups simply delete random.dat before every login. From a data analytics standpoint, this is a massive red flag. Legitimate players do not reinstall their game or clear their application data every time they log in. Generating a brand-new UID for every single session tells the anti-cheat system that the client is either deliberately tampering with its tracking files or operating in a volatile, non-standard environment. This makes the account highly suspicious before the character even takes a step in-game.

A better approach is to use profile sandboxing. To effectively stay undetected regarding random.dat, the system needs to maintain strict isolation:

  • 1 Account = 1 Proxy = 1 Persistent UID: Every distinct account (or group of accounts operating from a single "household" IP) should be assigned its own unique random.dat file.

When Account A is launched, the client must inject or load Account A's specific random.dat. When the session ends, any changes to that file should be saved to Account A's profile. By doing this, Jagex's servers see a consistent UID logging in from a consistent IP address over weeks or months. This approach more aptly mirrors a real player returning to the game on their personal computer.

Call Stack Check (Client Fingerprint)

The "Call Stack Fingerprint" is an integrity check used to verify that the client was started through legitimate means. By examining the Java call stack at the very beginning of the application's lifecycle, the client can detect if it was launched normally (e.g., via the official executable or JAR file) or if it was injected/loaded by an unauthorized third-party tool.

How the Call Stack Fingerprint is Generated

The client builds and cements this fingerprint incredibly early in the loading process to prevent spoofing. It achieves this using a combination of Java 11's ConstantDynamic (link) and dependency injection frameworks (like Google Guice).

Here is the step-by-step execution flow:

  1. The Core Builder Method (yq): The client contains a specific method (e.g., yq()) responsible for reading the current JVM stack trace and building the fingerprint string.
  2. ConstantDynamic Bootstrapping: The calls to the builder method are structured as ConstantDynamic bootstrap callsites. Because of how constant dynamic works, the JVM executes this method during the class linking/preload phase.
  3. Caching the First Execution: Constant dynamic guarantees that the bootstrap method is only executed once. The result is cached and used as a constant value for every subsequent call. This "cements" the fingerprint before malicious code can easily hook or modify it.
  4. Triggered by Dependency Injection (@Inject): The very first evaluation is typically triggered when RuneLite calls injectMembers() on the client object. Because Guice processes the @Inject annotations immediately upon client startup, the resulting call stack naturally includes the legitimate RuneLite.main and RuneLite.start execution paths.
  5. Assembly and Storage: The resulting string is truncated, concatenated, and ultimately dumped into the PlatformInfo field to be sent as a login packet.

⚠️ Important By the time Bytebuddy modifies the class at runtime, it is already too late and the callstack has already been built and cached. Modifying the actual callstack method yq at this point won't help as the value has already been calculated and stored in a field within the client. You must patch the place where the callstack is actually sent (i.e. PlatformInfo.write()) to ensure that the callstack value that was pre-computed is modified before it is sent to the server.

Decompiled Code Examples

Below is an example of what the obfuscated routines look like when properly decompiled.

java
// Method responsible for truncating the fingerprint (first 3 chars)
public static void cr() {
    rc = client.km(System.currentTimeMillis() % 1000L + client.tz(0L), 3);
}

// Triggered incredibly early via Guice dependency injection
@Inject
public void wb() {
    vd = client.km(System.currentTimeMillis() % 1000L + client.tz(0L), 3);
}

// Assembles the final string to be dumped into PlatformInfo
public static String yq() {
    return rc + vd + client.yq(0L); // yq(0L) returns the cached ConstantDynamic stack trace
}

Legitimate vs. Illegitimate Execution

Because the stack trace fingerprint acts as a historical record of exactly how the Java Virtual Machine loaded the client classes, the resulting string can vary wildly depending on the launch method. However, there is only one clean stack trace that is guaranteed to be consistent across all users when RuneLite is legitimately started.

Legitimate Execution Paths

A valid fingerprint will contain the expected class hierarchy of the official launcher. Legitimate ways to start the client include:

  • Launching the official runelite.exe or Jagex Launcher executable.
  • Executing the official JAR file directly via the command line (e.g., java -jar RuneLite.jar).
  • In these cases, the call stack will accurately reflect standard Java bootstrap mechanisms and RuneLite's internal main() methods.

Note: Reflection can be used to launch the client, i.e., via a program which wraps the RuneLite launcher. However, it is key that the RuneLite launcher is executed to start the normal flow. If the RuneLite client is launched directly via reflection, the call stack will be mostly correct but missing a line. Wrapper → RuneLite Launcher → RuneLite Client = OK Wrapper → RuneLite Client = WRONG