3
0
Mirror von https://github.com/ViaVersion/ViaVersion.git synchronisiert 2024-12-27 00:22:51 +01:00

Fix: delay chat acknowledgements instead of spoofing (#3997)

An easy reproduction case is to mute a player using the Social Interactions screen, let them chat, and then send a message. As ViaVersion produces its own acknowledgements, this now does not match with what the client produced and signed its messages with - so the signature will fail verification.

As an alternative, we *always* pass through the same acknowledgement bit set - and for 'spoofed' messages, we reuse the last one that we received. In the case of the chat ack packet with just an offset, we need to hold back the window by at least 20 messages to ensure we don't start making claims about whether the client saw a message that ViaVersion cannot yet make a judgement on.
Dieser Commit ist enthalten in:
Gegy 2024-07-03 10:52:01 +02:00 committet von GitHub
Ursprung e6da77cf47
Commit 17358120cd
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: B5690EEEBB952194
2 geänderte Dateien mit 67 neuen und 64 gelöschten Zeilen

Datei anzeigen

@ -56,6 +56,7 @@ import com.viaversion.viaversion.rewriter.SoundRewriter;
import com.viaversion.viaversion.rewriter.StatisticsRewriter;
import com.viaversion.viaversion.rewriter.TagRewriter;
import com.viaversion.viaversion.util.ProtocolLogger;
import java.util.BitSet;
import java.util.UUID;
import static com.viaversion.viaversion.util.ProtocolUtil.packetTypeMap;
@ -124,26 +125,6 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
}
});
// Big problem with this update: Without access to the client, this cannot 100% predict the
// correct offset. This means we have to entirely discard client acknowledgements and fake them.
registerClientbound(ClientboundPackets1_20_3.PLAYER_CHAT, wrapper -> {
wrapper.passthrough(Types.UUID); // Sender
wrapper.passthrough(Types.VAR_INT); // Index
final byte[] signature = wrapper.passthrough(Types.OPTIONAL_SIGNATURE_BYTES);
if (signature == null) {
return;
}
// Mimic client behavior for acknowledgements
final AcknowledgedMessagesStorage storage = wrapper.user().get(AcknowledgedMessagesStorage.class);
if (storage.add(signature) && storage.offset() > 64) {
final PacketWrapper chatAck = wrapper.create(ServerboundPackets1_20_3.CHAT_ACK);
chatAck.write(Types.VAR_INT, storage.offset());
chatAck.sendToServer(Protocol1_20_3To1_20_5.class);
storage.clearOffset();
}
});
registerServerbound(ServerboundPackets1_20_5.CHAT, wrapper -> {
wrapper.passthrough(Types.STRING); // Message
wrapper.passthrough(Types.LONG); // Timestamp
@ -161,7 +142,7 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
wrapper.write(Types.OPTIONAL_SIGNATURE_BYTES, null);
}
replaceChatAck(wrapper, storage);
fixChatAck(wrapper, storage);
});
registerServerbound(ServerboundPackets1_20_5.CHAT_COMMAND_SIGNED, ServerboundPackets1_20_3.CHAT_COMMAND, wrapper -> {
wrapper.passthrough(Types.STRING); // Command
@ -188,7 +169,16 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
}
}
replaceChatAck(wrapper, storage);
fixChatAck(wrapper, storage);
});
registerServerbound(ServerboundPackets1_20_5.CHAT_ACK, wrapper -> {
final int offset = wrapper.read(Types.VAR_INT);
final int fixedOffset = wrapper.user().get(AcknowledgedMessagesStorage.class).accumulateAckCount(offset);
if (fixedOffset > 0) {
wrapper.write(Types.VAR_INT, fixedOffset);
} else {
wrapper.cancel();
}
});
registerServerbound(ServerboundPackets1_20_5.CHAT_COMMAND, wrapper -> {
wrapper.passthrough(Types.STRING); // Command
@ -197,7 +187,7 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
wrapper.write(Types.LONG, 0L); // Salt
wrapper.write(Types.VAR_INT, 0); // No signatures
writeChatAck(wrapper, wrapper.user().get(AcknowledgedMessagesStorage.class));
writeSpoofedChatAck(wrapper, wrapper.user().get(AcknowledgedMessagesStorage.class));
});
registerServerbound(ServerboundPackets1_20_5.CHAT_SESSION_UPDATE, wrapper -> {
// Delay this until we know whether the server enforces secure chat
@ -214,7 +204,6 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
wrapper.cancel();
});
cancelServerbound(ServerboundPackets1_20_5.CHAT_ACK);
registerClientbound(ClientboundPackets1_20_3.START_CONFIGURATION, wrapper -> wrapper.user().put(new AcknowledgedMessagesStorage()));
@ -241,16 +230,19 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
cancelServerbound(ServerboundPackets1_20_5.DEBUG_SAMPLE_SUBSCRIPTION);
}
private void replaceChatAck(final PacketWrapper wrapper, final AcknowledgedMessagesStorage storage) {
wrapper.read(Types.VAR_INT); // Offset
wrapper.read(Types.ACKNOWLEDGED_BIT_SET); // Acknowledged
writeChatAck(wrapper, storage);
private void fixChatAck(final PacketWrapper wrapper, final AcknowledgedMessagesStorage storage) {
final int offset = wrapper.read(Types.VAR_INT);
final BitSet acknowledged = wrapper.read(Types.ACKNOWLEDGED_BIT_SET);
final int fixedOffset = storage.updateFromMessage(offset, acknowledged);
wrapper.write(Types.VAR_INT, fixedOffset);
// Never change this, as this message (and future ones) are signed with it
wrapper.write(Types.ACKNOWLEDGED_BIT_SET, acknowledged);
}
private void writeChatAck(final PacketWrapper wrapper, final AcknowledgedMessagesStorage storage) {
wrapper.write(Types.VAR_INT, storage.offset());
wrapper.write(Types.ACKNOWLEDGED_BIT_SET, storage.toAck());
storage.clearOffset();
private void writeSpoofedChatAck(final PacketWrapper wrapper, final AcknowledgedMessagesStorage storage) {
// As we don't have the new state from the client, replay what we last received
wrapper.write(Types.VAR_INT, 0); // Offset
wrapper.write(Types.ACKNOWLEDGED_BIT_SET, storage.createSpoofedAck()); // Acknowledged
}
@Override

Datei anzeigen

@ -23,46 +23,59 @@ import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_20_2to1_20_3.packet.ServerboundPackets1_20_3;
import com.viaversion.viaversion.protocols.v1_20_3to1_20_5.Protocol1_20_3To1_20_5;
import java.util.Arrays;
import java.util.BitSet;
import java.util.UUID;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Tracks the last Secure Chat state that we received from the client. This is important to always have a valid 'last
* seen' state that is consistent with future and past updates from the client (which may be signed). This state is
* used to construct signed command packets to the outdated server from unsigned ones from the modern client.
* <ul>
* <li>If we last forwarded a chat or command packet from the client, we have a known 'last seen' that we can
* reuse.</li>
* <li>If we last forwarded a chat acknowledgement packet, the previous 'last seen' cannot be reused. We
* cannot predict an up-to-date 'last seen', as we do not know which messages the client actually saw.</li>
* <li>Therefore, we need to hold back any acknowledgement packets so that we can continue to reuse the last valid
* 'last seen' state.</li>
* <li>However, there is a limit to the number of messages that can remain unacknowledged on the server.</li>
* <li>To address this, we know that if the client has moved its 'last seen' window far enough, we can fill in the
* gap with dummy 'last seen', and it will never be checked.</li>
* </ul>
*/
public final class AcknowledgedMessagesStorage implements StorableObject {
private static final int MAX_HISTORY = 20;
private final boolean[] trackedMessages = new boolean[MAX_HISTORY];
private static final int MINIMUM_DELAYED_ACK_COUNT = MAX_HISTORY;
private static final BitSet DUMMY_LAST_SEEN_MESSAGES = new BitSet();
private Boolean secureChatEnforced;
private ChatSession chatSession;
private int offset;
private int tail;
private byte[] lastMessage;
public boolean add(final byte[] message) {
if (Arrays.equals(message, lastMessage)) {
return false;
private BitSet lastSeenMessages = new BitSet();
private int delayedAckCount;
public int updateFromMessage(int ackCount, BitSet lastSeenMessages) {
// We held back some acknowledged messages, so flush that out now that we have a known 'last seen' state again
int delayedAckCount = this.delayedAckCount;
this.delayedAckCount = 0;
this.lastSeenMessages = lastSeenMessages;
return ackCount + delayedAckCount;
}
public int accumulateAckCount(int ackCount) {
delayedAckCount += ackCount;
int ackCountToForward = delayedAckCount - MINIMUM_DELAYED_ACK_COUNT;
if (ackCountToForward >= MAX_HISTORY) {
// Because we only forward acknowledgements above the window size, we don't have to shift the previous 'last seen' state
lastSeenMessages = DUMMY_LAST_SEEN_MESSAGES;
delayedAckCount = MINIMUM_DELAYED_ACK_COUNT;
return ackCountToForward;
}
this.lastMessage = message;
this.offset++;
this.trackedMessages[this.tail] = true;
this.tail = (this.tail + 1) % MAX_HISTORY;
return true;
return 0;
}
public BitSet toAck() {
final BitSet acks = new BitSet(MAX_HISTORY);
for (int i = 0; i < MAX_HISTORY; i++) {
final int messageIndex = (this.tail + i) % MAX_HISTORY;
acks.set(i, this.trackedMessages[messageIndex]);
}
return acks;
}
public int offset() {
return this.offset;
}
public void clearOffset() {
this.offset = 0;
public BitSet createSpoofedAck() {
return lastSeenMessages;
}
public void setSecureChatEnforced(final boolean secureChatEnforced) {
@ -98,10 +111,8 @@ public final class AcknowledgedMessagesStorage implements StorableObject {
}
public void clear() {
this.offset = 0;
this.tail = 0;
this.lastMessage = null;
Arrays.fill(this.trackedMessages, false);
lastSeenMessages = new BitSet();
delayedAckCount = 0;
}
@Override