Чит на телепортацію у Vintage Story: сервер довіряє вашим координатам
Зміст
Розбір синхронізації позиції у Vintage Story 1.21.6: координати клієнта, досліджений серверний обробник та обмеження прикладу. Протестовано на Vintage Story v1.21.6 (Stable), .NET 8.0. Розбір обмежено цією версією та показаними нижче шляхами обробки. Це технічна стаття для вивчення будови гри. Перевіряйте описані прийоми лише в одиночній грі, на власному сервері або з дозволу власника сервера. Дотримуйтеся правил сервера. Це продовження статті Чит на креативний режим у Vintage Story через реверс-інжиніринг. Мод на креативний режим використовує Harmony-патчі для перехоплення пакетів. Експлойт з телепортацією простіший — жодних патчів, просто присвоєння поля. Розбираємо клієнтський мод телепортації в перевіреній конфігурації. Для телепортації до гравця його сутність має бути завантажена на клієнті. У цьому прикладі не використовуються права адміністратора, серверний мод або Harmony-патчі. Ось як це виглядає на реальному сервері: Показаний шлях Кожен ігровий тик (~15мс) клієнт надсилає UDP-пакет з поточною позицією гравця: Пакет позиції формується в Сервер отримує цей пакет у А Показаний шлях Показаний обробник перевіряє версію позиції: Ця перевірка У показаному шляху немає перевірки В тому ж коді є перевірки А Інфраструктура для валідації існує. Її застосовано до блоків та верхових сутностей. До позиції гравця — ні. Ми встановлюємо і Жодних Harmony-патчів. Жодного створення пакетів вручну. Жодної рефлексії. Пишемо в публічне поле і дозволяємо існуючому мережевому циклу гри доставити його. Vintage Story використовує два різних початки відліку для координат. Помилка телепортує вас на інший край карти. Конвертація: Для стандартного світу 1024k спавн знаходиться приблизно в UI гри віднімає позицію спавну при відображенні координат: Виправлення в нашій функції телепортації: Мод реєструє три клієнтські команди чату через Ці повідомлення обробляються двома шляхами: Шукає в Перебирає всіх онлайн-гравців, показує їхні позиції відносно спавну (як в UI гри) та розраховує відстань: Приймає UI-координати (відносно спавну), конвертує в абсолютні та телепортує: У показаному шляху сервер обробляє оновлення як пакет руху. Це не робить переміщення непомітним: сервер отримує змінені координати. За цими фрагментами не можна встановити, чи реєструють або виявляють зміну інші системи. У вивченому шляху синхронізації клієнт обчислює рух і повідомляє координати. Показаний серверний обробник приймає їх після перевірки версії позиції, не перевіряючи швидкість чи відстань. Цей висновок стосується лише вивченого шляху та перевіреної конфігурації. Це свідоме проєктне рішення. Серверно-авторитетна фізика вимагала б: Приймання координат клієнта без перевірки руху дозволяє модифікованому клієнту повідомляти координати поза звичайними обмеженнями руху. З точки зору інших гравців, ви зникаєте з однієї точки і з’являєтесь в іншій на наступному серверному тику (~15мс). Немає анімації переміщення, немає сліду, немає переходу — ви просто виникаєте на цільовій позиції. Сервер транслює вашу нову позицію через UDP-пакети Запису в серверному логу немає. Хак телепортації — 12 рядків присвоєння позиції + конвертація координат. Жодних Harmony-патчів, жодних маніпуляцій з пакетами — просто запис у Чому це працює: Підхід із Harmony розібрано в статті про режим Creative: у ній локальний стан клієнта відокремлено від серверних прав. Обидві статті зібрано в розділі теми Vintage Story. Знайшли помилку? Напишіть мені в LinkedIn: додайте посилання на сторінку й зазначте, що потрібно виправити.Що ми робимо
HandlePlayerPosition перевіряє PositionVersion, потім копіює координати клієнта в entity.ServerPos без перевірки відстані, швидкості чи колізій. У перевіреній конфігурації зміни entity.Pos передаються серверу цим шляхом. За цим фрагментом не можна судити про перевірки чи моніторинг в інших частинах системи.Розбираємо синхронізацію позиції
Звичайний потік
public void SendPlayerPositionPacket()
{
Packet_EntityPosition entityPositionPacket =
ServerPackets.getEntityPositionPacket(game.EntityPlayer.Pos, game.EntityPlayer, 0);
Packet_UdpPacket udpPacket = new Packet_UdpPacket
{
Id = 2,
EntityPosition = entityPositionPacket
};
if (game.FallBackToTcp)
game.SendPacketClient(new Packet_Client { Id = 35, UdpPacket = udpPacket });
else
game.UdpNetClient.Send(udpPacket);
}getEntityPositionPacket, який серіалізує X/Y/Z, yaw, pitch, motion та лічильник PositionVersion:public static Packet_EntityPosition getEntityPositionPacket(EntityPos pos, Entity entity, int tick)
{
return new Packet_EntityPosition
{
EntityId = entity.EntityId,
X = CollectibleNet.SerializeDoublePrecise(pos.X),
Y = CollectibleNet.SerializeDoublePrecise(pos.Y),
Z = CollectibleNet.SerializeDoublePrecise(pos.Z),
Yaw = CollectibleNet.SerializeFloatPrecise(pos.Yaw),
Pitch = CollectibleNet.SerializeFloatPrecise(pos.Pitch),
Roll = CollectibleNet.SerializeFloatPrecise(pos.Roll),
MotionX = CollectibleNet.SerializeDoublePrecise(pos.Motion.X),
MotionY = CollectibleNet.SerializeDoublePrecise(pos.Motion.Y),
MotionZ = CollectibleNet.SerializeDoublePrecise(pos.Motion.Z),
PositionVersion = entity.WatchedAttributes.GetInt("positionVersionNumber"),
Tick = tick,
// ... теги, поворот тіла, контроли
};
}Обробка на сервері
ServerUdpNetwork.HandlePlayerPosition:public void HandlePlayerPosition(Packet_EntityPosition packet, ServerPlayer player)
{
if (packet == null)
return;
EntityPlayer entity = player.Entity;
int num = entity.WatchedAttributes.GetInt("positionVersionNumber");
if (packet.PositionVersion < num)
return;
player.LastReceivedClientPosition = server.ElapsedMilliseconds;
entity.ServerPos.SetFromPacket(packet, entity);
entity.Pos.SetFromPacket(packet, entity);
// ... повідомити фізичні поведінки, транслювати іншим гравцям
}SetFromPacket — це просто пряме присвоєння:public static void SetFromPacket(this EntityPos pos, Packet_EntityPosition packet, Entity entity)
{
pos.X = CollectibleNet.DeserializeDoublePrecise(packet.X);
pos.Y = CollectibleNet.DeserializeDoublePrecise(packet.Y);
pos.Z = CollectibleNet.DeserializeDoublePrecise(packet.Z);
pos.Yaw = CollectibleNet.DeserializeFloatPrecise(packet.Yaw);
pos.Pitch = CollectibleNet.DeserializeFloatPrecise(packet.Pitch);
pos.Roll = CollectibleNet.DeserializeFloatPrecise(packet.Roll);
pos.HeadYaw = CollectibleNet.DeserializeFloatPrecise(packet.HeadYaw);
pos.HeadPitch = CollectibleNet.DeserializeFloatPrecise(packet.HeadPitch);
pos.Motion.X = CollectibleNet.DeserializeDoublePrecise(packet.MotionX);
pos.Motion.Y = CollectibleNet.DeserializeDoublePrecise(packet.MotionY);
pos.Motion.Z = CollectibleNet.DeserializeDoublePrecise(packet.MotionZ);
// ...
}HandlePlayerPosition перевіряє PositionVersion, потім копіює координати клієнта в entity.ServerPos без перевірки відстані, швидкості чи колізій. У перевіреній конфігурації зміни entity.Pos передаються серверу цим шляхом. За цим фрагментом не можна судити про перевірки чи моніторинг в інших частинах системи.Яких перевірок немає в показаному обробнику
int num = entity.WatchedAttributes.GetInt("positionVersionNumber");
if (packet.PositionVersion < num)
return; // відхилити застарілі/повторно надіслані пакетиPositionVersion запобігає перезапису нових позицій старими пакетами (наприклад, після серверного телепорту). Вона не запобігає:AntiAbuse або звернення до опції Config для перевірки позиції. Інші обробники чи серверні моди можуть додавати перевірки.Порівняння з перевірками встановлення блоків
AntiAbuse для встановлення блоків:if (server.Config.AntiAbuse != EnumProtectionLevel.Off && !flag
&& num > (double)((player.WorldData.PickingRange + 0.7f)
* (player.WorldData.PickingRange + 0.7f)))
{
ServerMain.Logger.Audit("Client {0} tried to break/place a block out of range");
return false;
}HandleMountPosition — який обробляє верхових сутностей — перевіряє володіння перед прийняттям координат:Entity entityById = server.api.World.GetEntityById(packet.EntityId);
IMountable mountable = entityById?.GetInterface<IMountable>();
if (mountable == null || !mountable.IsMountedBy(player.Entity))
{
return; // відхилити — ви не їдете на цій сутності
}
// лише потім: entityById.ServerPos.SetFromPacket(packet, entityById);Експлойт
Ядро: 12 рядків
private void TeleportTo(double x, double y, double z)
{
var player = clientApi.World.Player;
if (player?.Entity == null) return;
EntityPlayer entity = player.Entity;
entity.Pos.X = x;
entity.Pos.Y = y;
entity.Pos.Z = z;
entity.Pos.Motion.Set(0, 0, 0);
entity.ServerPos.X = x;
entity.ServerPos.Y = y;
entity.ServerPos.Z = z;
entity.ServerPos.Motion.Set(0, 0, 0);
}Pos (клієнтська позиція рендерингу), і ServerPos (що клієнт вважає відомим серверу). Наступного тику SendPlayerPositionPacket зчитує entity.Pos, серіалізує та надсилає по UDP. Сервер викликає SetFromPacket — і ми на місці.Пастка системи координат
Контекст Початок відліку Приклад UI гри (оверлей F1, чат) Точка спавну світу (500, 148, -200)Внутрішні ( entity.Pos)Абсолютний початок світу (512500, 148, 511800)Внутрішні = UI-координати + позиція спавну
UI-координати = Внутрішні - позиція спавну(512000, ?, 512000). Отже UI-координата (500, 148, -200) насправді абсолютна (512500, 148, 511800).asBlockPos.Sub(game.SpawnPosition.AsBlockPos.X, 0, game.SpawnPosition.AsBlockPos.Z);var spawn = clientApi.World.DefaultSpawnPosition;
double absX = uiX + spawn.X;
double absY = uiY; // Y не зміщується
double absZ = uiZ + spawn.Z;
TeleportTo(absX, absY, absZ);Команди
api.RegisterCommand. Команди з префіксом . обробляються локально; спричинені ними зміни позиції все одно надсилаються серверу.HudDialogChat.HandleClientMessage перевіряє message.StartsWith("."), викликає HandleClientCommand і повертається без надсилання в мережу.ServerMain.HandleChatMessage мовчки відкидає будь-яке повідомлення, що починається з . — навіть якщо клієнт якимось чином його надіслав..tp <name> — телепортація до гравцяclientApi.World.AllOnlinePlayers відповідне ім’я (без урахування регістру, підтримка часткового збігу), зчитує targetPlayer.Entity.Pos та викликає TeleportTo:private void OnTpCommand(int groupId, CmdArgs args)
{
string targetName = args.PopWord();
IPlayer targetPlayer = null;
foreach (var p in clientApi.World.AllOnlinePlayers)
{
if (p.PlayerName.Equals(targetName, StringComparison.OrdinalIgnoreCase))
{
targetPlayer = p;
break;
}
}
// ... запасний варіант — частковий збіг через StartsWith ...
EntityPos targetPos = targetPlayer.Entity.Pos;
TeleportTo(targetPos.X, targetPos.Y, targetPos.Z);
}.tplist — список усіх гравців з координатами[BHacks] === Online Players ===
Me: (512.3, 148.0, -203.7) [0m away]
SomePlayer: (-1024.5, 155.0, 800.2) [1847m away]
AnotherOne: (200.0, 72.0, 100.5) [380m away].tppos <x> <y> <z> — телепортація за координатами.tppos 500 148 -200
[BHacks] Teleported to (500.0, 148.0, -200.0)Повний потік пакетів
Client Server
| |
| .tp SomePlayer |
| (оброблено локально, не надіслано серверу) |
| |
| entity.Pos.X = target.X |
| entity.Pos.Y = target.Y |
| entity.Pos.Z = target.Z |
| |
| ~15мс потому: звичайний тик позиції |
| |
| UDP Packet { |
| Id: 2 |
| X: target.X (serialized) |
| Y: target.Y (serialized) |
| Z: target.Z (serialized) |
| PositionVersion: current |
| } |
| ----------------------------------------------> |
| |
| | HandlePlayerPosition:
| | перевірка версії: OK
| | SetFromPacket()
| | // немає перевірки дистанції
| | // немає перевірки швидкості
| | // позицію прийнято
| |
| | Сервер тепер вважає гравця
| | в цільових координатах.
| | Чанки завантажуються навколо.
| | Інші гравці бачать вас там.
| |Чому це працює
Обмеження
Телепортація лише до завантажених гравців
.tp <name> зчитує координати з targetPlayer.Entity.Pos. Але Entity заповнений лише для гравців, чиї сутності завантажені на вашому клієнті — зазвичай ті, хто в межах вашої дальності промальовки. Далекі гравці присутні в AllOnlinePlayers з Entity == null:if (targetPlayer.Entity == null)
{
clientApi.ShowChatMessage("[BHacks] Player has no entity (not fully loaded?)");
return;
}.tppos не має такого обмеження — можна телепортуватися за будь-якими координатами, завантаженими чи ні. Сервер завантажить чанки навколо вашої нової позиції.Що бачать інші гравці
BulkEntityPosition усім гравцям, що відстежують вашу сутність. Якщо ви телепортуєтесь за межі чиєїсь дальності промальовки, ви деспавнитесь на їхньому клієнті. Якщо телепортуєтесь у чиюсь дальність промальовки — з’явитесь.HandlePlayerPosition не логує зміни позиції, незалежно від дистанції. Єдиний спосіб, яким адмін може помітити — візуально спостерігаючи за вами або перевіряючи ваші координати вручну.Підсумок
entity.Pos та очікування наступного мережевого тику.SendPlayerPositionPacket зчитує з entity.Pos кожні ~15мс і надсилає серверу.HandlePlayerPosition викликає SetFromPacket без валідації дистанції, швидкості чи колізій.. гарантує, що команди не потраплять на сервер — клієнт перехоплює їх, а сервер відкидає як запасний варіант.DefaultSpawnPosition, ви опинитесь на іншому краї карти.AntiAbuse. У верхових позицій — перевірка володіння. У позиції гравця — нічого.Читайте також
Повідомити про помилку