Post

Behind the curtain: Rise

Intro

As a developer, I love being challenged whether it’s in improving my own work, or poking around with other people’s. Recently something has Risen to the occasion (good joke right?). Rise, its a well-known client in the 1.8 hacking community, having created bypasses for servers like Hypixel, Cubecraft, BlocksMC, etc. With each update, it consistently outwits AntiCheat developers by discovering new ways to circumvent the patches that have been applied specially targeted at them. I set my target on this client a few months back but only recently have I gotten around to cracking it. Without any further ado let’s get into it!

Analysis

You know, it might not be everyone’s go-to move, but when I’m poking about where I’m not supposed to, I usually start things off by peeking into the DNS records of the main domain or host.

DomainProviderLocation
riseclient.comCLOUDFLARENETUnited States
auth.riseclient.comCLOUDFLARENETUnited States
testauth.riseclient.comCLOUDFLARENETUnited States

Based on the information from the DNS records, we can see the website and some authentication servers. The auth and testauth servers are likely used to verify client access and are protected by Cloudflare’s services against DDoS attacks. However, for our objective, the DDoS protection isn’t particularly relevant. We can also make an assumption that we are going to be looking for something along the lines of auth.riseclient.com in the code of the client. Lets take a deeper look at the client using a tool like recaf.

When you first open RiseCompressed.jar in recaf there are ALOT of possible packages but with my incredible knowledge I know that alan uses the 2 paths com.alan.clients and hackclient. Cross-referencing the obfuscated classes with a previous leak of Rise 6 we can determine that the “main” class is hackclient.rise.Quizzical. This class is where the client is initialized and modules and such are registered.

hackclient/rise/Quizzical.class

There isn’t alot of useful information in this class regarding the authentication process, so we will have to look elsewhere. We are going to look at the other packages for clues to see if we can find any usages of sockets, http requests or websockets. In the jar we can see a package called org.glassfish.tyrus which is a Java API for WebSockets. This is a good sign as it means the client is likely using websockets to communicate with the authentication server. Lets use this to our advantage and see if we can find any usages of tyrus in rise.

org/glassfish/tyrus

Bingo! We have found a class called adc which is using the tyrus package. This class is likely the one that is handling the websocket connection to the authentication server. Let’s look at the bytecode of the connect method.

hackclient/rise/adc

As you might be able to see the control flow graph is quite large and complex. This is a good sign that the code is obfuscated. I’ll do the hard work for you and analyze the bytecode to show you only the important parts. The bytecode below shows that they are retrieving a string from hackclient/rise/acy:esc and then creating a new URI object with the string. This is likely the URI of the authentication server. (auth.riseclient.com) It invokes the connectToServer method of the tyrus ClientManager with the URI initiating the websocket connection.

hackclient/rise/adc

Now if we want to check our hypothesis we can create a simple java agent to intercept the URI object and print it to the console. This will allow us to see the URI of the authentication server and confirm that we are right. Using the agent we can look through each classes’ fields and methods to find the connectToServer method and then patch it to print the URI to the console.

If you look at this code snippet you can see that we are first getting our System.out field and then getting the third method parameter (the URI) and then printing it to the console.

1
2
3
4
5
6
7
InsnList insnList = new InsnList();

insnList.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
insnList.add(new VarInsnNode(ALOAD, 3));
insnList.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false));
                        
methodNode.instructions.insertBefore(methodNode.instructions.getFirst(), insnList);

And running the agent with the client we can see the URI printed to the console.

1
wss://auth.riseclient.com:(redacted)

This confirms 2 things. Firstly, the client is using websockets to communicate with the authentication server and secondly, the URI string is being retrieved from hackclient/rise/acy:esc. This is a good start, and using this information and our previous knowledge of the connect method we could create a transformer to intercept and modify the string before it is used to create the URI. This will allow us to specify our own server to authenticate with, and we can then reverse engineer the authentication process.

And here is that put into practice. We are iterating through the instructions of the connect method and checking if the instruction is a GETSTATIC instruction and if the field owner is hackclient/rise/acy and the field name is esc. If it is we are replacing the GETSTATIC instruction with a LDC instruction with the string ws://localhost:8080/ and then removing the GETSTATIC instruction. Long story short, we are replacing the URI string with our own.

