Русский
Русский
English
Статистика
Реклама

Cheats

Как мы вырастили и победили читеров в своем онлайн-шутере

02.03.2021 22:15:14 | Автор: admin

Часто сижу на форумах читеров, не потому что нравится, а просто разработчику PvP-шутера всегда нужно быть в тонусе. Некоторые из взломщиков в прямом смысле слова выросли на моих глазах вместе с проектом. Вспоминал эти истории после очередного апдейта в проекте и захотелось ими поделиться.

Несколько лет назад мы написали тестовый прототип шутера, который быстро стал популярным защиты никакой не было, и некоторые побежали этим пользоваться. Мы затыкали пробоины скотчем, читеры находили новые дыры, а мы учились у них с помощью социальной инженерии и повышения технических навыков. И в итоге пересмотрели отношение к защите будущих мобильных проектов (об этом в конце статьи).

До появления мобильной Pixel Gun 3D в 2013 году команда Lightmap писала мини-игры на движке Cocos2d. То есть опыта ни в трехмерных, ни тем более мультиплеерных играх у нас тогда не было. Мы посещали конференции, очень много общались с другими разработчиками и, посмотрев на их опыт, все-таки решили попробовать себя в 3D на Unity.

Изучать движок решили на прототипе шутера, чтобы посмотреть, как отреагируют игроки. В итоге проект выстрелил так, что собирать опытную команду (а на старте было всего 3 разработчика) и разрабатывать качественный продукт с нуля уже не было времени. Всему пришлось учиться на своих ошибках после релиза.

Читеры из младших классов

С самого начала база игроков росла достаточно быстро, а с ними приходили первые читеры. Но из-за ограниченных ресурсов в приоритете стояли совсем другие задачи на старте было мало контента, игровых механик, да и цельной меты как таковой не было. Только когда в обзорах на YouTube слишком часто стали всплывать упоминания о взломах, пришлось заняться этим вопросом.

То есть изначально мы даже не рассматривали проблему появления читеров, а думали только о том, как сделать интересную мобильную игру на Unity. Возможно это и была первая ошибка, но напомню, что мы впервые собирали подобный прототип.

Первыми атаковали самые настоящие школьники в прямом и переносном смыслах слова, потому что взломать игру было проще простого. Со многими из них мы познакомились заочно, так как пришлось погрузиться в форумы и чаты, где они делились уязвимостями было даже интересно наблюдать, как их навыки росли вместе с проектом, а с ними росли наши познания в защите.

Сначала они находили элементарные дыры это использование простых переменных для данных в оперативной памяти и хранение данных на устройстве в незашифрованном виде. Поэтому тот же GameGuardian легко находил слабые места: валюту, время до конца матча, здоровье и так далее.

Сразу же решили передать хранение прогресса в защищенное зашифрованное значение и переделать все переменные, в которых хранились ответственные данные, на классы, где для скрытия используется рандомная соль. Не обошлось без факапов.

В месте, где валюта переносилась из старого хранилища в новое, мы допустили баг, и когда пользователь, который не входил в игру больше недели, обновлялся до новой версии то вся его валюта обнулялась. Самым сложным было понять, в чем дело, потому что при наших тестах такой сценарий не воспроизводился. Оказалось, что место отправки аналитики входа в игру после долгого отсутствия (в которой косвенно использовалась валюта по коду) располагалось раньше нашей миграции.

Насчет прогресса тоже оставалась дыра можно было ломать предыдущие версии и накатываться на новые. Тогда мы параллельно ввели новые валюты и сущности в игре, рассчитывая, что хотя бы их не будут ломать. Ценность старой валюты постепенно снижалась, так как новый контент на нее уже было не приобрести.

Также поставили хоть и локальную, но, все-таки, защиту на взлом инапов через LuckyPatcher. Когда позакрывали места, с которыми мог справиться и первоклассник, к взлому подключились ученики средних классов.

Читеры постарше

Они начали менять код приложения, дописывать свои менюшки для взлома различных параметров, переподписывать и выкладывать apk, которые пользовались успехом на форумах.

Все старания спрятать в коде проверку подписи быстро находились и вырезались, как и другие проверки на взломы приложения. Для обфускации мы стали использовать появившуюся в Unity IL2cpp, но в итоге выяснилось, что движок хранит в проекте дамп всех методов с адресами памяти чем стали пользоваться уже старшеклассники.

