ScanningScriptingGame Hacking

1 month ago - 1869 views

Playing on the LiveOverflow Minecraft Hacking Server

The hacking YouTuber LiveOverflow recently started a series called Minecraft: HACKED where he learns Minecraft Hacking and walks the viewer through it. In the series, he has a testing survival server to test out hacks and progress in the game.

He challenged the viewers to try and find the server, and play on it as well. I was one of the people that eventually found the server, and played on it for about a month. During the uptime of the server LiveOverflow also made some hacking challenges in-game for the players to complete, which this post will cover. All in all, it was a really cool learning experience from which I learned a ton of stuff about technical Minecraft, Fabric mods, looking at source code, and much more. And as a bonus, I got to meet some amazing people on this server.

Finding the Server IP

In the "Are Resource Packs Safe?" video, LiveOverflow told the viewers that the server was now open to the public, you only have to find the IP address to connect to. This meant that if you found the server's IP, you could play on it as well.

At the time I didn't quite have the motivation or knowledge to go searching myself, but several other people did. In some later videos, it was revealed that people found the IP by it being leaked in one of the terminals in videos. I was still interested in finding the server, so for the next few videos, I was really alert in looking at terminals, going frame-by-frame to find any IP address. Eventually, this paid off and in the "Minecraft Force-OP Exploit!" video I saw a sign in the background saying "ip of this server" with an IP address below it.

Screenshot of a YouTube leaking the server IP on a sign in the background

At first, I thought it might have been a joke, but when I added it to my Minecraft server list it actually said "LiveOverflow Let's Play". I tried joining, but whenever I did I just got the This server is full! message.

Screenshot of the server in the Minecraft server list saying "LiveOverflow Let's Play"

Joining the Server

I thought many people must have found the IP and tried joining due to the video being out for only an hour. I didn't have a hacked client at the time, so I just made a macro on my mouse to automatically join the server every 5 seconds. Anything faster and I would get the Connection throttled! message. After about 20 minutes of repeatedly trying to connect, I finally got in! But remember I still didn't have a hacked client, and I was stuck in this bedrock box!

Bedrock box where players are spawned on their first join

To escape this box you need some sort of fly/teleport hack, so I searched around online for an existing hacked client. One I came across and ended up using is Meteor Client. It's made specifically for Anarchy Servers, which are servers without rules, and thus allow hacking. This seems perfect because this server also allows hacking.

Using the Flight module I was able to fly up and out of the box to start my journey. I played for a little bit, but when I came back a few days later I couldn't connect to the server anymore!

Note: Meteor Client is made for the newest 1.19.2, but this LiveOverflow server runs on 1.18.2. Luckily we can use a mod like ViaFabric to be able to connect to these servers in different versions. It basically translates the packets to a different version and works surprisingly well.

Finding the new IP

In the few days I was on the server I found a link to a Discord server that talked about this Minecraft server. They were hinting that the IP had changed because of so many people joining via the leak on the sign. This meant I finally had to do some real work and scan for the server myself instead of reading it off of a sign.

I checked on ipinfo.io for information about the IP, and what hosting company it belonged to. I guessed that LiveOverflow only changed the IP of his machine, but stayed with the same hosting provider. It turned out the be in the AS24940 range from Hetzner Online, with an IP range of 176.9.0.0/16. If the new IP is in this same range that would greatly reduce the number of IP addresses needed to be scanned from the whole internet (2**32 = 4294967296) to only this range (2**16 = 65536).

I knew masscan was a tool specifically for scanning TCP ports incredibly fast and on a bunch of different hosts. Minecraft servers run on the 25565/tcp port so we can just go through this whole /16 range and check if this port is open. Then we can try enumerating the Message Of The Day (MOTD) for these found hosts which should contain "LiveOverflow".

As I didn't want to risk getting blocked by my ISP for scanning too quickly, I used a Virtual Private Server (VPS) on Oracle Cloud to do the scanning. The following command scans the whole Hetzner IP range on the Minecraft port and puts the results in a masscan.json file. In a few seconds it scans all 65536 IP addresses and found 185 possible servers:

Shell

$ sudo masscan -p 25565 -oJ masscan.json 176.9.0.0/16 --rate 25000  # Do scan
Starting masscan 1.3.2
Initiating SYN Stealth Scan
Scanning 65536 hosts [1 port/host]
$ jq -r '.[].ip' masscan.json > ips.txt  # Save IPs to a file
$ cat ips.txt | wc -l  # Count IPs
185

