fix: Fixes encryption support for pre-6.0.5 clients (#2365)
### Summary Fixes encryption detection for clients pre-6.0.5.0. To limit the amount of brute-force key checking we are only checking 4.0.11 to 6.0.4.
This commit is contained in:
parent
76395b77ec
commit
f150458578
4 changed files with 71 additions and 23 deletions
|
|
@ -23,8 +23,6 @@ namespace Server;
|
|||
|
||||
public class ClientVersion : IComparable<ClientVersion>, IComparer<ClientVersion>, IEquatable<ClientVersion>
|
||||
{
|
||||
public static readonly ClientVersion Version400a = new("4.0.0a");
|
||||
public static readonly ClientVersion Version407a = new("4.0.7a");
|
||||
public static readonly ClientVersion Version500a = new("5.0.0a");
|
||||
public static readonly ClientVersion Version502b = new("5.0.2b");
|
||||
public static readonly ClientVersion Version6000 = new("6.0.0.0");
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public sealed class LoginEncryption : IClientEncryption
|
|||
/// <summary>
|
||||
/// Attempts to initialize login encryption and validate the packet.
|
||||
/// Returns true if the packet appears to be validly encrypted with this scheme.
|
||||
/// When version is null (pre-6.0.5 clients that don't send 0xEF), tries all known legacy keys.
|
||||
/// </summary>
|
||||
public static bool TryDecrypt(
|
||||
ClientVersion version,
|
||||
|
|
@ -52,17 +53,41 @@ public sealed class LoginEncryption : IClientEncryption
|
|||
|
||||
encryption = null;
|
||||
|
||||
var keys = LoginKeys.GetKeys(version);
|
||||
if (keys is { Key1: 0, Key2: 0 })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (encryptedPacket.Length < LoginPacketSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version != null)
|
||||
{
|
||||
var keys = LoginKeys.GetKeys(version);
|
||||
return keys is not { Key1: 0, Key2: 0 } && TryDecryptWithKeys(keys, seed, encryptedPacket, out encryption);
|
||||
}
|
||||
|
||||
// No version available (pre-6.0.5 client) - try all known legacy keys
|
||||
for (var i = 0; i < LoginKeys.LegacyKeys.Length; i++)
|
||||
{
|
||||
var legacyKeys = LoginKeys.LegacyKeys[i];
|
||||
if (TryDecryptWithKeys(legacyKeys, seed, encryptedPacket, out encryption))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts decryption with a specific key pair and validates the result.
|
||||
/// </summary>
|
||||
private static bool TryDecryptWithKeys(
|
||||
LoginKeys keys,
|
||||
uint seed,
|
||||
ReadOnlySpan<byte> encryptedPacket,
|
||||
out LoginEncryption encryption)
|
||||
{
|
||||
const int LoginPacketSize = 62;
|
||||
|
||||
// Copy and decrypt
|
||||
Span<byte> decrypted = stackalloc byte[LoginPacketSize];
|
||||
encryptedPacket[..LoginPacketSize].CopyTo(decrypted);
|
||||
|
|
@ -75,6 +100,7 @@ public sealed class LoginEncryption : IClientEncryption
|
|||
// - Byte 60 must be 0x00 (null terminator for password)
|
||||
if (decrypted[0] != 0x80 || decrypted[30] != 0x00 || decrypted[60] != 0x00)
|
||||
{
|
||||
encryption = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
|
|
@ -28,6 +29,8 @@ public readonly struct LoginKeys
|
|||
|
||||
private static readonly Dictionary<ClientVersion, LoginKeys> _cache = [];
|
||||
|
||||
private static LoginKeys[] _legacyKeys;
|
||||
|
||||
public uint Key1 { get; }
|
||||
public uint Key2 { get; }
|
||||
|
||||
|
|
@ -37,6 +40,12 @@ public readonly struct LoginKeys
|
|||
Key2 = key2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets pre-computed login keys for all known client versions before 6.0.5
|
||||
/// that do not send the 0xEF packet (and thus have no version available at login time).
|
||||
/// </summary>
|
||||
public static ReadOnlySpan<LoginKeys> LegacyKeys => _legacyKeys ??= BuildLegacyKeys();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or computes encryption keys for the specified client version.
|
||||
/// Results are cached for performance.
|
||||
|
|
@ -54,20 +63,16 @@ public readonly struct LoginKeys
|
|||
return keys;
|
||||
}
|
||||
|
||||
keys = ComputeKeys(version);
|
||||
keys = ComputeKeys((uint)version.Major, (uint)version.Minor, (uint)version.Revision);
|
||||
_cache[version] = keys;
|
||||
return keys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes encryption keys from client version using the UO key derivation algorithm.
|
||||
/// Computes encryption keys from version components using the UO key derivation algorithm.
|
||||
/// </summary>
|
||||
private static LoginKeys ComputeKeys(ClientVersion version)
|
||||
private static LoginKeys ComputeKeys(uint major, uint minor, uint revision)
|
||||
{
|
||||
uint major = (uint)version.Major;
|
||||
uint minor = (uint)version.Minor;
|
||||
uint revision = (uint)version.Revision;
|
||||
|
||||
// Key1 derivation
|
||||
uint key1 = (major << 23) | (minor << 14) | (revision << 4);
|
||||
key1 ^= (revision * revision) << 9;
|
||||
|
|
@ -86,4 +91,28 @@ public readonly struct LoginKeys
|
|||
|
||||
return new LoginKeys(key1, key2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the static array of pre-computed keys for pre-6.0.5 client versions.
|
||||
/// These versions don't send 0xEF, so login encryption must be detected by trying known keys.
|
||||
/// </summary>
|
||||
private static LoginKeys[] BuildLegacyKeys()
|
||||
{
|
||||
// All unique (Major, Minor, Revision) tuples from 4.0.11 through 6.0.4
|
||||
ReadOnlySpan<(uint Major, uint Minor, uint Revision)> versions =
|
||||
[
|
||||
(4, 0, 11),
|
||||
(5, 0, 0), (5, 0, 1), (5, 0, 2), (5, 0, 3), (5, 0, 4),
|
||||
(5, 0, 5), (5, 0, 6), (5, 0, 7), (5, 0, 8), (5, 0, 9),
|
||||
(6, 0, 0), (6, 0, 1), (6, 0, 2), (6, 0, 3), (6, 0, 4),
|
||||
];
|
||||
|
||||
var keys = new LoginKeys[versions.Length];
|
||||
for (var i = 0; i < versions.Length; i++)
|
||||
{
|
||||
keys[i] = ComputeKeys(versions[i].Major, versions[i].Minor, versions[i].Revision);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,9 +63,6 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
|||
// Managed socket with buffers (handles lifecycle automatically)
|
||||
internal RingSocket _socket;
|
||||
|
||||
// General packet throttle state (used for other throttled packets)
|
||||
internal bool _isThrottled;
|
||||
|
||||
private IAccount _account;
|
||||
|
||||
internal enum ParserState
|
||||
|
|
@ -664,8 +661,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
|||
_encryption = loginEncryption;
|
||||
|
||||
// Decrypt the buffer in place for processing
|
||||
var mutableBuffer = _socket.RecvBuffer.GetReadSpan();
|
||||
loginEncryption.ClientDecrypt(mutableBuffer[..62]);
|
||||
loginEncryption.ClientDecrypt(buffer[..62]);
|
||||
}
|
||||
|
||||
// Now process as normal (first byte should now be 0x80)
|
||||
|
|
@ -714,7 +710,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
|||
case ProtocolState.GameServer_AwaitingGameServerLogin:
|
||||
{
|
||||
// Some clients send 0x80 on game server connection
|
||||
if (packetId == 0x80)
|
||||
if (packetId == 0x80 || length == 62)
|
||||
{
|
||||
goto case ProtocolState.LoginServer_AwaitingLogin;
|
||||
}
|
||||
|
|
@ -767,8 +763,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
|||
_encryption = gameEncryption;
|
||||
|
||||
// Decrypt the buffer in place for processing
|
||||
var mutableBuffer = _socket.RecvBuffer.GetReadSpan();
|
||||
gameEncryption.ClientDecrypt(mutableBuffer[..65]);
|
||||
gameEncryption.ClientDecrypt(buffer[..65]);
|
||||
}
|
||||
|
||||
// Now process as normal (first byte should now be 0x91)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue