Add shockwave crypto

This commit is contained in:
UnfamiliarLegacy 2024-06-21 17:25:37 +02:00
parent b1d5faef11
commit e126048fe4
11 changed files with 196 additions and 78 deletions

View File

@ -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);
}
}

View File

@ -24,7 +24,7 @@ public class HConnection {
private volatile ExtensionHandler extensionHandler = null; private volatile ExtensionHandler extensionHandler = null;
private volatile Object[] trafficObservables = {new Observable<TrafficListener>(), new Observable<TrafficListener>(), new Observable<TrafficListener>()}; private volatile Observable<TrafficListener>[] trafficObservables = new Observable[]{new Observable<TrafficListener>(), new Observable<TrafficListener>(), new Observable<TrafficListener>()};
private volatile Observable<StateChangeListener> stateObservable = new Observable<>(); private volatile Observable<StateChangeListener> stateObservable = new Observable<>();
private volatile Observable<Consumer<Boolean>> developerModeChangeObservable = new Observable<>(); private volatile Observable<Consumer<Boolean>> developerModeChangeObservable = new Observable<>();
@ -136,7 +136,7 @@ public class HConnection {
return extensionHandler; return extensionHandler;
} }
public Object[] getTrafficObservables() { public Observable<TrafficListener>[] getTrafficObservables() {
return trafficObservables; return trafficObservables;
} }

View File

@ -88,7 +88,7 @@ public class ShockwaveProxy implements ProxyProvider, ConnectionInterceptorCallb
final Semaphore abort = new Semaphore(0); final Semaphore abort = new Semaphore(0);
final ShockwavePacketOutgoingHandler outgoingHandler = new ShockwavePacketOutgoingHandler(server.getOutputStream(), hConnection.getExtensionHandler(), hConnection.getTrafficObservables()); 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. // TODO: Non hardcoded version "20". Not exactly sure yet how to deal with this for now.
// Lets revisit when origins is more mature. // Lets revisit when origins is more mature.

View File

@ -1,9 +1,11 @@
package gearth.protocol.memory.habboclient.macOs; package gearth.protocol.memory.habboclient.macOs;
import gearth.encoding.HexEncoding;
import gearth.misc.Cacher; import gearth.misc.Cacher;
import gearth.protocol.HConnection; import gearth.protocol.HConnection;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
import gearth.protocol.memory.habboclient.HabboClient; import gearth.protocol.memory.habboclient.HabboClient;
import org.bouncycastle.util.encoders.Hex;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@ -44,7 +46,7 @@ public class MacOsHabboClient extends HabboClient {
return new ArrayList<>(); return new ArrayList<>();
for (String s : possibleResults) for (String s : possibleResults)
result.add(hexStringToByteArray(s)); result.add(HexEncoding.toBytes(s));
} catch (IOException | URISyntaxException e) { } catch (IOException | URISyntaxException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -124,25 +126,11 @@ public class MacOsHabboClient extends HabboClient {
ArrayList<String> possibleData = readPossibleBytes(false); ArrayList<String> possibleData = readPossibleBytes(false);
for (String possibleHexStr : possibleData) { for (String possibleHexStr : possibleData) {
result.add(hexStringToByteArray(possibleHexStr)); result.add(HexEncoding.toBytes(possibleHexStr));
} }
} catch (IOException | URISyntaxException e) { } catch (IOException | URISyntaxException e) {
e.printStackTrace(); e.printStackTrace();
} }
return result; 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;
}
} }

View File

@ -1,5 +1,6 @@
package gearth.protocol.memory.habboclient.rust; package gearth.protocol.memory.habboclient.rust;
import gearth.encoding.HexEncoding;
import gearth.protocol.HConnection; import gearth.protocol.HConnection;
import gearth.protocol.memory.habboclient.HabboClient; import gearth.protocol.memory.habboclient.HabboClient;
@ -48,18 +49,8 @@ public class RustHabboClient extends HabboClient {
List<byte[]> ret = new ArrayList<>(); List<byte[]> ret = new ArrayList<>();
for (String possibleHexStr : possibleData) for (String possibleHexStr : possibleData)
ret.add(hexStringToByteArray(possibleHexStr)); ret.add(HexEncoding.toBytes(possibleHexStr));
return ret; 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;
}
} }

