From e126048fe40e17f23b7e61d84a772f3b71b7398e Mon Sep 17 00:00:00 2001 From: UnfamiliarLegacy <74633542+UnfamiliarLegacy@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:25:37 +0200 Subject: [PATCH] Add shockwave crypto --- .../java/gearth/encoding/HexEncoding.java | 23 +++++++ .../java/gearth/protocol/HConnection.java | 4 +- .../proxy/shockwave/ShockwaveProxy.java | 2 +- .../habboclient/macOs/MacOsHabboClient.java | 20 ++---- .../habboclient/rust/RustHabboClient.java | 13 +--- .../windows/WindowsHabboClient.java | 15 +--- .../shockwave/ShockwavePacketHandler.java | 53 +++++++++++++-- .../ShockwavePacketIncomingHandler.java | 45 ++++++++---- .../ShockwavePacketOutgoingHandler.java | 22 +++--- .../shockwave/crypto/RC4Shockwave.java | 68 +++++++++++++++++++ .../services/always_admin/AdminService.java | 9 ++- 11 files changed, 196 insertions(+), 78 deletions(-) create mode 100644 G-Earth/src/main/java/gearth/encoding/HexEncoding.java create mode 100644 G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/crypto/RC4Shockwave.java diff --git a/G-Earth/src/main/java/gearth/encoding/HexEncoding.java b/G-Earth/src/main/java/gearth/encoding/HexEncoding.java new file mode 100644 index 0000000..f09f08f --- /dev/null +++ b/G-Earth/src/main/java/gearth/encoding/HexEncoding.java @@ -0,0 +1,23 @@ +package gearth.encoding; + +import org.bouncycastle.util.encoders.Hex; + +import java.nio.charset.StandardCharsets; + +public class HexEncoding { + + public static byte[] toBytes(String s) { + return Hex.decode(s); + } + + public static byte[] toHex(byte[] bytes, boolean upperCase) { + String data = Hex.toHexString(bytes); + + if (upperCase) { + data = data.toUpperCase(); + } + + return data.getBytes(StandardCharsets.ISO_8859_1); + } + +} diff --git a/G-Earth/src/main/java/gearth/protocol/HConnection.java b/G-Earth/src/main/java/gearth/protocol/HConnection.java index fc544f5..5a7d1a1 100644 --- a/G-Earth/src/main/java/gearth/protocol/HConnection.java +++ b/G-Earth/src/main/java/gearth/protocol/HConnection.java @@ -24,7 +24,7 @@ public class HConnection { private volatile ExtensionHandler extensionHandler = null; - private volatile Object[] trafficObservables = {new Observable(), new Observable(), new Observable()}; + private volatile Observable[] trafficObservables = new Observable[]{new Observable(), new Observable(), new Observable()}; private volatile Observable stateObservable = new Observable<>(); private volatile Observable> developerModeChangeObservable = new Observable<>(); @@ -136,7 +136,7 @@ public class HConnection { return extensionHandler; } - public Object[] getTrafficObservables() { + public Observable[] getTrafficObservables() { return trafficObservables; } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/shockwave/ShockwaveProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/shockwave/ShockwaveProxy.java index d9a3001..ae35369 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/shockwave/ShockwaveProxy.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/shockwave/ShockwaveProxy.java @@ -88,7 +88,7 @@ public class ShockwaveProxy implements ProxyProvider, ConnectionInterceptorCallb final Semaphore abort = new Semaphore(0); final ShockwavePacketOutgoingHandler outgoingHandler = new ShockwavePacketOutgoingHandler(server.getOutputStream(), hConnection.getExtensionHandler(), hConnection.getTrafficObservables()); - final ShockwavePacketIncomingHandler incomingHandler = new ShockwavePacketIncomingHandler(client.getOutputStream(), hConnection.getExtensionHandler(), hConnection.getTrafficObservables()); + final ShockwavePacketIncomingHandler incomingHandler = new ShockwavePacketIncomingHandler(client.getOutputStream(), hConnection.getExtensionHandler(), hConnection.getTrafficObservables(), outgoingHandler); // TODO: Non hardcoded version "20". Not exactly sure yet how to deal with this for now. // Lets revisit when origins is more mature. diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macOs/MacOsHabboClient.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macOs/MacOsHabboClient.java index 1c20100..3fccccb 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macOs/MacOsHabboClient.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macOs/MacOsHabboClient.java @@ -1,9 +1,11 @@ package gearth.protocol.memory.habboclient.macOs; +import gearth.encoding.HexEncoding; import gearth.misc.Cacher; import gearth.protocol.HConnection; import gearth.protocol.HMessage; import gearth.protocol.memory.habboclient.HabboClient; +import org.bouncycastle.util.encoders.Hex; import org.json.JSONArray; import org.json.JSONObject; @@ -44,7 +46,7 @@ public class MacOsHabboClient extends HabboClient { return new ArrayList<>(); for (String s : possibleResults) - result.add(hexStringToByteArray(s)); + result.add(HexEncoding.toBytes(s)); } catch (IOException | URISyntaxException e) { e.printStackTrace(); } @@ -124,25 +126,11 @@ public class MacOsHabboClient extends HabboClient { ArrayList possibleData = readPossibleBytes(false); for (String possibleHexStr : possibleData) { - result.add(hexStringToByteArray(possibleHexStr)); + result.add(HexEncoding.toBytes(possibleHexStr)); } } catch (IOException | URISyntaxException e) { e.printStackTrace(); } return result; } - - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - if (len % 2 == 1) { - s = "0" + s; - len += 1; - } - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; - } } diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/rust/RustHabboClient.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/rust/RustHabboClient.java index b5c88a8..23c0a3f 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/rust/RustHabboClient.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/rust/RustHabboClient.java @@ -1,5 +1,6 @@ package gearth.protocol.memory.habboclient.rust; +import gearth.encoding.HexEncoding; import gearth.protocol.HConnection; import gearth.protocol.memory.habboclient.HabboClient; @@ -48,18 +49,8 @@ public class RustHabboClient extends HabboClient { List ret = new ArrayList<>(); for (String possibleHexStr : possibleData) - ret.add(hexStringToByteArray(possibleHexStr)); + ret.add(HexEncoding.toBytes(possibleHexStr)); return ret; } - - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; - } } diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/windows/WindowsHabboClient.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/windows/WindowsHabboClient.java index 7178b06..f950d20 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/windows/WindowsHabboClient.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/windows/WindowsHabboClient.java @@ -1,5 +1,6 @@ package gearth.protocol.memory.habboclient.windows; +import gearth.encoding.HexEncoding; import gearth.misc.Cacher; import gearth.protocol.HConnection; import gearth.protocol.HMessage; @@ -44,7 +45,7 @@ public class WindowsHabboClient extends HabboClient { return new ArrayList<>(); for (String s : possibleResults) - result.add(hexStringToByteArray(s)); + result.add(HexEncoding.toBytes(s)); } catch (IOException | URISyntaxException e) { e.printStackTrace(); } @@ -126,21 +127,11 @@ public class WindowsHabboClient extends HabboClient { ArrayList possibleData = readPossibleBytes(false); for (String possibleHexStr : possibleData) { - result.add(hexStringToByteArray(possibleHexStr)); + result.add(HexEncoding.toBytes(possibleHexStr)); } } catch (IOException | URISyntaxException e) { e.printStackTrace(); } return result; } - - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; - } } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketHandler.java index ece52b6..4571ddb 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketHandler.java @@ -1,18 +1,27 @@ package gearth.protocol.packethandler.shockwave; +import gearth.encoding.HexEncoding; +import gearth.misc.listenerpattern.Observable; import gearth.protocol.HMessage; import gearth.protocol.HPacket; +import gearth.protocol.TrafficListener; import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.packethandler.shockwave.buffers.ShockwaveBuffer; +import gearth.protocol.packethandler.shockwave.crypto.RC4Shockwave; import gearth.services.extension_handler.ExtensionHandler; +import org.bouncycastle.util.encoders.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; public abstract class ShockwavePacketHandler extends PacketHandler { + // The first 20 bytes of the artificialKey. + public static final byte[] ARTIFICIAL_KEY = Hex.decode("14d288cdb0bc08c274809a7802962af98b41dec8"); + protected static final Logger logger = LoggerFactory.getLogger(ShockwavePacketHandler.class); private final HMessage.Direction direction; @@ -21,19 +30,53 @@ public abstract class ShockwavePacketHandler extends PacketHandler { protected final OutputStream outputStream; - ShockwavePacketHandler(HMessage.Direction direction, ShockwaveBuffer payloadBuffer, OutputStream outputStream, ExtensionHandler extensionHandler, Object[] trafficObservables) { + private boolean isEncrypted; + private final RC4Shockwave decryptCipher; + private final RC4Shockwave encryptCipher; + + ShockwavePacketHandler(HMessage.Direction direction, ShockwaveBuffer payloadBuffer, OutputStream outputStream, ExtensionHandler extensionHandler, Observable[] trafficObservables) { super(extensionHandler, trafficObservables); this.direction = direction; this.payloadBuffer = payloadBuffer; this.outputStream = outputStream; this.flushLock = new Object(); + this.isEncrypted = false; + this.decryptCipher = new RC4Shockwave(0, ARTIFICIAL_KEY); + this.encryptCipher = new RC4Shockwave(0, ARTIFICIAL_KEY); + } + + protected void setEncrypted() { + isEncrypted = true; + } + + @Override + public boolean sendToStream(byte[] buffer) { + return sendToStream(buffer, isEncrypted); + } + + private boolean sendToStream(byte[] buffer, boolean isEncrypted) { + synchronized (sendLock) { + try { + if (!isEncrypted) { + outputStream.write(buffer); + } else { + outputStream.write(HexEncoding.toHex(encryptCipher.crypt(buffer), true)); + } + return true; + } catch (IOException e) { + logger.error("Failed to send packet to stream", e); + return false; + } + } } @Override public void act(byte[] buffer) throws IOException { - logger.info("Direction {} Received {} bytes", this.direction, buffer.length); - - payloadBuffer.push(buffer); + if (!isEncrypted) { + payloadBuffer.push(buffer); + } else { + payloadBuffer.push(decryptCipher.crypt(Hex.decode(buffer))); + } flush(); } @@ -43,6 +86,8 @@ public abstract class ShockwavePacketHandler extends PacketHandler { final HPacket[] packets = payloadBuffer.receive(); for (final HPacket packet : packets){ + packet.setIdentifierDirection(direction); + final HMessage message = new HMessage(packet, direction, currentIndex); awaitListeners(message, x -> sendToStream(x.getPacket().toBytes())); diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketIncomingHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketIncomingHandler.java index d4596e9..bfef136 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketIncomingHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketIncomingHandler.java @@ -1,28 +1,47 @@ package gearth.protocol.packethandler.shockwave; +import gearth.misc.listenerpattern.Observable; import gearth.protocol.HMessage; +import gearth.protocol.TrafficListener; +import gearth.protocol.packethandler.ByteArrayUtils; import gearth.protocol.packethandler.shockwave.buffers.ShockwaveInBuffer; +import gearth.protocol.packethandler.shockwave.packets.ShockPacket; import gearth.services.extension_handler.ExtensionHandler; -import java.io.IOException; import java.io.OutputStream; public class ShockwavePacketIncomingHandler extends ShockwavePacketHandler { - public ShockwavePacketIncomingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Object[] trafficObservables) { + + private static final byte[] PACKET_END = new byte[] {0x01}; + private static final int ID_SECRET_KEY = 1; + + public ShockwavePacketIncomingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Observable[] trafficObservables, ShockwavePacketHandler outgoingHandler) { super(HMessage.Direction.TOCLIENT, new ShockwaveInBuffer(), outputStream, extensionHandler, trafficObservables); + + trafficObservables[0].addListener(new TrafficListener() { + @Override + public void onCapture(HMessage message) { + if (!(message.getPacket() instanceof ShockPacket)) { + return; + } + + final ShockPacket packet = (ShockPacket) message.getPacket(); + + if (!packet.canSendToClient()) { + return; + } + + if (packet.headerId() == ID_SECRET_KEY) { + logger.info("Received SECRET_KEY from server, enabling encryption / decryption."); + trafficObservables[0].removeListener(this); + outgoingHandler.setEncrypted(); + } + } + }); } @Override - public boolean sendToStream(byte[] buffer) { - synchronized (sendLock) { - try { - outputStream.write(buffer); - outputStream.write(new byte[] {0x01}); - return true; - } catch (IOException e) { - logger.error("Error while sending packet to stream.", e); - return false; - } - } + public boolean sendToStream(byte[] packet) { + return super.sendToStream(ByteArrayUtils.combineByteArrays(packet, PACKET_END)); } } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketOutgoingHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketOutgoingHandler.java index c155b81..313e501 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketOutgoingHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketOutgoingHandler.java @@ -1,31 +1,25 @@ package gearth.protocol.packethandler.shockwave; import gearth.encoding.Base64Encoding; +import gearth.misc.listenerpattern.Observable; import gearth.protocol.HMessage; +import gearth.protocol.TrafficListener; +import gearth.protocol.packethandler.ByteArrayUtils; import gearth.protocol.packethandler.shockwave.buffers.ShockwaveOutBuffer; import gearth.services.extension_handler.ExtensionHandler; -import java.io.IOException; import java.io.OutputStream; public class ShockwavePacketOutgoingHandler extends ShockwavePacketHandler { - public ShockwavePacketOutgoingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Object[] trafficObservables) { + public ShockwavePacketOutgoingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Observable[] trafficObservables) { super(HMessage.Direction.TOSERVER, new ShockwaveOutBuffer(), outputStream, extensionHandler, trafficObservables); } @Override - public boolean sendToStream(byte[] buffer) { - synchronized (sendLock) { - try { - byte[] bufferLen = Base64Encoding.encode(buffer.length, 3); + public boolean sendToStream(byte[] packet) { + byte[] bufferLen = Base64Encoding.encode(packet.length, 3); + byte[] buffer = ByteArrayUtils.combineByteArrays(bufferLen, packet); - outputStream.write(bufferLen); - outputStream.write(buffer); - return true; - } catch (IOException e) { - logger.error("Error while sending packet to stream.", e); - return false; - } - } + return super.sendToStream(buffer); } } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/crypto/RC4Shockwave.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/crypto/RC4Shockwave.java new file mode 100644 index 0000000..102f491 --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/crypto/RC4Shockwave.java @@ -0,0 +1,68 @@ +package gearth.protocol.packethandler.shockwave.crypto; + +/** + * Habbo Shockwave RC4 is broken, meaning this is not a standard RC4 implementation. + * Thanks to Joni and DarkStar851 for discovering this. + */ +public class RC4Shockwave { + + private static final int TABLE_SIZE = 256; + + private final int[] table; + + private int x; + private int y; + + public RC4Shockwave(int key, byte[] artificialKey) { + this.table = buildTable(key, artificialKey); + } + + public byte[] crypt(byte[] data) { + byte[] result = new byte[data.length]; + + for (int i = 0; i < data.length; i++) { + x = (x + 1) % TABLE_SIZE; + y = (y + table[x] & 0xff) % TABLE_SIZE; + + swap(table, x, y); + + int xorIndex = ((table[x] & 0xff) + (table[y] & 0xff)) % TABLE_SIZE; + + result[i] = (byte) (data[i] ^ table[xorIndex & 0xff]); + } + + return result; + } + + private static int[] buildTable(int key, byte[] artificialKey) { + byte[] modKey = new byte[20]; + + for (int i = 0, j = 0; i < modKey.length; i++, j++) { + if (j >= artificialKey.length) { + j = 0; + } + + modKey[i] = (byte) (key & modKey[j]); + } + + int[] table = new int[TABLE_SIZE]; + + for (int i = 0; i < TABLE_SIZE; i++) { + table[i] = (byte) i; + } + + for (int q = 0, j = 0; q < TABLE_SIZE; q++) { + j = (j + (table[q] & 0xff) + modKey[q % modKey.length]) % TABLE_SIZE; + + swap(table, q, j); + } + + return table; + } + + private static void swap(int[] table, int i, int j) { + int temp = table[i]; + table[i] = table[j]; + table[j] = temp; + } +} diff --git a/G-Earth/src/main/java/gearth/services/always_admin/AdminService.java b/G-Earth/src/main/java/gearth/services/always_admin/AdminService.java index afa406b..7608768 100644 --- a/G-Earth/src/main/java/gearth/services/always_admin/AdminService.java +++ b/G-Earth/src/main/java/gearth/services/always_admin/AdminService.java @@ -36,17 +36,16 @@ public class AdminService { } public void onMessage(HMessage message) { + if (!enabled) return; HPacket packet = message.getPacket(); if (message.getDestination() == HMessage.Direction.TOCLIENT && (originalPacket == null || packet.headerId() == originalPacket.headerId()) && packet.length() == 11 && (packet.readByte(14) == 0 || packet.readByte(14) == 1)) { originalPacket = new HPacket(packet); - if (enabled) { - packet.replaceInt(6, 7); - packet.replaceInt(10, 7); - packet.replaceBoolean(14, true); - } + packet.replaceInt(6, 7); + packet.replaceInt(10, 7); + packet.replaceBoolean(14, true); } }