Ситуация по сути не менялась, так как мы все время боролись с симптомами, а не с болезнью. Даже не смотря на то, что иногда нам удавалось закрыть уязвимости еще до их использования для этого приходилось выдавать себя за тех самых читеров.

Социальная инженерия против читеров

Мы следили за взломщиками на форумах, общались, изучали, откуда они черпают знания и старались работать на опережение. У нас натурально были постоянные взломщики, которые на глазах перешли из средней школы в институт, набирались опыта и знаний.

Однажды один из таких ребят начал активно общаться с читером, который ломал другие игры. Мы это заметили, скачали его взломы, изучили принцип работы и заранее расставили необходимые детекты по своему приложению. Позже на YouTube мы увидели, что наш читер действительно пытался сделать аналогичный мод для Pixel Gun, но ничего рабочего в итоге не получилось.

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

Но в итоге все равно различные взломы регулярно выпускались разными известными и не очень взломщиками. Все-таки дыра с валютой была лишь одной из многих на тот момент и далеко не самой опасной, потому что за ее использование надо было платить.

А тут случился еще один серьезный факап с нашей стороны. В сеть утекла версия для разработчиков годовалой давности с дев-консолью, в которой можно было легко намешать любую сущность. К этом моменту CCU в пике доходил до 70 тысяч игроков, а DAU стабильно переваливал за миллион пользователей.

Стало понятно, что дальнейшее развитие приложения невозможно при таком состоянии дел со взломами. Все старания с поднятием уровня основных метрик нивелировались, а часто даже занижались вывешенными взломанными версиями.

Приоритетной задачей стало: убрать возможность читерства из приложения.

Решение

Итак, что мы имели на входе:

  • Огромное приложение с несколькими годами активной разработки и большим легаси.

  • Возможность изменения версии и выкладки пользователям.

  • Хранение прогресса локально на девайсе. Как следствие отсутствие контроля над обновлениями с прошлых версий, в том числе взломанных.

  • Синхронизация прогресса между девайсами через сторонние сервисы прогресс можно было беспрепятственно суммировать между игроками.

  • Отсутствие надежной возможности бана читера в измененных версиях его тоже вырезали.

  • Отсутствие надежного отслеживания фактов читерства.

  • В качестве игрового сервера Photon Unity Networking (Cloud), который не имеет серверной логики и, следовательно, не дает возможности контролировать игровую логику.

  • Наличие измененных версий игры в китайских сторонних сторах.

  • Связь с сервером через www-запросы, которые легко отследить и подделать.

  • Возможность подставить из кода любой id, который мы не сможем идентифицировать как подставной.

В итоге: безбожное ломание всего кора и меты игры на тот момент. Поэтому решение тоже должно было быть комплексным от обфускации кода и переноса хранения прогресса на наш сервер до защиты от переподписывания версий и введения собственной аналитической системы.

Причем эффект от всего этого был бы только в случае одномоментного выпуска всех мер защиты, потому что постепенный ввод изменений в проект значительно облегчил бы отслеживание апдейтов взломщиками, которые и так уже хорошо ориентировались в проекте. Другими словами, время, потраченное на взлом, не должно быть сопоставимо с выгодой.

Началась масштабная работа, для чего практически удвоили команду разработчиков, привлекая их с другого проекта.

