[Разработка мобильных приложений, Разработка игр, Unity, Дизайн игр] Первые пять шагов для перелома ситуации с читерами в PvP-шутере

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
18-Мар-2021 23:35


Мы прошли долгий путь от появления в игре первых читеров до полного пересмотра подхода к разработке, чтобы создавать защищенные по умолчанию проекты. О том, как в игре появились читеры, я рассказал в прошлом материале. И там же привел список подзадач, которые выкатили одновременно, чтобы закрыть вопрос со взломами, — от обфускации кода до подсчета хеша всех библиотек и надежной системы бана.Итак, эти шаги:
  • Обфускация.
  • Хранение данных.
  • Миграция прогресса.
  • Система бана.
  • Подсчет хеша всех библиотек.
  • Защита от переподписывания версий.
  • Photon Plugin.
  • Серверная валидация инаппов.
  • Защита от взлома оперативной памяти.
  • Собственная аналитика.
  • И одновременный релиз всех решений.
Сегодня поговорим про первые пять пунктов.Для удобства я пронумеровал все разделы, но напомню, что приведенные ниже решения мы разрабатывали и выкатили одновременно. Потому что постепенный ввод изменений сильно облегчит взломщикам отслеживание апдейтов. Шаг №1. ОбфускацияПервым делом ввели новые стандарты написания кода, позволяющие плагину обфускации сделать свое дело, и переписали под них всё приложение. Задача не особо сложная, но ресурсоемкая, потому что кодовая база к этому времени уже была довольно большая.Для защиты приложения от взлома обфускация играет большую роль, так как на всех платформах можно изменять код приложения, подменяя результаты выполнения различных свойств и методов тем или иным способом. И чем проще он читается, тем ниже порог для входа. Цена инапов, жизни, урон скорость и так далее — все это нужно обязательно скрывать.Мы используем плагин Beebyte Obfuscator. Но для работы плагинов обфускаторов надо придерживаться определенных правил оформления кода, иначе названия свойств и методов не будут скрываться.Какие-то из приведенных ниже правил были выведены изучением материалов о работе обфускаторов, что-то получили экспериментальным путем, ну а что-то нам подсказал разработчик плагина, с которым мы связывались при внедрении. Наш набор:
  • internal вместо **public** и **private** или **internal** вместо **protected**.
  • Все члены классов, помеченных [Serializable], не обфусцируются.
  • Свести к минимуму использование Parse/ToString для enum (при обфускации результат, как правило, бесполезен), но если это все-таки необходимо, то помечаем атрибутом **[Obfuscation(Exclude = true)]**.
    Например:
[Obfuscation(Exclude = true)]
public enum GameEventItemContainerContentType
{
   None = 0,
   SingleItem = 1,
   ItemsCollection = 2,
     Start = 3,
}
  • По возможности заменяем `const` на `static`. Статики нельзя использовать в switch. Например, вместо internal const string A_B = "my_constant" делать internal static readonly string A_B = "my_constant" либо internal static string A_B { get{...} }.
  • События анимаций — их нужно оставлять/делать public или помечать атрибутом [Obfuscation(Exclude = true)].
  • Лямбды — имя лямбды включает имя того метода/класса, в котором она определена, вне зависимости от модификатора доступа метода или класса. По возможности нужно заменять лямбды методами.
  • Kорутины в IL не обфусцируются. Поэтому их можно обфусцировать вручную, например, назвать vfg45_00.
Сейчас, когда под плагин переделано абсолютное большинство нашего кода, весь новый мы пишем согласно этим правилам. Также периодически пересматриваем дамп сборки со списком методов в проекте на предмет, нет ли каких-то важных необфусцированных методов. Отмечу минус использования обфускации — время сборки неминуемо увеличивается (у нас примерно на 30%), но польза несравнимо выше.На случай, когда надо получить сборку максимально быстро, мы добавили на билд-сервере возможность сборки без обфускации. Но обычно стараемся собирать с ней, потому что есть опасность пропустить баги связанные с обфускацией (обычно это места, где результат выполнения кода зависит от имени метода или энума). Шаг №2. Хранение данныхПри реализации общего плана по защите от взломов игры это был один из самых важных и в то же время масштабных пунктов. Важным, потому что без полного контроля над прогрессом игроков невозможно полноценно защититься от взломов. А масштабным, потому что более чем за пять лет жизни проекта в игре было огромное количество сущностей и функционала, каждый из которых хранил данные в собственном формате, сохранялся беспорядочно и не имел никаких ограничений по количеству обращений к диску на чтение и запись.Для реализации этой задачи необходимо было переработать все функционалы, в которых что-либо сохранялось. На этапе составления плана действий мы попытались найти кого-нибудь с подобным опытом, прошерстили интернет и пообщались с коллегами из других студий, но многие вообще не верили, что это возможно, учитывая количество накопившегося у нас легаси.Что ж, для начала составили критерии, которым должна удовлетворять наша система хранения данных:
  • Пользовательский опыт, когда все действия в игре происходят моментально без задержек на серверную валидацию, не должен пострадать.
  • При запуске приложения одним пользователем на нескольких устройствах игрок должен видеть абсолютно одинаковые состояния.
  • Нельзя допустить, чтобы какие-либо проблемы с сервером приводили к запрету входа в игру.
  • Минимизировать трафик и нагрузку на сервер.
  • После реализации основного этапа поддержка и развитие новых функционалов должны быть максимально простыми и быстрыми.
  • Все должно быть надежным и исключать возможность несанкционированного накручивания прогресса.
  • Желательно оставить возможность пользоваться игрой оффлайн, так как такие режимы востребованы среди наших игроков.
