[Перевод] Практическое руководство по взлому (и защите) игр на Unity

9c3413a4ba2f44feac37262f5e93f346.png

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


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

46fc59cf365a4467ab55dd961d624b0a.png


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

a3b4aa8232cc4609b01a0e1611e13873.png

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


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

b5b612153fcf4606803344afe96fb77f.png

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

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


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

70b59c6dbb1b48518d75ee0a2bc70b09.png


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

26375bd419a7459b83b3f84c148222d3.png

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


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

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

    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.

32ebc5b2d9da4cadb8c1d96e04890de9.png

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

42dd2844087b437585466ad109a979df.png

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


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

fef6ddbcd7dd44c79af70bb57be77d82.png

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


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

ee101bffd23f4c7b9c14ddad80a45045.png

Эта программа пользуется тем фактом, что разработчики игр очень редко защищают значения переменных. Рассмотрим следующий пример: в некой игре у нас есть 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.

© Habrahabr.ru