Гайд Пишем чит на Unreal Engine 5 #1

dev
Забаненный
Статус
Оффлайн
Регистрация
5 Апр 2022
Сообщения
303
Реакции[?]
222
Поинты[?]
2K
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Доброго времени суток дорогие форумчане. Довольно долгое время пишу софты на движке Unreal Engine, как таковых полноценных гайдов я особо не видел, а может и я не умею искать стать, но не суть важно. Решил взять себя в руки и написать свою статью о разработке чита на Unreal Engine

В статье очень много буковок и картинок, будьте аккуратны

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

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

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

1. Основные программы для работы
  1. Microsoft Visual Studio ( ссылка:
    Пожалуйста, авторизуйтесь для просмотра ссылки.
    ) - Собственно самая важная программа, в которой мы и будем писать код. В статье я буду использовать MSVS 2022. При установке вам нужно будет выбрать "Разработка классических приложений на C++"
  2. x64dbg ( ссылка:
    Пожалуйста, авторизуйтесь для просмотра ссылки.
    ) - Программа для отладки. На первое время нам понадобится из его функционала только Scylla ( при желании вы можете скачать её отдельно
    Пожалуйста, авторизуйтесь для просмотра ссылки.
    ), для того чтоб сдампить саму игру.
  3. ReClass ( ссылка:
    Пожалуйста, авторизуйтесь для просмотра ссылки.
    ) - Программа для реверса структур, нам оно понадобится для создания классов
  4. IDA Pro ( ссылкa:
    Пожалуйста, авторизуйтесь для просмотра ссылки.
    (no ad) ) - Софт для анализа дампа игры, в котором мы и будем собирать офсеты

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

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

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

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

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



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

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


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

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


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

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'ы


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


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


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


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

Обращаем внимание на данные строки:
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)"
Переходим декомпилируем


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

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

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

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


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

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

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


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

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


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


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


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

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

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

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


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

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

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

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



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

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


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



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

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

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


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

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


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


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

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

Вставляем наш код и немного редактируем его. Во первых надо задать все имена для классов. Например:
Было
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. Я был бы нелюдем если бы не дал эти структурки вам:
Пожалуйста, авторизуйтесь для просмотра ссылки.


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

Теперь нам надо изменить 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 );
            }

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


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

А так же расскажу:
1. Как получить игровой никнейм
2. Как собрать бокс / Скелет есп
3. Напишем простой аимбот

Это моя первая статья, писал я её долго может что то не понятно и не правильно будет, но я старался

Чуть позже доработаю, пока что оставлю всё так
 
@removespread
Пользователь
Статус
Оффлайн
Регистрация
13 Янв 2018
Сообщения
351
Реакции[?]
147
Поинты[?]
0
бро, хорош, статья бомба.

но как насчет результат своей ебли над полигоном скинуть сюда в формате длл? :LUL:
 
dev
Забаненный
Статус
Оффлайн
Регистрация
5 Апр 2022
Сообщения
303
Реакции[?]
222
Поинты[?]
2K
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
бро, хорош, статья бомба.

но как насчет результат своей ебли над полигоном скинуть сюда в формате длл? :LUL:
длл выложу только когда завершу серию этих статей. Она ещё не 1 будет
 
Начинающий
Статус
Оффлайн
Регистрация
17 Янв 2021
Сообщения
49
Реакции[?]
4
Поинты[?]
0
бро, хорош, статья бомба.

но как насчет результат своей ебли над полигоном скинуть сюда в формате длл? :LUL:
Мне кажется в этом и суть, чтобы человек сам сделал, а не дали готовое :bayan:
 
На самом деле я Админ
Пользователь
Статус
Оффлайн
Регистрация
22 Дек 2020
Сообщения
856
Реакции[?]
139
Поинты[?]
9K
Сенсей, хорош!

Этот человек овердохуя чему меня научил, и как я вижу сейчас вас научит)
 
Начинающий
Статус
Оффлайн
Регистрация
16 Май 2022
Сообщения
2
Реакции[?]
0
Поинты[?]
0
Статья просто огонь <3
 
Начинающий
Статус
Оффлайн
Регистрация
17 Ноя 2020
Сообщения
8
Реакции[?]
1
Поинты[?]
0
Просто подарок какой то. Человечище, спасибо большое за полезную статью.
 
Начинающий
Статус
Оффлайн
Регистрация
29 Мар 2021
Сообщения
103
Реакции[?]
12
Поинты[?]
0
а че делать если я фейсом инжекчу а имгуй не появляется, а если захожу без античита то появляется
 
Начинающий
Статус
Оффлайн
Регистрация
17 Дек 2022
Сообщения
16
Реакции[?]
20
Поинты[?]
6K
Спасибо, информативно!
 
Начинающий
Статус
Оффлайн
Регистрация
16 Мар 2018
Сообщения
11
Реакции[?]
0
Поинты[?]
1K
Топовый гайд!
 
Сверху Снизу