Потом начали составлять план работ. Выделили несколько глобальных этапов:
  • Введение постоянного сокетного соединения клиент-сервер. Используемая до этого связь через https-запросы сильно ограничивала нас в реализации необходимых функционалов. Виделось, что при ней реализовать систему по всем требованиям не получится.
    Тогда опыта использования постоянного сокетного соединения у нас еще не было (предполагалось, что в дальнейшем без соединения нельзя будет находиться в основных разделах игры), поэтому решили обкатать соединение постепенно.
    Сначала сделали фоновое подключение и продублировали часть второстепенных функционалов на сокетное соединение с возможностью удаленного переключения, каким каналом связи клиенту пользоваться. Выкатили на пользователей, настроили сервера и убедились, что соединение работает стабильно. 
    Затем ввели полноценную работу с аккаунтами и добавили поддержку сокетного соединения у большего количества функционалов. До этого момента мы поддерживали оба канала связи. Когда убедились, что новая архитектура держит постоянную связь с сервером и справляется с нагрузками, то можно было выкатывать полноценную работу с прогрессом и все остальные переработанные функционалы.
  • Формат хранения данных определили JSON. Может он и не самый оптимизированный по трафику и работе, но удобен в использовании. Его мы расширяем и часто используем по проекту.
    Глобально прогресс представляет собой Dictionary<int, object>, где каждая пара — это так называемые слоты для хранения данных. Каждый слот служит для хранения своего типа данных: слот валюты, слот инвентаря, слот ачивок и так далее. Ключ — он же номер слота данных — решили сделать интовым, чтобы сократить использование трафика, значение в каждом слоте может иметь свой формат, но это всегда JSON.
    Чтобы все действия на клиенте отрабатывались моментально, прогресс хранится и на клиенте, и на сервере (команды по изменению прогресса пишутся и там, и там). Отрабатывают сначала на клиенте: визуально пользователь видит, что все окей, а в это время на сервер отправляются параметры для команды. Там они проходят необходимые проверки, и если все хорошо, то прогресс меняется аналогичным способом, как и в клиенте. По результату сравниваются итоговые хеши изменившихся слотов на сервере и клиенте, если не сходятся — рвется соединение с клиентом, клиент переавторизовывается и подтягивает последнее валидное состояние слотов.
    Чтобы сократить трафик между клиентом и сервером, для сравнения слотов отправляется хеш слота, а не слот целиком. И только при несовпадении хешей пересылается состояние слота от сервера клиенту при авторизации.
    На случай временной потери интернета (или если, например, приложение закроется до отправки команды на сервер), команды сохраняются локально и при следующем запуске перед началом сравнения слотов на сервере и клиенте сначала отправляются они и только потом хеши имеющихся слотов.
    Чтобы прогресс всегда был в консистентном состоянии, добавили возможность отправки нескольких команд на изменение прогресса в виде одной и назвали это снапшотом. Если не проходит валидацию одна из команд в снапшоте, то все команды в нем фейлятся и не применяются. Где это необходимо использовать: при добавлении опыта повышается уровень игрока, а за это ему необходимо выдать награду. Если не сойдется слот уровня (например, в случае локального его изменения до команды), то и добавление опыта не запишется, не создав ситуацию, когда у игрока опыта больше, чем может быть на уровне.
  • Для сохранения возможности входа в Pixel Gun 3D в те моменты, когда необходимо по каким-либо причинам остановить сервер прогресса, реализовали для него аварийный режим. При его включении все команды отрабатывают только локально. А когда включается штатный режим, клиент присылает серверу текущие слоты. В эти моменты, конечно, есть возможность что-либо накрутить через какой-нибудь мод. Но аварийный режим мы включаем крайне редко, да и визуально на клиенте никак этого не понять, поэтому вероятность, что этим воспользуются — минимальна.
  • Оставили возможность входа в игру при отсутствии интернета. Игрокам доступны одиночные режимы без наград и ряд других локальных возможностей, которые не требуют изменений прогресса.
