Информация Unity Hacking


X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 125
Репутация
8 243
Перевод:


Введение

Недавно я участвовал в Global Game Jam в очень крутом коллективе разработчиков Glitch City, где я провел 48 часов, создавая игру на Unity вместе с командой профессиональных разработчиков игр и несколькими новичками, как я. Мой вклад был скромным и запутанным, но с помощью отличных программистов в моей команде я получил гораздо лучшее понимание основ работы движка.

Во время джема я немного пообщался с разработчиками в моей команде о своей повседневной работе реверс-инженером вредоносного ПО. Я сказал, что в конце джема мы могли бы вместе разобрать нашу игру в качестве небольшого обмена навыками в ответ на те знания по Unity, которые они передали мне во время джема. Ну, если вы знаете, как проходят гейм-джемы, конец был суматошным (но веселым!) и в целом выглядел примерно так:

1717237725118.png


Так что не осталось ни времени, ни сил на реверс-инжиниринг :(. Но это значит, что я могу сделать мануалл, который сможет изучить любой желающий! И в течение джема я понял, что Unity - это отличное место для начала хакинга/моддинга игр по нескольким причинам:

Unity написан на C#, языке программирования для платформы .NET, который позволяет нам использовать , очень мощный инструмент, встроенный в .NET, который позволяет исследовать и манипулировать запущенными процессами - об этом мы поговорим позже.

Рефлексия в C# — это механизм, предоставляемый платформой .NET, который позволяет программе анализировать и изменять свою структуру и поведение во время выполнения. Это позволяет получать информацию о типах, методах, свойствах и других элементах программы, а также создавать и вызывать методы, получать и устанавливать значения свойств и полей, даже если они объявлены как private или protected.

Скомпилированный код C# также декомпилируется невероятно чисто и с символами, что делает игры на Unity отличным местом для начала, если вас интересует реверс-инжиниринг в целом.

Чтобы сначала разобраться с основами архитектуры Unity, давайте сначала займемся простой прямой разработкой.

Змейка: введение в Unity

Взгляните на нашу невероятно сложную цель для "хакинга":

1717237863787.png


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

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

Это не займет много времени, обещаю. Это поможет вам ознакомиться с основными концепциями GameObjects и понять, как работает сценарий на C# в движке для построения игровой логики.

ПОЖАЛУЙСТА, ОБРАТИТЕ ВНИМАНИЕ: Версия Unity имеет значение! Я создал свою версию змейки с помощью 2021.3.16f1 - вам тоже стоит! В противном случае, некоторый мой код далее может не работать у вас.

Если вы не хотите проходить это руководство или уже знакомы с Unity, я включил свою сборку игры на

Единственное отличие моей сборки от руководства заключается в том, что я добавил следующую строку для перезагрузки сцены, когда вы умираете:
Код:
SceneManager.LoadScene(SceneManager.GetActiveScene().name);

Теперь, когда у нас есть игра для хакинга, давайте взломаем её!

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

DNSpy: Первое знакомство с реверс-инжинирингом

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

1717238166169.png


Если вы следовали руководству, сначала соберите свою игру в известное место. В противном случае, в известное место. Перейдите туда и найдите файл, расположенный по пути .\snake_Data\Managed\Assembly-CSharp.dll. Для большинства игр этот файл содержит основную игровую логику, написанную разработчиками. Перетащите его в боковую панель DNSpy для декомпиляции.

В боковой панели теперь вы должны иметь возможность открыть пространство имен по умолчанию внутри Assembly-CSharp.dll, которое выглядит как маленькие фигурные скобки с тире рядом с ними ({} -), и исследовать игровую логику классов внутри нашей игры змейка:

1717238263979.png


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

Код:
// Token: 0x02000002 RID: 2
public class Snake : MonoBehaviour
{
    // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
    private void Move()
    {
        this.dir = this.tickDir;
        Vector2 v = base.transform.position;
        base.transform.Translate(this.dir);
        if (this.ate)
        {
            GameObject gameObject = Object.Instantiate<GameObject>(this.tailPrefab, v, Quaternion.identity);
            this.tail.Insert(0, gameObject.transform);
            this.ate = false;
            return;
        }
        if (this.tail.Count > 0)
        {
            this.tail.Last<Transform>().position = v;
            this.tail.Insert(0, this.tail.Last<Transform>());
            this.tail.RemoveAt(this.tail.Count - 1);
        }
    }
}

Отлично. Быстрый взгляд на это показывает, что внутри функции Move есть проверка значения булева типа ate. Если ate истинно, то добавляется сегмент к хвосту. Это означает, что один из способов обеспечить добавление сегмента к хвосту — установить ate в true для нашей змейки, а затем вызвать Move. Мы уже рассмотрели функцию Move, так что давайте проверим поле ate.

Вы можете щелкнуть правой кнопкой мыши по this.ate внутри функции Move в DNSpy и выбрать Analyze в появившемся меню. Это создаст новое окно анализатора внизу, которое покажет, где this.ate устанавливается и читается, но нас это не интересует, потому что мы собираемся установить его самостоятельно (исследование этих моментов могло бы выявить другой способ добавления хвоста!). Для нашего метода добавления хвоста нас больше интересуют детали самого поля ate. Для этого нажмите на Snake.ate в Анализаторе:

1717238420158.png


Это приведет вас к его определению внутри класса Snake, которое я включил ниже:

C:
// Token: 0x04000004 RID: 4
private bool ate;

Итак, это булевое значение и частная переменная, принадлежащая классу Snake. "О нет!" - можете сказать вы. "Это значит, что никакая функция вне класса Snake не может получить доступ к этой переменной! Урок окончен!" На что я отвечу: хватит истерить! Где есть воля, там есть и путь. И этот путь - рефлексия!

Рефлексия
- это самая крутая часть .NET.

Теперь, в игре, скомпилированной на C, мы бы, вероятно, просто нашли структуру нашего экземпляра объекта Snake после запуска игры, затем изменили бы бит, связанный с булевым значением ate, на true. Это очень круто и хакерски, и вы можете ознакомиться , чтобы узнать, как это сделать.
Но в .NET вы можете сделать нечто еще более крутое и хакерское. Вы можете написать код, который находит, читает и изменяет экземпляры объектов как встроенную функцию .NET!

Используя рефлексию, наш базовый план будет таким:
  1. Получить выполнение кода в игре змейка во время выполнения через внедрение в процесс.
  2. Создать Unity GameObject, который использует рефлексию для поиска объекта змейка в памяти и его изменения.
  3. Заставить Unity загрузить наш GameObject в игру, в этот момент он изменит значение ate для объекта змейки в игре на true и увеличит наш хвост.
Внедрение в игру

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

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

Как и с DNSpy, на странице GitHub под вкладкой "tags" есть готовый релиз. Вы можете использовать либо GUI, либо CLI версию, я буду использовать CLI в своих примерах. Убедитесь, что все содержимое загруженного zip-файла находится в одной директории.

Настройка проекта

Мы можем разбить создание нашего полезного кода на два этапа. Первый этап - создать тестовый полезный код, чтобы показать, что мы выполняем код в Unity, что поможет нам настроить наш шаблонный код. Затем мы сможем фактически реализовать наш чит.

Есть несколько подводных камней в том, как нужно настроить проект в Visual Studio, поэтому давайте пройдем это вместе.

Сначала создайте новый проект "Class Library" в Visual Studio.

project.png


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

1717238802908.png


Загрузчик шаблона

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

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

C#:
using System;
// Мы импортируем это прямо из игровых файлов!
using UnityEngine;
// Наше пространство имен, которое мы создадим в другом файле
using hax;

namespace cheat
{
    public class Loader
    {
        public static GameObject L;
        public static void Load()
        {
            // Создаем экземпляр GameObject
            Loader.L = new GameObject();

            // Добавляем наш класс, который будет содержать всю логику для читов
            Loader.L.AddComponent<hax.Hacks>();

            // Сообщаем Unity не уничтожать наш GameObject при смене уровня
            UnityEngine.Object.DontDestroyOnLoad(Loader.L);
        }

        public static void Unload()
        {
            // Уничтожаем наш GameObject при вызове
            UnityEngine.Object.Destroy(L);
        }
    }
}

Теперь внимательно посмотрите на это, потому что, когда вы вставите это в Visual Studio, тобудет полно красных волнистых линий.

Это потому, что мы еще не создали наш другой файл, который будет содержать пространство имен hax и класс hax.Hacks, и также потому, что мы еще не импортировали Unity engine как зависимость.

Это одна из вещей, которые делают хакинг игр с движком .NET таким увлекательным - вы можете добавить в Visual Studio фактические DLL, поставляемые с игрой, в качестве зависимостей, и они будут бесшовно интегрироваться с IDE!

Чтобы добавить Unity engine, перейдите в обозреватель решений и щелкните правой кнопкой мыши на "Dependencies > Add Project Reference".

1717239017688.png


В новом диалоговом окне нажмите "Обзор" на левой боковой панели и снова "Обзор" на нижней панели. Вы увидите, что я уже добавил кучу DLL-файлов из игр на Unity, поэтому они отображаются в моей истории, но этих файлов не будет, когда вы откроете это диалоговое окно в первый раз.

1717239057405.png


Когда появится диалоговое окно выбора файла, перейдите в ту же директорию .\snake_Data\Managed\ внутри сборки Snake, где вы нашли Assembly-CSharp.dll. Выберите UnityEngine.dll как файл для импорта. Теперь сделайте то же самое, чтобы добавить UnityEngine.CoreModule.dll. После того, как они будут добавлены в качестве зависимостей в ваш проект, вы сможете ссылаться на функции и классы Unity, такие как GameObject, в вашем коде.

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

Внедрение GUI в игру

Для первого шага по выполнению кода в змейке мы заставим в игре появиться небольшой компонент GUI, так как мы все равно хотим привязать наш "хак" к кнопке в игре. Для этого мы создадим новый файл в нашем проекте Visual Studio со следующим шаблоном:

C#:
using System;
using UnityEngine;

namespace hax
{
    public class Hacks : MonoBehaviour
    {
        public void OnGUI()
        {
            // Здесь будет код GUI!
        }
    }
}

Пространство имен hax и класс Hacks — это просто временные имена, но функция OnGUI является унаследованной функцией каждого объекта внутри движка Unity и является единственной функцией, используемой для рендеринга и обработки событий GUI. Если вам интересно, базовым объектом внутри движка Unity является класс MonoBehaviour, который мы расширяем.

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

C#:
// Практически дословно взято с https://docs.unity3d.com/ScriptReference/GUI.Window.html
public void OnGUI()
{
    // Создаем окно в центре верхней части экрана игры, которое будет содержать нашу кнопку
    Rect windowRect = new Rect(Screen.width / 2, Screen.height / 8, 120, 50);

    // Регистрируем окно. Обратите внимание, что третий параметр - это функция окна для создания содержимого окна, определенная ниже
    windowRect = GUI.Window(0, windowRect, DoMyWindow, "HackBox");

    // Создаем содержимое окна
    void DoMyWindow(int windowID)
    {
        // Комбинированная строка, которая создает кнопку и проверяет, была ли она нажата
        if (GUI.Button(new Rect(10, 20, 100, 20), "Add Tail"))
        {
            // Логика добавления хвоста должна быть добавлена здесь!
        }
    }
}

Когда вы сохраните этот код, вы заметите, что объект GUI не определен. Нам нужно будет добавить еще одну зависимость для этого, в данном случае DLL-файл UnityEngine.IMGUIModule.dll, который также можно найти в директории Managed. Теперь вы должны проверить оба .cs файла, которые вы создали в проекте Visual Studio, и убедиться, что в них нет ошибок.

Теперь мы готовы скомпилировать и загрузить наш "хак" в экземпляр игры. Ура!

Соберите проект Visual Studio и запомните путь к созданному DLL. Нам нужно будет передать его SharpMonoInjector вместе с пространством имен, классом и именем функции нашего загрузочного класса в созданном нами шаблоне загрузчика.

Тестирование инжектора

Откройте командную строку, перейдите в директорию, в которую вы загрузили SharpMonoInjector, и запустите его без аргументов, чтобы увидеть справочное сообщение:

Код:
.\smi.exe
SharpMonoInjector 2.2

Usage:
smi.exe <inject/eject> <options>

Options:
-p - The id or name of the target process
-a - When injecting, the path of the assembly to inject. When ejecting, the address of the assembly to eject
-n - The namespace in which the loader class resides
-c - The name of the loader class
-m - The name of the method to invoke in the loader class

Теперь запустите змейку, затем переключитесь на окно командной строки и внедрите ваш DLL. Для нашей игры ваша команда инъекции будет выглядеть примерно так:
Код:
.\smi.exe inject -p snake -a <path to built DLL> -n cheat -c Loader -m Load

Если инъекция успешна, SharpMonoInjector выведет смещение внедренного DLL.

Вы также увидите ваш интерфейс в игре змейка. Если это не удается, попробуйте снова запустить команду от имени администратора. Иногда Microsoft Defender также не любит инструменты инъекции процессов, так как многие вредоносные программы используют инъекцию процессов.

Вы также можете попробовать отключить Defender, если это не работает и вас это не пугает. Если все пройдет хорошо, вы должны увидеть что-то вроде этого в игре:

1717239413075.png


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

Добавление хвоста с использованием рефлексии

Рефлексия полезна для доступа и манипуляции экземплярами объектов во время выполнения. Unity также имеет несколько отличных встроенных функций для этого. Мы будем использовать оба метода для реализации нашей функции добавления сегмента хвоста.

Сначала получим объект змейки, который был создан. Unity имеет функцию GameObject.FindObjectOfType<T> для этой цели.

C#:
Snake snake = GameObject.FindObjectOfType<Snake>();

Обратите внимание, что эта функция возвращает только первый найденный экземпляр объекта, поэтому она полезна только в тех случаях, когда вы знаете, что существует только один экземпляр объекта. В противном случае вы можете использовать GameObject.FindObjectsOfType<T>, чтобы получить массив всех объектов, которые вы можете перебирать, чтобы найти нужный объект или манипулировать всеми сразу.

Для того чтобы наш хак понял класс Snake, нам нужно добавить Assembly-CSharp.dll, DLL с логикой нашей игры, которую мы рассматривали ранее в DNSpy, в качестве зависимости проекта.

Теперь мы можем создать объект типа Type для типа Snake в нашем коде:

C#:
// Создаем объект "Type" для типа "Snake"
Type snakeType = snake.GetType();

Теперь давайте изменим поле ate нашей змейки на true. Мы будем использовать рефлексию, чтобы создать объект FieldInfo для конкретного поля ate внутри объекта Snake.

C#:
// Используем объект System.Reflection.FieldInfo, чтобы узнать атрибуты поля и получить доступ к его метаданным
// https://learn.microsoft.com/en-us/dotnet/api/system.reflection.fieldinfo?view=net-7.0
FieldInfo ateField = snakeType.GetField("ate", flags);

Обратите внимание на второй аргумент flags. Нам нужно использовать BindingFlags рефлексии, чтобы описать, какого рода переменной является наше целевое поле:
  • BindingFlags.Instance: позволяет получить доступ к переменной экземпляра объекта
  • BindingFlags.Public: публичные переменные
  • BindingFlags.NonPublic: частные переменные
  • BindingFlags.Static: статические переменные
Так как флаги можно объединять с помощью оператора OR и не важно, если переменная не имеет одного из атрибутов, которые мы указали в BindingFlags, мы можем задать широкий диапазон значений и охватить большинство типов переменных, с которыми мы сталкиваемся в объекте Unity.

C#:
// Широкий диапазон значений с помощью BindingFlags для охвата большинства переменных, с которыми мы сталкиваемся. Уменьшите диапазон при необходимости.
// https://learn.microsoft.com/en-us/dotnet/api/system.reflection.bindingflags?redirectedfrom=MSDN&view=net-7.0
BindingFlags flags = BindingFlags.Instance
       | BindingFlags.Public
       | BindingFlags.NonPublic
       | BindingFlags.Static;

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

Синтаксис изменения поля в объекте с использованием рефлексии немного странный. Объект FieldInfo фактически содержит метод для этого. Метод SetValue принимает два аргумента: объект, содержащий поле, и значение, которое вы хотите установить. Для нас это значение true.

C#:
// Устанавливаем значение поля "ate" в объекте Snake нашей игры на true
ateField.SetValue(snake, true);

Теперь давайте соберем все это вместе в наш работающий "хак" для змейки:

C#:
using System;
using UnityEngine;
using System.Reflection;

namespace hax
{
    public class Hacks : MonoBehaviour
    {
        // Широкий диапазон значений с помощью BindingFlags для охвата большинства переменных, с которыми мы сталкиваемся. Уменьшите диапазон при необходимости.
        // https://learn.microsoft.com/en-us/dotnet/api/system.reflection.bindingflags?redirectedfrom=MSDN&view=net-7.0
        BindingFlags flags = BindingFlags.Instance
               | BindingFlags.Public
               | BindingFlags.NonPublic
               | BindingFlags.Static;

        // Зарезервированная функция Unity Engine, которая обрабатывает события интерфейса для всех объектов
        // Практически дословно взято с https://docs.unity3d.com/ScriptReference/GUI.Window.html
        public void OnGUI()
        {
            // Создаем окно в центре верхней части экрана игры, которое будет содержать нашу кнопку
            Rect windowRect = new Rect(Screen.width / 2, Screen.height / 8, 120, 50);

            // Регистрируем окно. Обратите внимание, что третий параметр - это функция обратного вызова для создания окна, определенная ниже
            windowRect = GUI.Window(0, windowRect, DoMyWindow, "HackBox");

            // Создаем содержимое окна
            void DoMyWindow(int windowID)
            {
                // Комбинированная строка, которая создает кнопку и проверяет, была ли она нажата
                if (GUI.Button(new Rect(10, 20, 100, 20), "Add Tail"))
                {
                    this.AddTail();
                }
            }
        }

        public void AddTail()
        {
            // Получаем созданный объект Snake
            Snake snake = GameObject.FindObjectOfType<Snake>();

            // Создаем объект "Type" для типа "Snake"
            Type snakeType = snake.GetType();
            // Используем объект System.Reflection.FieldInfo, чтобы узнать атрибуты поля и получить доступ к его метаданным
            // https://learn.microsoft.com/en-us/dotnet/api/system.reflection.fieldinfo?view=net-7.0
            FieldInfo ateField = snakeType.GetField("ate", flags);
            // Устанавливаем значение поля "ate" в объекте Snake нашей игры на true
            ateField.SetValue(snake, true);
        }
    }
}

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

Отличная работа! Вы взломали свою первую игру на Unity.

Раз уж вы здесь, я могу показать вам, как вызывать функции.

Существует класс System.Reflection.MethodInfo, который работает аналогично FieldInfo. Вы можете вызвать метод Invoke объекта MethodInfo для данного объекта и любых необходимых аргументов. В данном случае аргументов нет, поэтому мы просто передаем null.

C#:
// MethodInfo/GetMethod работает практически так же, как FieldInfo/GetField для целевых методов в созданных объектах
MethodInfo dynMethod = snakeType.GetMethod("Move", flags);
// Вызываем метод "Move" в нашем объекте Snake без аргументов
dynMethod.Invoke(snake, null);

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

1717240110990.png


Надеюсь, вам было весело! Если что-то не работает, вы всегда можете ознакомиться с материалами на
 

HMCoba

Активный пользователь
Активный
Регистрация
22.04.2023
Сообщения
185
Репутация
139
Как всегда всё самое лучщее от X-Shar!
Забавно, отличный мануал для старта!
 
Автор темы Похожие темы Форум Ответы Дата
MKII GameHacking 7
MKII GameHacking 6
Верх Низ