Когда речь идёт о программном обеспечении, термин «взлом» зачастую ассоциируют с пиратством и нарушением авторских прав. Данная статья не об этом; напротив, я решительно не одобряю любые действия, которые прямо или косвенно могут навредить другим разработчикам. Тем не менее, эта статья всё же является практическим руководством по взлому. Используя инструменты и методы о которых далее пойдёт речь, вы сможете проверить защиту собственной Unity игры и узнаете, как обезопасить её от взлома и кражи ресурсов.

Введение


В основе взлома лежит знание: необходимо понимать особенности компиляции Unity-проекта, чтобы его взломать. Прочитав статью, вы узнаете, каким образом Unity компилирует ресурсы игры и как извлечь из них исходные материалы: текстуры, шейдеры, 3D-модели и скрипты. Эти навыки будут полезны не только для анализа безопасности проекта, но также для его продвинутой отладки. В связи с закрытостью исходного кода, Unity часто работает как «черный ящик» и порой единственный способ понять, что именно в нём происходит — это изучение скомпилированной версии скриптов. Кроме прочего, декомпиляция чужой игры может стать серьёзным подспорьем в поиске её секретов и «пасхальных яиц». Например, именно таким образом было найдено решение финальной головоломки в игре FEZ.



Находим ресурсы игры


Рассмотрим для примера игру, собранную под ОС Windows и загруженную через Steam. Чтобы добраться до директории, в которой находятся нужные нам ресурсы, откроем окно свойств игры в библиотеке Steam и в закладке «Local files» нажмём «Browse local files…».



Когда Unity компилирует проект под Windows, ресурсы всегда упаковываются по схожей схеме: исполняемый файл (например, Game.exe) будет находится в корне директории игры, а по соседству расположится директория, содержащая все игровые ресурсы — Game_Data.

Извлекаем текстуры и шейдеры


Большинство ресурсов Unity-проекта упаковываются в файлы проприетарного формата с расширениями .assets и .resources. Наиболее популярный на сегодняшний день инструмент для просмотра таких файлов и извлечения из них ресурсов — Unity Assets Explorer.



Графический интерфейс программы не отличается удобством, а также она страдает от нескольких критических багов. Не взирая на это, программа вполне способна извлечь большинство текстур и шейдеров из игры. Полученные в результате текстуры будут иметь формат DDS, который можно «прочитать» с помощью Windows Texture Viewer.

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

Извлекаем 3D-модели


Трёхмерные модели в типовой Unity-сборке «разбросаны» по различным ресурсам, а некоторые из них и вовсе могут генерироваться во время игры. Вместо копания в файлах, существует интересная альтернатива — получить данные о геометрии прямиком из памяти графического ускорителя. Когда игра запущена, вся информация о текстурах и моделях, видимых на экране, находится в памяти видеокарты. С помощью утилиты 3D Ripper DX можно извлечь всю эту информацию и сохранить в формате, понятном 3D-редакторам (например, 3D Studio Max). Учтите, что программа не самая простая в обращении — возможно, придётся обратиться к документации.



Взламываем PlayerPrefs


PlayerPrefs — это класс из стандартной библиотеки Unity, который позволяет сохранять данные в долговременную память устройства. Он часто используется разработчиками для хранения различных настроек, достижений, прогресса игрока и другой информации о состоянии игры. На ОС Windows эти данные сохраняются в системном реестре по следующему пути: HKEY_CURRENT_USER\Software\[company name]\[game name].



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

Защищаем PlayerPrefs


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

public class SafePlayerPrefs 
{
    private string key;
    private List<string> properties = new List<string>();

    public SafePlayerPrefs (string key, params string [] properties)
    {
        this.key = key;
        foreach (string property in properties)
            this.properties.Add(property);
        Save();
    }

    // Вычисляем контрольную сумму
    private string GenerateChecksum ()
    {
        string hash = "";
        foreach (string property in properties)
        {
            hash += property + ":";
            if (PlayerPrefs.HasKey(property))
                hash += PlayerPrefs.GetString(property);
        }
        return Md5Sum(hash + key);
    }