Пока кратко о том, что мы сделали для перелома ситуации с читерами:

  1. Для обфускации ввели новые стандарты написания кода, позволяющие плагину обфускации сделать свое дело, и переписали под них все приложение. Задача не особо сложная, но ресурсоемкая, так как кодовая база к этому времени уже была довольно большой.

  2. Переработали хранение данных еще более глобальная и ресурсоемкая задача. Сущностей к этому времени было несчетное количество, данные сохранялись беспорядочно в произвольных форматах, что никак нам не подходило. Для синхронизации прогресса с сервером реализовали постоянное сокетное соединение, переведя на него все функционалы, связанные с метой игры (ну и, само собой, сам сервер, но это отдельная история). Для игроков оставили возможность поиграть оффлайн в несколько режимов, правда без наград и изменения прогресса.

  3. Чтобы мигрировать прогресс игроков, на время разработки глобальной защиты стали выдавать специальные ключи. Перенести прогресс в результате смогли только те, кто заходил в приложение в этот временной интервал. После релиза новой версии выдачу ключей остановили. И хотя эти ключи выдавались всем и честным игрокам и читерам основная цель была: исключить взломы через прошлые версии, так как без ключа прогресс не принимался сервером при миграции.

  4. Реализовали надежную систему бана, которую нельзя было вырезать из клиента, так как у забаненного игрока блочится весь серверный функционал (при том что большая часть логики была перенесена на сервер).

  5. Добавили подсчет хеша всех библиотек и начали сравнивать его с разрешенными при авторизации на сервере, куда они добавляются при сборке релизных билдов.

  6. Расставили в дополнительные места защиту от переподписывания версий, лаунчеров (на Android), твиков (на iOS), спрятав уже в обфусцированном коде.

  7. Для предотвращения взлома игровых параметров серверной логикой ввели Photon Plugin, доступный на тарифе Enterprise Cloud. Он позволяет мониторить пересылаемый между пользователями игровой трафик, чтобы вычислять тех, у кого действия выходят за рамки допустимого (жизни, урон, скорость перемещения/стрельбы, использование запрещенных предметов и так далее). Для возможности отслеживания взломов переписали сетевое взаимодействие игроков.

  8. Добавили серверную валидацию инапов.

  9. Добавили защиту от взлома оперативной памяти.

  10. Практически одномоментно выкатили все в продакшен.

  11. Продолжили реализовывать и улучшать собственную систему аналитики.

Сейчас аналитика основной инструмент постоянного отслеживания попыток взлома. Мы можем отслеживать все действия каждого конкретного пользователя и реагировать даже на единичные попытки взлома, не давая распространиться лазейкам. А когда-то эту роль для команды играли YouTube и профильные сайты.

Путь проделан не маленький и можно еще многое расписать про подходы, инструменты, нашу архитектуру и инхаус-разработки, вроде аналитики. Но рассказ обо всем сразу будет слишком объемный, поэтому лучше оставлю место для вопросов в комментариях и для будущих материалов.

ААА-защита для мобильных игр

Сейчас игра надежно защищена от взломов, у нее, как минимум, нет распространенных методов взлома. Бывают единичные, но их оперативно отслеживаем и реагируем.

К новым проектам в разработке тоже стали относиться по-другому и сразу пишем их с защитой, почти как у полноценных ААА-игр. Максимально возможное количество защит на старте приложения означает, что у взломщиков слишком большой порог входа, который надо преодолеть это отпугивает абсолютное большинство.

Также всегда проще писать сразу под необходимые меры защиты, а не переделывать проект под них в будущем.

На текущий момент наша программа-минимум для новых проектов это:

  • Плагин обфускации для осложнения понимания кода приложения.

  • Хранение и синхронизация прогресса через наш сервер с использованием валидации всех основных операций.

  • Зашифрованное хранилище на диске, чтобы исключить перенос данных от девайса к девайсу копированием данных.

  • Надежная система бана.

  • Photon Plugin, как минимум, для валидации действий пользователя.

  • Защита от изменения, переподписывания версий, лаунчеров и твиков.

  • Серверная валидация инапов.

  • Повсеместное использование засоленных данных как защита от изменений в оперативной памяти.

  • Собственная система аналитики с отслеживанием подозрительных действий конкретных пользователей.

И продолжаем сидеть на форумах под видом читеров, общаться и мониторить новые уязвимости. Например, одна из последних это скрытие использования тиков после выхода джейлбрейка на свежие версии iOS. Об этом тоже узнали из форумов, изучили механизмы работы и оперативно выкатили обновление но об этом и других кейсах уже в следующий раз.

Подробнее..

Первые пять шагов для перелома ситуации с читерами в PvP-шутере

18.03.2021 20:06:33 | Автор: admin

Мы прошли долгий путь от появления в игре первых читеров до полного пересмотра подхода к разработке, чтобы создавать защищенные по умолчанию проекты. О том, как в игре появились читеры, я рассказал в прошлом материале. И там же привел список подзадач, которые выкатили одновременно, чтобы закрыть вопрос со взломами, от обфускации кода до подсчета хеша всех библиотек и надежной системы бана.

Итак, эти шаги:

  • Обфускация.

  • Хранение данных.

  • Миграция прогресса.

  • Система бана.

  • Подсчет хеша всех библиотек.

  • Защита от переподписывания версий.

  • 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. Хранение данных