View File

@ -1,5 +1,6 @@
package gearth.protocol.memory.habboclient.windows; package gearth.protocol.memory.habboclient.windows;
import gearth.encoding.HexEncoding;
import gearth.misc.Cacher; import gearth.misc.Cacher;
import gearth.protocol.HConnection; import gearth.protocol.HConnection;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
@ -44,7 +45,7 @@ public class WindowsHabboClient extends HabboClient {
return new ArrayList<>(); return new ArrayList<>();
for (String s : possibleResults) for (String s : possibleResults)
result.add(hexStringToByteArray(s)); result.add(HexEncoding.toBytes(s));
} catch (IOException | URISyntaxException e) { } catch (IOException | URISyntaxException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -126,21 +127,11 @@ public class WindowsHabboClient extends HabboClient {
ArrayList<String> possibleData = readPossibleBytes(false); ArrayList<String> possibleData = readPossibleBytes(false);
for (String possibleHexStr : possibleData) { for (String possibleHexStr : possibleData) {
result.add(hexStringToByteArray(possibleHexStr)); result.add(HexEncoding.toBytes(possibleHexStr));
} }
} catch (IOException | URISyntaxException e) { } catch (IOException | URISyntaxException e) {
e.printStackTrace(); e.printStackTrace();
} }
return result; 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;
}
} }

View File

