Статья Пишем чит на Unreal Engine 5 #1

  • 8
  • 6
Доброго времени суток дорогие форумчане. Довольно долгое время пишу софты на движке Unreal Engine, как таковых полноценных гайдов я особо не видел, а может и я не умею искать стать, но не суть важно. Решил взять себя в руки и написать свою статью о разработке чита на Unreal Engine

мини предисловие: я писал эту статью 2 часа, словил бсод, и примерно 80% статьи у меня убило. Придётся писать заново. Странно что сохранение черновика не сработало как надо. Хорошо что я хотя бы додумался сохранить скриншоты все. Текст написать проще чем заново делать скрины :С

Собственно в мои руки попала довольно не новая игра, но одна из немногих игр на Unreal Engine 5 - POLYGON. Игра Free 2 Play, а в качестве анти-чита используется бесплатная версия Easy Anti Cheat. Так как статью я пишу об Internal чите, для инжекта вам может потребоваться kernel-mode инжектор, но так как там бесплатный EAC вы можете использовать банальный face-injector ( GitHub - KANKOSHEV/face-injector-v2: update face injector by KANKOSHEV ). Для тестов он вам годится

Мельком пробежавшись по дампу игры понял что разницы с Unreal Engine 4 в плане чит-дева не особо много, но всё же отличия есть

Собственно небольшое оглавление данной статьи:
  1. Основные программы для работы
  2. Делаем дамп игры
  3. Настраиваем IDA Pro для работы с дампом
  4. Объяснение по основным офсетам
  5. Сбор офсетов
  6. Пишем первый код
Конечно будут ещё подпункты в статьях, но дописывать их не хочу