При реализации общего плана по защите от взломов игры это был один из самых важных и в то же время масштабных пунктов. Важным, потому что без полного контроля над прогрессом игроков невозможно полноценно защититься от взломов. А масштабным, потому что более чем за пять лет жизни проекта в игре было огромное количество сущностей и функционала, каждый из которых хранил данные в собственном формате, сохранялся беспорядочно и не имел никаких ограничений по количеству обращений к диску на чтение и запись.

Для реализации этой задачи необходимо было переработать все функционалы, в которых что-либо сохранялось.

На этапе составления плана действий мы попытались найти кого-нибудь с подобным опытом, прошерстили интернет и пообщались с коллегами из других студий, но многие вообще не верили, что это возможно, учитывая количество накопившегося у нас легаси.

Что ж, для начала составили критерии, которым должна удовлетворять наша система хранения данных:

  • Пользовательский опыт, когда все действия в игре происходят моментально без задержек на серверную валидацию, не должен пострадать.

  • При запуске приложения одним пользователем на нескольких устройствах игрок должен видеть абсолютно одинаковые состояния.

  • Нельзя допустить, чтобы какие-либо проблемы с сервером приводили к запрету входа в игру.

  • Минимизировать трафик и нагрузку на сервер.

  • После реализации основного этапа поддержка и развитие новых функционалов должны быть максимально простыми и быстрыми.

  • Все должно быть надежным и исключать возможность несанкционированного накручивания прогресса.

  • Желательно оставить возможность пользоваться игрой оффлайн, так как такие режимы востребованы среди наших игроков.

Потом начали составлять план работ. Выделили несколько глобальных этапов:

  1. Введение постоянного сокетного соединения клиент-сервер. Используемая до этого связь через https-запросы сильно ограничивала нас в реализации необходимых функционалов. Виделось, что при ней реализовать систему по всем требованиям не получится.

    Тогда опыта использования постоянного сокетного соединения у нас еще не было (предполагалось, что в дальнейшем без соединения нельзя будет находиться в основных разделах игры), поэтому решили обкатать соединение постепенно.

    Сначала сделали фоновое подключение и продублировали часть второстепенных функционалов на сокетное соединение с возможностью удаленного переключения, каким каналом связи клиенту пользоваться. Выкатили на пользователей, настроили сервера и убедились, что соединение работает стабильно.

    Затем ввели полноценную работу с аккаунтами и добавили поддержку сокетного соединения у большего количества функционалов. До этого момента мы поддерживали оба канала связи. Когда убедились, что новая архитектура держит постоянную связь с сервером и справляется с нагрузками, то можно было выкатывать полноценную работу с прогрессом и все остальные переработанные функционалы.

  2. Формат хранения данных определили JSON. Может он и не самый оптимизированный по трафику и работе, но удобен в использовании. Его мы расширяем и часто используем по проекту.

    Глобально прогресс представляет собой Dictionary<int, object>, где каждая пара это так называемые слоты для хранения данных. Каждый слот служит для хранения своего типа данных: слот валюты, слот инвентаря, слот ачивок и так далее. Ключ он же номер слота данных решили сделать интовым, чтобы сократить использование трафика, значение в каждом слоте может иметь свой формат, но это всегда JSON.

    Чтобы все действия на клиенте отрабатывались моментально, прогресс хранится и на клиенте, и на сервере (команды по изменению прогресса пишутся и там, и там). Отрабатывают сначала на клиенте: визуально пользователь видит, что все окей, а в это время на сервер отправляются параметры для команды. Там они проходят необходимые проверки, и если все хорошо, то прогресс меняется аналогичным способом, как и в клиенте. По результату сравниваются итоговые хеши изменившихся слотов на сервере и клиенте, если не сходятся рвется соединение с клиентом, клиент переавторизовывается и подтягивает последнее валидное состояние слотов.

    Чтобы сократить трафик между клиентом и сервером, для сравнения слотов отправляется хеш слота, а не слот целиком. И только при несовпадении хешей пересылается состояние слота от сервера клиенту при авторизации.

    На случай временной потери интернета (или если, например, приложение закроется до отправки команды на сервер), команды сохраняются локально и при следующем запуске перед началом сравнения слотов на сервере и клиенте сначала отправляются они и только потом хеши имеющихся слотов.

    Чтобы прогресс всегда был в консистентном состоянии, добавили возможность отправки нескольких команд на изменение прогресса в виде одной и назвали это снапшотом. Если не проходит валидацию одна из команд в снапшоте, то все команды в нем фейлятся и не применяются. Где это необходимо использовать: при добавлении опыта повышается уровень игрока, а за это ему необходимо выдать награду. Если не сойдется слот уровня (например, в случае локального его изменения до команды), то и добавление опыта не запишется, не создав ситуацию, когда у игрока опыта больше, чем может быть на уровне.

  3. Для сохранения возможности входа в Pixel Gun 3D в те моменты, когда необходимо по каким-либо причинам остановить сервер прогресса, реализовали для него аварийный режим. При его включении все команды отрабатывают только локально. А когда включается штатный режим, клиент присылает серверу текущие слоты. В эти моменты, конечно, есть возможность что-либо накрутить через какой-нибудь мод. Но аварийный режим мы включаем крайне редко, да и визуально на клиенте никак этого не понять, поэтому вероятность, что этим воспользуются минимальна.

  4. Оставили возможность входа в игру при отсутствии интернета. Игрокам доступны одиночные режимы без наград и ряд других локальных возможностей, которые не требуют изменений прогресса.

