Skip to content

2. Reverse Engineering OSRS c Client: Simulating Menu Actions

chsami edited this page Apr 19, 2025 · 1 revision

Reverse Engineering OSRS Native Client: Simulating Menu Actions with Frida

20250419-1402-51.7861779.mp4

In this post, I'll walk through how I reverse engineered the native Old School RuneScape (OSRS) client to simulate in-game menu actions like "Talk-to" using Frida. It started with a string search, turned into low-level function analysis, and ended with full control over in-game interactions.

🔍 Step 1: Finding the Right Function with Ghidra

Everything began with the simple idea: search for the string "Walk here" in Ghidra. Since all menu options in OSRS like "Talk-to", "Examine", etc., are built around strings, it's a good entry point.

To do this:

  1. Open the binary in Ghidra.
  2. Open the Defined Strings window (Window → Defined Strings).
  3. Filter or search for "Walk here".
  4. Once found, double-click the address (1409f9308 in my case).

You'll see something like this in the disassembly:

1409f9308 57 61 6c 6b 20 68 65 72 65    ds  "Walk here"

To the right, you'll notice XREFs (cross-references) – places in the code that use this string.

XREF[4,6]: 
  FUN_140009d40
  FUN_140099db0

After taking a look at each of the functions I quickly noticed that FUN_140099db0 had a lot of paramaters and was a big function. This was in line with the 216 leaked debug build. (https://rune-server.org/threads/the-great-nxt-osrs-dump-thread.706428/)

This tells us that the string is referenced inside FUN_140099db0. That's our function of interest.

🔬 Step 2: Hooking the Function with Frida

Now that we know FUN_140099db0 handles menu actions, we attach to it using Frida:

Interceptor.attach(ptr("0x140099db0"), {
  onEnter(args) {
    for (let i = 0; i <= 10; i++) {
      console.log(`param ${i} ${args[i]}`);
    }
  }
});

After hooking, I clicked on the Ranged combat tutor NPC in-game and got the following output:

param 0 0x1479828c730
param 1 0x1f
param 2 0x25
param 3 0x9
param 4 0x13e
param 5 0xffffffff
param 6 0x2262fff450
param 7 0x147bbef03e0
param 8 0x305
param 9 0x20c
param 10 0x2262fff440

🧩 Step 3: Decoding the Parameters

Here's where the fun begins: figuring out what each parameter means.

I started clicking the same NPC from different angles and noting which values changed:

  • coordinates changed.
  • Screen coordinates changed.
  • The menu index stayed consistent.

I also clicked different NPCs, different actions ("Talk-to", "Claim"), and compared the logs. I slowly matched parameters by trial and error, then cross-referenced the DoAction function in Ghidra to see how those arguments were used.

To help with this, I also asked AI to analyze the function based on the values I saw.

Here's what each argument turned out to be:

Frida param Meaning
0 this pointer (Client instance)
1 Scene X tile where the entity is located
2 Scene Y tile
3 Menu index (slot for "Talk-to" = 9)
4 NPC spawn index (e.g. 318 for the tutor)
5 MinimenuSubject enum (always -1 for basic actions)
6 Pointer to the action string ("Talk-to", "Claim", etc.)
7 Pointer to a world coordinate struct (CoordType)
8 Screen X position of the mouse click
9 Screen Y position
10 Pointer to shared_ptr of the target

🛠️ Step 4: Calling the Function Ourselves

After getting all parameters mapped, I wrote a Frida script to invoke the function. At first, it didn't work — the handler was called, but nothing happened in-game.

Turned out: I had swapped the enum and the string pointer. Classic.

Here's the fixed version that worked:

const DoAction = new NativeFunction(ptr("0x140099db0"), 'void', [
  'pointer', // this
  'int',     // sceneX
  'int',     // sceneY
  'int',     // menuIndex
  'int',     // npc Index
  'int',     // subject (always -1)
  'pointer', // pointer to action string
  'pointer', // worldCoordPtr
  'int',     // screenX
  'int',     // screenY
  'pointer'  // worldview
]);

DoAction( ptr("0x1479828c730"), // Client this 50, 55, // Scene X, Y 9, // Menu index ("Talk-to") 318, // NPC index -1, // MinimenuSubject ptr("0x2262fff450"), // Pointer to "Talk-to" ptr("0x147bbef03e0"), // CoordType pointer 773, 524, // Screen X, Y ptr("0x2262fff440") // worldView );

Boom — it worked. My character started walking to the NPC and talking just like a normal in-game click.

✅ Conclusion

What started with a basic string search for "Walk here" turned into a full reverse engineering process:

  • Searching for references in Ghidra
  • Hooking the native function with Frida
  • Logging and analyzing parameters through repeated interaction
  • Decompiling and verifying with AI
  • Finally, successfully invoking it

With this in place, you can now simulate any in-game menu action like "Talk-to", "Claim", "Examine", and more.