1. Основные программы для работы
  1. Microsoft Visual Studio ( ссылка: Visual Studio: IDE и редактор кода для разработчиков и групп, работающих с программным обеспечением ) - Собственно самая важная программа, в которой мы и будем писать код. В статье я буду использовать MSVS 2022. При установке вам нужно будет выбрать "Разработка классических приложений на C++"
  2. x64dbg ( ссылка: x64dbg ) - Программа для отладки. На первое время нам понадобится из его функционала только Scylla ( при желании вы можете скачать её отдельно GitHub - NtQuery/Scylla: Imports Reconstructor ), для того чтоб сдампить саму игру.
  3. ReClass ( ссылка: GitHub - ReClassNET/ReClass.NET: More than a ReClass port to the .NET platform. ) - Программа для реверса структур, нам оно понадобится для создания классов
  4. IDA Pro ( ссылкa: https://cracklab.team/threads/6/ (no ad) ) - Софт для анализа дампа игры, в котором мы и будем собирать офсеты

2. Делаем дамп игры
После того как Вы скачали весь нужный софт из 1 "главы" (буду называть это так), нам нужно сделать дамп игры.
Так как игра у нас защищена кернел анти читом, просто так сдампить не получится. Конечно Вы можете немного извратиться и скачать KsDumper ( ссылка: GitHub - EquiFox/KsDumper: Dumping processes using the power of kernel space ! ) для дампа игры под анти читом. Но с огромной вероятностью Вас либо не пустит в игру, либо же Вам забанит аккаунт.
Но пока что мы обойдёмся x64dbg'ом и его плагином Scylla.
Чтобы сдампить игру, нам надо запустить её без анти чита. Переходим в стим, нажимаем ПКМ на нашу игру, наводим на "Управление" и кликаем "Посмотреть локальные файлы". Мы попадаем в папку с игрой, и дальше идём по пути - POLYGON\Binaries\Win64. Ищем там бинарник POLYGON-Win64-Shipping.exe и запускаем его.

После того как игра прогрузилась до ошибки инициализации, запускаем x64dbg
В x64dbg переходим во вкладку Plugins и выбираем Scylla:
1672921069442.png

В открывшемся окне, в поле "Attach to an active process" выбираем наш процесс игры. В данной ситуации это "POLYGON-Win64-Shipping.exe"

Далее кликаем "IAT Autosearch" и немного ждём ( На слабых пк может и не немного ). В появившемся окошке Information кликаем да
1672921210945.png

В следующем окошке кликаем "ОК" и в окне Scylla нажимаем на "Get Imports"
После чего нам надо удалить инвалидные импорты ( помечены красным крестиком ) из полученного списка. Делается это нажатием на ПКМ по нему и нажатием "Delete Tree Node"

1672921220606.png


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

3. Настраиваем IDA Pro для работы с дампом
Настройку IDA Pro можно проводить в любой момент, но в идеала дождаться пока дамп полностью проанализируется. Понять что дамп полностью проанализировался можно по левому нижнему углу, там должно быть написано idle
1672921247658.png


Далее мы нажимаем Shift + F12 для того чтоб сгенерировать строки. После того как они сгенерируется, в любом месте вкладки Strings кликаем ПКМ, выбираем Setup
1672921253772.png

Там мы выбираем в табличке "Allowed Strings Types" - C-Style, Unicode C-Style
1672921264907.png


На этом этапе для сбора основных офсетов нам хватит этих настроек

4. Объяснение по основным офсетам
Для данной статьи я сделал такую простую диаграмму где идёт зависимости офсетов
Untitled Diagram.png


Пояснения по цветам там есть, и оно должно быть для вас более-менее понятным

Сами классы ( офсеты ):
UWorld - Основной класс. По документации Unreal Engine этот класс отвечает за сам наш игровой мир, и всё что в нём хранится
ULevel - Наша игровая локация, и всё что на неё хранится
AActor - Все наши акторы на карте ( например игроки, деревья, лут, звуки, свет и тд и тп )
USceneComponent - Параметры наших акторов на определённой сцене
GetObjectName - Получение имени акторов
APawn - Базовый класс всех акторов
RelativeLocation - Позиция наших акторов на сцене

UGameInstance - Тут хранятся все параметры
ULocalPlayer - Из названия понятно что это наш локальный игрок
APlayerController - Класс управления нашим актором
APawn - В данном случае базовый класс нашего локального актора
ProjectWorldToScreen - Преобразование Vector3 координат в Vector2 координаты для вывода есп на экран

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

5. Сбор офсетов
Теперь перейдём к самому сбору офсетов. Поиск их будет происходить в основном по строкам, я приведу пример строк по которым 100% можно что либо найти, так же вы сможете попробовать поискать свои, так как Unreal Engine опен сурс движок.

Начнём мы с UWorld. Для того чтоб найти его, в поиск по строкам вставляете следующую строку: "No world was found for object (%s) passed in to UEngine::GetWorldFromContextObject()."
Переходим к нему 2 раза кликнув, потом нажимаем X дабы открыть Xref'ы
1672921295095.png


Переходим к функции и декомпилируем её нажатием F5
1672921305422.png


Обращаем внимание на 44-45 строки
if ( !v15 ) return qword_7FF7A2139140
7FF7A2139140 - Аддрес UWorld'a. Из него нам надо получить сам офсет. Сделать это можно перейдя в IDA Output и ввести простенькую команду
0x7FF7A2139140-get_imagebase(). Второе значение с буквой H в конце, это и есть наш офсет. По итогу выходит 0x7E49140. Записываем его в какой нибудь блокнот на пк, и идём искать всё остальное
1672921315423.png


Далее на очереди у нас UGameInstance. Его мы будем искать по строке "InWorld->GetGameInstance() is null"
Переходим к строчке, нажимаем xref и переходим к функции. Тут можно даже не декомпилировать её, тут её прекрасно видно 0x190 - Наш офсет. Записываем и бежим дальше
1672921324727.png


Теперь ищем офсетик ULevel. Ищется по строке "RequestLevel: World is pending kill %s". Переходим xref'aем и декомпилируем
1672921333089.png

Обращаем внимание на данные строки:
pseudocode:
 if ( (*(v25 + 8) & 0x60000000) != 0 )
    {
      if ( byte_7FF7A2120658 >= 6u )
      {
        v47 = sub_7FF79C7262E0(&v68, &v111);
        if ( *(v47 + 8) )
          v28 = *v47;
        v86 = v28;
        sub_7FF79FEDAAB0(&v71, &byte_7FF7A2120658, aRequestlevelWo, &v86);
        v19 = v111;
        goto LABEL_158;
      }
      return 0;
    }
    v46 = *(v25 + 48);
    if ( v46 != *(a1 + 344) )
    {
      *(v25 + 274) = *(a2 + 274);
      *(v46 + 184) = a2;
      sub_7FF79EF24080(a1, *(v25 + 48), v29);
    }
    return 1;

Тут "v46 = *(v25 + 48);" где 48 наш офсет (функцию можно представить в виде: ULevel = ( UWorld + 0x30 ) ). кликаем по нему мышкой, и нажимаем на клавиатуре H ( или же русская Р ). Выходит 0x30 - это наш офсет ULevel

Теперь у нас ULocalPlayer. Ищется по строчке: "UGameInstance::RemovePlayer: Removed player %s with ControllerId %i at index %i (%i remaining players)"
Переходим декомпилируем
1672921368535.png


Тут где мы видим a1 + 56 (Функцию можно представить в виде UGameInstance + 0x38) - 56 наш офсет. Точно так же кликаем H на него и получаем 0x38. Это и есть наш офсет. Сохраняем идём дальше

Дальше у нас APlayerController
Ищем по строчке - "Failed to find local player for controller id %d". Переходим декомпилируем и смотрим
1672921381041.png

Тут у нас строка v8 = *(v7 + 48); - где 48 наш офсет ( можно представить в виде: APlayerController = (ULocalPlayer + 0x30) ). Кликаем H и получаем офсет 0x30 - Это наш APlayerController

AActor: ищем по стрчоке "Num Actors: %i".
Переходим к функции декомпилируем

1672921392606.png

Тут переходим на функцию которая находится в v10 ( Эта функция является GetActorCount ).
1672921403272.png

Листаем в самый низ, и видим строчку "v1 += *(v8 + 160);" ( представить её можно в виде ActorCount = (ULevel + 0xA0) ) тут 160 мы преобразуем в HEX и получаем 0xA0. Это ActorCount офсет. Чтобы получить нам класс AActor нужно из 0xA0 вычесть 0x8. Получается 0xA0 - 0x8 = 0x98. 0x98 это наш офсет класса AACtor

Дальше у нас идут функции, начнём пожалуй с WorldToScreen
Ищем по строчке "ProcessServerTravel: %s". Переходим к строке, и листаем вниз на 2 функции.
Наш WorldToScreen должен выглядеть так:
1672921411858.png


Копируем всё что после sub_ и считаем в IDC путём 0x7FF79EDEC1A0-get_imagebase(). На выходе получаем офсет 0x4AFC1A0. Это собственно и есть наш world to screen

Теперь ищем GetObjectName. Для его поиска в Strings пишем GetObjectName. Нажимаем X и переходим ко второму xref'y
1672921428018.png


Потом переходим на функцию под нашей строчкой
1672921437622.png


декомпилируем эту функцию и внимательно смотрим
1672921447904.png


Там где v8 = sub_7FF79EED2720(v10, v7); всё что после sub_ это наш аддрес функции. Считаем по тому же пути что и world to screen ( 0x7FF79EED2720-get_imagebase() ) получаем офсет 0x4BE2720 это наш офсет GetObjectName

На этом наш поиск офсетов закончен. Теперь мы можем перейти к написанию кода

6. Пишем код
Для написания кода вам понадобится база чита. Объективно хукнуть Present и Resize и подключить имгуи. Вам этого хватит.
Я для этого буду использовать свою готовую базу ( просить её не надо, я всё равно не дам )

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

1672921507907.png

2 раза кликаем по красному значению ( Это значение является base address )
1672921517633.png

и прибавляем к этому значению наш офсет UWorld
1672921527453.png

Нажимаем Enter и кликаем ПКМ по офсету 0000 выбираем там Change Type и выбираем Pointer
1672921539542.png

После того как кликнули нажимаем на 2 стрелочки и выбираем Class Instance


1672925840278.png

Далее путём клика пкм по появившемуся классу добавляем ему 2 раза 256 байт
1672925851440.png

У вас должно выйти примерно такое:
1672925861621.png


Теперь вы знаете как создавать классы в реклассе. Но это не конец. Во 1х где строчка класс переименовывайте в UWorld так как мы находимся в нём
Дальше смотрим на офсет 0030. Тут у нас находится ULevel. Делаем класс и добавляем 3 раза 64 байта или 1 раз 256 как удобнее. Понять что вы находитесь в верном классе, можно по офсету 0058. Там обычно пишется имя карты на которой мы находимся

1672925885754.png


Но это нам не особо надо, мы переходим к нашему офсету 0x98 (0098) - AActor. Делаем из него класс. Если вы хотите можете поменять значение офсета 00A0 на uint32. Это будет количество акторов которые находятся в данный момент на карте. А 00A4 - Максимальное количество акторов на карте

но они нам особо не нужны

Дальше спускаемся до 0190 в UWorld к нашему классу UGameInstance. Делаем из него класс и добавляем 64 байта этого нам хватит. Тут же делаем класс по офсету 0038 - Это наш ULocalPlayer. У вас должно получится так:
1672925894826.png


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

По итогу у вас должно выйти вот так:
1672925905324.png


Этого нам собственно хватит на данном этапе. Вверху кликаем Project -> Generate C++ code
Нам выведет это:
1672925912787.png


Копируем всё что идёт от class UWorld и до самого конца. Первые 2 класса можно не затрагивать. Теперь переходим в MSVS

Как я и сказал вам нужна будет база с хуком и какой нибудь рисовкой ( Как пример можно использовать ( главное не используйте это на постоянной основе, с вероятностью 99% вы получите бан ): GitHub - rdbo/ImGui-DirectX-11-Kiero-Hook: Universal ImGui implementation through DirectX 11 Hook (kiero). Better version: https://github.com/rdbo/DX11-BaseHook )

Вставляем наш код и немного редактируем его. Во первых надо задать все имена для классов. Например:
Было
UWorld class:
class UWorld
{
public:
    char pad_0000[ 48 ]; //0x0000
    class ULevel* N000006CC; //0x0030
    char pad_0038[ 344 ]; //0x0038
    class UGameInstance* N000006F8; //0x0190
    char pad_0198[ 112 ]; //0x0198
}; //Size: 0x0208
Стало
UWorld class:
class UWorld
{
public:
    char pad_0000[ 48 ]; //0x0000
    class ULevel* mylevel; //0x0030
    char pad_0038[ 344 ]; //0x0038
    class UGameInstance* owninggameinstance; //0x0190
    char pad_0198[ 112 ]; //0x0198
}; //Size: 0x0208

И так далее. Суть вы должны понять, ибо это не сложно. Ставите так как удобно вам

Далее нам потребуется SDK Unreal Engine'a. А конкретно функции Vector2, 3; FString; TArray. Я был бы нелюдем если бы не дал эти структурки вам: unreal sdk - Pastebin.com

копируем и вставляем над нашими классами.

Теперь нам надо изменить 2 офсета в классах. А конкретно
ULevel -> AActor и UGameInstance -> ULocalPlayer
Их надо поместить в TArray.

TArray < class AActor *> actors;
TArray< class ULocalPlayer* > localplayer;

Выйти у вас должно так:
C++:
class ULevel
{
public:
    char pad_0000[ 152 ]; //0x0000
    TArray< class AActor* > actors; //0x0098
}; //Size: 0x00C8

class UGameInstance
{
public:
    char pad_0000[ 56 ]; //0x0000
    TArray< class ULocalPlayer* > localplayer; //0x0038
}; //Size: 0x0048

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

Для начала напишем функцию WorldToScreen. У неё должно быть 3 аргумента ( 4 вообще если считать бул, но он в движке стоит по дефолту и изменять нам его не обязательно ) аргументы у неё это APlayerController - наше управление игроком, Vector3 - позиция актора в мире, Vector2 - позиция на экране . Сама функция имеет тип bool. По итогу объявление функции должно иметь такой вид:
engine functions:
bool world_to_screen( APlayerController*, Vector3, Vector2* );

Теперь давайте её определим. Мы знаем нужные нам аргументы и офсет функции.
Пишем такой код:

WorldToScreen function:
    bool world_to_screen( APlayerController* controller, Vector3 world_pos, Vector2* screen_pos )
    {
        auto base = ( uintptr_t ) GetModuleHandle( L"POLYGON-Win64-Shipping.exe" ); // Получаем базовый аддрес приложения

        using worldtoscreen = bool( * )( APlayerController*, Vector3, Vector2* ); // Объявляем тип функции

        const auto WorldToScreen = ( worldtoscreen ) ( base + 0x4AFC1A0 ); // Инициализируем функцию читая её офсет

        return WorldToScreen( controller, world_pos, screen_pos ); // Вызываем функцию
    }

В идеале сделать чек на валид, но до этого мы дойдём потом

Теперь делаем функцию GetObjectName. Функция имеет 1 аргумент AActor - наш актор имя которого мы получаем. Тип функции FString ( Наша кастомная структурка )
Объявляем её:
GetObjectName:
FString get_object_name( AActor* );
Определяем:
GetObjectName function:
    FString get_object_name( AActor* actor )
    {
        auto base = ( uintptr_t ) GetModuleHandle( L"POLYGON-Win64-Shipping.exe" ); // Получаем базовый аддрес приложения

        using getobjectname = FString( * )( AActor* ); // Объявляем тип функции

        const auto GetObjectName = ( getobjectname ) ( base + 0x4BE2720 ); // Инициализируем функцию читая её офсет

        return GetObjectName( actor ); // Вызываем функцию
    }

Готово! Мы объявили 2 функции которые нашли, и теперь можем спокойно их использовать.

Единственное что надо немного дописать класс AActor и добавить ещё 1. Так как не хочу запаривать вам голову реверсом. Эти 2 офсета ищутся в реклассе или иде. Но я просто дам вам готовые классы
classes:
class AActor
{
public:
    char pad_0000[ 0x190 ]; //0x0000
    class USceneComponent* rootcomponent;
}; //Size: 0x0008

class USceneComponent
{
public:
    char pad_0000[ 0x138 ];
    Vector3 relativelocation;
};

Так мы сможем получить позицию наших акторов.

Теперь мы можем перейти к написанию самого кода ЕСП. Я решил расписать всё это в виде комментариев так как тут и так много текста. Так что держите и анализируйте:
Simple ESP:
bool execute( )
    {
        auto base_address = ( uintptr_t ) GetModuleHandle( L"POLYGON-Win64-Shipping.exe" ); // Получаем базовый аддрес приложения
    
        auto myworld = *( UWorld** ) ( base_address + 0x7E49140 ); // Инициализируем класс UWorld
        if ( !myworld ) return false; // Проверяем на валид наш класс

        auto level = myworld->mylevel; // Получаем класс ULevel из под класса UWorld
        if ( !level ) return false; // Проверяем на валид наш класс

        auto gameinstance = myworld->owninggameinstance; // Получаем класс UGameInstance из под класса UWorld
        if ( !gameinstance ) return false; // Проверяем на валид наш класс

        auto localplayer = gameinstance->localplayer[ 0 ];// Получаем класс ULocalPlayer из под класса UGameInstance. Важно что ULocalPlayer находится в TArray. По этому надо указать [ 0 ]
        if ( !localplayer ) return false; // Проверяем на валид наш класс

        auto playercontroller = localplayer->playercontroller; // Получаем класс APlayerController из под класса ULocalPlayer
        if ( !playercontroller ) return false; // Проверяем на валид наш класс

        auto actors = level->actors; // Получаем класс AActor из под класса ULevel

        for ( auto i = 0; i < actors.Num( ); i++ ) // Запускаем цикл перебора акторов. Где actors.Num( ) - количество акторов
        {
            if ( !actors.IsValidIndex( i ) ) continue; // Проверяем индекс на валид. Тут вызывается проверка что если индекс < количества акторов, то возвращается false

            auto actor = actors[ i ]; // Получаем текущего актора ( в цикле )
            if ( !actor ) continue; // проверяем этого актора на валид

            auto rootcomponent = actor->rootcomponent; // Получаем USceneComponent для текущего актора
            if ( !rootcomponent ) continue; // Проверяем USceneComponent на валид

            auto world_position = rootcomponent->relativelocation; // Получаем позицию актора в мире

            auto actor_name = engine::get_object_name( actor ); // Объявляем и вызываем функцию actor_name где будут хранится имена акторов
            if ( !actor_name.IsValid( ) ) continue; // Проверяем имя актора на валид

            Vector2 screen_position{}; // Инициализируем переменную позиции на экране
            if ( engine::world_to_screen( playercontroller, world_position, &screen_position ) ) // вызываем WorldToScreen ( 1 аргумент наш playercontroller, 2 аргумент позиция актора в мире, 3 аргумент позиция на экране в которую мы записываем значения
            {
                // Желательно держать WorldToScreen в if ( ) { } чтобы не было лишней нагрузки на наш пк. Пока актор находится в пределах видимости экрана w2s возвращает true, иначе вернёт false.
                // Соответственно рисовать мы будем только тогда, когда w2s будет true

                // Так как значение FString при вызове .c_str() отдаётся в виде wchar_t, нам надо записать его в char
                char buffer[ 2048 ];
                ImFormatString( buffer, IM_ARRAYSIZE( buffer ), "%ws", actor_name.c_str( ) );

                // Рисуем наших акторов
                ImGui::GetOverlayDrawList( )->AddText( ImVec2( screen_position.x, screen_position.y ), IM_COL32( 255, 255, 255, 255 ), buffer );
            }

        }
    }

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

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

Чуть позже доработаю, пока что оставлю всё так
 

Вложения

  • 1672921195631.png
    1672921195631.png
    900.1 KB · Просмотры: 147
Последнее редактирование модератором:
Сверху Снизу