Шаг 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, серверную валидацию инапов, защиту от взлома оперативной памяти, одновременный релиз всех решений и собственную аналитику. А про некоторые объемные пункты уже готовим отдельные, более подробные материалы.

Подробнее..

Еще пять инструментов против читеров на мобильном проекте с DAU 1 млн пользователей

27.04.2021 20:11:49 | Автор: admin

Когда-то нам пришлось полностью переработать защиту популярного PvP-шутера. Результатом стал ряд инструментов, которые мы готовили и зарелизили одновременно, чтобы не дать читерам возможность постепенно отслеживать апдейты.

Про первые пять решений обфускацию, хранение данных, миграцию прогресса и систему бана шла речь в этойстатье. Сегодня расскажу про остальные, а именно:

  • Защита от измененных версий.

  • Photon Plugin.

  • Серверная валидация инаппов.

  • Защита от взлома оперативной памяти.

  • Собственная аналитика.

И немного про то, почему так важен был одновременный релиз всех решений.

Чтобы не возникло путаницы, продолжим нумерации из прошлой статьи, где уже были опубликованы пять пунктов.

Решение 6. Защита от измененных версий

В дополнительные места мы расставили защиту от переподписывания версий, лаунчеров (на Android) и твиков (на iOS), спрятав уже в обфусцированном коде.

Проверка на твики (iOS)

На устройствах с Jailbreak с помощью Cydia пользователи могут устанавливать твики, которые способны внедрять свой код в системные и установленные приложения. Каждый твик имеет информацию (файл *.plist), с какими бандлами они должны работать.

Механизм детекта осуществляется проверкой этих файлов в папке /Library/MobileSubstrate/DynamicLibraries/ (на наличие внутри нашего бандла).

Большинство твиков, скрывающих Jailbreak, способно спрятать эту папку от просмотра, поэтому есть способ просматривать ее через созданный нами симлинк.

string finalPath = string.Empty;string substratePath = "/Library/MobileSubstrate/DynamicLibraries/";bool bySymlink = false;if (!Directory.Exists(substratePath)) //Если папки не существует (скрыт твиком xCon), то пытаемся получить доступ к файлам через созданный нами симлинк{string symlinkPath = CreateSymlimk(substratePath);if (!string.IsNullOrEmpty(symlinkPath)){bySymlink = true;finalPath = symlinkPath;}}else{finalPath = substratePath;}bool detected = false;string detectedFile = string.Empty;try{if (!string.IsNullOrEmpty(finalPath)){string[] plistFiles = Directory.GetFiles(finalPath, "*.plist"));foreach (var plistFile in plistFiles){if (File.Exists(plistFile)){StreamReader file = File.OpenText(plistFile);string con = file.ReadToEnd();string bundle = "app_bundle"; if (con.Contains(bundle)){detectedFile = plistFile;detected = true;break;}}}}}catch (Exception ex){Debug.LogError(ex.ToString());}

Но также есть твики, которые запрещают создание симлинков по проверяемому нами пути (KernBypass, A-Bypass). При их наличии мы не можем осуществить проверку, поэтому считаем это за возможное читерство.

Общего механизма детекта таких твиков нет, тут нужен индивидуальный подход.

Детект KernBypass (который был активен в отношении нашего бандла):

