Intro

So it has been a while, i didn’t really have the time to finish this follow up post and after some weeks i just forgot. So here we are now. In the meantime i made a little modloader which can, who would have thought, load mods which allow the use of mixins. Luckily there are many open-source project like FabricMC for Minecraft which gave me a great introduction to how java classloaders work and how modding java games works in general. It also makes hooking functions and changing game behavior extremely easy. Because of this and the fact that PokeMMO uses illegal names as part of the obfuscation, which may break bytebuddy, i don’t really like my old approach anymore. But because this really helped me reverse engineering the network protocol and for completeness sake we gonna finish this here and today.

We will continue working on our old project. You may need to redo the early steps. Extracting the PokeMMO.jar and including it into your project since the game got some updates.

Receiving data with NIO

We already talked about how PokeMMO uses NIO to send and receive data. Last time we used SocketChannel.write(Ljava/nio/ByteBuffer;)I to find where the game sends data. This time we will use SocketChannel.read(Ljava/nio/ByteBuffer;)I to find where the game receives data. Again, searching for references to this method we find this method.

public final void Hr(SelectionKey selectionKey) {
  boolean isValid = true;
  SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
  server server = (server) selectionKey.attachment();
  ByteBuffer byteBuffer = server.KB;
  ByteBuffer byteBuffer2 = server.T4;
  try {
    // read data from socketChannel
    int read = socketChannel.read(byteBuffer);
    
    // disconnect server if we didnt read any data
    if (read == -1 || read == 0) {
      Ut0(server);
      return;
    } 

    byteBuffer.flip();
    if (!server.ea0) {
      server.ea0 = true;
    }

    // Loop over data until all packets are read
    while (true) {
      // Packets have a leading short (2 byte) value with the total size of the packet
      // If we dont have enough data left or we couldn't fit enough data into our buffer then break
      if (byteBuffer.remaining() <= 2 || byteBuffer.remaining() < byteBuffer.getShort(byteBuffer.position())) {
        break;
      }

      try {
        short packetSize = byteBuffer.getShort();
        if (packetSize > 1) {
          packetSize = (short) (packetSize - 2);
        }
        // is our packet complete?
        if (packetSize < 1) {
          server.Cj0.warn("Invalid packet size from client : " + server + " packet size: " + packetSize + " real size:" + byteBuffer.remaining());
          isValid = false;
        } else {
          byteBuffer2.limit(byteBuffer.position() + packetSize).position(byteBuffer.position());
          byteBuffer.position(byteBuffer.position() + packetSize);
          // let the LoginServer/GameServer/ChatServer handle the data
          isValid = server.cv0(byteBuffer2);
        }

      // shortened...
      } catch (Exception e) {
        server.logger.warn("Invalid packet size from client : " + server + " packet size: " + packetSize + " real size:" + byteBuffer.remaining());
        isValid = false;
      }

      // disconnect server if the packet isn't valid
      if (!isValid) {
        Ut0(server);
        break;
      }
    }

    // is data remaining?
    // if yes then we got only one part of the next packet and need to compact the buffer for the next socket read. 
    // else clear the buffer
    if (byteBuffer.hasRemaining()) {
      server.KB.compact();
    } else {
      byteBuffer.clear();
    }

  // disconnect server if there was a error
  } catch (IOException unused) {
    Ut0(server);
  }
}

So we could just hook the cv0 method and print out the arguments out. Sadly it’s not that easy. We already know that the client and server communication is encrypted. So we need to follow the bytebuffer and check where it is decrypted.

Looking at the first occurrence of cv0 we will find an abstract declaration of the method public abstract boolean cv0(ByteBuffer var1);. Let’s look at one of the implementations of this method.

@Override
public final boolean cv0(ByteBuffer byteBuffer) {
    if (this.w60 == 1) {
        return super.cv0(byteBuffer);
    }
    if (!ax(byteBuffer)) {
        rb();
        return false;
    }
    Runnable packet = zz.Tl0(byteBuffer, this, false);
    if (packet == null || !packet.sw0()) {
        return true;
    }
    try {
        packet.Wx();
    } catch (Throwable th) {
        Bb.this.warn(packet.toString(), th);
    }
    NuL.Xp.gg(packet);
    return true;
}

If you want take a look around and try to figure out what some of these method calls do, by yourself. If not we gonna go over it together.

if (this.w60 == 1) {
    return super.cv0(byteBuffer);
}

First of all we check the current stage of the server. 1 means we have not exchanged public keys between the server and the client and set up the encryption/decryption functions. So the game passes the data of to the super class which handles the all packets related to the initial handshake between server and client.

if (!ax(byteBuffer)) {
    rb();
    return false;
}

We already finished the handshake and now need to decrypt the data. ax is implemented by the super class and decrypts the content of the byteBuffer and checks if the appended hash matches. If not it will fail and disconnect.

// use a PacketFactory to find the matching packet class 
Runnable packet = zz.Tl0(byteBuffer, this, false);
if (packet == null || !packet.sw0()) {
    return true;
}
// decode packet
try {
    packet.Wx();
} catch (Throwable th) {
    Bb.this.warn(packet.toString(), th);
}
// queue packet to execute in a separate thread
NuL.Xp.gg(packet);
return true;

The rest is straight forward. We create a packet instance from the packet id, decode the packet and queue it to be executed in a separate thread. Lets take a look at some packets we will find that it every packet inherits from the Runnable interface so it can easily be run in a new thread. The run method will be implemented like this:

@Override
public final void run() {
    try {
        this.HA0();
    }
    catch (Throwable throwable) {
        this.warn(this.toString(), throwable);
    }
}

The run method is really simple and just a wrapper around the HA0() method. We can hook this method to log packets later. The only problem here is that we can’t see from which server the packet came.

Implementing our Hook

In the last post we used rebasing to place hooks in our target classes. I looked somemore into ByteBuddy and it seems that just attaching the ByteBuddy-Agent and using this to transform classes is just easier and gives us a little bit more freedom on what we can do and what not.

The code alone is pretty simple and straight forward. The following code just hooks received packets but you can find the full source here.

import com.pokeemu.client.Client
import net.bytebuddy.ByteBuddy
import net.bytebuddy.agent.ByteBuddyAgent
import net.bytebuddy.agent.builder.AgentBuilder
import net.bytebuddy.asm.Advice
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy
import net.bytebuddy.matcher.ElementMatchers

val HANDLE_PACKET_METHOD = "HA0()V" // every incoming packet inherits a handle method that is called in a new thread after the packet got decoded

val POKEMMO_PACKAGE = "f." // the obfuscated package name of all PokeMMO classes

object RecvInterceptor {
    @JvmStatic
    @Advice.OnMethodEnter
    fun intercept(@Advice.This packet: Any) {
        // Do what ever you want with it...
    }
}

fun initHooks() {
    ByteBuddyAgent.install()

    val methodName = HANDLE_PACKET_METHOD.substringBefore('(')
    AgentBuilder.Default()
        .with(AgentBuilder.InitializationStrategy.SelfInjection.Eager())
        .type(ElementMatchers.nameStartsWith(POKEMMO_PACKAGE))
        .transform { builder, _, _, _, _ ->
            builder.method(ElementMatchers.named(methodName))
                .intercept(Advice.to(RecvInterceptor::class.java))
        }
        .installOnByteBuddyAgent()
}

fun main() {
    initHooks()
    Client.main(emptyArray())
}

The End

Thanks for reading. I hope this will help someone and give you a little intro into reversing games in java. Looking at actual data being send between the server and the client helped me making a headless client for PokeMMO. It is currently running in the background of PokeMMOHub and scraped item prices from the GTL.