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:
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.
| Field | Example |
|---|---|
| osType | 1 |
| osVersion | 12 |
| vendor | 2 |
| javaMajor | 11 |
| javaMinor | 1 |
| javaPatch | 25 |
| maxMemory | 4096 |
| cpuCores | 32 |
| systemMemory | 16365 |
| signers | sun.nio.ch.FileDispatcher |
| Unknown_3 | (unknown) |
| Unknown_4 | (unknown) |
| process | java.exe |
| parentProcess | java.exe |
| Unknown_7 | (unknown) |
| Unknown_8 | (unknown) |
| Unknown_9 | (unknown) |
| Unknown_10 | (unknown) |
| Unknown_11 | (unknown) |
| Unknown_12 | (unknown) |
| Unknown_13 | (unknown) |
| clientName | 304+411+client4122yq\nclient19537qj\nnrc.RuneLite297start\nnrc.RuneLite274main\nnrcpk.KrakenLoader+21main |
| agent | JvmAgent.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:
clientNameagent
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 Check
TODO Once I understand more about this
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.datfile.
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:
- 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. ConstantDynamicBootstrapping: The calls to the builder method are structured asConstantDynamicbootstrap callsites. Because of how constant dynamic works, the JVM executes this method during the class linking/preload phase.- 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.
- Triggered by Dependency Injection (
@Inject): The very first evaluation is typically triggered when RuneLite callsinjectMembers()on the client object. Because Guice processes the@Injectannotations immediately upon client startup, the resulting call stack naturally includes the legitimateRuneLite.mainandRuneLite.startexecution paths. - Assembly and Storage: The resulting string is truncated, concatenated, and ultimately dumped into the
PlatformInfofield 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
yqat 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.
// 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.exeor 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