if (File.Exists("/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist") {StreamReader file = File.OpenText("/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist"); string con = file.ReadToEnd();if (con.Contains("app_bundle") {//detected}}

Определение запуска через лаунчер (Android)

Запуск приложения через лаунчер это, по сути, запуск вашего приложения внутри другого приложения (по типу Parallel Space). Некоторые реализации взломов используют такой механизм для внедрения своего кода, и для этого на устройстве не требуется root-доступ. Обычно они имитируют всю среду: выделяют папку под файлы приложения, возвращают фейковый Application Info и так далее.

При таком запуске у нас все равно сохраняется доступ ко всем файлам, к которым имеет доступ сам лаунчер. Самый простой способ детекта это проверить доступ к материнской папке от нашего приложения (dataDir в applicationInfo) через функцию access (в нативном коде). В обычном случае операционная система не предоставит доступ, а в случае лаунчера это будет папка, которая все еще находится внутри Persistent Data приложения.

Код для плагина на C:

JavaVM*java_vm;jint JNI_OnLoad(JavaVM* vm, void* reserved) {        java_vm = vm;    return JNI_VERSION_1_6;}int CheckParentDirectoryAccess(){    JNIEnv* jni_env = 0;    (*java_vm)->AttachCurrentThread(java_vm, &jni_env, NULL);    jclass uClass = (*jni_env)->FindClass(jni_env, "com/unity3d/player/UnityPlayer");    jfieldID activityID = (*jni_env)->GetStaticFieldID(jni_env, uClass, "currentActivity", "Landroid/app/Activity;");    jobject obj_activity = (*jni_env)->GetStaticObjectField(jni_env, uClass, activityID);    jclass classActivity = (*jni_env)->FindClass(jni_env, "android/app/Activity");        jmethodID mID_func = (*jni_env)->GetMethodID(jni_env, classActivity,                                                      "getPackageManager", "()Landroid/content/pm/PackageManager;");        jobject pm = (*jni_env)->CallObjectMethod(jni_env, obj_activity, mID_func);        jmethodID pmmID = (*jni_env)->GetMethodID(jni_env, classActivity,                                                      "getPackageName", "()Ljava/lang/String;");        jstring pName = (*jni_env)->CallObjectMethod(jni_env, obj_activity, pmmID);    jclass pm_class = (*jni_env)->GetObjectClass(jni_env, pm);    jmethodID mID_ai = (*jni_env)->GetMethodID(jni_env, pm_class, "getApplicationInfo","(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;");    jobject ai = (*jni_env)->CallObjectMethod(jni_env, pm, mID_ai, pName, 128);    jclass ai_class = (*jni_env)->GetObjectClass(jni_env, ai);        jfieldID nfieldID = (*jni_env)->GetFieldID(jni_env, ai_class,"dataDir","Ljava/lang/String;");    jstring nDir = (*jni_env)->GetObjectField(jni_env, ai, nfieldID);        const char *nDirStr = (*jni_env)->GetStringUTFChars(jni_env, nDir, 0);    char parentDir[200];    snprintf(parentDir, sizeof(parentDir), "%s/..", nDirStr);    if (access(parentDir, W_OK) != 0)    {         return 1;    }else{ return 0;}}

Защита от переподписи apk (Android)

При любом вмешательстве внутрь apk-файла пакет необходимо переподписать, иначе система не позволит его установить. Поэтому можно определить модифицированную игру, если хеш подписи не совпадает с нашим.

Однако такую системную защиту можно отключить, если на устройстве есть root-доступ, и устанавливать пакеты с невалидной подписью. Поэтому данный пункт не является каким-то особенно важным, но и не будет лишним в общей массе.

Получение хеша подписи в С# через обращение в Java-код:

Lazy<byte[]> defaultResult = new Lazy<byte[]>(() => new byte[20]);            if (Application.platform != RuntimePlatform.Android)                return defaultResult.Value;#if UNITY_ANDROIDvar 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");            var packageManager = _currentActivity.Call<AndroidJavaObject>("getPackageManager");            if (packageManager == null)                throw new InvalidOperationException("getPackageManager() == null");            // http://developer.android.com/reference/android/content/pm/PackageManager.html#GET_SIGNATURES            const int getSignaturesFlag = 64;            var packageInfo = packageManager.Call<AndroidJavaObject>("getPackageInfo", PackageName, getSignaturesFlag);            if (packageInfo == null)                throw new InvalidOperationException("getPackageInfo() == null");            var signatures = packageInfo.Get<AndroidJavaObject[]>("signatures");            if (signatures == null)                throw new InvalidOperationException("signatures() == null");            using (var sha1 = new SHA1Managed())            {                var hashes = signatures.Select(s => s.Call<byte[]>("toByteArray"))                    .Where(s => s != null)                    .Select<byte[], byte[]>(sha1.ComputeHash);                var result = hashes.FirstOrDefault() ?? defaultResult.Value;                return result;            }#else            return defaultResult.Value;#endif

Решение 7. Photon Plugin

Для организации сетевого взаимодействия между пользователями в игровых комнатах мы используем Photon Cloud, который сам по себе предполагает отсутствие серверной логики и отвечает только за пересылку пакетов между пользователями. А для защиты кора нам нужна была именно серверная логика.

Переделывать всё на Photon Server было бы достаточно долгой и сложной задачей, так как в игре уже было много различных режимов, механик и прочего. Поэтому, пообщавшись с ребятами из Photon, мы решили попробовать Photon Plugin.

Photon Plugin доступен на тарифе Enterprise Cloud и пишется на С#. Он запускается на серверах Photon и позволяет мониторить пересылаемый между пользователями игровой трафик, добавлять серверную логику, которая может:

  • блокировать или добавлять сетевые сообщения;

  • контролировать изменения свойств комнат и игроков;

  • кикать из комнаты;

  • взаимодействовать при помощи http-запросов со сторонними серверами.

Мы переписали сетевое взаимодействие игроков так, чтобы можно было отследить тех, у кого действия выходят за рамки допустимого (например, количество здоровья, урон, скорость перемещения, использование запрещенных предметов и так далее), или код изменен так, что нарушается логика режимов.

Про это решение хотелось рассказать подробнее, но тогда бы статья разрослась слишком сильно. Поэтому эту тему оставили для отдельного материала.

Решение 8. Серверная валидация иннапов

В нашей игре сотни видов оружия и предметов. Несмотря на баланс и большое влияние личного скилла игроков на результат матча, все инаппы нужно защитить от взлома. Для этого мы ввели валидацию покупок.

Валидация на сервере состоит из двух этапов:

  1. Превалидация. Когда данные по платежу отправляются на сервер соответствующей платформы для проверки валидности.

  2. Начисление. В случае успешно пройденной валидации купленных позиций.

Сначала сервер получает в качестве входных параметров данные, необходимые для проведения валидации (например, на Android это id инаппа и токен).

Отдельной командой/набором команд происходит начисление купленных позиций. Одна позиция может содержать разнотипные итемы (например, деньги и оружие) и для каждого типа итема на сервере существует отдельная команда начисления.

Чтобы логически объединить несколько команд, привязанных к одному действию игрока, на клиенте и на сервере мы ввели понятие снапшота. Это специальная конструкция из последовательности команд, которые одновременно выполняются или не выполняются на сервере.

Команда валидации проверяет транзакцию в случае, если есть данные превалидации, то используются они. В противном случае данные отправляются на сервер валидации для соответствующей платформы.

В случае успешного начисления id транзакции сохраняется в соответствующий слот игрока запись в БД, которая хранит данные по платежным транзакциям данного игрока. Во избежание взлома платежки методом, когда одну валидную транзакцию используют для многократного начисления, в рамках валидации осуществляется проверка на существование данного id транзакции.

Кроме того, в завершение начисления отправляется соответствующая статистика на сервер аналитики.

Подробнее про интеграцию инаппов и серверную валидацию вместе с кодом расскажем в отдельной статье.

Решение 9. Защита от взлома оперативной памяти

Локальные данные, которые не защищены валидацией с сервера, можно взломать в памяти (например, с помощью GameGuardian на Android). Механизм взлома заключается в поиске значений в памяти путем отсеивания. Ищется текущее значение, затем оно изменяется в игре, а среди найденных адресов в памяти они отсеиваются по новому значению, пока не будет найден нужный адрес.

Для их защиты они засаливаются при помощи случайно сгенерированной соли:

 internal int Value{get { return _salt ^ _saltedValue; }set { _saltedValue = _salt ^ value; }}

Когда пользователь не в состоянии изменить искомое значение для отсеивания, он может попытаться заменить все найденные значения на свои. Для их детекта используется простая ловушка. Следующий пример показывает, как можно определить вмешательство в память с числами от 0 до 1000 (заранее храним массив чисел, которые никогда не должны измениться, кроме как после редактирования памяти).

private static int[] refNumbers;internal static void Start(){refNumbers = new int[1000];for (int i = 0; i < refNumbers.Length; i++) {refNumbers[i] = i;}}internal static bool Check(){for (int i = 0; i < 1000; i++) {if (!refNumbers [i].Equals(i))return true;}}

Решение 10. Собственная аналитика

Изначально мы пользовались платным решением от devtodev и бесплатным от Flurry. Основная проблема была в отсутствии детализации происходящих в игре событий. Мы собирали только агрегированные данные и поверхностные метрики.

Но с ростом экспертизы внутри команды стала очевидной необходимость писать собственное решение с нужными именно нам фичами. Основная цель была в повышении продуктовых метрик и повороте компании в сторону Data-Driven подхода. Но в итоге аналитика также стала незаменимым инструментом в борьбе с читерами.Например, раньше система не привязывалась к пользователю, а просто считала ивенты. То есть мы знали, что 500 человек совершили какие-то действия, но кто эти 500 человек, и что они делали до этого нет. Сейчас можно посмотреть все действия каждого конкретного игрока и, соответственно, отследить подозрительные операции.

Все пользовательские ивенты отправляются в одну большую SQL-евскую базу. Там есть как элементарные ивенты (игрок залогинился, сколько раз в день он залогинился и так далее), так и другие. Например, прилетает ивент, что игрок покупает оружие за столько-то монет, а вместо суммы написано 0. Очевидно, что он сделал что-то неправомерное.Большинство выгрузки с подозрительными действиями нарабатываются с опытом. Например, у нас есть скрипт, который показывает, что столько-то людей с конкретными id получили определенное большое количество монет. Но это не всегда читеры обязательно нужно проверять.

Также читеров опознаем по несоответствию значений начисления валют. Аналитик знает, что за покупку инаппа начисляется конкретное количество гемов. У читеров часто это количество бывает 9999 значит, что-то взломали в памяти. Еще бывают игроки с аномальными киллрейтами. По ним у нас тоже есть специально обученное поле, и когда появляется пользователь, у которого киллрейт 15 или 30, становится понятно, что, скорее всего, это читер.В основном отслеживанием занимается один скрипт, который пачкой прогоняет по детектам и сгружает все в таблицу. Аналитики получают id и видят игроков, которые залогинились утром с огромным количеством голды, в соседнем листе лежат игроки, открывшие 1000 сундуков, в следующем игроки с тысячей гач и так далее. Затем вариантов несколько.

Предполагаемых читеров можно прогнать по флагам, а так как подобные способы читерства задетекчены и пофиксены, то подобные игроки уже могли ранее получить значок читера и попасть в отдельную базу. Если они там есть, то бан прилетает автоматически с помощью скрипта.

Если нет детекта, данные передаются разработчикам. Они думают, как читер смог обойти защиту и решают, как закрыть эти дыры, как таких игроков детектить и банить.А про причины перехода на собственную аналитическую систему и внутренние фичи тоже расскажем в отдельном материале.

Одновременный релиз всех решений

Чтобы достичь максимального эффекта от всех новых защит, было решено выкатить их одномоментно. Так значительно повышался входной порог для тех взломщиков, которые уже были хорошо знакомы с нашим необфусцированным кодом.

Кроме того, мы не просто применили обфускацию приложения, но и изменили структуру большинства значимых частей приложения. В корне была изменена работа внутри, и все это свело к минимуму имеющиеся к тому моменту у читеров наработки. А разбираться в новом стало куда гораздо сложнее.

Всего на глобальный ввод большинства защит ушло около семи месяцев.

Самым масштабным пунктом была реализация системы хранения на наших серверах именно она определяла запуск в продакшен всех решений из списка. Кроме аналитики, которая развивалась самостоятельно.

Также было важно развивать приложение в целом и релизить апдейты для игроков, чтобы не потерять их интерес, пока глобальные решения против читеров складывались отдельно в ожидании своего момента.

Сейчас игра надежно защищена и не имеет распространенных методов взлома. Иногда встречаются единичные случаи, но они быстро отслеживаются благодаря введенным инструментам.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru