• We see that you're not registered. Please read this thread and if you want, sign up on the forum.

Tutorial [Ultimate Guide] How to Write a Runescape Injection Bot

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Hello, and welcome to my tutorial on how to write an Injection Bot for Runescape. The other tutorials that I've seen on other forums are pretty bad in my opinion, mainly because they don't explain how crucial aspects of the bot architecture works, leaving it up to the reader to figure out exactly how and why something works in the first place. This is my attempt to explain everything in detail, from how bytecode works, how to manipulate bytecode with the ASM library, to how the classloaders can be used to make the injection work, and why it works. This tutorial will be very detailed, so if you're looking for a quick explanation, you're probably in the wrong place. This tutorial will follow a bottom-up teaching approach, meaning that I'll start talking about the core basics of what you need to know to write your own bot, and then things will get increasingly more advanced until we've got a working prototype of our very own bot. I will assume that you already know Java, or at least the basics. If you don't know Java, you might as well stop reading now unless you like reading gibberish that doesn't make any sense. Anyways, let's get started!

IMPORTANT: If you have any questions or if you need help, make a new thread describing your specific problem. I won't be able to answer questions on Discord unless they are very minor.


Table of Contents

1. How do injection bots work?
2. Bytecode Manipulation, what is it?
3. Creating our own Runescape loader
4. Bytecode Basics
5. ASM Basics

6. Accessor methods & interfaces
7. The ClassLoader Hierarchy
8. Hijacking the canvas and drawing our own stuff
9. Creating our own mouse events and keyboard events
10. Conclusion


How do injection bots work?
I'm sure you already have some idea of what an injection is, and what it does. And, you're correct! An injection bot is a bot that automates gameplay through injecting accessor and mutator methods, along with additional logic, into various classes, and using these methods at runtime to control the behaviour of the client. Have you ever seen those puppets that are controlled by pieces of string? Well, you can think of the client as the puppet, and the injection process as you attaching your own strings to a puppet that's already being controlled by someone else. Here is what the entire injection process looks like:


As you might have guessed already, the thing we are injecting into the class files is called bytecode.


Bytecode Manipulation, what is it?
The Java Virtual Machine, the platform on which Java programs are ran, does not understand Java code. The JVM only understands bytecode. Bytecode for the JVM is like assembly for the processor. Java code must at some point be compiled into bytecode, and only then can it be ran on the JVM. This is what happens when you turn a Java file into a class file. Bytecode manipulation must then be the manipulation of the bytecode in a class file. We accomplish this by either using the Instrumentation API or depending on third-party libraries like ASM, JavaAssist or any other library. For this tutorial, we'll be using the ASM library. Because it is based on the Visitor design pattern and it's quite low-level, it might have a steep learning curve for you. However, if you're already familiar with JVM bytecode and the Visitor design pattern, this will probably be much easier for you. I'll be explaining how everything works nevertheless.

However, before we can get started actually using the ASM library, we need to download the jar file(s) and add them to our project. You can start out by downloading the required jar files.


Links:

When you're done downloading the jar files, open up your IDE of choice and add the jar files to your external libraries folder. If you're using Eclipse, check out this tutorial on how to do it. If you're on Intellij IDEA like me, you can add the jar files to the project by doing the following:

1. Click File -> Project Structure...
2. Click Modules in the sidebar to the left.
3. Click the "Dependencies" tab.
4. Click the plus symbol right beside "Scope", and click "JARs or directories..."
5. Locate the two jar files on your system, and pick both.

Done!


Creating our own Runescape loader
I lied. I said this would be an injection bot, but in truth, it's a hybrid bot. The truth is, we're going to be using the Reflection library to actually load the client inside our own JFrame. Sorry for lying! To make it even worse, I won't be explaining the actual bytecode injection part until we're done loading the client. We absolutely need to do this before we can start changing the bytecode, since we obviously need class files to change in the first place.

Before we can get started with making our loader, we first need to actually download the client. Because I'm a bit paranoid, I'll be using a Runescape Private Server client to demonstrate bytecode injection instead of the real deal. However, the same concepts still apply for the real thing. If you want to try on the real thing, proceed at your own risk!

Click here to download the client.

You might be asking yourself: what is Reflection? Well, we can use bytecode injection and reflection to accomplish the same thing. The only difference is that bytecode injection obviously involves changing the class files, while reflection simply copies them so we can retrieve the data we want. For loading the applet into our own JFrame, reflection is sufficient. But once we start getting into things like overlaying the canvas and stuff, bytecode injection can make our job a whole lot easier. With reflection, you're limited to manipulating the attributes of class files, interfaces, and so on. Bytecode injection is a little faster too.

Are you ready? If so, start by creating a new project in your IDE of choice. Make sure you don't forget about importing the ASM jars to your project. Even though we won't be using them quite yet, it's best to just get it over with.

Alright. Before we start, let me explain just what we're going to be doing exactly. We're going to be creating our own JFrame object, and we'll be adding the applet of our target client into our JFrame object. To illustrate further, let's open up the client in jd-gui, which is a Java decompiler so we can see the Java code of the class files in the Jar file. I always like to do this first because we need a general idea of the architeccture of the program before we can start thinking about ways to manipulate it. This rings true for other programs that aren't java programs.

Okay, so we're greeted with this window.

pic1.png


If we look in the manifest of the JAR file, we can see that the class called "Main" is the main class. How appropriate! Let's take a look, shall we?

