Чит на телепортацию в 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. У верховых позиций — проверка владения. У позиции игрока — ничего.Читайте также
Сообщить об ошибке