This jq command parses the JSON output from masscan and returns the IP address, then we just count the lines. Any of these servers could be the real LiveOverflow server, but I can't be bothered to manually try to join 185 servers. So we'll use another tool to attempt a connection like we see when we add a server to the Minecraft server list. This protocol is called Server List Ping. We don't have to reinvent the wheel here, as this protocol is already heavily documented and implementations exist. I found this gist for example that pings the server for its description, online players, and some more information. I changed the script slightly to make it simpler as I only need the description, and automated it to go through all the IPs in the ips.txt file we made earlier from our masscan.

Python

import struct
import socket
import json

def ping(ip, port=25565):
    def read_var_int():  # https://wiki.vg/VarInt_And_VarLong
        i = 0
        j = 0
        while True:
            k = sock.recv(1)
            if not k:
                return 0
            k = k[0]
            i |= (k & 0x7f) << (j * 7)
            j += 1
            if j > 5:
                raise ValueError('var_int too big')
            if not (k & 0x80):
                return i

    with socket.socket() as sock:
        sock.connect((ip, port))

        host = ip.encode('utf-8')
        data = b''  # wiki.vg/Server_List_Ping
        data += b'\x00'  # packet ID
        data += b'\x04'  # protocol variant
        data += struct.pack('>b', len(host)) + host
        data += struct.pack('>H', port)
        data += b'\x01'  # next state
        data = struct.pack('>b', len(data)) + data
        sock.sendall(data + b'\x01\x00')  # handshake + status ping

        length = read_var_int()  # string length
        sock.recv(1)  # packet type, 0 for pings
        length = read_var_int()  # string length
        data = b''
        while len(data) != length:
            chunk = sock.recv(length - len(data))
            if not chunk:
                raise ValueError('connection aborted')

            data += chunk

        return json.loads(data)

if __name__ == '__main__':
    socket.setdefaulttimeout(1)  # Wait max 1 second

    with open("ips.txt") as f:  # Read from list
        ips = [l.strip() for l in f.readlines()]

    for host in ips:
        try:
            description = ping(host)["description"]["text"]
            print(f"[OK] {host}: {description!r}")
        except Exception as e:  # On socket error
            print(f"[ERR] {host}: {e}")

This gives an [OK] message for online servers and also prints the description (MOTD) so we can run it and look for anything with "LiveOverflow" in it:

Shell

$ python server_list_ping.py > results.txt
$ grep -i 'liveoverflow' results.txt
[OK] 176.9.XXX.XXX: '"LiveOverflow Let\'s Play"'

We found it again! It turns out our guess that LiveOverflow didn't change hosting was correct, it only became a different IP address in the same subnet. The server is now also significantly less full as people are kicked, meaning we can actually get to play a bit on the server.

Note: While this script does the job, it's very slow when scaled to a larger number of hosts. A person named Cleo shared a Multithreaded Rust implementation of the same thing on the Discord server. Because sending these TCP packets to multiple hosts can be done in parallel, as opposed to synchronously, this implementation is a lot faster and can crank out over 500 hosts per second.

Playing on the Server

This part will cover some things I did in-game while playing on the server, not very technical but might still be interesting.

I first got some gear using X-Ray hacks to find Diamonds and Netherite, and then got to enchanting stuff as good as possible using the Enderman Farm in The End. I used this time to learn some hacks in the Meteor Client, as well as using Baritone to automate certain things. It can for example mine automatically for you, dig holes or build things. If you want to be lazy sometimes, I highly recommend checking it out and understanding what it can do for you.

Reaching the World Border

I wanted to make a little mark on the server and decided going to the World Border at 30.000.000 blocks out would be a cool idea. I had seen similar things done on anarchy servers like the infamous 2b2t. On 2b2t there were extremely long tunnels dug to ride an entity all the way to the World Border in The Nether (easier as 1 block in The Nether is 8 blocks in The Overworld, meaning we only need to travel 3.750.000 blocks). But this would take weeks or months to do and I certainly didn't have that much time to spend on it. I also didn't know how long the server would still be around. I decided I needed to think about this a bit more.

One thing that makes it hard on 2b2t is the fact that you cannot go on the bedrock Nether roof. There is a plugin that prevents you from doing so because it's really easy to move quickly on there as there are no obstacles. Another difference is that on 2b2t there is a speed limit where you get set back if you move too quickly. Again this also isn't the case on this server, so we can use this to our advantage.

The idea is that we go on the nether roof, and then just move into one direction as fast as the server can load chunks. One slight problem with this is food. Moving this quickly also consumes a lot of food. It's possible to bypass this by not sending sprint packets to the server, as walking does not lose hunger, or we can use another trick. I remembered that you don't lose hunger when you are riding in a boat! A hack like BoatFly can do the same as normal Flight, but for boats. This allows us to move super fast, and not lose a single bar of hunger in the process.

