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 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 Version500a = new("5.0.0a");
|
||||||
public static readonly ClientVersion Version502b = new("5.0.2b");
|
public static readonly ClientVersion Version502b = new("5.0.2b");
|
||||||
public static readonly ClientVersion Version6000 = new("6.0.0.0");
|
public static readonly ClientVersion Version6000 = new("6.0.0.0");
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ public sealed class LoginEncryption : IClientEncryption
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to initialize login encryption and validate the packet.
|
/// Attempts to initialize login encryption and validate the packet.
|
||||||
/// Returns true if the packet appears to be validly encrypted with this scheme.
|
/// 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>
|
/// </summary>
|
||||||
public static bool TryDecrypt(
|
public static bool TryDecrypt(
|
||||||
ClientVersion version,
|
ClientVersion version,
|
||||||
|
|
@ -52,17 +53,41 @@ public sealed class LoginEncryption : IClientEncryption
|
||||||
|
|
||||||
encryption = null;
|
encryption = null;
|
||||||
|
|
||||||
var keys = LoginKeys.GetKeys(version);
|
|
||||||
if (keys is { Key1: 0, Key2: 0 })
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encryptedPacket.Length < LoginPacketSize)
|
if (encryptedPacket.Length < LoginPacketSize)
|
||||||
{
|
{
|
||||||
return false;
|
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
|
// Copy and decrypt
|
||||||
Span<byte> decrypted = stackalloc byte[LoginPacketSize];
|
Span<byte> decrypted = stackalloc byte[LoginPacketSize];
|
||||||
encryptedPacket[..LoginPacketSize].CopyTo(decrypted);
|
encryptedPacket[..LoginPacketSize].CopyTo(decrypted);
|
||||||
|
|
@ -75,6 +100,7 @@ public sealed class LoginEncryption : IClientEncryption
|
||||||
// - Byte 60 must be 0x00 (null terminator for password)
|
// - Byte 60 must be 0x00 (null terminator for password)
|
||||||
if (decrypted[0] != 0x80 || decrypted[30] != 0x00 || decrypted[60] != 0x00)
|
if (decrypted[0] != 0x80 || decrypted[30] != 0x00 || decrypted[60] != 0x00)
|
||||||
{
|
{
|
||||||
|
encryption = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
*************************************************************************/
|
*************************************************************************/
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
|
@ -28,6 +29,8 @@ public readonly struct LoginKeys
|
||||||
|
|
||||||
private static readonly Dictionary<ClientVersion, LoginKeys> _cache = [];
|
private static readonly Dictionary<ClientVersion, LoginKeys> _cache = [];
|
||||||
|
|
||||||
|
private static LoginKeys[] _legacyKeys;
|
||||||
|
|
||||||
public uint Key1 { get; }
|
public uint Key1 { get; }
|
||||||
public uint Key2 { get; }
|
public uint Key2 { get; }
|
||||||
|
|
||||||
|
|
@ -37,6 +40,12 @@ public readonly struct LoginKeys
|
||||||
Key2 = key2;
|
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>
|
/// <summary>
|
||||||
/// Gets or computes encryption keys for the specified client version.
|
/// Gets or computes encryption keys for the specified client version.
|
||||||
/// Results are cached for performance.
|
/// Results are cached for performance.
|
||||||
|
|
@ -54,20 +63,16 @@ public readonly struct LoginKeys
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
keys = ComputeKeys(version);
|
keys = ComputeKeys((uint)version.Major, (uint)version.Minor, (uint)version.Revision);
|
||||||
_cache[version] = keys;
|
_cache[version] = keys;
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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
|
// Key1 derivation
|
||||||
uint key1 = (major << 23) | (minor << 14) | (revision << 4);
|
uint key1 = (major << 23) | (minor << 14) | (revision << 4);
|
||||||
key1 ^= (revision * revision) << 9;
|
key1 ^= (revision * revision) << 9;
|
||||||
|
|
@ -86,4 +91,28 @@ public readonly struct LoginKeys
|
||||||
|
|
||||||
return new LoginKeys(key1, key2);
|
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)
|
// Managed socket with buffers (handles lifecycle automatically)
|
||||||
internal RingSocket _socket;
|
internal RingSocket _socket;
|
||||||
|
|
||||||
// General packet throttle state (used for other throttled packets)
|
|
||||||
internal bool _isThrottled;
|
|
||||||
|
|
||||||
private IAccount _account;
|
private IAccount _account;
|
||||||
|
|
||||||
internal enum ParserState
|
internal enum ParserState
|
||||||
|
|
@ -664,8 +661,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
_encryption = loginEncryption;
|
_encryption = loginEncryption;
|
||||||
|
|
||||||
// Decrypt the buffer in place for processing
|
// Decrypt the buffer in place for processing
|
||||||
var mutableBuffer = _socket.RecvBuffer.GetReadSpan();
|
loginEncryption.ClientDecrypt(buffer[..62]);
|
||||||
loginEncryption.ClientDecrypt(mutableBuffer[..62]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now process as normal (first byte should now be 0x80)
|
// 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:
|
case ProtocolState.GameServer_AwaitingGameServerLogin:
|
||||||
{
|
{
|
||||||
// Some clients send 0x80 on game server connection
|
// Some clients send 0x80 on game server connection
|
||||||
if (packetId == 0x80)
|
if (packetId == 0x80 || length == 62)
|
||||||
{
|
{
|
||||||
goto case ProtocolState.LoginServer_AwaitingLogin;
|
goto case ProtocolState.LoginServer_AwaitingLogin;
|
||||||
}
|
}
|
||||||
|
|
@ -767,8 +763,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
_encryption = gameEncryption;
|
_encryption = gameEncryption;
|
||||||
|
|
||||||
// Decrypt the buffer in place for processing
|
// Decrypt the buffer in place for processing
|
||||||
var mutableBuffer = _socket.RecvBuffer.GetReadSpan();
|
gameEncryption.ClientDecrypt(buffer[..65]);
|
||||||
gameEncryption.ClientDecrypt(mutableBuffer[..65]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now process as normal (first byte should now be 0x91)
|
// Now process as normal (first byte should now be 0x91)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue