Skip to content

Commit

Permalink
fix(Kcp): V1.41 [2024-04-28]
Browse files Browse the repository at this point in the history
- fix: KcpHeader is now parsed safely, handling attackers potentially sending values out of enum range
- fix: KcpClient RawSend may throw ConnectionRefused SocketException when OnDisconnected calls SendDisconnect(), which is fine
- fix: less scary cookie message and better explanation
  • Loading branch information
miwarnec committed Apr 28, 2024
1 parent 694cd0d commit 63e9191
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 8 deletions.
5 changes: 5 additions & 0 deletions Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
V1.41 [2024-04-28]
- fix: KcpHeader is now parsed safely, handling attackers potentially sending values out of enum range
- fix: KcpClient RawSend may throw ConnectionRefused SocketException when OnDisconnected calls SendDisconnect(), which is fine
- fix: less scary cookie message and better explanation

V1.40 [2024-01-03]
- added [KCP] to all log messages
- fix: #3704 remove old fix for #2353 which caused log spam and isn't needed anymore since the
Expand Down
1 change: 1 addition & 0 deletions Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ protected override void RawSend(ArraySegment<byte> data)
// at least log a message for easier debugging.
Log.Info($"[KCP] Client.RawSend: looks like the other end has closed the connection. This is fine: {e}");
// base.Disconnect(); <- don't call this, would deadlock if SendDisconnect() already throws

}
}

Expand Down
31 changes: 31 additions & 0 deletions Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;

namespace kcp2k
{
// header for messages processed by kcp.
Expand All @@ -23,4 +25,33 @@ public enum KcpHeaderUnreliable : byte
// disconnect always goes through rapid fire unreliable (glenn fielder)
Disconnect = 5,
}

// save convert the enums from/to byte.
// attackers may attempt to send invalid values, so '255' may not convert.
public static class KcpHeader
{
public static bool ParseReliable(byte value, out KcpHeaderReliable header)
{
if (Enum.IsDefined(typeof(KcpHeaderReliable), value))
{
header = (KcpHeaderReliable)value;
return true;
}

header = KcpHeaderReliable.Ping; // any default
return false;
}

public static bool ParseUnreliable(byte value, out KcpHeaderUnreliable header)
{
if (Enum.IsDefined(typeof(KcpHeaderUnreliable), value))
{
header = (KcpHeaderUnreliable)value;
return true;
}

header = KcpHeaderUnreliable.Disconnect; // any default
return false;
}
}
}
24 changes: 20 additions & 4 deletions Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,16 @@ bool ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment<byte> me
return false;
}

// extract header & content without header
header = (KcpHeaderReliable)kcpMessageBuffer[0];
// safely extract header. attackers may send values out of enum range.
byte headerByte = kcpMessageBuffer[0];
if (!KcpHeader.ParseReliable(headerByte, out header))
{
OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {headerByte} is not defined in {typeof(KcpHeaderReliable)}.");
Disconnect();
return false;
}

// extract content without header
message = new ArraySegment<byte>(kcpMessageBuffer, 1, msgSize - 1);
lastReceiveTime = (uint)watch.ElapsedMilliseconds;
return true;
Expand Down Expand Up @@ -529,9 +537,17 @@ protected void OnRawInputUnreliable(ArraySegment<byte> message)
// need at least one byte for the KcpHeader enum
if (message.Count < 1) return;

// parse header and subtract it from message content.
// safely extract header. attackers may send values out of enum range.
byte headerByte = message.Array[message.Offset + 0];
if (!KcpHeader.ParseUnreliable(headerByte, out KcpHeaderUnreliable header))
{
OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {headerByte} is not defined in {typeof(KcpHeaderUnreliable)}.");
Disconnect();
return;
}

// subtract header from message content
// (above we already ensure it's at least 1 byte long)
KcpHeaderUnreliable header = (KcpHeaderUnreliable)message.Array[message.Offset + 0];
message = new ArraySegment<byte>(message.Array, message.Offset + 1, message.Count - 1);

switch (header)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,17 @@ public void RawInput(ArraySegment<byte> segment)
// parse the cookie and make sure it matches (except for initial hello).
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);

// compare cookie to protect against UDP spoofing.
// messages won't have a cookie until after handshake.
// so only compare if we are authenticated.
// security: messages after authentication are expected to contain the cookie.
// this protects against UDP spoofing.
// simply drop the message if the cookie doesn't match.
if (state == KcpState.Authenticated)
{
if (messageCookie != cookie)
{
Log.Warning($"[KCP] ServerConnection: dropped message with invalid cookie: {messageCookie} expected: {cookie} state: {state}");
// Info is enough, don't scare users.
// => this can happen for malicious messages
// => it can also happen if client's Hello message was retransmitted multiple times, which is totally normal.
Log.Info($"[KCP] ServerConnection: dropped message with invalid cookie: {messageCookie} from {remoteEndPoint} expected: {cookie} state: {state}. This can happen if the client's Hello message was transmitted multiple times, or if an attacker attempted UDP spoofing.");
return;
}
}
Expand Down

0 comments on commit 63e9191

Please sign in to comment.