I .vclip'ed onto the roof of the nether, and got to work. Setting up BoatFly to go as fast as possible, and using AutoWalk to not even be required to press forward myself. The start of this journey went extremely smoothly, I got to 1.000.000 blocks in only a few hours. I thought this would only take about a day in total.

Screenshot of me in The Nether at 1 million blocks on the X axis

In the evening I got to 2.300.000 blocks, almost 2/3rds of the way there already! But here is where it went downhill a bit. For some reason, the server suddenly started slowing down tremendously when I was flying in The Nether. I would have expected this as I was loading in tons of chunks very quickly, but it was fine for the first 2 million blocks. Traveling at the same speed as before, going even 100k blocks would almost bring the server to a stop. This was awful for the other people trying to play the server while I was traveling, as the TPS would drop to below 5 consistently. I'm still not sure what caused this switch, but this caused me to reevaluate my strategy for reaching the World Border. I started testing more different configurations and ended up just moving slower. I found that moving too fast would move you into unloaded chunks, which would create the TPS drops. But if I tinker with the speed so the server can just barely load the chunks before I reach them it was fine.

Now moving about 3 times slower it took some more time, but as I had already gotten most of the way there this wasn't much of a problem. After the 4-day journey in the end I made it to the world border:

A screenshot of me at the World Border on the server, with text made of blocks in the background

My Base

After some time I wanted to make a real base to store items and look nice. It took this time to experiment with Baritone and dug a sizable cave with some lights and chests/utilities. It's nothing too special, but here's the final base:

Screenshot of the inside of my base, and a screenshot of it from the outside

I spent most of my time on the server gearing up, talking to people, and doing challenges. These challenges made the server interesting and kept me playing.

Challenge 1: Only Bots Allowed

As seen in the "The End Of Humans In Minecraft" video, LiveOverflow decided he had enough of humans, and made a plugin that only allowed very precise movement. He decided on a plugin that would kick players if their x and z position wasn't perfectly rounded to 1/100th. The code he ended up with can be seen when you get kicked:

Java

int x = ((int) (position.x*1000)) % 10;
int z = ((int) (position.z*1000)) % 10;
if (x != 0 && z != 0) player.kick();

It simply multiplies your coordinate by 1000, then rounds it to an integer and takes the remainder when dividing by 10. You can only continue playing if this remainder is 0. If we as players want to continue moving, we need to make sure our position is rounded so that this remainder we send will always be 0. We're gonna need to make our own hack now. Let's learn some Fabric modding!

Fabric is a popular modding toolchain that Meteor Client also uses. We can make our own mods for it in Java to try and bypass this check as a real player. I highly recommend using IntelliJ together with the Minecraft Development plugin. This provides you with a template and makes building and checking your code a breeze. If you want to follow along you can just create a new project and select Minecraft -> Fabric Mod. The next few parameters don't matter too much, but the GroupId is basically a domain name in reverse. Something like com.jorianwoltjer, but you don't necessarily need to own the DNS domain. The ArtifactId is the package name of your plugin, often in lowercase, like liveoverflowmod. As we'll be using it together with the Meteor Client, we'll create a 1.19.2 mod so they can load together.

Once you have created the project, it will take some time to make the template and decompile Minecraft for you. This means you can view the source code and look at how exactly Minecraft handles things like sending the position packets. After the decompilation is done you can find almost anything with a search for the class name using Ctrl+N and making sure to select All Places in the top right. If that doesn't find it, you can also use Ctrl+Shift+F to find text in any file, making sure to select Scope and All Places again to search everywhere.

Fabric works using Mixins that alter the source code of Minecraft. To change the behavior of Minecraft like sending position packets in a rounded way, we need to find where Minecraft sends these packets and we need to change that code. I searched for classes with "positionpacket" and found PlayerPositionLookS2CPacket. This looked promising at first, but the S2C means Server to Client, so we as the client are not sending this packet. Next, I searched for "playermove", and found PlayerMoveC2SPacket which is more promising. This is a packet that sends the position from the Client to the Server. Looking at the source code there are a few different constructors, including .PositionAndOnGround(x, y, z, onGround) and .Full(x, y, z, yaw, pitch, onGround) which both contain an x and z we're looking for.

When looking at examples of Mixins I found @ModifyArgs which looked pretty simple:

Java

@ModifyArgs(method = "foo()V", at = @At(value = "INVOKE", target = "La/b/c/Something;doSomething(IDZ)V"))
private void injected(Args args) {
    int a0 = args.get(0);
    double a1 = args.get(1);
    boolean a2 = args.get(2);
    args.set(0, a0 + 3);
    args.set(1, a1 * 2.0D);
    args.set(2, !a2);
}