Code:
public final class Main
{
  public static void main(String[] args) {
    if (args.length > 1) {
      
      System.out.println("Running local");
      ClientSettings.SERVER_IP = "127.0.0.1";
    } 
    try {
      Game game = new Game();
      Game.nodeID = 10;
      Game.portOff = 0;
      Game.setHighMem();
      Game.isMembers = true;
      Signlink.storeid = 32;
      Signlink.startpriv(InetAddress.getLocalHost());
      game.createClientFrame(503, 765);
    } catch (UnknownHostException e) {
      e.printStackTrace();
    } 
  }
}
Interesting... As we investigate further, we quickly realize that the Game class is where everything happens. The Game class' superclass is RSApplet. Hmm, I think we're getting close. I wrote earlier about how we wanted to add the client's applet to our own JFrame object. Well, take a look at the RSApplet class code:

Code:
public class RSApplet
  extends Applet
  implements Runnable, MouseListener, MouseMotionListener, KeyListener, FocusListener, WindowListener {
This class extends the Applet class. That means we can basically treat this class like an applet. This is great news! To make things easier for us, however, we're going to be mimicking the Main class. We'll be creating the Game object and configuring the Signlink class, all of this with reflection, so we can set the same values that are present in the Main class file. We're going to do almost exactly the same thing as the Main class, with one exception: the last piece of code. We want to avoid calling the createClientFrame method because this creates its own JFrame. But if you take a look in the RSApplet class (where the createClientFrame code actually resides), you can see another interesting method right beside it. The initClientFrame method which does everything the createClientFrame method does, except for actually creating the JFrame object (RSFrame). This is amazing, this is just what we need. Let's get started with the basics of Reflection, so we can start creating our loader!
 
Last edited:

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
PART TWO

Reflection basics

Before we can "reflect" anything, we need to load the classes into memory. To do that, we need to load the jar itself.

Code:
File file = new File("path/to/jarfile.jar");
ClassLoader cl = new URLClassLoader(new URL[] { file.toURI().toURL() });
That's all. We can now start instantiating classes left and right, assuming that the class loader actually managed to locate the jar file. Let's try instantiating the Game class, to get a feel for how Reflection works.

Code:
Class<?> clazz = cl.loadClass("Game");
            Constructor<?> init = clazz.getDeclaredConstructor();
            init.setAccessible(true);
            Applet applet = (Applet) init.newInstance();
This looks interesting. The first thing we do here is reference the class loader instance we made earlier, and through that we could get a Constructor object from the Class object we retrieved from the class loader. We set the Constructor object to accessible, just in case it's not. ;)

The last line there is especially interesting. We create an Applet object, and then we cast the returned object from the newInstance method to an Applet. If you remember earlier, we verified that the RSApplet class actually extends the Applet class. That's why we can cast the Game object to an Applet. The Game class doesn't extend the Applet class directly, but since it extends the RSApplet class, which in turn extends the Applet class, this will work anyways.

How do we get and set fields with Reflection? We do it with Field objects that we can retrieve from the clazz instance we made earlier. Let's start configuring the Game instance to our liking, just like how it's done in the Main class.

Code:
Field field = clazz.getDeclaredField("nodeID");
            field.setAccessible(true);
            field.setInt(applet, 10);
I probably don't need to explain how this code works. It pretty much speaks for itself. We get the field with the name "nodeID", we set it to accessible just in case it's not accessible, and then we set its value. nodeID is an integer, so we use the setInt method.

I challenge you to configure the rest of the class the exact same way the Main class does it, without looking at the complete code that I'll be posting shortly. Can you do it? I believe in you.

If you're too lazy, here it is:
Code:
            Field portOff = clazz.getDeclaredField("portOff");
            portOff.setAccessible(true);
            portOff.setInt(applet, 0);

            Method setHighMem = clazz.getDeclaredMethod("setHighMem");
            setHighMem.setAccessible(true);
            setHighMem.invoke(applet);

            Field isMembers = clazz.getDeclaredField("isMembers");
            isMembers.setAccessible(true);
            isMembers.setBoolean(applet,true);

            Class<?> signLink = cl.loadClass("Signlink");
            Field storeid = signLink.getDeclaredField("storeid");
            storeid.setAccessible(true);
            storeid.setInt(signLink, 32);

            Method startpriv = signLink.getDeclaredMethod("startpriv", InetAddress.class);
            startpriv.setAccessible(true);
            startpriv.invoke(signLink, InetAddress.getLocalHost());

            Method initClientFrame = clazz.getSuperclass().getDeclaredMethod("initClientFrame", int.class, int.class);
            initClientFrame.setAccessible(true);
            int width = 503;
            int height = 765;
            initClientFrame.invoke(applet, width, height);

We see a few new things here too. The Method class just lets us retrieve a method from our specified class. If the method requires a few parameters, we have to specify that, just like how it's done in the code. Take a look at the initClientFrame method we tried to retrieve. First of all, since that method is not in the Game class, but in the superclass of the Game class (AKA the RSApplet class), we have to write
Java:
clazz.getSuperclass()
and then we can get our dear method. The initClientFrame method takes two parameters, two integers that specify the width and height of the applet. So, we supply int.class twice, since there are two integers.

That's pretty much it for the reflection part. Let's create the JFrame.

Java:
JFrame frame = new JFrame("Our bot!");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setMinimumSize(new Dimension(765 + 8, 503 + 28));
            frame.setLocationRelativeTo(null);
            frame.add(applet);
            frame.setVisible(true);
Nothing special here. Make sure you add this code BEFORE you invoke the initClientFrame method through reflection. Try running it, and if everything works perfectly, you should see the client loading. If it's your first time opening the client, you might have to download the game cache. Just let it download completely before you close the client again to continue developing, so you're not prompted with the cache downloading message every single time.


Bytecode Basics
Alright, we've come quite a long way. We're finally getting to the interesting part. If we want to be able to inject bytecode into an application, we first need to understand how JVM bytecode works. And this is no easy feat, but we'll get through it together!

JVM Bytecode works in very much the same way as assembly, except that it's much simpler because the JVM architecture is simpler. The JVM bytecode instruction set consists of one opcode and one or more operands. Each opcode is exactly 1 byte. The opcode simply tells the JVM what to do. For instance, let's add two integers together. Since the JVM architecture is stack-based, the following code will make sense:
Code:
iconst_1
iconst_2
iadd
The iconst_1 and iconst_2 instructions simply push the numbers 1 and 2 onto the stack. The iadd instruction simply pops two integers from the stack, and adds them together. There are many more instructions in the JVM, but we don't have to go into much more detail about those until we absolutely need to. I'll get into more detail about bytecode once we actually start manipulating classes.


ASM Basics
We're finally ready to start manipulating bytecode! However, before we jump into the actual coding, I'll have to explain how the ASM library works.

The ASM library is based upon the Visitor design pattern.
Click this link for a great resource that explains how the Visitor design pattern works, and why it's used. When using the ASM library, you'll be operating on ClassNodes. You'll generate one new ClassNode for every class in the jar. You can make changes to the bytecode of a certain class by first locating its corresponding ClassNode, and then applying changes to that ClassNode with the ASM library.

This would be a good time to start writing some utility methods that we will use for loading the jar file, and saving the modified version later on. Let's create a new class JarHelper which will contain these utility methods. Let's start writing the code for actually loading the classes in the jar file, and creating the corresponding ClassNode objects. We can start by adding a field to our class where we will store the manifest of our target jar when we parse it.

private Manifest manifest;

We should also add a HashMap where we can store the ClassNode objects, and link them to their respective keys which would be the name of the corresponding class.


private Map<String, ClassNode> classes = new HashMap<>();

We should now create a public static method that will parse a jar, and generate our ClassNode objects for us.

Java:
public void parseJar(JarFile file) {
        Objects.requireNonNull(file, "The jar file must not be null!");
        try {
            Enumeration<JarEntry> entries = file.entries();
            manifest = file.getManifest();
            do {
                JarEntry entry = entries.nextElement();

                if (!entry.getName().contains(".class")) continue;

                ClassReader classReader = new ClassReader(file.getInputStream(entry));
                ClassNode classNode = new ClassNode();
                classReader.accept(classNode, ClassReader.SKIP_DEBUG);
                classes.put(entry.getName(), classNode);

            } while (entries.hasMoreElements());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
Alright, I understand if you find some of this confusing. Let's walk through it step by step.

Enumeration<JarEntry> entries = file.entries();

This grabs the file entries of our jar file. We loop through the entries, and we check first if the file entry is a class file or something else.

Java:
JarEntry entry = entries.nextElement();

if (!entry.getName().contains(".class")) continue;
If it's not a class file, we skip this element and proceed on to the next one. Now we get to the confusing part.
 

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Part Three


Java:
ClassReader classReader = new ClassReader(file.getInputStream(entry));

ClassNode classNode = new ClassNode();

classReader.accept(classNode, ClassReader.SKIP_DEBUG);

classes.put(entry.getName(), classNode);

Let's go through this step by step. We create a ClassReader object that basically takes the bytes of our jar file entry which happens to be a class file. We then create a ClassNode object which will correspond to the class file. We call the accept method of the ClassReader object, and passing our ClassNode object in as a parameter. The ClassReader essentially parses the class file, and puts the information into the ClassNode object for us to manipulate later on. Lastly, we put the ClassNode instance into our HashMap class member, and we correlate it with the name of the Class file.

This is it for the actual parsing of our target jar file. The only remaining thing we need to do for this class specifically is to create the method that will save our jar file. It's pretty straightforward.

Java:
public void saveJar() {
    try (JarOutputStream jos = new JarOutputStream(new FileOutputStream("newjar.jar"), manifest)) {

        Collection<ClassNode> classNodes = classes.values();

        List<String> names = new ArrayList<>();

        for (ClassNode node : classNodes) {
            if (names.contains(node.name)) continue;

            JarEntry newEntry = new JarEntry(node.name + ".class");
            jos.putNextEntry(newEntry);

            ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            node.accept(writer);
            jos.write(writer.toByteArray());

            names.add(node.name);
        }

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
This method has slightly more lines of code than the parsing method, but it's not very complicated. Let's go through this method step by step also.

We start out by creating a JarOutputStream instance. We will use this instance to write out the data of our class files, to create our new modified jar file with the modified classes. I added a List of strings that I add to after every loop cycle. At the start of the loop, you can see that I check the name of the current ClassNode against the names in the list. This is simply to prevent any duplicate entries from occurring.

Java:
JarEntry newEntry = new JarEntry(node.name + ".class");
jos.putNextEntry(newEntry);

ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
node.accept(writer);
jos.write(writer.toByteArray());
We create a new JarEntry instance and give it the name of our ClassNode, and we append ".class" at the end because the ".class" part of the name is stripped when we parse the jar early on.

We instantiate a ClassWriter instance, and we give it this weird constant value from the ClassWriter class. If you're interested in what this is, and what it means, just read the text inside the following spoiler tag. The truth is, you don't really need to know what it is, or what it does. You can get by without that kind of knowledge, but I'll explain why anyway.

Certain optimizations have been introduced to the JVM. One of these optimizations is the requirement for class files to specify certain information, the maximum stack size and number of variables used, specifically. This information is required for every method in the class. With this information, the JVM doesn't have to calculate this itself, which saves some time. The ClassWriter.COMPUTE_MAXS is a flag that enables automatic calculation of the maximum stack size and number of local variables in methods.

Next we call the accept method on the ClassNode and we pass in the ClassWriter instance, so we can give its information over to the ClassWriter. We then proceed to write out the bytes of the ClassNode object, which is now stored in the ClassWriter instance, and we can continue on to the start of the loop again. I skipped over a few lines of code in my explanation, but I only did so because I deem those lines of code self-explanatory.

We can now finally start manipulating some bytecode! One last thing: make sure you add a getter for the HashMap class member in the JarHelper class, so we can retrieve it in our main class.

Let's now go on to our Main class, and test to see if the classnodes are actually stored in our classes instance in the JarHelper object.

Java:
JarHelper helper = new JarHelper(new JarFile("path/to/your/jarfile.jar"));
ClassNode clientClass = helper.getClasses().get("Client.class");
if (clientClass != null) System.out.println("The client class is here! "+clientClass.name);
If everything works, the program should output the following:

The client class is here! Client

How cool is that? Now that we've got a ClassNode instance, we can work with it and manipulate its bytecode.

Let's create a new abstract class called ClassTransformer. The idea is that we create a new ClassTransformer class for every class we want to change. Inside the abstract class, let's add an abstract method called transform, the method which will manipulate the ClassNode object passed to it.

Java:
import org.objectweb.asm.tree.ClassNode;

public abstract class ClassTransformer {

    public abstract void transform(ClassNode node);
}
We're now ready to create our first concrete implementation of this class. Since we've already got a ClassNode object in our Main class that represents the Client class of our target jar, it seems appropriate that we'll start by manipulating that class.

Create a new class, call it ClientClassTransformer. Make it extend the ClassTransformer class, and add in the transform method in there. Let's start manipulating the ClassNode object. We'll start with something super easy, which only takes one line of code. What is that exactly? Well, all we're going to do is make the class implement a new interface of our choice. Alright, let's make our target class implement the List interface. From a hacking perspective, this would make no sense. However, I'm simply doing this to demonstrate how we would do this. Let's go!

node.interfaces.add("java/util/List");

Wait, what? That's it? Yes, that's it. Quite anticlimactic, I dare say. This is only the basics though, it'll get more advanced, don't you worry. ;)

Before we move on, let's test and see if this actually works. Go to your Main class, and add these lines of code BEFORE you start instantiating the jar with reflection. It only makes sense to do this before we instantiate the jar because we have to create the modified jar first, so we can load that one in later on with reflection.

Java:
ClassTransformer transformer = new ClientClassTransformer();
transformer.transform(clientClass);
helper.saveJar();
If you run your program now, it should work. Open up your decompiler of choice, and take a look at the Client class file. If you did everything correctly, you should see this:

public final class Client implements List {

It's alive! However, this code really serves no purpose for us, so we can remove it for now. We're not going to be making any changes to the Client class, but we are going to be changing a few other classes. The Game class is of great importance for us because that is where the magic happens. Let's try creating a small method inside the Game class. But first we need to get the ClassNode object that represents our Game class.

ClassNode gameNode = helper.getClasses().get("Game.class");

To be continued in the next part.
 

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Part Four

Let's create the ClassTransformer for our Game class. Let's call it GameClassTransformer. Inside the transform method, we'll start by creating a MethodNode object. This object represents the new method we want to make, hence the word MethodNode.

MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC, "simpleMethod", "()V", null, null);

Wow, it takes quite a few parameters that probably don't make sense to you. Let's go through it.

The first parameter it takes is the access modifier. In our case, we want to make the method public, so we pass in the opcode which represents the public access modifier. Next we have the method name. We'll just call it simpleMethod for now. The next parameter looks really weird. This is the method descriptor. The method descriptor is simply a String that describes the return type of the method, along with the type of the parameters that must be passed into it. "()V" means that it will take no parameters (there is nothing inside the paranthesis), and it will not return any type (aka it's a void method). If we wanted for it to accept a String parameter, we would write:

(Ljava/lang/String;)V

There is a weird L prefix at the start, that was not a typo. You have to specify what kind of variable you're supposed to pass in. Is it an integer, a boolean, or maybe a class file? In our case it's a class file, so we have to put the L prefix in there to tell it that you have to pass in an object reference of type String. At the end, we put a semi-colon. The JVM needs to know where the end of the line is, so it can take the entire name between the L and the semi-colon, and then parse it. Take a look at
this table for all the prefixes that exist.

The next parameter we put as null. This is the method signature of the method we're creating. It's basically just metadata about the the generics of the method, but since we won't be working with generics, we can just ignore this. Since we won't trigger any exceptions either, we can put null in the next parameter. That's all. Next line of code!

methodNode.instructions.add(new InsnNode(Opcodes.RETURN));

We add a new instruction. Since we want to keep the method simple, we're just going to add a return instruction. The interesting thing to take away here is the InsnNode object that we pass in as a parameter. InsnNode stands for instruction node, and there are several types. If we want to add an instruction that calls another method, we would use a MethodInsnNode and pass in the parameters accordingly. If we want to work with fields, we use a FieldInsnNode and do the same there. Next line of code!

Java:
int size = methodNode.instructions.size();
methodNode.visitMaxs(size, size);
methodNode.visitEnd();
If you read my explanation on the ClassWriter.COMPUTE_MAXS constant I wrote about earlier, this might not make sense to you. That constant is supposed to tell the ASM library to calculate the MAXS for us automatically, and yet here we are calculating them ourselves. Well, the truth is, since we specified the COMPUTE_MAXS constant into our ClassWriter instance, it will ignore what we put into the visitMaxs method. You could put 0, 0 in there and it'll work. Heck, you could probably put your dog in there and it'll work.

Last step, we add this new method of ours to our ClassNode object.

node.methods.add(methodNode);

Alright, let's apply our transformer on our Game object in the main class.

Java:
GameClassTransformer gct = new GameClassTransformer();
gct.transform(gameNode);
If you run the program now, and open your new target jar file in a decompiler, and view the Game class file, you should see your new method at the bottom. It's nothing special, but it's something! And we'll only get more and more advanced. Let's move on to more practical things at last.


Accessor methods & interfaces

Here we go. We've come a long way, and we're finally going to do something meaningful. We're going to extract data from the client with bytecode injection. Let's first figure out what we want to extract. If we take a look at the RSApplet class, we can find a few interesting fields. One that caught my attention is the Graphics object. Let's try and create an accessor method for that. Accessor method? Yes, we're going to inject a getter method into the RSApplet class that returns the graphics object to us. Let's do it. Start by creating a new ClassTransformer class called RSAppletClassTransformer, and put this in the transform method.

Java:
MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC, "getGraphicsObject", "()Ljava/awt/Graphics;", null, null);
methodNode.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
methodNode.instructions.add(new FieldInsnNode(Opcodes.GETFIELD, "RSApplet", "graphics", "Ljava/awt/Graphics;"));
methodNode.instructions.add(new InsnNode(Opcodes.ARETURN));
int size = methodNode.instructions.size();
methodNode.visitMaxs(size, size);
methodNode.visitEnd();
node.methods.add(methodNode);
Alright, let's walk through it. We create a MethodNode, we make it public, we call it "getGraphicsObject", we make it take no parameters and return a java.awt.Graphics object which is what we want, we put null in the next two parameters since we aren't dealing with generics and exceptions this time either.

We add in the instruction aload_0, this loads the very first parameter in our method, parameter 0. This is a parameter that's secretly passed to every class method that's not static, and that parameter is simply a reference to itself. It's the "this" object. Since we're going to get an object reference from our instance, we need to reference the instance itself to get it. return this.graphics;

We load the "this" reference by pushing it onto the stack, and then we move on to the next instruction which pops the "this" reference from the stack to be used. The next line of code:

methodNode.instructions.add(new FieldInsnNode(Opcodes.GETFIELD, "RSApplet", "graphics", "Ljava/awt/Graphics;"));

This instruction simply gets a field with the GETFIELD instruction. The field we're getting is a part of the RSApplet class, it's called graphics and its descriptor is Ljava/awt/Graphics;.

We have loaded the field, now we need to return it. We can do that with the ARETURN opcode, which returns an object reference. You know what comes next.

Let's apply this transformer on our ClassNode representing our RSApplet class.

Java:
ClassNode rsApplet = helper.getClasses().get("RSApplet.class");
RSAppletClassTransformer ract = new RSAppletClassTransformer();
ract.transform(rsApplet);
Run the program, and open up the new jar in a decompiler, and take a look at the RSApplet class. If you did everything right, you should see a method at the bottom called getGraphicsObject. Okay, so now what? We have a getter method inside the RSApplet class, but that won't help much. I mean, sure, we could always call that method we made with reflection, but that defeats the whole purpose of what we're doing. Plus it's slow. So, what else can we do? Any ideas? When we initialize the class via reflection when we're loading the client, and we cast the loaded class to an applet, we can't just call the getGraphicsObject method we made on the applet object because the Applet class doesn't actually have a method like that. Ironically, the applet object does have a getGraphics method which does the same thing, but I'm only demonstrating this for educational purposes.

However, there is something we can do, a trick if you will. If we make the RSApplet class implement an interface we wrote, and that interface contains a method called getGraphicsObject, we can then cast our applet to that interface, and we can then call that method on the interface, and it'll work. This sounds way more complicated than it actually is, so I'll demonstrate it for you real quick.

Create an interface, call it RSAppletInterface, and inside that interface, add this method:

public Graphics getGraphicsObject();

Now, go back to your RSAppletClassTransformer class, and add this line of code at the start of the transform method:

node.interfaces.add("RSAppletInterface");

This method doesn't require that we add the L prefix behind the class name, and the semi-colon at the end. Alright, we've now made the RSApplet class implement an interface called RSAppletInterface. In your Main class, you can now cast the applet object (that you get from using the Reflection library) to an RSAppletInterface object. Theoretically, if you were to now call the getGraphicsObject method on the RSAppletInterface object, it would give you the Graphics object. But you might be thinking to yourself: how can this be? There is no interface called RSAppletInterface in our target jar file, so how can our RSApplet class implement an interface that doesn't even exist?

Well, the short answer is that due to how the classloaders work, if they don't find the class or interface in the target jar, it looks elsewhere to try and find it, including your code. I'll be dedicating the next section to explaining how the class loader hierarchy works, but if you couldn't care less, just skip that section as it's not really necessary to know about this with regards to writing your own bot.
 

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Part Five - The ClassLoader Hierarchy
Class loaders follow the parent-delegation model when it's loading a class. This means that when a class loader wants to load a class, it first asks its parent class loader to look for the class, and it only looks for the class itself if the parent class loader cannot find the class. But before that parent class loader can try loading the class, that loader has to ask its parent class loader to load the class first. This request keeps going up the chain until it arrives at the top class loader that has no parent. If this top class loader doesn't find the class, it delegates the request to the loader below it in the hierarchy. If that loader doesn't find it, it delegates the request to the loader below it again in the hierarchy. This continues until a class loader finds the class, or if none of them find the class, which would throw a ClassNotFoundException.

There are always at least three class loaders present, even in a super simple Java program. At the top of the hierarchy, you've got the Bootstrap Classloader. This classloader looks for platform classes, and classes in a jar file called rt.jar. A cool trick you can do with the Bootstrap loader is supplying the JVM with a -Xbootclasspath VM argument, which gives the Bootstrap classloader additional directories to search for classes in. This can actually be used to your advantage when writing a Runescape bot, or any bot for that matter. I'll leave it to you to figure out how to abuse this VM argument to your advantage. ;)

The classloader below the Bootstrap classloader in the classloader hierarchy is called the Extension Classloader. This classloader looks for the class file in your third-party libraries among a few other places. If this classloader doesn't find the class either, it delegates it further down the hierarchy to the System Classloader. This is the classloader that loads your application's main method. It looks for the classes in the CLASSPATH, in other words, your project's directory. This is where the magic happens when it comes to loading the interface that we made the RSApplet class implement earlier.

If the System Classloader doesn't find the class, it's delegated to the Classloader under it in the hierarchy, our user-defined classloader. So, you see, only if all the other classloaders fail will our user-defined classloader actually attempt to look for the class. This also means that if any of the other classloaders higher up in the hierarchy actually does find the class, our user-defined classloader simply won't bother looking for it. So, even if our target jar actually had an interface named RSAppletInterface, it still wouldn't find it because our System classloader will find a class with the same name in our project's directory. This is why our program will work, even if there is no RSAppletInterface in the target jar file.


Hijacking the canvas and drawing our own stuff
You have all the power in the world now. Once you hijack the canvas, you can draw whatever you want on the screen. In fact, I've taken the liberty to do just that...

amazingcreation.png


Isn't it beautiful? I'll teach you how to hijack the canvas too.

Before we start, let me just remind you to change the path of your loader to the path of the modified jar instead of the old one.

Alright, it's going to get a little complicated here, but bear with me! Before I start explaining what I did to hijack the canvas, let's take a closer look at the RSApplet class. It's quite apparent that most of the magic happens in the Game class, but everything seems to start with the RSApplet class, including getting the Graphics object that the client will use to draw.

In the initClientFrame method where things get initialized, we can see that the graphics object is instantiated with a call to getGameComponent and getGraphics.

this.graphics = this.getGameComponent().getGraphics();

Since the RSFrame object is null, the getGameComponent method will return this, which means that the instantiation of the Graphics object is actually this:

this.graphics = this.getGraphics();

Since there is no getGraphics method in the RSApplet class, that means that this call will go to the super class. The super class happens to be the Applet class, but what if we changed the super class to one of our own classes? We can basically create a class that acts as an intermediary between the RSApplet class and the Applet class which is the RSApplet's original superclass. If we did that, we would then have control over what happens in the getGraphics method, since that method will be called on our class now. We can then, inside our own getGraphics method, start a new thread that constantly draws on the screen. This way, we can hijack the canvas and draw our own stuff. This is our plan.

Let's start by creating our class that the RSApplet class will extend. Let's call it BotApplet. Make the class extend the Applet class, and create a getGraphics method. The idea here is to create a new thread inside this method, and make that new thread draw stuff. However, the getGraphics method actually gets called twice, and we don't want to create two threads that will be drawing the same thing. So, we'll have to implement some sort of checking mechanism to see if the getGraphics method has been called before. We can do this by adding a class member called "threadStarted" and setting it to false initially.

private boolean threadStarted = false;

Now, inside the getGraphics method, we can check to see if the threadStarted variable is false or not. If it's false, we start the new thread. Here's an example implementation of the getGraphics method:

Java:
public Graphics getGraphics() {
        Graphics g = super.getGraphics();
        if (!threadStarted) {
            Thread thread = new Thread(new Runnable() {

                @Override
                public void run() {
                    g.setColor(Color.RED);
                    g.drawString("Some string here!", 100, 100);
                }
            });
            
            threadStarted = true;
        }
        
        // We eventually have to return the Graphics object, since that's what the RSApplet class wanted in the first place
        return g;
    }
Alright, our class is done now. The last thing we need to do is make the RSApplet extend the class we just made. This is much more complicated than adding an interface, but I'll walk through it step by step.

If you don't have a RSAppletClassTransformer class by now, create one, and make it extend the ClassTransformer class. The Transform method will look like this:

Java:
@Override
    public void transform(ClassNode node) {
        Objects.requireNonNull(node, "Node must not be null!");
        node.superName = "BotApplet";
        Iterator<MethodNode> mi = node.methods.iterator();
        // Loop through every method node
        while (mi.hasNext()) {
            MethodNode mNode = mi.next();
            if (mNode.name.equals("<init>")) {
                Iterator<AbstractInsnNode> instructs = mNode.instructions.iterator();
                while (instructs.hasNext()) {
                    AbstractInsnNode ain = (AbstractInsnNode) instructs.next();
                    if (ain.getOpcode() == Opcodes.INVOKESPECIAL) {
                        //cast to MethodInsnNode so we can change the "owner" variable to our own
                        MethodInsnNode min = (MethodInsnNode) ain;
                        min.owner = "BotApplet";
                    }
                }
            }
        }
    }
Alright, this is quite a bit of code. Let's walk through it. First we set the name of the superclass to "BotApplet". However, we still have to do one more thing. Every time you instantiate a class by calling its constructor, that class instantiates its own parent class. Even though we've set the name of the super class to that of our BotApplet class, it will still instantiate the Applet class in the constructor because we haven't changed that piece of bytecode yet. So, that's what we're doing next.

We iterate through all of the methods present in the node until we find the constructor. The constructor always has the name "<init>", so it's pretty easy to find. Once we find the constructor, we loop through the instructions of the constructor. We keep looping through the instructions until we find an instruction with the opcode INVOKESPECIAL. This is the JVM bytecode instruction that calls the constructor (<init>) method of the parent class. Once we find this special instruction, we simply change the name of the class that we call the <init> method on by setting the owner field of the instruction to "BotApplet".

If we now go to the Main class, create an instance of RSAppletClassTransformer and apply it to the RSApplet node, we should see a message on the client canvas once it starts up! That's the gist of it.


Creating our own mouse events and keyboard events

This is actually super easy. All we need to do is loop through all of the MouseListeners or KeyboardListeners for the Applet instance in our Main class, and call the appropriate event on all of them. Here's some example code which creates a mouse click event.

Java:
MouseEvent click = new MouseEvent(applet, 0, 0, 0, 400, 305, 1, false);

            for (MouseListener ml : applet.getMouseListeners()) {
                ml.mousePressed(click);
                System.out.println("Event sent! "+click.getX()+", "+click.getY());
            }
That's literally it! Try running the program, and you'll see that it will trigger a mouse click event. Try finding the coordinates on the screen for the login button on the menu, and then add a delay before you send the mouse event. If you do it right, it should automatically take you to the login screen where you can type your username and password. The same concept applies for sending keyboard events. You could easily combine the two to create a bot that logs you in automatically. If you do create that bot, feel free to show off by recording it and posting a video of it here in the programming section on the forums!
CONCLUSION

There's a lot of information to take in here. You'll probably struggle to understand certain concepts just like the rest of us. Just keep re-reading, and googling things until it starts making sense. Experiment, try out new things.

The truth is, most of what we've done in this thread can easily be done with reflection. In fact, you can easily write a Runescape bot with reflection only. But with reflection you're limited to manipulating only the attributes of the class, you can't really add your own logic, or edit the existing logic outside of simply editing the attributes to get the code to accomplish what you want. I'll concede to the fact that I did everything the hard way, but that was entirely on purpose. I chose to do things that way to try and teach concepts that I might've not been able to teach if I'd done things in an easier way.

If you have any questions or if you need any help with anything, please leave a reply down below, and I'll try and get to you as fast as possible! I hope you learned something from this, and if you really found it helpful, please share it with your friends and whoever you think might also find it helpful! Thanks for reading. :)
 

Rat

New member
Joined
Apr 14, 2020
Posts
1
Points
3
Reaction score
2
Quality Posts
Very informative post! Do u happen to have a reference project that i can download? I kinda lost track when the bytecode stuff started.
Trying to make this work on a local RSPS. I love writing bot scripts for runescape but dont want to ruin the real game with it

Thanks in advance!
 

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Very informative post! Do u happen to have a reference project that i can download? I kinda lost track when the bytecode stuff started.
Trying to make this work on a local RSPS. I love writing bot scripts for runescape but dont want to ruin the real game with it

Thanks in advance!
Hey, welcome to the forum mate! I can't find the project unfortunately, which really sucks. I'll try and look through my old laptop for the project files, but I can't promise anything. What bytecode stuff are you struggling with the most? If you're struggling to understand specific parts, feel free to create a thread asking for help, and I'll get back to you ASAP. I'm kind of an expert in that area.
 

abdul

New member
Joined
Apr 27, 2020
Posts
20
Points
3
Reaction score
6
Quality Posts
Hey so I came to the point where I call the jar helper file from the main class
Java:
 JarHelper helper = new JarHelper(new JarFile("client.jar"));
But I'm getting errors, I put logging to see where its happening * this is from the JarHelper file in the parseJar function*
Java:
 JarEntry entry = entries.nextElement();
System.out.println(entry); // my own logging I added
if (!entry.getName().contains(".class")) continue;
ClassReader classReader = new ClassReader(file.getInputStream(entry));

the error is happening at ClassReader classReader = new ClassReader(file.getInputStream(entry));

So I run the code and it prints out a bunch of entry but here is last 5 and the error
1587949781604.png


If I comment out ClassReader classReader = new ClassReader(file.getInputStream(entry));, it prints out the last entry with no errors

.project[/ICODE=java] is the last entry that gets printed which as seen from the screenshot above it was NOT being printed, I'm assuming this is what's causing the error
[ATTACH type="full" width="316px" alt="1587949878712.png"]60[/ATTACH]

Any idea?
 

Attachments

Last edited:

abdul

New member
Joined
Apr 27, 2020
Posts
20
Points
3
Reaction score
6
Quality Posts
I'm assuming what's happening is that when we do JarEntry entry = entries.nextElement(); lets say .project was the last one and it does .nextElement() it acutally is null since .project was the last one

EDIT
I don't think that's the case because looking at my logging .project was the last to be printed, otherwise it would've been NULL
 

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Hey so I came to the point where I call the jar helper file from the main class
Java:
 JarHelper helper = new JarHelper(new JarFile("client.jar"));
But I'm getting errors, I put logging to see where its happening * this is from the JarHelper file in the parseJar function*
Java:
 JarEntry entry = entries.nextElement();
System.out.println(entry); // my own logging I added
if (!entry.getName().contains(".class")) continue;
ClassReader classReader = new ClassReader(file.getInputStream(entry));

the error is happening at ClassReader classReader = new ClassReader(file.getInputStream(entry));

So I run the code and it prints out a bunch of entry but here is last 5 and the error
View attachment 59

If I comment out ClassReader classReader = new ClassReader(file.getInputStream(entry));, it prints out the last entry with no errors

.project[/ICODE=java] is the last entry that gets printed which as seen from the screenshot above it was NOT being printed, I'm assuming this is what's causing the error
[ATTACH type="full" width="316px" alt="1587949878712.png"]60[/ATTACH]

Any idea?
I assume you've done the proper filtering to make sure that you're only passing in class file entries to the ClassReader instance. I've read that this exception can be thrown if there is some incompatibility between the ASM and Java version. What version of ASM are you using? And what Java version?
 

abdul

New member
Joined
Apr 27, 2020
Posts
20
Points
3
Reaction score
6
Quality Posts
I assume you've done the proper filtering to make sure that you're only passing in class file entries to the ClassReader instance. I've read that this exception can be thrown if there is some incompatibility between the ASM and Java version. What version of ASM are you using? And what Java version?

Yes, I copied your code exactly I have this if (!entry.getName().contains(".class")) continue;
My java version is jdk1.8.0_181
My ASM version is 7.2 (which is the version you linked)

I did fix the issue by breaking when it got to the 2nd last entry

Java:
if (entry.getName().contains(".classpath")) break; // <-- added this
if (!entry.getName().contains(".class")) continue;
But this is not an ideal solution
 

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Yes, I copied your code exactly I have this if (!entry.getName().contains(".class")) continue;
My java version is jdk1.8.0_181
My ASM version is 7.2 (which is the version you linked)

I did fix the issue by breaking when it got to the 2nd last entry

Java:
if (entry.getName().contains(".classpath")) break; // <-- added this
if (!entry.getName().contains(".class")) continue;
But this is not an ideal solution
Okay, so it's definitely a code issue. So the error is caused by either the .classpath file or the last file. What happens if you make it skip the .classpath file instead, by replacing break with continue? The ClassReader should only take in actual class files as a parameter anyways, so filtering out the .classpath file is necessary because .classpath is not a class file.
 

abdul

New member
Joined
Apr 27, 2020
Posts
20
Points
3
Reaction score
6
Quality Posts
Okay, so it's definitely a code issue. So the error is caused by either the .classpath file or the last file. What happens if you make it skip the .classpath file instead, by replacing break with continue? The ClassReader should only take in actual class files as a parameter anyways, so filtering out the .classpath file is necessary because .classpath is not a class file.

Sweet! changing to continue worked, and it makes sense now, we're doing .contains(".class") and "classpath" has the word class in it, which in turn would think its a class and continue to the code below.

Thank you so much for the fast replies! Amazing :)
 

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Sweet! changing to continue worked, and it makes sense now, we're doing .contains(".class") and "classpath" has the word class in it, which in turn would think its a class and continue to the code below.