1
2
3
4
5
6
7
8
9
10
11
12
13
for (AbstractInsnNode insnNode : methodNode.instructions.toArray()) {
    if (insnNode.getOpcode() == GETSTATIC) {
        FieldInsnNode fieldInsnNode = (FieldInsnNode) insnNode;
        // getstatic hackclient/rise/acy.esc Ljava/lang/String;
        if (fieldInsnNode.owner.equals("hackclient/rise/acy") && fieldInsnNode.name.equals("esc")) {
            System.out.println("[hackclient/rise/adc] found esc");
            InsnList insnList = new InsnList();
            insnList.add(new LdcInsnNode("ws://localhost:8080/"));
            methodNode.instructions.insert(insnNode, insnList);
            methodNode.instructions.remove(insnNode);
        }
    }
}

Now we have patched the client to connect to our own server we can create a simple websocket to handle the websocket connection and return whatever is needed to authenticate with the client. When attempting to login to the client it sends a packet to the server with the username and hwid. That packet looks like this:

1
2
3
4
5
6
7
{
  "a": "ThnksCJ",
  "b": "fc2d06ad92d60dca572f6b0b7134bf8491453deff5419bc36522cd590a942a6a",
  "c": 0,
  "d": "",
  "id": 1
}

The a field is the username, the b field is the hwid and the id field is the packet id, I’m not sure what the c and d fields are for but im sure we can ignore them. Politely asking a friend to run our agent and dump the packets send to and from the client we can see that the server sends a packet back which goes like this:

1
2
3
4
5
6
7
{
  "a": true,
  "b": 1,
  "c": 1,
  "d": 1708291615383,
  "id": 1
}

The a field is the authentication status, (true for success, false for failure) and d is the current time since the epoch in milliseconds. The b and c fields are unknown, but we can ignore them as in my tests they could be anything and the client would still authenticate. (In the dumped packet b was PI and c was 90).

There are a few more things we need to do before we can start creating our own authentication server. We need reverse the encryption of the packets, with the power of time travel I already created methods to encrypt and decrypt the packets. I did this by looking through the relevant classes and virtualizing them till I got a key.

1
2
3
4
5
6
7
8
byte[] bytes = new byte[]{
    -29, -127, -75, -29, -127, -75, -29, -127,
    -127, -29, -127, -75, -29, -127, -125, -29,
    -127, -75, -29, -127, -85, -29, -127, -117,
    -29, -126, -109, -29, -127, -124, -29, -127,
    -117, -29, -127, -104, -29, -127, -114, -29,
    -127, -106
}; // magic bytes from rise xd

The bytes array is the key used to encrypt and decrypt the packets using xor. You can undo the encryption from the client by xor’ing the bytes and some other stuff throw it all together and you get this:

1
2
3
4
5
6
7
8
9
10
11
12
final byte[] decodedBytes = Base64.getDecoder().decode(packet);
final int length = decodedBytes.length;
final byte[] resultBytes = new byte[length];

int index = 0;
for (byte decodedByte : decodedBytes) {
    byte xorByte = (byte) (decodedByte ^ bytes[index % bytes.length]);
    resultBytes[index] = xorByte;
    index++;
}

return new String(resultBytes, StandardCharsets.UTF_8);

Which takes in the encrypted packet and returns a string of the decrypted counterpart. Using a similar method of poking around and virtualizing classes I found the method to encrypt the packets. However, its a bit lengthy and I don’t want to bore you with the details.

Now we have all the parts we need to create our own authentication server. Encrypting and decrypting the packets, handling the login sequence and returning the correct response we are ready to roll. I created a simple server using java.net.ServerSocket and made a hacky way to handle the upgrade request and the websocket handshake.

Hey presto, we have successfully authenticated with the client and can use it to our hearts content. Is what I would say if this was the end of the story. However, the client is not as simple as it seems. It has a few more tricks up its sleeve, and we are not done yet. The client has a few more checks to make sure we are not tampering with it, and we need dump and replicate a few more packets to get past them.

Second Layer of Checks

To help with the next part of the article I created a transformer to dump the packets that where sent from the server to the client.