Шаг №3. Миграция прогрессаПри переходе от хранения прогресса игроков в сторонних сервисах к хранению на собственных серверах, необходимо было мигрировать эти данные, относящиеся именно к состоянию профиля. То есть полноценно перенести прогресс у честных игроков и не допустить, чтобы после ввода новой схемы работы прогресса можно было взламывать на прошлых версиях и путем обновления переносить данные к нам на сервер.Для этого мы добавили выдачу уникальных ключей всем, кто заходил в игру во время реализации новой схемы прогресса (примерно полгода). В дальнейшем, как только выпустили версию с обновленным прогрессом, мы отключили на сервере выдачу этих ключей, а принимали мигрированный прогресс только от тех, у кого был ключ и только один раз (чтобы нельзя было вернуться в прошлую версию и начислить прогресс тем, кто этого не сделал ранее).Так мы мигрировали прогресс всем активным игрокам и решили проблему взлома через старые незащищенные версии.Шаг №4. Система бана
Реализовали надежную систему бана, которую нельзя вырезать из клиента, так как у забаненного игрока блокируется весь серверный функционал (при том, что большая часть логики уже была перенесена на сервер).Ранее, когда не было постоянного соединения с сервером, не было возможности надежно забанить клиент — информация о бане запрашивалась отдельным http-запросом с сервера, и в случае положительного ответа в клиенте выводился баннер, что аккаунт забанен. Хоть мы и старались это спрятать в коде, появлялись различные моды, где весь мешающий жизни функционал был нейтрализован.Сейчас, когда реализована постоянная связь с сервером, большинство функционалов просто не работает без соединения. Для бана достаточно запретить авторизацию при открытии соединения с отдельной ошибкой (для показа баннера, что игрок забанен), чтобы сделать невозможным пользование приложением с заблокированным аккаунтом.Для защиты онлайна на серверах фотона от забаненных мы также сделали проверку с плагина фотона на факт бана. Если там выясняется, что такой id забанен, то игрока выкидывает из комнаты. Шаг №5. Подсчет хеша всех библиотекОдним из традиционных способов взлома является модификация библиотек приложения напрямую. В случае с приложениями на Unity — это libil2cpp.so (при билде через IL2CPP).Для обнаружения таких изменений может использоваться проверка на несовпадение контрольных сумм (хеша библиотек). Самым контролируемым способом будет вычисление текущего хеша на клиенте и отправка его на сервер (где он будет сравнен с эталонным).Получить путь до наших библиотек можно так:
public string GetLibraryDirectory()
{
  var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
  if (unityPlayer == null)
    throw new InvalidOperationException("unityPlayer == null");
  var _currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
  if (_currentActivity == null)
    throw new InvalidOperationException("_currentActivity == null");
  AndroidJavaObject packageManager = _currentActivity.Call<AndroidJavaObject>("getPackageManager");
  if (packageManager == null)
    throw new InvalidOperationException("packageManager == null");
  string packageName = _currentActivity.Call<string>("getPackageName");
  if (string.IsNullOrEmpty(packageName))
    throw new InvalidOperationException("string.IsNullOrEmpty(packageName)");
  const int GetMetaData = 128;
  AndroidJavaObject packageInfo = packageManager.Call<AndroidJavaObject>("getPackageInfo", packageName, GetMetaData);
  if (packageInfo == null)
    throw new InvalidOperationException("packageInfo == null");
  AndroidJavaObject applicationInfo = packageInfo.Get<AndroidJavaObject>("applicationInfo");
  if (applicationInfo == null)
    throw new InvalidOperationException("applicationInfo == null");
  string nativeLibraryDir = applicationInfo.Get<string>("nativeLibraryDir");
  if (string.IsNullOrEmpty(nativeLibraryDir))
    throw new InvalidOperationException("string.IsNullOrEmpty(nativeLibraryDir)");
  return nativeLibraryDir;
}
Для автоматизации процесса при сборке билдов можно использовать OnPostprocessBuild в Unity и производить расчет эталонного хеша. Обратите внимание на то, что при сборке с включением нескольких платформ (ARM, x86) необходимо вычислять хеш по каждой платформе.Что дальшеВ следующий раз поговорим про остальные решения, а именно: защиту от переподписывания версий, Photon Plugin, серверную валидацию инапов, защиту от взлома оперативной памяти, одновременный релиз всех решений и собственную аналитику. А про некоторые объемные пункты уже готовим отдельные, более подробные материалы.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_igr (Разработка игр), #_unity, #_dizajn_igr (Дизайн игр), #_unity, #_chitery (читеры), #_shuter (шутер), #_pvp, #_gejmdev (геймдев), #_razrabotka_igr (разработка игр), #_cheats, #_gamedev, #_obfuskatsija (обфускация), #_blog_kompanii_lightmap (
Блог компании Lightmap
)
, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
, #_razrabotka_igr (
Разработка игр
)
, #_unity, #_dizajn_igr (
Дизайн игр
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 18:35
Часовой пояс: UTC + 5