    // Сохраняем контрольную сумму
    public void Save()
    {
        string checksum = GenerateChecksum();
        PlayerPrefs.SetString("CHECKSUM" + key, checksum);
        PlayerPrefs.Save();
    }

    // Проверяем, изменялись ли данные
    public bool HasBeenEdited ()
    {
        if (! PlayerPrefs.HasKey("CHECKSUM" + key))
            return true;

        string checksumSaved = PlayerPrefs.GetString("CHECKSUM" + key);
        string checksumReal = GenerateChecksum();

        return checksumSaved.Equals(checksumReal);
    }

    // ...
}


Приведенный выше класс — упрощенный пример реализации, работающий со строковыми переменными. Для инициализации ему необходимо передать секретный ключ и список PlayerPrefs-ключей, значения которых должны быть защищены:

SafePlayerPrefs spp = new SafePlayerPrefs("MyGame", "PlayerName", "Score");


Затем его можно использовать следующим образом:

// Сохраняем данные в PlayerPrefs как обычно
PlayerPrefs.SetString("PlayerName", name);
PlayerPrefs.SetString("Score", score);

spp.Save();

// Проверяем, редактировались ли значения
if (spp.HasBeenEdited())
    Debug.Log("Error!");


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

Взламываем исходный код


Для Windows-сборок Unity компилирует и сохраняет исходный код всех игровых скриптов в директорию Managed. Интересуют нас следующие библиотеки: Assembly-CSharp.dll, Assembly-CSharp-firstpass.dll и Assembly-UnityScript.dll.



Для декомпиляции и просмотра managed-кода .NET библиотек (коими и являются наши жертвы) существуют довольно удобные и при этом бесплатные утилиты: IlSpy и dotPeek.



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

Защищаем исходный код


Раз Unity не заботится о сохранности нашего кода — сделаем это сами. Благо, существует утилита, готовая автоматически зашифровать плоды нашего интеллектуального труда: Unity 3D Obfuscator.



И хотя программа отлично справляется со своими обязанностями, многие классы, адресуемые извне родной библиотеки, всё же не могут быть зашифрованы без риска нарушения связанности — будьте осторожны!

Взламываем память игры


Cheat Engine — широко известная программа для взлома игр. Она находит ту область оперативной памяти, которая принадлежит процессу запущенной игры и позволяет произвольно её изменять.



Эта программа пользуется тем фактом, что разработчики игр очень редко защищают значения переменных. Рассмотрим следующий пример: в некой игре у нас есть 100 патронов; используя Cheat Engine, можно выполнить поиск участков памяти, которые хранят значение «100». Затем мы делаем выстрел — запас патронов составляет 99 единиц. Снова сканируем память, но теперь ищем значение «99». После нескольких подобных итераций можно с легкостью обнаружить расположение большинства переменных игры и произвольно их изменять.

Защищаем память игры


Cheat Engine столь эффективна от того, что значения переменных хранятся в своём изначальном виде, без какой-либо защиты. Серьёзно усложнить жизнь «читерам» довольно просто: нужно лишь немного изменить способ работы с переменными. Создадим структуру SafeFloat, которая послужит нам безопасной заменой стандартного float:

public struct SafeFloat 
{
    private float offset;
    private float value;

    public SafeFloat (float value = 0) 
    {
        offset = Random.Range(-1000, +1000);
        this.value = value + offset;
    }
	
    public float GetValue ()
    {
        return value - offset;
    }

    public void Dispose ()
    {
        offset = 0;
        value = 0;
    }

    public override string ToString()
    {
        return GetValue().ToString();
    }

    public static SafeFloat operator +(SafeFloat f1, SafeFloat f2) 
    {
        return new SafeFloat(f1.GetValue() + f2.GetValue());
    }

    // ...похожим образом перегружаем остальные операторы
}


Использовать нашу новую структуру можно следующим образом:

SafeFloat health = new SafeFloat(100);
SafeFloat damage = new SafeFloat(5);

health -= damage;

SafeFloat nextLevel = health + new SafeFloat(10);