1
2
3
4
5
6
7
8
9
10
11
12
13
if (methodNode.name.equals("gU")) {
    System.out.println("[hackclient/rise/adc] found gU");

    InsnList insnList = new InsnList();

    insnList.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
    insnList.add(new VarInsnNode(ALOAD, 1));
    insnList.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false));

    methodNode.instructions.insertBefore(methodNode.instructions.getFirst(), insnList);

    System.out.println("[hackclient/rise/adc] injected print");
}

This does a similar thing to when we dumped the connection URI, get the method parameter and print them to the console. There are more checks that the client does to make sure we are connected to their server which attempt to stop us from using the client offline or on a different server.

Sever connection check

When you want to connect to a server the client sends a packet to the server with the server’s IP, port and your username. The server then just sends the packet straight back to the client. This is done to make sure the client is actually connecting to the server it is supposed to be connecting to.

1
2
3
4
5
6
{
  "a": "mc.hypixel.net.",
  "b": 25565,
  "c": "ThnksCJ",
  "id": 3
}

This is very easy to fix as all we have to do is send the packet back to the client.

Target Checks

With modules like killaure, esp and such the client needs to compile a list of targets to use in these modules. All this happens server-side, so I had to analyze the packets from my dump and attempt to replicate what happens. The server receives a packet along these lines and array of items with a being the entity id in each object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "a":[
    {
      "a": 116,
      "b": 1,
      "c": false
    },
    {
      "a": 124, 
      "b": 1,
      "c": false
    }
  ],
  "b": true,
  "c": false,
  "d": false, 
  "e": false,
  "f": 1,
  "id": 8
}

Using this we can create a simple list of entity ids and send it back to the client. The f values seems to be some type of identifier and its sent back to the server in the next packet. I’m not sure what it is for though.

1
2
3
4
5
6
7
8
9
10
11
12
{
  "a": [
    {
      "a": 116
    },
    {
      "a": 124
    }
  ],
  "b": 1,
  "id": 8
}

This then fixes esp, killaura and other modules that use this target list.

Config

The client sends the json config file stored on the user’s computer to the server, the server then heavily modifies it and sends it back. After analyzing the packets I created a method to replicate it, and it works so I left it at that. This fixes the config system and allows you to load and save configs.

Other notable things

While messing around with the client I found a few interesting things.

IRC

The client has an IRC system built in and it uses the auth.riseclient.com server.

1
2
3
4
5
{
  "a": "a",
  "b": "63d0f9bc46ca6bf7ad9572b7",
  "id": 4
}

The client sends this packet to the server and the server sends back a packet with the a field being the message and the b field is a hard-coded value. The server then echos out the packet to the rest of the clients connected to the server.

1
2
3
4
5
6
{
  "a": "7thnkscj",
  "b": 0,
  "c": "a",
  "id": 4
}

padding

A few notes on this, a is the username and the 7 in front of it is padding (LOL) without that the ingame chat renderer cuts off the first character. c is the message and b is a kinda of id for which “channel” to send the message to. When changing this value i found that it would show a different prefix in the chat based on the number.

0 = [RISE] 1 = [WEED] 2 = [PRESTIGE] 3 = [Monsoon]

irc

Other packets

The packet id 10 is used for selecting a script from the community tab and the packet id 11 is used to populate the community tab. Id 6 is some kinda of information packet and looks like this:

1
2
3
4
5
6
7
8
{
  "a": [
    "ipv8address", 
    "PrismarineJS", 
    "SyncClient"
  ],
  "id": 6
}

and the server responds with something like:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "a":{
    "Rise":{
      "f": "ipv8address",
      "h": false,
      "g": false,
      "i": false,
      "j": ""
    }
  },
  "id":6
}

I’m not really sure what this is used for since I have limited examples of it in my dump.

There is also a ping packet with id 0 which just goes back and forth between the client and server.

Conclusion

For fun, I added a few more transformers to the client that replace instances of RiseClient.com with github.com/thnkscj and modify the title window to have my name in it. I’ve cracked a few versions of this client now and they don’t seem to be changing much drm wise. I’m not sure if this is because they just don’t care or that they don’t know how to fix it. This has been fun though, sadly I won’t be releasing the full source for my agent or server but you might be able to piece it together from the information I’ve given you. I hope you enjoyed this little write-up and I hope you learned something from it. If you have any questions feel free to ask me on discord thnkscj or email me at me@cjstevenson.com

This post is licensed under CC BY 4.0 by the author.