You can inject this code into a function and change the arguments that get passed to it. Looking at the usages of both PositionAndOnGround class constructors we find that Minecraft indeed uses them in ClientPlayerEntity.java:

Java

private void sendMovementPackets() {
    ...
    if (this.hasVehicle()) {
        Vec3d vec3d = this.getVelocity();
        this.networkHandler.sendPacket(new PlayerMoveC2SPacket.Full(vec3d.x, -999.0, vec3d.z, this.getYaw(), this.getPitch(), this.onGround));
        bl3 = false;
    } else if (bl3 && bl4) {
        this.networkHandler.sendPacket(new PlayerMoveC2SPacket.Full(this.getX(), this.getY(), this.getZ(), this.getYaw(), this.getPitch(), this.onGround));
    } else if (bl3) {
        this.networkHandler.sendPacket(new PlayerMoveC2SPacket.PositionAndOnGround(this.getX(), this.getY(), this.getZ(), this.onGround));
    } else if (bl4) {
        this.networkHandler.sendPacket(new PlayerMoveC2SPacket.LookAndOnGround(this.getYaw(), this.getPitch(), this.onGround));
    } else if (this.lastOnGround != this.onGround) {
        this.networkHandler.sendPacket(new PlayerMoveC2SPacket.OnGroundOnly(this.onGround));
    }
    ...
}

Both the .PositionAndOnGround() and .Full() constructors get used so we would need to change both of them. If we just modify the arguments passed to these constructors we would be sending the packets modified, while for our client it looks like we're moving smoothly.

Making your first Mixin is hard, but when you succeed it clicks and you can make much more. It starts by defining a new class and an @Mixin decorator that defines the class you're altering. Let's start with the .Full() constructor:

Java

@Mixin(PlayerMoveC2SPacket.Full.class)
public abstract class PlayerPositionFullPacketMixin {
    ...
}

Then inside this class, we define the @ModifyArgs on whatever function we want. The <init> method is the constructor we want the change, and we need to provide a full target path with the right function, but IntelliJ will help us out when we press Ctrl+Space for autocompletion inside this string. Here we'll make some simple code that takes the x and z parameters, and passes them to a roundCoordinate() function:

Java

import static com.jorianwoltjer.liveoverflowmod.LiveOverflowMod.roundCoordinate;