Debug.Log(health);


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

Заключение


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

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

Дополнительная информация


  • Unity3D Attack By Reverse Engineering: Интересная статья, описывающая распространённые ошибки безопасности при реализации систем подсчёта очков в играх на Unity;
  • disunity: Одна из лучших утилит для просмотра и извлечения ресурсов из Unity игр. К сожалению, она несовместима с последней версией движка;
  • Unity Studio: Программа для визуализации и извлечения 3D моделей. Также не работает с Unity 5.

Комментарии (20)


  1. Haoose
    09.09.2015 15:18
    +5

    Самая свежия версия Unity Assets Explorer лежит на дропбоксе. На Nexus'е версия немного устарела.
    Начал делать ее еще в январе 2013го для русификации игр на Unity. Первой ласточкой стала One Late Night.
    За перевод статьи спасибо.


    1. Elringus
      09.09.2015 15:21

      Обновил ссылку в статье. Спасибо.


  1. Haoose
    09.09.2015 15:33
    +2

    Для декомпиляции и просмотра managed-кода .NET библиотек (коими и являются наши жертвы) существуют довольно удобные и при этом бесплатные утилиты: IlSpy и dotPeek.

    Еще удобная программа .NET Reflector с плагином Reflexil. Помимо просмотра позволяет еще и править код. Что может использоваться как для взлома игр, так и активации всяческих чит-режимов или исправления багов.


    1. PsyHaSTe
      16.09.2015 09:17

      Рефлектор платный, плюс ко всему его делали те же ребята, что пилят сейчас ILSpy, так что полагаю там этот режим тоже введут.


  1. loreglean
    09.09.2015 16:04
    +1

    Есть еще такой вариант, как проверить, играет ли пользователь со взломанной .apk-шки (т.е. используя патченную Assembly-CSharp.dll) — хранить хеш сборки на сервере для каждой версии. При старте игры вычисляется хеш локальной сборки, затем отправляется серверу. Сервер ее проверяет, и, если она неверная, запоминает это. Затем через определенное время включается банхаммер, и всем юзерам, от которых был прислан неверный хеш, присваивается бан, показывается окно «поди прочь» (или еще что-то, зависит от реализации). Конечно, вычисление и отправку хеша нет смысла класть в Assembly-CSharp — если в ней поломали уже что-то, то могут поломать и это, а значит нужно как можно меньше зависимости от c# и java. Вариант — сделать либу на JNI, которая этим занимается. Тогда ее использование сводится к строке System.loadLibrary(«you_native_library_name») (при использовании JNI_OnLoad), которую найти посложнее. Но и это не гарантирует полной защиты, конечно. Тем не менее планка взлома слегка повышается. Так же, отложенный банхаммер не дает сразу понять читеру, на основании чего он произошел.


    1. coperius
      09.09.2015 17:24
      +1

      Ничего не мешает изменить dll и отправлять корректный хэш.

      Недавно как раз отучал от защиты Битву чемпионов от kabam.

      1. На данные, отправляемые на сервер, вычисляется hmac и добавляется в сообщение.
      Соль зашита в игре.

      2. При логине происходит следующие действия
      Вычисляется sha1 для dll файлов в папке managed и отправляется на сервер
      Если хэш верен, то в ответ приходит соль, с некоторой модернизацией которой вычисляется hmac все тех же dll

      Все вычисления и логика встроены в so библиотеку.
      Расковыряв алгоритм, в классе защиты заменил вызовы внешней библиотеки на реализующий его код.

      Профит. Теперь можно менять тем же рефлектором все что нам нужно


      1. loreglean
        09.09.2015 17:31

        Я не писал, что что-то этому мешает, вопрос лишь в сложности. C# сборку легче расковырять чем найти .so и расковырять ее. Соответственно, из .so не должны торчать концы (кроме как loadLibrary), и уж точно она не должна общаться или обмениваться данными с managed-кодом (в идеале). Опять же, расковырять все что угодно можно, конечно. Но иногда достаточно увеличить сложность взлома, и энтузиастов это сделать может уже не найтись. А могут и найтись :)


      1. Nagg
        09.09.2015 18:57

        Если бы логику отправки хеша встроили в .so причем зашифровав содержимое каким AES-ом с ключем, забитым там же в native — врядли бы вы что-то с этим сделали.


        1. REU
          10.09.2015 10:50

          Пффф, это только немного усложнит задачу


          1. Nagg
            10.09.2015 12:27

            Напишите, как бы вы взломали? А то вы привели слишком много аргументов.


            1. REU
              10.09.2015 13:47

              Я так понял вы имели ввиду зашифровать код, который отправляет хеш. Т.к. ключ зашит в саму либу, то просто ищем место когда расшифровывается код, оттуда берем ключ и адреса по каким идет расшифровка. Расшифровываем сами код, патчим, шифруем обратно, заменяем либу, профит. Ну это вариант для симметричного шифрования. А если взять например RSA с ключом более 1024 бит, то такой трюк не прокатит. Тогда делаем немного по другому. Мы можем расшифровать код и посмотреть что и как там делается, узнаем адрес по которому нужно пропатчить. Далее в либе после кода расшифровки втыкаем переход на свой код, который патчит в памяти уже расшифровываний код, и переходит к дальнейшему выполнению оригинального кода. Это самые простые решения в лоб, но их может быть намного больше.


    1. shai_hulud
      10.09.2015 13:58

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

      1) Берется EasyHooks
      2) Вешается хук на fopen
      3) При первом(2,3,4) срабатывании, когда считается хеш, подсовывается оригинал, во всех остальных модифицированный файл

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


      1. loreglean
        10.09.2015 14:01

        Разве я писал, что это возможно? Наоборот в конце подчеркнул, что вовсе нет


        1. shai_hulud
          10.09.2015 14:11

          Вы не подчеркнули что это «принципиально невозможно», а все попытки только делают желания взломать тверже :)


          1. loreglean
            10.09.2015 14:43

            «Расковырять можно все, что угодно» равно по смыслу «принципиально невозможно защитить», как по мне :) Я писал про то, что можно усложнить это по максимуму. Выводя проверку хешей в нативку, а баны и контрольные чеки различной логики на сервер. Если брать не сферический проект в вакууме, а реальный, этого часто бывает достаточно. Конечно, есть много факторов, которые на это влияют, в частности объем аудитории и тип проекта (оффлайн — онлайн — частично онлайн)


  1. Dywar
    09.09.2015 20:38

    Нормально, спасибо за пост, еще бы «CHECKSUM» в реестре переименовать в популярный параметр который якобы используется в игре, и без реверса не будет понятно что hash вообще используется.

    Трюк — заводим switch case конструкцию, определяем первые 2-3 варианта (которые по хорошему стилю кодирования должны быть наиболее часто встречаемыми) с похожими на правду параметрами, а настоящая работа будет проходить в других ветках или вообще в default. Потом самому не запутаться, но это чуть сложнее обфускации на которую будет антиобфускация :)


  1. AlmazDelDiablo
    10.09.2015 02:16

    Как раз начал изучать вопросы анти-читов для своей игры; статья очень в тему, спасибо! Кстати, не пробовали ли использовать AntiCheat Toolkit из Asset Store? Какие есть о нём отзывы?

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


    1. coperius
      10.09.2015 08:52
      +3

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

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

      Опишу один из многочисленный фэйлов того же kabam в игре Герои камелота.

      В игре есть «хранилище» с различными итемами (в том числе картами героев). Для всех итемов хранится их количество, но используют для этого не целое число, а с точкой. При этом при использовании на сервер уходит количество итемов. Отсутствие проверки на сервере дает два замечательных чита. Первый — это использование количества в виде 0.0000001. Что есть по сути бесконечный итем. Второе — использование отрицательных значений, т.е. возможность клонирования.


  1. patch1
    12.09.2015 14:47

    не много не в тему, но в защиту Ассетов ссылка


    1. coperius
      14.09.2015 09:33

      Эфемерная защита. Берем метод для дешифровки из приложения и получаем нормальный ассет