Thank you so much for the fast replies! Amazing :)
Great! And yes, that is true. :)

And no worries, I'm happy to help! If you ever have any more troubles, feel free to make a new thread describing your problem and I'll be there to help!
 

abdul

New member
Joined
Apr 27, 2020
Posts
20
Points
3
Reaction score
6
Quality Posts
Hey so I'm just about to finish this awesome tutorial, was extremely useful information and it was explained very well, only question I have is, how do I take this to the next step and make a "bot".

e.g.
get the npc id's around me and attack them
get the game id's of items on floor/inventory and pick/drop etc.
 

Shenandoah

Access Write Violation
Admin
Legend
Joined
Nov 1, 2019
Posts
93
Points
18
Reaction score
52
Quality Posts
1
Hey so I'm just about to finish this awesome tutorial, was extremely useful information and it was explained very well, only question I have is, how do I take this to the next step and make a "bot".

e.g.
get the npc id's around me and attack them
get the game id's of items on floor/inventory and pick/drop etc.
I'm glad you found it helpful!

As for your question, that's actually really hard to answer. I might get around to making a tutorial on that in the future, but I'm working on some other projects right now so I simply don't have the time.

Applying this knowledge and creating a fully functioning bot will require intricate knowledge on how the client works, which means you will have to spend quite some time deobfuscating and analyzing the code. Luckily, people have already done this so you can just check out the source code for other bot frameworks on Github. Parabot has a pretty decent framework that is also easy to understand. Unfortunately, there is a good chance you'll have to find which variable is which in the code of your target client, which will take some time unless you're very experienced already.
 
Top