@ -1,18 +1,27 @@
package gearth.protocol.packethandler.shockwave; package gearth.protocol.packethandler.shockwave;
import gearth.encoding.HexEncoding;
import gearth.misc.listenerpattern.Observable;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
import gearth.protocol.HPacket; import gearth.protocol.HPacket;
import gearth.protocol.TrafficListener;
import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.packethandler.PacketHandler;
import gearth.protocol.packethandler.shockwave.buffers.ShockwaveBuffer; import gearth.protocol.packethandler.shockwave.buffers.ShockwaveBuffer;
import gearth.protocol.packethandler.shockwave.crypto.RC4Shockwave;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public abstract class ShockwavePacketHandler extends PacketHandler { 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); protected static final Logger logger = LoggerFactory.getLogger(ShockwavePacketHandler.class);
private final HMessage.Direction direction; private final HMessage.Direction direction;
@ -21,19 +30,53 @@ public abstract class ShockwavePacketHandler extends PacketHandler {
protected final OutputStream outputStream; 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<TrafficListener>[] trafficObservables) {
super(extensionHandler, trafficObservables); super(extensionHandler, trafficObservables);
this.direction = direction; this.direction = direction;
this.payloadBuffer = payloadBuffer; this.payloadBuffer = payloadBuffer;
this.outputStream = outputStream; this.outputStream = outputStream;
this.flushLock = new Object(); 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 @Override
public void act(byte[] buffer) throws IOException { public void act(byte[] buffer) throws IOException {
logger.info("Direction {} Received {} bytes", this.direction, buffer.length); if (!isEncrypted) {
payloadBuffer.push(buffer);
payloadBuffer.push(buffer); } else {
payloadBuffer.push(decryptCipher.crypt(Hex.decode(buffer)));
}
flush(); flush();
} }
@ -43,6 +86,8 @@ public abstract class ShockwavePacketHandler extends PacketHandler {
final HPacket[] packets = payloadBuffer.receive(); final HPacket[] packets = payloadBuffer.receive();
for (final HPacket packet : packets){ for (final HPacket packet : packets){
packet.setIdentifierDirection(direction);
final HMessage message = new HMessage(packet, direction, currentIndex); final HMessage message = new HMessage(packet, direction, currentIndex);
awaitListeners(message, x -> sendToStream(x.getPacket().toBytes())); awaitListeners(message, x -> sendToStream(x.getPacket().toBytes()));

View File

@ -1,28 +1,47 @@
package gearth.protocol.packethandler.shockwave; package gearth.protocol.packethandler.shockwave;
import gearth.misc.listenerpattern.Observable;
import gearth.protocol.HMessage; 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.buffers.ShockwaveInBuffer;
import gearth.protocol.packethandler.shockwave.packets.ShockPacket;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
public class ShockwavePacketIncomingHandler extends ShockwavePacketHandler { 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<TrafficListener>[] trafficObservables, ShockwavePacketHandler outgoingHandler) {
super(HMessage.Direction.TOCLIENT, new ShockwaveInBuffer(), outputStream, extensionHandler, trafficObservables); 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 @Override
public boolean sendToStream(byte[] buffer) { public boolean sendToStream(byte[] packet) {
synchronized (sendLock) { return super.sendToStream(ByteArrayUtils.combineByteArrays(packet, PACKET_END));
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;
}
}
} }
} }

View File

@ -1,31 +1,25 @@
package gearth.protocol.packethandler.shockwave; package gearth.protocol.packethandler.shockwave;
import gearth.encoding.Base64Encoding; import gearth.encoding.Base64Encoding;
import gearth.misc.listenerpattern.Observable;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
import gearth.protocol.TrafficListener;
import gearth.protocol.packethandler.ByteArrayUtils;
import gearth.protocol.packethandler.shockwave.buffers.ShockwaveOutBuffer; import gearth.protocol.packethandler.shockwave.buffers.ShockwaveOutBuffer;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
public class ShockwavePacketOutgoingHandler extends ShockwavePacketHandler { public class ShockwavePacketOutgoingHandler extends ShockwavePacketHandler {
public ShockwavePacketOutgoingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Object[] trafficObservables) { public ShockwavePacketOutgoingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables) {
super(HMessage.Direction.TOSERVER, new ShockwaveOutBuffer(), outputStream, extensionHandler, trafficObservables); super(HMessage.Direction.TOSERVER, new ShockwaveOutBuffer(), outputStream, extensionHandler, trafficObservables);
} }
@Override @Override
public boolean sendToStream(byte[] buffer) { public boolean sendToStream(byte[] packet) {
synchronized (sendLock) { byte[] bufferLen = Base64Encoding.encode(packet.length, 3);
try { byte[] buffer = ByteArrayUtils.combineByteArrays(bufferLen, packet);
byte[] bufferLen = Base64Encoding.encode(buffer.length, 3);
outputStream.write(bufferLen); return super.sendToStream(buffer);
outputStream.write(buffer);
return true;
} catch (IOException e) {
logger.error("Error while sending packet to stream.", e);
return false;
}
}
} }
} }

View File

@ -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 <a href="https://github.com/aromaa">Joni</a> and <a href="https://github.com/scottstamp">DarkStar851</a> 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;
}
}

View File

@ -36,17 +36,16 @@ public class AdminService {
} }
public void onMessage(HMessage message) { public void onMessage(HMessage message) {
if (!enabled) return;
HPacket packet = message.getPacket(); HPacket packet = message.getPacket();
if (message.getDestination() == HMessage.Direction.TOCLIENT if (message.getDestination() == HMessage.Direction.TOCLIENT
&& (originalPacket == null || packet.headerId() == originalPacket.headerId()) && (originalPacket == null || packet.headerId() == originalPacket.headerId())
&& packet.length() == 11 && (packet.readByte(14) == 0 || packet.readByte(14) == 1)) { && packet.length() == 11 && (packet.readByte(14) == 0 || packet.readByte(14) == 1)) {
originalPacket = new HPacket(packet); originalPacket = new HPacket(packet);
if (enabled) { packet.replaceInt(6, 7);
packet.replaceInt(6, 7); packet.replaceInt(10, 7);
packet.replaceInt(10, 7); packet.replaceBoolean(14, true);
packet.replaceBoolean(14, true);
}
} }
} }