diff --git a/Library/RSBot.Core/Components/ClientManager.cs b/Library/RSBot.Core/Components/ClientManager.cs index 3f87181a..d3692061 100644 --- a/Library/RSBot.Core/Components/ClientManager.cs +++ b/Library/RSBot.Core/Components/ClientManager.cs @@ -14,6 +14,15 @@ namespace RSBot.Core.Components; public class ClientManager { + private const int SroClientImageBase = 0x00400000; + private static readonly (int Address, byte[] Expected, byte[] Patch)[] ChineseClientPatches = + { + (0x0060C6DB, new byte[] { 0x75, 0x2E }, new byte[] { 0xEB, 0x2E }), + (0x0060C6A1, new byte[] { 0x75, 0x26 }, new byte[] { 0xEB, 0x26 }), + (0x0067BF56, new byte[] { 0x75, 0x2C }, new byte[] { 0xEB, 0x2C }), + (0x00AD449E, new byte[] { 0x68, 0x5C, 0x54, 0xE1, 0x00 }, new byte[] { 0xEB, 0x2D, 0x90, 0x90, 0x90 }), + }; + /// /// The client process /// @@ -153,6 +162,13 @@ out var pi WaitForSingleObject(remoteThread, uint.MaxValue); + if (Game.ClientType == GameClientType.Chinese) + { + var sroProcess = System.Diagnostics.Process.GetProcessById((int)pi.dwProcessId); + WaitForChineseClientPatchTargets(sroProcess, pi.hThread); + ApplyChineseClientPatch(sroProcess, pi); + } + VirtualFreeEx(handle, remotePath, 0, MEM_RELEASE); CloseHandle(remoteThread); @@ -190,10 +206,6 @@ public static void BypassLauncherCheck(Process process, PROCESS_INFORMATION pi) out _ ); - var patchNop = new byte[] { 0x90, 0x90 }; - var patchNop2 = new byte[] { 0x90, 0x90, 0x90, 0x90, 0x90 }; - var patchJmp = new byte[] { 0xEB }; - string signature = "55 8B EC 83 EC ?? 8B 45 ?? 50 E8 ?? ?? ?? ?? 83 C4 04 89 45 ?? 8B 4D ?? 89 4D ?? 68 ?? ?? ?? ?? 6A 00 6A 00"; @@ -367,6 +379,130 @@ out _ GC.Collect(); } + /// + /// Runs the suspended Chinese client until the target bytes are unpacked and readable. + /// On return the primary thread is left suspended so the caller can patch and resume. + /// + private static void WaitForChineseClientPatchTargets(Process process, IntPtr hThread) + { + const int sliceMs = 8; + const int maxWaitMs = 5000; + + var sw = Stopwatch.StartNew(); + + while (true) + { + ResumeThread(hThread); + Thread.Sleep(sliceMs); + SuspendThread(hThread); + + if (process.HasExited) + return; + + process.Refresh(); + + if (ChineseClientPatchTargetsReady(process.Handle, process.MainModule.BaseAddress.ToInt32())) + return; + + if (sw.ElapsedMilliseconds >= maxWaitMs) + { + Log.Warn("Chinese client patch: unpack timeout."); + return; + } + } + } + + private static bool ChineseClientPatchTargetsReady(IntPtr processHandle, int moduleBaseAddress) + { + foreach (var patch in ChineseClientPatches) + { + if ( + !TryReadClientBytes( + processHandle, + moduleBaseAddress, + patch.Address, + patch.Expected.Length, + out var current + ) + ) + return false; + + if (!current.SequenceEqual(patch.Expected) && !current.SequenceEqual(patch.Patch)) + return false; + } + + return true; + } + + /// + /// Applies fixed in-memory patches for the Chinese sro_client.exe. + /// + private static void ApplyChineseClientPatch(Process process, PROCESS_INFORMATION pi) + { + int moduleBaseAddress = process.MainModule.BaseAddress.ToInt32(); + + foreach (var patch in ChineseClientPatches) + PatchKnownClientBytes(pi.hProcess, moduleBaseAddress, patch.Address, patch.Expected, patch.Patch); + } + + private static void PatchKnownClientBytes( + IntPtr processHandle, + int moduleBaseAddress, + int clientAddress, + byte[] expected, + byte[] patch + ) + { + if (!TryReadClientBytes(processHandle, moduleBaseAddress, clientAddress, expected.Length, out var current)) + { + Log.Warn($"Chinese client patch: could not read 0x{clientAddress:X8}."); + return; + } + + if (current.SequenceEqual(patch)) + return; + + if (!current.SequenceEqual(expected)) + { + Log.Warn( + $"Chinese client patch: unexpected bytes at 0x{clientAddress:X8}. Expected {FormatBytes(expected)}, got {FormatBytes(current)}." + ); + return; + } + + PatchProcessMemory(processHandle, GetClientAddress(moduleBaseAddress, clientAddress), patch); + } + + private static bool TryReadClientBytes( + IntPtr processHandle, + int moduleBaseAddress, + int clientAddress, + int length, + out byte[] bytes + ) + { + bytes = new byte[length]; + + return ReadProcessMemory( + processHandle, + GetClientAddress(moduleBaseAddress, clientAddress), + bytes, + bytes.Length, + out var read + ) + && read.ToInt32() == bytes.Length; + } + + private static IntPtr GetClientAddress(int moduleBaseAddress, int clientAddress) + { + return (IntPtr)(moduleBaseAddress + clientAddress - SroClientImageBase); + } + + private static string FormatBytes(byte[] bytes) + { + return BitConverter.ToString(bytes).Replace("-", " "); + } + /// /// Finds the XIGNCODE init call anchor in WinMain by locating the standalone "XIGNCODE" Unicode /// string and its reference in the code pattern: push 0; push VA("XIGNCODE"); push code_string; call func. diff --git a/Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs b/Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs index 729cdc4e..78f60a45 100644 --- a/Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs +++ b/Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using RSBot.Core.Event; using RSBot.Core.Objects; using RSBot.Core.Objects.Item; @@ -30,14 +30,17 @@ internal class InventoryUpdateItemResponse : IPacketHandler public void Invoke(Packet packet) { if (Game.ClientType == GameClientType.Global || Game.ClientType == GameClientType.RuSro) - if (packet.ReadByte() != 0) //sometimes appears 9 with unknown structure - return; - - var sourceSlot = packet.ReadByte(); + { + InvokeModernClients(packet); + return; + } - if (Game.ClientType == GameClientType.Global || Game.ClientType == GameClientType.RuSro) - packet.ReadByte(); //0 - normal, 2 - item disappearing + InvokeLegacy(packet); + } + private static void InvokeLegacy(Packet packet) + { + var sourceSlot = packet.ReadByte(); var itemUpdateFlag = (ItemUpdateFlag)packet.ReadByte(); var item = Game.Player.Inventory.GetItemAt(sourceSlot); @@ -79,4 +82,136 @@ public void Invoke(Packet packet) EventManager.FireEvent("OnUpdateInventoryItem", sourceSlot); } + + private static void InvokeModernClients(Packet packet) + { + var updateType = packet.ReadByte(); + + if (updateType == 9) + { + var objectId = packet.ReadUInt(); + var containerSlot = packet.ReadByte(); + var updateFlags = (ModernItemUpdateFlag)packet.ReadUShort(); + var inventory = GetInventoryForObject(objectId); + var containerItem = inventory?.GetItemAt(containerSlot); + + ApplyModernClientsContainerUpdate(packet, containerItem, updateFlags); + + if (containerItem != null && ReferenceEquals(inventory, Game.Player.Inventory)) + EventManager.FireEvent("OnUpdateInventoryItem", containerSlot); + + return; + } + + var sourceSlot = packet.ReadByte(); + var itemUpdateFlags = (ModernItemUpdateFlag)packet.ReadUShort(); + var item = Game.Player.Inventory.GetItemAt(sourceSlot); + if (item == null) + return; + + ApplyModernClientsItemUpdate(packet, item, sourceSlot, itemUpdateFlags, updateType); + EventManager.FireEvent("OnUpdateInventoryItem", sourceSlot); + } + + private static void ApplyModernClientsContainerUpdate( + Packet packet, + InventoryItem item, + ModernItemUpdateFlag updateFlags + ) + { + if (updateFlags.HasFlag(ModernItemUpdateFlag.Durability)) + { + var durability = packet.ReadUInt(); + if (item != null) + item.Durability = durability; + } + + if (updateFlags.HasFlag(ModernItemUpdateFlag.BindingOptions)) + ReadBindingOptions(packet, item); + } + + private static void ApplyModernClientsItemUpdate( + Packet packet, + InventoryItem item, + byte sourceSlot, + ModernItemUpdateFlag updateFlags, + byte updateType + ) + { + if (updateFlags.HasFlag(ModernItemUpdateFlag.RefObjID)) + item.ItemId = packet.ReadUInt(); + + if (updateFlags.HasFlag(ModernItemUpdateFlag.Quantity)) + item.Amount = packet.ReadUShort(); + + if (updateFlags.HasFlag(ModernItemUpdateFlag.Durability)) + item.Durability = packet.ReadUInt(); + + if (updateFlags.HasFlag(ModernItemUpdateFlag.OptLevel)) + item.OptLevel = packet.ReadByte(); + + if (updateFlags.HasFlag(ModernItemUpdateFlag.Variance)) + item.Attributes = new ItemAttributesInfo(packet.ReadULong()); + + if (updateFlags.HasFlag(ModernItemUpdateFlag.MagParams) && item.Record != null) + ReadMagicOptions(packet, item); + + if (updateFlags.HasFlag(ModernItemUpdateFlag.BindingOptions) && (updateType == 0 || updateType == 8)) + ReadBindingOptions(packet, item); + + if (updateFlags.HasFlag(ModernItemUpdateFlag.RemainTime)) + packet.ReadUInt(); + + if (updateFlags.HasFlag(ModernItemUpdateFlag.ItemState)) + { + var itemState = (InventoryItemState)packet.ReadByte(); + + if (sourceSlot >= 0x11) + item.State = itemState; + } + } + + private static void ReadMagicOptions(Packet packet, InventoryItem item) + { + item.MagicOptions = new List(); + + var magParamCount = packet.ReadByte(); + for (var i = 0; i < magParamCount; i++) + item.MagicOptions.Add(MagicOptionInfo.FromPacket(packet)); + } + + private static void ReadBindingOptions(Packet packet, InventoryItem item) + { + item ??= new InventoryItem(); + item.BindingOptions = new List(); + + for (var bindingIndex = 0; bindingIndex < 4; bindingIndex++) + { + var bindingType = (BindingOptionType)packet.ReadByte(); + var bindingAmount = packet.ReadByte(); + + for (var i = 0; i < bindingAmount; i++) + item.BindingOptions.Add(BindingOption.FromPacket(packet, bindingType)); + } + } + + private static InventoryItemCollection GetInventoryForObject(uint objectId) + { + if (Game.Player.UniqueId == objectId) + return Game.Player.Inventory; + + if (Game.Player.AbilityPet?.UniqueId == objectId) + return Game.Player.AbilityPet.Inventory; + + if (Game.Player.Fellow?.UniqueId == objectId) + return Game.Player.Fellow.Inventory; + + if (Game.Player.Growth?.UniqueId == objectId) + return Game.Player.Growth.Inventory; + + if (Game.Player.Vehicle?.UniqueId == objectId) + return Game.Player.Vehicle.Inventory; + + return null; + } } diff --git a/Library/RSBot.Core/Objects/Inventory/Item/ModernItemUpdateFlag.cs b/Library/RSBot.Core/Objects/Inventory/Item/ModernItemUpdateFlag.cs new file mode 100644 index 00000000..8b03a83a --- /dev/null +++ b/Library/RSBot.Core/Objects/Inventory/Item/ModernItemUpdateFlag.cs @@ -0,0 +1,17 @@ +using System; + +namespace RSBot.Core.Objects.Item; + +[Flags] +public enum ModernItemUpdateFlag : ushort +{ + RefObjID = 0x0001, + Quantity = 0x0002, + Durability = 0x0010, + OptLevel = 0x0020, + Variance = 0x0040, + MagParams = 0x0080, + BindingOptions = 0x0100, + RemainTime = 0x0200, + ItemState = 0x8000, +} diff --git a/Plugins/RSBot.General/Components/AutoLogin.cs b/Plugins/RSBot.General/Components/AutoLogin.cs index 5e76661e..5c0f567f 100644 --- a/Plugins/RSBot.General/Components/AutoLogin.cs +++ b/Plugins/RSBot.General/Components/AutoLogin.cs @@ -191,7 +191,15 @@ private static void SendLoginRequest(Account account, Server server) loginPacket.WriteUShort(server.Id); - if (opcode == 0x610A) + if (Game.ClientType == GameClientType.Chinese) + { + if (!ChineseGatewayLogin.TryWriteVerificationAndTicket(loginPacket, account.Username)) + { + _busy = false; + return; + } + } + else if (opcode == 0x610A) loginPacket.WriteByte(account.Channel); PacketManager.SendPacket(loginPacket, PacketDestination.Server); diff --git a/Plugins/RSBot.General/Components/ChineseGatewayLogin.cs b/Plugins/RSBot.General/Components/ChineseGatewayLogin.cs new file mode 100644 index 00000000..71953aae --- /dev/null +++ b/Plugins/RSBot.General/Components/ChineseGatewayLogin.cs @@ -0,0 +1,163 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using RSBot.Core; +using RSBot.Core.Network; + +namespace RSBot.General.Components; + +internal static class ChineseGatewayLogin +{ + private const int MachineTokenKeySeed = 0x0118860C; + private const string TicketKey = "UGCKernel"; + private const int MaxSilpsetLength = 4096; + + private static readonly byte[] TicketMarker = { 0x11, 0x00, 0x25, 0x56, 0x00, 0x33 }; + private static readonly Lazy TicketSeed = new(CreateTicketSeed); + + public static bool TryWriteVerificationAndTicket(Packet packet, string username) + { + try + { + var ticket = BuildTicket(); + + packet.WriteUInt(ComputeVerifyResult(username)); + packet.WriteUInt((uint)ticket.Length); + packet.WriteBytes(ticket); + + return true; + } + catch (Exception ex) + { + Log.Error($"Chinese gateway ticket could not be created: {ex.Message}"); + return false; + } + } + + private static byte[] BuildTicket() + { + var machineToken = BuildMachineToken(); + var silpsetToken = ReadSilpsetToken(); + + if (machineToken.Length > ushort.MaxValue || silpsetToken.Length > ushort.MaxValue) + throw new InvalidOperationException("Chinese gateway ticket token is too large."); + + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream); + + writer.Write((long)(int)TicketSeed.Value); + writer.Write(TicketMarker); + writer.Write((ushort)machineToken.Length); + writer.Write(machineToken); + writer.Write((ushort)silpsetToken.Length); + writer.Write(silpsetToken); + + var ticket = stream.ToArray(); + Rc4Xor(ticket, Encoding.ASCII.GetBytes(TicketKey)); + + return ticket; + } + + private static byte[] BuildMachineToken() + { + string hostName; + try + { + hostName = Dns.GetHostName(); + } + catch + { + hostName = Environment.MachineName; + if (hostName.Length > 16) + hostName = hostName.Substring(0, 16); + } + + if (string.IsNullOrEmpty(hostName)) + throw new InvalidOperationException("Computer name is empty."); + + if (hostName.Length > byte.MaxValue) + hostName = hostName.Substring(0, byte.MaxValue); + + var token = Encoding.ASCII.GetBytes(hostName); + var key = Encoding.ASCII.GetBytes(MachineTokenKeySeed.ToString(CultureInfo.InvariantCulture)); + Rc4Xor(token, key); + + return token; + } + + private static byte[] ReadSilpsetToken() + { + var silkroadDirectory = GlobalConfig.Get("RSBot.SilkroadDirectory"); + if (string.IsNullOrWhiteSpace(silkroadDirectory)) + throw new InvalidOperationException("Silkroad directory is not configured."); + + var path = Path.Combine(silkroadDirectory, "setting", "SRsilpset.dat"); + if (!File.Exists(path)) + throw new FileNotFoundException("SRsilpset.dat was not found.", path); + + var bytes = File.ReadAllBytes(path); + if (bytes.Length > MaxSilpsetLength) + throw new InvalidOperationException("SRsilpset.dat is larger than the client buffer."); + + return bytes; + } + + private static uint ComputeVerifyResult(string username) + { + if (string.IsNullOrEmpty(username)) + return 0; + + uint crc = 0xFFFFFFFF; + var bytes = Encoding.GetEncoding(950).GetBytes(username); + + foreach (var value in bytes) + { + crc ^= value; + + for (var bit = 0; bit < 8; bit++) + crc = (crc & 1) == 0 ? crc >> 1 : (crc >> 1) ^ 0xEDB88320; + } + + return ~crc; + } + + private static uint CreateTicketSeed() + { + if (CoCreateGuid(out var guid) != 0) + guid = Guid.NewGuid(); + + var bytes = guid.ToByteArray(); + return BitConverter.ToUInt32(bytes, 0) + BitConverter.ToUInt16(bytes, 4) + BitConverter.ToUInt16(bytes, 6); + } + + private static void Rc4Xor(byte[] buffer, byte[] key) + { + var state = new byte[256]; + for (var i = 0; i < state.Length; i++) + state[i] = (byte)i; + + var j = 0; + for (var i = 0; i < state.Length; i++) + { + j = (j + state[i] + key[i % key.Length]) & 0xFF; + (state[i], state[j]) = (state[j], state[i]); + } + + var x = 0; + j = 0; + for (var index = 0; index < buffer.Length; index++) + { + x = (x + 1) & 0xFF; + j = (j + state[x]) & 0xFF; + (state[x], state[j]) = (state[j], state[x]); + + buffer[index] ^= state[(state[x] + state[j]) & 0xFF]; + } + } + + [DllImport("ole32.dll")] + private static extern int CoCreateGuid(out Guid guid); +} diff --git a/Plugins/RSBot.General/PacketHandler/GatewayLoginRequest.cs b/Plugins/RSBot.General/PacketHandler/GatewayLoginRequest.cs index 32727d31..67c92945 100644 --- a/Plugins/RSBot.General/PacketHandler/GatewayLoginRequest.cs +++ b/Plugins/RSBot.General/PacketHandler/GatewayLoginRequest.cs @@ -44,7 +44,18 @@ public void Invoke(Packet packet) var shardId = packet.ReadUShort(); - if (Game.ClientType >= GameClientType.Chinese) + if (Game.ClientType == GameClientType.Chinese) + { + if (packet.Remaining >= 8) + { + packet.ReadUInt(); // verify_result + var ticketLength = packet.ReadUInt(); + + if (ticketLength <= packet.Remaining) + packet.ReadBytes((int)ticketLength); // ticket + } + } + else if (Game.ClientType >= GameClientType.Chinese) packet.ReadByte(); // channel Serverlist.SetJoining(shardId); diff --git a/Plugins/RSBot.General/PacketHandler/GatewayServerListResponse.cs b/Plugins/RSBot.General/PacketHandler/GatewayServerListResponse.cs index a13ea943..d9c0e993 100644 --- a/Plugins/RSBot.General/PacketHandler/GatewayServerListResponse.cs +++ b/Plugins/RSBot.General/PacketHandler/GatewayServerListResponse.cs @@ -58,7 +58,7 @@ public void Invoke(Packet packet) byte status; - if (Game.ClientType < GameClientType.Global) + if (Game.ClientType < GameClientType.Chinese) { currentCapacity = packet.ReadUShort(); maxCapacity = packet.ReadUShort(); @@ -74,6 +74,9 @@ public void Invoke(Packet packet) if (Game.ClientType == GameClientType.Global) serverName = serverName.Remove(0, 1); + if (Game.ClientType == GameClientType.Chinese && serverName.EndsWith("#$C")) + serverName = serverName.Remove(serverName.Length - 3); + if (Game.ClientType == GameClientType.VTC_Game) if (serverName.EndsWith("Thien_Kim")) serverName = serverName.Remove(0, 3);