@Mixin(PlayerMoveC2SPacket.Full.class)
public abstract class PlayerPositionFullPacketMixin {
    @ModifyArgs(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/c2s/play/PlayerMoveC2SPacket;<init>(DDDFFZZZ)V"))
    private static void init(Args args) {
        args.set(0, roundCoordinate(args.get(0)));  // Round x
        args.set(2, roundCoordinate(args.get(2)));  // Round z
    }
}

This function does not exist yet, so we'll make one. It just needs to round the number to 1/100th. As we'll be using the same function in the .PositionAndOnGround() constructor, we should make this a method outside of this class in the main file for example, LiveOverflowMod.java in my case:

Java

public class LiveOverflowMod implements ModInitializer {
    ...
    public static double roundCoordinate(double n) {
        return Math.round(n * 100) / 100d;  // Round to 1/100th
    }
}

We'll also add the other .PositionAndOnGround() packet right now, only changing the class names:

Java

@Mixin(PlayerMoveC2SPacket.PositionAndOnGround.class)
public class PlayerPositionPacketMixin {
    @ModifyArgs(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/c2s/play/PlayerMoveC2SPacket;<init>(DDDFFZZZ)V"))
    private static void init(Args args) {
        args.set(0, roundCoordinate(args.get(0)));  // Round x
        args.set(2, roundCoordinate(args.get(2)));  // Round z
    }
}

Finally, we need to make sure the Mixins are included in the list so Fabric knows to load them. This happens in the liveoverflowmod.mixins.json file. Just add your class names to the "mixins": [] array:

JSON

{
  ...
  "mixins": [
    "PlayerPositionFullPacketMixin",
    "PlayerPositionPacketMixin",
  ],
  ...
}

The Minecraft Development plugin for IntelliJ allows you to run a Minecraft Client right away from the code you made. In the build configurations, select Minecraft Client and click the green play button. This will launch a Minecraft instance with the mod loaded, but note that this client is in offline mode, meaning you cannot connect to online Minecraft servers. Nevertheless, you can still create a simple server for yourself on localhost, making sure to set online-mode=false in server.properties (or just Single Player for now). Then when you join your test server you can use /tp ~ ~ ~ to let the server teleport you to the location it thinks you are or use /data get entity @s Pos to only get the position and see if it is rounded to the 1/100th. For example:

Minecraft

/data get entity @s Pos
Player has the following entity data: [-15.87d, 118.81324182784323d, 13.53d]

Here we see that the X and Z positions are indeed rounded perfectly! Now we can build this mod for real by selecting "build" in the build configuration instead of Minecraft Client. After the build completes you should have a .jar file in the build/libs directory. Copy this JAR (not the -sources variant) to your .minecraft/mods directory, and launch Minecraft with Fabric. This should now load the mod and will be in online mode so we can join the LiveOverflow server with it.

Joining the server for the first time, the mod works instantly! We can move around all we want without getting kicked by the anti-human plugin.

Floating Point Errors

There is one small problem though, that stumped a lot of people. This simple approach we took will fail on some specific coordinates because of Floating Point being binary and not decimal. You experiment with it on this website. In the example below you can see a coordinate of 128.140 would be stored as 128.13999... and when multiplied by 1000 the remainder when dividing by 10 would become 9 instead of 0.

Screenshot of floating point converter showing 128.14 turning into 128.139

This means that some "cursed coordinates" like the one above will round to the wrong remainder and get you kicked. To fix this, the site already has some useful buttons. The +1 button adds one to the Mantissa, meaning it will increase the float by the smallest possible amount. The number would then become 128.14001 which does have the right remainder and would solve our problem. In Java, we can use the Math.nextAfter() function which does the same as the +1 button on the site, just add one to the mantissa. We still need to make sure it also works with negative numbers, and this Math.nextAfter() function accepts a number and a direction to move in. This direction needs to be the sign of the number (+1 or -1), and we can use Math.signum() to simply get this sign:

Java

public static double roundCoordinate(double n) {
    n = Math.round(n * 100) / 100d;  // Round to 1/100th
    return Math.nextAfter(n, n + Math.signum(n));  // Fix floating point errors
}

When we now join the LiveOverflow server and walk around near coordinates like 128.14, we're fine! No floating point errors anymore.

Challenge 2: Club Mate

After some time on the server, LiveOverflow decided to make an in-game challenge for a special item. In videos, he had shown him holding a bottle of Club Mate using a custom resource pack. Now he told people there was a hidden challenge somewhere, so I started looking.

I looked around the spawn a lot, using X-Ray and Freecam to find anything hidden, but I could not find anything. After about an hour of searching, I gave up and would just make a base. One of the first places I went after joining the server for the first time is 1337x 1337z. I placed a sign there, and now I wanted to build my base there. So off I went and when I got to the coordinates I found the Club Mate challenge completely by chance! Turns out LiveOverflow and I thought alike and both chose 1337x 1337z as a location, so let's look at the challenge.

From some distance I could see some sort of shrine with chests around:

A shrine with a chest inside of the protected area

But whenever I got near it, I got teleported away from it with the following message in the chat:

ERROR: Noob Detected!
Only real hackers can open the chest

Java

@EventHandler
void onPlayerMove(PlayerMoveEvent e) {
    // [... pseudocode ...]
    if (protectedArea(position.x, position.z)) player.teleport();
}

"Only real hackers can open the chest", so we need to somehow open the chest inside of the protected area. The small bit of code shows us exactly how the check works: Whenever a PlayerMoveEvent is fired, it checks the position of that event and teleports the player away if they are inside of the protected area around the shrine. So how would we get close enough to the chest without being able to move in the area?

The protected area also seemed to be quite large, so there is no way we would be able to reach the chest from the outside. Here we need to brainstorm a little bit. Our goal is to get a valid player position close enough to the chest. When we send a PlayerMove packet to the server, it validates some things and sends a PlayerMoveEvent. We want to avoid sending this event so the check is not triggered, but how do we change our position without sending move packets?

One way I thought might work is using Boats, Minecarts, or riding other entities. Maybe this PlayerMoveEvent would not be triggered if the moving entity is not a player. I tried some things but didn't get this working in the end. It kept triggering the PlayerMoveEvent and setting me back. Another idea I had was Ender Pearls. These allow you to teleport to wherever they land when you throw them, so what if we threw one from outside the protected area all the way to the chest?

When you throw an Ender Pearl, the server decides where it lands and then tells the client where they teleported. When the pearl lands, the server also sets your position where it lands so you cannot cancel the teleport. But the crucial part is that the PlayerMoveEvent only gets triggered when a client sends a PlayerMove packet. But in this case with Ender Pearls, the client could just not send any PlayerMove packets and let the server set the position for them. This is the trick!

We can use a hacked client to cancel all PlayerMove packets, a common hack called Blink does the job. Then we can throw an Ender Pearl at the chest to teleport into the area. When the pearl lands the server teleports us to the chest, and we can reach it. While still using Blink, we can simply click on the chest and we get the Club Mate!

Tip: Something that helped me understand the packets is Pakkit, a proxy for Minecraft where you can see exactly what packets are sent and what data they contain, basically Wireshark. Try turning on a hack like Blink, or throwing an Ender Pearl, to see what happens to the packets.

Challenge 3: WorldGuard Bypass

When making the previous Club Mate challenge, there turned out to be a lot of unintended solutions, including the Ender Pearl trick I explained. It was supposed to be a lot harder, so LiveOverflow made another challenge that patched all the unintended solutions by using the WorldGuard plugin to create a protected area. This is a well-known plugin used by lots of other servers, I had even used it before myself.

There was another shrine, this one high above spawn and in the sky. The goal of this challenge was to die in the lava, which is behind two zig-zag walls you need to traverse:

The lava fountain behind two walls with openings inside the WorldGuard area

This new plugin thinks about every way you can move, Ender Pearls, boats, minecarts, everything. This is because the plugin is meant designed to do just this, keep players away from areas and protect blocks inside the area. This challenge is a lot harder and took me multiple days to solve.

We'll get a lot more technical, and look at more source code. This time, the server-side code that handles our move packets. The server runs on Paper which can be seen on F3, so we should look at the Paper source code. It's completely open-source on Github, so this will be easy. It works by using small patches to the Minecraft source code, but applying these patches to get the final code is easy and is explained in Contributing to Paper on their Github. Basically, just clone the Github and run a gradlew command:

Shell

./gradlew applyPatches
cd Paper-Server

This will take a while, but after some time you should get a Paper-Server folder with all the source code in .java files, we don't even need to decompile anything.

Looking at the Source Code

There are a lot of files and functions here, but only a few have to do with our movement. I searched for "PlayerMove" using Ctrl+Shift+F and found src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java. This file contains the PlayerMoveEvent from the plugin we saw in the Club Mate challenge, and this is where the server generates this event from the movement packet you send. There is an interesting handleMovePlayer(ServerboundMovePlayerPacket packet) function in here which does a lot of checks like if the position is NaN, if you're sending too many packets in one tick, or if you're moving too fast. Eventually line 1603 fires a new PlayerMoveEvent event:

Java

public void handleMovePlayer(ServerboundMovePlayerPacket packet) {
    ...
    Player player = this.getCraftPlayer();
    Location from = new Location(player.getWorld(), this.lastPosX, this.lastPosY, this.lastPosZ, this.lastYaw, this.lastPitch); // Get the Players previous Event location.
    Location to = player.getLocation().clone(); // Start off the To location as the Players current location.

    // If the packet contains movement information then we update the To location with the correct XYZ.
    if (packet.hasPos) {
        to.setX(packet.x);
        to.setY(packet.y);
        to.setZ(packet.z);
    }

    // If the packet contains look information then we update the To location with the correct Yaw & Pitch.
    if (packet.hasRot) {
        to.setYaw(packet.yRot);
        to.setPitch(packet.xRot);
    }

    // Prevent 40 event-calls for less than a single pixel of movement >.>
    double delta = Math.pow(this.lastPosX - to.getX(), 2) + Math.pow(this.lastPosY - to.getY(), 2) + Math.pow(this.lastPosZ - to.getZ(), 2);
    float deltaAngle = Math.abs(this.lastYaw - to.getYaw()) + Math.abs(this.lastPitch - to.getPitch());

    if ((delta > 1f / 256 || deltaAngle > 10f) && !this.player.isImmobile()) {
        this.lastPosX = to.getX();
        this.lastPosY = to.getY();
        this.lastPosZ = to.getZ();
        this.lastYaw = to.getYaw();
        this.lastPitch = to.getPitch();

        // Skip the first time we do this
        if (from.getX() != Double.MAX_VALUE) {
            Location oldTo = to.clone();
            PlayerMoveEvent event = new PlayerMoveEvent(player, from, to);  // <-- Creates the PlayerMoveEvent
            this.cserver.getPluginManager().callEvent(event);  // <-- Calls WorldGuard

            // If the event is cancelled we move the player back to their old location.
            if (event.isCancelled()) {
                this.teleport(from);
                return;
            }
            ...
    this.player.absMoveTo(d0, d1, d2, f, f1); // Set the new postion
    ...
    this.lastGoodX = this.player.getX();
    this.lastGoodY = this.player.getY();
    this.lastGoodZ = this.player.getZ();
}

This piece of code is crucial. Any time we send a movement packet, it is called. But here we see the new PlayerMoveEvent is only created if some conditions are true, seen in the if() statements. One comment says "Skip the first time we do this" before the from.getX() != Double.MAX_VALUE check. But this would not be of use to us, because if our previous position was at Double.MAX_VALUE, we would definitely move too fast.

The other check here is (delta > 1f / 256 || deltaAngle > 10f). These deltas come from the lines above, with a comment saying "Prevent 40 event-calls for less than a single pixel of movement". The delta variable is calculated as simply the sum of the differences squared. If this sum is less than 1/256, no event will be triggered. So the smallest distance we can move in one direction without triggering the event is sqrt(1/256) because that is the inverse of the square. This is equal to 0.0625, which isn't a lot but is not that small either. If we could just move very slowly staying under this threshold the entire time, we might be able to move inside the protected area by not triggering any events. Let's try it in-game to see what happens.

Making the Fabric mod

Like we did with the Anti-Human check, we'll make a Fabric mod that does this so we have full control over the packets. We can try to make a keybind that when we press it, moves the player 0.06 blocks into say, the X direction. Then we can try to move into the WorldGuard protected area by repeatedly pressing this keybind. Let's start by making a keybind that does what we want in a Keybinds.java file:

Java

public class Keybinds {
    public static final String LIVEOVERFLOW_CATEGORY = "category.liveoverflowmod";

    private static final KeyBinding testKeybind = new KeyBinding("key.liveoverflowmod.test",
            GLFW.GLFW_KEY_UP, LIVEOVERFLOW_CATEGORY);  // Up arrow key

    public static void registerKeybinds() {  // Make keybinds show up in settings
        KeyBindingHelper.registerKeyBinding(testKeybind);
    }

    public static void checkKeybinds(MinecraftClient client) {
        ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
        if (client.player == null || networkHandler == null) {
            return;
        }
        while (testKeybind.wasPressed()) {  // Check if keybind was pressed
            client.player.setPosition(client.player.getPos().add(0.06, 0, 0));  // Set client side position
            networkHandler.sendPacket(new PlayerMoveC2SPacket.PositionAndOnGround(
                    client.player.getX(), client.player.getY(), client.player.getZ(), client.player.isOnGround())
            );  // Send to server
        }
    }
}

Then we just have to register these registerKeybinds() and checkKeybinds() functions. We do this using a Client Entrypoint, ClientEntrypoint.java for example:

Java

public class ClientEntrypoint implements ClientModInitializer {
    public static final MinecraftClient mc = MinecraftClient.getInstance();

    @Override
    public void onInitializeClient() {
        Keybinds.registerKeybinds();  // Register keybinds
        ClientTickEvents.END_CLIENT_TICK.register(Keybinds::checkKeybinds);  // Register a function to be called every tick
    }
}

Then we finally have to register this class in the fabric.mod.json file:

JSON

{
  ...
  "entrypoints": {
    "main": [
      "com.jorianwoltjer.liveoverflowmod.LiveOverflowMod"
    ],
    "client": [
      "com.jorianwoltjer.liveoverflowmod.client.ClientEntrypoint"
    ]
  },
  ...
}

When we now build and run the mod, we can press the Up Arrow on the keyboard to move into the positive X direction by 0.06 blocks. I quickly set up a test server with WorldGuard and created a region with the entry flag set to DENY, just like on the real server. When a non-opped player now tries to enter the created area, they are sent back.

Trying the mod we just made, it seems we can move one packet into the protected area, but when we send the next packet by moving again or looking around, we are still sent back. It seems our bypass doesn't quite work yet, let's find out why.

If you looked at the Paper source code snippet from above well, you might have noticed that the this.lastPosX = to.getX() lines that set the last position the delta uses, are after the delta check. This means our previous position only gets set if we are above the delta, and then a PlayerMoveEvent triggers which we want to avoid. We need to somehow change this lastPos value because we can't make any progress otherwise. This is the tricky bit of this exploit.

I was trying all kinds of weird things that were possible in the code, until I stumbled upon the "moved too quickly!" warning. Whenever you try to move over 10 blocks in a packet, a warning in the server console is shown and you get teleported back. This is a small anti-cheat built into Minecraft. When we follow this teleport() function, we find internalTeleport which sets the lastPos!

Note: You could also find this code by searching for where this.lastPosX is set, and going backward to find where functions are called from.

Java

public void handleMovePlayer(ServerboundMovePlayerPacket packet) {
    ...
    float f2 = this.player.isFallFlying() ? 300.0F : 100.0F;

    if (d11 - d10 > Math.max(f2, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2)) && !this.isSingleplayerOwner()) {
        ServerGamePacketListenerImpl.LOGGER.warn("{} moved too quickly! {},{},{}", new Object[]{this.player.getName().getString(), d7, d8, d9});
        // VVV player gets teleported and lastPos is set
        this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot());
        return;
    }
    ...
}

public boolean teleport(double d0, double d1, double d2, float f, float f1, Set<ClientboundPlayerPositionPacket.RelativeArgument> set, boolean flag, PlayerTeleportEvent.TeleportCause cause) {
    ...
    this.internalTeleport(d0, d1, d2, f, f1, set, flag);
    ...
}

public void internalTeleport(double d0, double d1, double d2, float f, float f1, Set<ClientboundPlayerPositionPacket.RelativeArgument> set, boolean flag) {
    ...
    this.lastPosX = this.awaitingPositionFromClient.x;
    this.lastPosY = this.awaitingPositionFromClient.y;
    this.lastPosZ = this.awaitingPositionFromClient.z;
    ...
}

So when we send a packet with a position far away, the "moved too quickly!" warning will trigger, teleporting the player and settings the lastPos values. We can use this to fix the problem with our delta, where this position was only updated when also triggering a PlayerMoveEvent. This way we reset the position without triggering any event.

We just need to send another packet with a position that is far away, right after we moved 0.06 blocks. This will reset the last position meaning we can simply repeat it to keep moving in that direction:

Java

public static void checkKeybinds(MinecraftClient client) {
    ...
    while (testKeybind.wasPressed()) {  // Check if keybind was pressed
        client.player.setPosition(client.player.getPos().add(0.06, 0, 0));  // Set client side position
        networkHandler.sendPacket(new PlayerMoveC2SPacket.PositionAndOnGround(
                client.player.getX(), client.player.getY(), client.player.getZ(), client.player.isOnGround())
        );  // Send to server

        // Send a position far away to trigger a teleport back, resetting the position
        networkHandler.sendPacket(new PlayerMoveC2SPacket.PositionAndOnGround(
                client.player.getX() + 1337, client.player.getY() + 1337, client.player.getZ() + 1337, client.player.isOnGround())
        );
    }
}

Changing the code to include this, and running it again we can test if this did something. When we try to move into the protected area with this method it works! We can move perfectly fine into the area protected by WorldGuard and even look around now that our position is reset.

So we found a bypass, perfect! Now we just need to make it usable. We want to move into all 6 cardinal directions, but we can simply do this with 6 different keybinds. We also need to think about falling, because if we would teleport upwards 0.06 blocks, our client still obeys the laws of gravity and moves us back down. This is very annoying when trying to climb stairs or get over gaps like in on the real server. So we need a way to disable gravity.

One simple way is to use the player abilities. When you are in creative mode, you can simply fly around. In the code this is done using the flying and allowFlying abilities, which we as the client can simply modify with the press of a button:

Java

public static void checkKeybinds(MinecraftClient client) {
    ...
    while (toggleFlightKeybind.wasPressed()) {  // Check if keybind was pressed
        client.player.getAbilities().allowFlying = !client.player.getAbilities().allowFlying;  // Allow flying like in creative mode
    }
    client.player.getAbilities().flying = client.player.getAbilities().allowFlying;  // Start flying
}

This now contains everything we need to complete the challenge. We can join the real server and slowly move into the WorldGuard protected area, traversing the shrine while flying and moving in cardinal directions with the keybinds. After a few attempts, I eventually got through it and successfully died in the lava inside the shrine!

3 Discord messages from the point of moving in the area, to completing the challenge on the server and obtaining the HACKER rank

As a cool reward, you got a red <HACKER> rank before your name and a gold name in chat.

Improvements

After completing the challenge I was inspired to read more code and try more things to make my bypass more usable. The first improvement was to be able to move in the direction you're facing, which involved some sin() and cos() math, but wasn't too bad in the end. But I also saw some people in Discord being able to move much faster in the area, causing me to try and find a faster way than moving 0.06 blocks every tick.

I found that WorldGuard only checks if the position is protected when you cross a block boundary. You can move perfectly fine inside a block. This means you would only need to do the 0.06 block movement right as you're crossing the block boundary, and you can move freely otherwise. I made this improvement where it teleports as far as possible until it crosses a block boundary. On the real server, this occasionally fails but it is way faster than the previous method. You can see it in action in the following video recoding:

Conclusion

The first implementation of my Fabric mod was a mess, but after I cleaned it up I made it open source on Github. There you can read all the code and try different things:
https://github.com/JorianWoltjer/LiveOverflowMod

This whole journey was a really cool way to get into game hacking. I had not done any modding or game hacking before this, but with the supporting community in the Discord server, there were a lot of nice people helping out. In the end, I developed a whole new interest in game hacking, which I will likely also explore in other games.