From ed71cdc55a553b1cc24c994dd65d72ddc186b329 Mon Sep 17 00:00:00 2001 From: ChomeNS <95471003+ChomeNS@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:57:46 +0700 Subject: [PATCH] feat: prevent replay attacks on chomens mod for those kind of server owners and aren't very nice and also the kind of people that spies on cores (like me !!!) --- build-number.txt | 2 +- .../data/chomeNSMod/PayloadMetadata.java | 40 ++++++++++++++ .../plugins/ChomeNSModIntegrationPlugin.java | 52 ++++++++++++++++--- 3 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/main/java/me/chayapak1/chomens_bot/data/chomeNSMod/PayloadMetadata.java diff --git a/build-number.txt b/build-number.txt index 018e3be6..e73d010f 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -2502 \ No newline at end of file +2513 \ No newline at end of file diff --git a/src/main/java/me/chayapak1/chomens_bot/data/chomeNSMod/PayloadMetadata.java b/src/main/java/me/chayapak1/chomens_bot/data/chomeNSMod/PayloadMetadata.java new file mode 100644 index 00000000..2f1eae06 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/data/chomeNSMod/PayloadMetadata.java @@ -0,0 +1,40 @@ +package me.chayapak1.chomens_bot.data.chomeNSMod; + +import io.netty.buffer.ByteBuf; + +import java.util.Arrays; + +public record PayloadMetadata(byte[] nonce, long timestamp) { + public static PayloadMetadata deserialize (ByteBuf buf) { + final byte[] nonce = new byte[8]; + + buf.readBytes(nonce); + + final long timestamp = buf.readLong(); + + return new PayloadMetadata(nonce, timestamp); + } + + public void serialize (ByteBuf buf) { + buf.writeBytes(nonce); + buf.writeLong(timestamp); + } + + @Override + public boolean equals (final Object object) { + if (object == null || getClass() != object.getClass()) return false; + + final PayloadMetadata metadata = (PayloadMetadata) object; + + // java is so fucky about byte[]......... i have to use Arrays.equals() + return timestamp == metadata.timestamp && Arrays.equals(nonce, metadata.nonce); + } + + @Override + public String toString () { + return "PayloadMetadata{" + + "nonce=" + Arrays.toString(nonce) + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java index 0a9df16d..195848e2 100644 --- a/src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java @@ -12,6 +12,7 @@ import me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets.ClientboundHandsha import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundRunCommandPacket; import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundRunCoreCommandPacket; import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundSuccessfulHandshakePacket; +import me.chayapak1.chomens_bot.data.chomeNSMod.PayloadMetadata; import me.chayapak1.chomens_bot.data.chomeNSMod.PayloadState; import me.chayapak1.chomens_bot.data.player.PlayerEntry; import me.chayapak1.chomens_bot.util.UUIDUtilities; @@ -21,14 +22,18 @@ import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslationArgument; import java.lang.reflect.InvocationTargetException; +import java.security.SecureRandom; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; // This is inspired from the ChomeNS Bot Proxy which is in the JavaScript version of ChomeNS Bot. public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, PlayersPlugin.Listener, TickPlugin.Listener { private static final String ID = "chomens_mod"; private static final int ENCODED_PAYLOAD_LENGTH = 31_000; // just 32767 trimmed "a bit" - private static final Random RANDOM = new Random(); + private static final long NONCE_EXPIRATION_MS = 30 * 1000; // 30 seconds + + private static final SecureRandom RANDOM = new SecureRandom(); public static final List> SERVERBOUND_PACKETS = new ArrayList<>(); @@ -44,9 +49,11 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players private final List listeners = new ArrayList<>(); - public final List connectedPlayers = new ArrayList<>(); + public final List connectedPlayers = Collections.synchronizedList(new ArrayList<>()); - private final Map> receivedParts = new HashMap<>(); + private final Map> receivedParts = new ConcurrentHashMap<>(); + + private final List seenMetadata = Collections.synchronizedList(new ArrayList<>()); public ChomeNSModIntegrationPlugin (Bot bot) { this.bot = bot; @@ -60,14 +67,20 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players @Override public void onSecondTick () { tryHandshaking(); + + seenMetadata.removeIf( + metadata -> System.currentTimeMillis() - metadata.timestamp() > NONCE_EXPIRATION_MS + ); } public void send (PlayerEntry target, Packet packet) { - if (!connectedPlayers.contains(target) && !(packet instanceof ClientboundHandshakePacket)) - return; // LoL sus check + if (!connectedPlayers.contains(target) && !(packet instanceof ClientboundHandshakePacket)) return; final ByteBuf buf = Unpooled.buffer(); + final PayloadMetadata metadata = generateMetadata(); + metadata.serialize(buf); + buf.writeInt(packet.getId()); packet.serialize(buf); @@ -103,9 +116,36 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players } catch (Exception ignored) { } } + private PayloadMetadata generateMetadata () { + final byte[] nonce = new byte[8]; + RANDOM.nextBytes(nonce); + + final long timestamp = System.currentTimeMillis(); + + return new PayloadMetadata(nonce, timestamp); + } + + private boolean isValidPayload (PayloadMetadata metadata) { + // check if the timestamp is less than the expiration time + if (System.currentTimeMillis() - metadata.timestamp() > NONCE_EXPIRATION_MS) return false; + + // check if nonce is replayed in case the server owner + // is not being very nice and decided to pull out + // a replay attack + final boolean valid = !seenMetadata.contains(metadata); + + if (valid) seenMetadata.add(metadata); + + return valid; + } + private Packet deserialize (byte[] data) { final ByteBuf buf = Unpooled.wrappedBuffer(data); + final PayloadMetadata metadata = PayloadMetadata.deserialize(buf); + + if (!isValidPayload(metadata)) return null; + final int id = buf.readInt(); final Class packetClass = SERVERBOUND_PACKETS.get(id); @@ -155,7 +195,7 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players final PayloadState payloadState = PayloadState.values()[payloadStateIndex]; - if (!receivedParts.containsKey(player)) receivedParts.put(player, new HashMap<>()); + receivedParts.putIfAbsent(player, new ConcurrentHashMap<>()); final Map playerReceivedParts = receivedParts.get(player);