• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья [0x3]Легкий Геймхакинг: Шаг за шагом.

Unseen

(L3) cache
Пользователь
Регистрация
17.01.2022
Сообщения
262
Реакции
246
Author: Unseen
Источник: xss.pro


Предыдущая часть: https://xss.pro/threads/104114/

Вступление...

Доброго времени суток, дорогие Форумчане! Не получилось выпустить эту часть пораньше в связи с тем, что винт решил перед НГ катапультироваться и унести всю информацию в том числе и эту статью в мир иной. НО! Праздничное настроение спасли бэкапы =)

Слова благодарности...

Прежде всего хочу поблагодарить admin этого форума, за его поддержку и отзывчивость.

Благодарю snoww за его материальную поддержку в виде 300$.

И всех, кто принимает активное участие в развитии статей своими идеями и советами.

Предыстория… :rolleyes:

Как вы помните в прошлых наших частях 1 и 2, мы разобрались, что из себя представляет память, байт коды, типы данных, и разобрались каких видов(External, Internal) бывает софт и написали свою первую программу(проще говоря Memory Manager) для работы с памятью другой программы(в данном случае игры). Если какие-то моменты подзабыли – рекомендую быстро пройтись по предыдущим частям.

Новые проблемы…

Конечно все бы ничего, если адреса к которым мы обращаемся не менялись… Наверное, вы замечали, что после нахождения адреса какого-то значения в игре, после перезапуска уровня или игры этот адрес в 90% случаев уже был не рабочим. Это происходит, потому что как мы обсуждали про ОЗУ в 1 части статьи, из-за того что ОС(*В данном случае Windows) после запуска программы на исполнение, копирует ее в выделенную под нее Виртуальную область памяти(Virtual Memory) из-за соображений безопасности и производительности. При чем при каждом перезапуске, модуль игры будет загружена в случайную область памяти.

Теория…

Иллюстрация загрузки игры в данном случае AssaultCube из предыдущей части в память:
1.png
Как видим на абстрактной картине, после запуска на исполнение, ОС выделила участок виртуальной памяти и скопировала туда модуль ac_client.exe.

После перезапуска наш исполняемый модуль(.exe) может быть скопирован в любую случайную область выделенной под нее памяти.

Иллюстрация Случайное расположение:
2.png
И поэтому, те адреса, которые мы нашли уже будут не рабочими. Если поглубже подойти к этому, то будет верно еще то, что когда вы запускаете уровень / миссию и т.д. игра будет подгружать некоторые объекты/структуры/модули динамически. По мере их необходимости.

Иллюстрация создания новых объектов/структур и размещение их в памяти:
3.png
Как выяснили ранее, порядок размещения модулей, объектов, структур в памяти может быть случайным.
Например: То есть синий участок(модуль игры) окажется в самом краю памяти(внизу), а объекты выделенные динамично в процессе взаимодействия пользователя с игрой выше.

Иллюстрация Случайного размещения:
4.png

Немного примеров из мира разработки игр…

При разработке игры, программисту нужно построить логику всей игры, чтобы она была производительной и не занимала много ОЗУ. Когда вы запускаете уровень в игре, игра динамически выделяет память под те или иные объекты и структуры. Например: создание игрока, ботов, предметов, квестов и т.д. на уровне. Ведь было бы глупо сразу статично загружать все уровни в память игры, даже те, которые недоступны игроку. Это бы нагружало нашу память. А она имеет свойство заканчиваться. То есть к чему я веду вас, если бы игра создавала эти объекты сразу после запуска ее на исполнение, то скорее у вас бы были жуткие лаги, просадка FPS - во точно, возможно игра крашилась и завершалась аварийно! А обычно, создание объектов осуществляется по мере необходимости. Например: из-за действий игрока/событий, вы открываете дверь и через некоторое время на вас нападают боты из-за угла. Вот тогда происходит создание новых объектов. Это называется динамическим выделением памяти. То есть под новые объекты будет выделено место из свободного пространства памяти процесса в этот момент времени. А ненужные объекты будут удаляться и происходить освобождение памяти.

Еще это называют аллокация и деаллокация.

Представим ситуацию, мы зашли в игру, нас встречает меню и как обычно нажимаем на «Новая игра», у нас происходит загрузка уровня: загружается карта, игрок, инвентарь игрока, предметы на карте, боты, рисуется интерфейс, еще могут быть подключены модули в виде dll(физика, particle системы и т.д.). Проще говоря, все нужные ресурсы для игры. Это все происходит динамично, по мере их необходимости. И под это все будет выделен новый участок памяти из свободного пространства.

Более подробно можете прочитать об Динамичном распределении памяти Здесь

То есть каждый объект/структура/класс будет подгружать нужные ему такие же структуры и объекты в процессе – динамично.

Как эта структура будет выглядеть графически:
5.png

Как видим на иллюстрации, после загрузки в память AssaultCube, мы начинаем игру и в этот момент произошли некоторые действия: Загрузилась локация/карта, она в свою очередь загрузила предметы(гранаты, патроны), ботов и наконец игрока, а игрок в свою очередь загрузил некий инвентарь, если это можно так назвать. И интерфейс, который отображает некоторые данные игрока(количество здоровья, брони, патронов), если заметили он тоже связан с нашим инвентарем игрока. Чтобы считывать количество наших предметов.

Вот поэтому, наши адреса постоянно распределяются в случайных позициях при перезапуске и перезагрузке уровня, каждый раз этот модуль игры, эти объекты меняют свое положение в адресном пространстве. Но как это можно решить? Может все же имеется какой-то способ узнать, в каких адресах окажутся эти модули/объекты? Ответа – да!

Размышления…0x…указатели…
Для начала давайте подумаем и найдем какую то логику в этом. У нас есть модуль игры(*Синий участок на картине), в нашем случае ac_client.exe , который копируется в память на исполнение. Возможно нам поможет снова ОС? Что если при помощи запросов WinApi к ОС(*Windows) спросить, в какой участок памяти был загружен этот модуль? Да, Вы будете абсолютно правы!

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

НО! Почему бы нам не спросить ОС, где находятся адреса объектов? Это же очень легкий способ.

Я должен огорчить, что эти объекты, как мы узнали выше, будут выделяться в процессе игры, и из-за этого данный метод будет выведен на – нет! Но как вы заметили, на иллюстрациях выше я выделил красным стрелки от одного модуля/объекта к другому. Это называется указателем.

Иллюстрация:
3.png

Вот они то и придут нам в помощь.

Что такое указатель? Указатель (pointer) в программировании представляет собой переменную, которая содержит адрес в памяти другой переменной. Он "указывает" на местонахождение в памяти, где хранится значение определенной переменной или структуры. Указатели позволяют эффективно работать с памятью, обеспечивая доступ к данным по их адресам.

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

Важно: Указатель являясь тоже переменной имеет свой адрес в памяти, а значением его будет адрес другой переменной. Размер типа указатель будет зависеть от разрядности системы. Для х86(32бит) это – 4 байта, а для х64бит это обычно 8 байт.

Более подробно про указатели можно почитать Здесь
Пример указателя:
8.png

Как видим на примере выше, есть некая переменная с адресом 00123ABC, а в его ячейки лежит другой адрес. Вот это и есть указатель! Мы можем обращаться к указателю и прямо получать доступ к ячейки переменной на который указывает этот сам УКАЗАТЕЛЬ!

Для наглядности, написал тестовую программу, которая отлично демонстрирует указатели:
12.png
Видим у нас есть переменная hp и ее значение 100, а есть еще указатель, который тоже имеет свой адрес и содержимое его ячейки будет адресом переменной hp, на которую он указывает.

Сам код:
C++:
#include <iostream>
using namespace std;
int main
{
    cout << "\t\t\tСпециально для форума: xss.pro || Author: Unseen"<<endl;
    cout << "\t\t\tОбычные переменные и переменные-УКАЗАТЕЛИ!<" << endl;
    int hp = 100;
    int* ptrHP = &hp;
    cout << "\n\tАдрес переменной hp = " << &hp << " || Значение переменной hp в ячейки: " << hp << endl;;
    cout << "\n\tАдрес переменной-Указатель ptrHP = " << &ptrHP << " || Значение переменной-указатель в ячейки: " << ptrHP<<endl;
    cin.get();
    return 0;
}

Тот же пример с инвентарем игрока, где-то в коде игры может быть класс или структура UI интерфейс и в ее полях перемененная указатель pointerToInventoryPlayer, которая будет указывать на другую структуру или класс Player в котором есть возможно указатель на другую структуру или класс вида Inventory. Может и быть такое, что она прямо указывает на саму структуру Inventory.

Не открывай(Указатель):
pp0-768x432.jpg

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

Пройтись через модуль игры + локация/Карта + Структура игрока + Инвентарь

Итого цель наша достигнута будет через 3 указателя. Точнее глубина указателей будет равна 3.

Пройтись через модуль игры + структура Интерфейс UI.

Глубина здесь 1.

P.S. Это всего лишь пример, возможно в игре, как увидим далее будет все реализовано по другому!

Еще более подробный пример:

Задача: Нам нужно получить доступ к адресу переменной здоровья игрока. В этом случае мы бы пошли через такой путь: модуль игры(*ac_client.exe) + локация/уровень(Level) + структура игрока(Player);

То есть через 2 указателя мы придем к объекту игрока и уже могли работать с его полями в структуре.

Важно отметить, что структуру можно представить как некую сущность, которая имеет свойства(поля), та же структура игрока(Player), может иметь такие поля как количество здоровья, брони, имя, и т.д.

Структура — это объединение нескольких объектов, возможно, различного типа под одним именем, которое является типом структуры. В качестве объектов могут выступать переменные, массивы, указатели и другие структуры. Структуры позволяют трактовать группу связанных между собой объектов не как множество отдельных элементов, а как единое целое. Структура представляет собой сложный тип данных, составленный из простых типов.

Структуры помогают улучшить организацию кода, разбив его на более логические и удобные части. Например, вы можете использовать структуры для представления объектов в вашей программе. Еще этот метод называют ООП. Мы сегодня базово узнаем, что оно из себя представляет.

Более подробно про ООП прочитать Здесь.
Предлагаю посмотреть это на уровне кода:

9.png

Как видно схеме, у нас есть несколько структур, на практике имеем очень много других.

Рассмотрим их для понимания сути и логики. Как мы узнали выше игра грузит нужные структуры и объекты – динамично. Допустим мы начали играть и у нас модуль игры(ac_client.exe) загружает выбранную карту пользователем – структуру Level, а в этой структуре содержатся такие данные: размеры карты – float Width и float Height. Указатель типа Player* player на структуру Player. P.S. указатель отмечается символом *(звездочка). К сведению, указатель будет указывать на адрес структуры Player, а именно на ее первый элемент массива char nickname[0]. Индекс массива в языке С++ будет начинаться с 0, а не как нам привычно 1. Значит можно сказать, что структура Level загружает структуру Player.

По условию задачи, нам нужно было получить доступ к адресу здоровья игрока. Для этого, нам нужно узнать расстояние(в байтах) относительно начала структуры Player до ее поля health. Обычно под «расстоянием» предполагается смещение(Offset - оффсет). Таким образом, смещение до поля health будет равно сумме размеров полей, предшествующих полю health:

Давайте как раз вычислим это смещение. Значит будем считать от начала структуры Player, первое ее поле это массив nickname из 32 элементов с типом char, как знаем из прошлых частей char имеет размер 1 байт:

  • char nickname[32] = 32 байта
  • int armor = 4 байта
Итого оффсет для health будет: 32 + 4 + 4 = 36 байт или же 0x24 в hex.

Значит, чтобы вычислить адрес здоровья, мы должны взять адрес структуры, в данном случае Player и прибавить + 0x24. А адрес самой структуры Player мы можем вычислить через переменную указатель, который находится в структуре Level.

Выходит, чтобы добраться нам до адреса здоровья, нужно пройтись через все структуры, которые создаются по цепочке: сам модуль – ac_client.exe -> структура Level -> структура Player.

Более подробный план:

  • Попросить ОС при помощи WinApi предоставить нам базовый адрес модуля игры - ac_client.exe
  • Вычислить смещение в модуле игры для указателя Level* level, который хранит адрес новой структуры Level.
  • Вычислить смещение в структуре Level для указателя Player* player, который хранит адрес новой структуры Player.
  • Вычислить смещение в структуре Player до ее поля int health, который и является искомым адресом.
А теперь для примера представим, что ОС выдала нам базовый адрес модуля в расположенный памяти, допустим этот адрес будет 00223344 или же 0x36870.

Следуя плану:

  • Базовый адрес модуля 00223344
  • Далее считаем смещение указателя level.
    P.S. Предполагается, что система 32битная. Размер указателя = 4 байт.
  • Interface* interface = 4
  • SoundSystem* sound = 4
  • PhysicsSystem* physics = 4
Итого оффсет для указателя на структуру Level = 12 или же 0xC.

  • Считаем смещение для указателя player.
  • float Width = 4
  • float Height = 4
Итого оффсет указателя = 8 или же 0x8.

  • Считаем смещение до health.
  • char nickname[32] = 32
  • int armor = 4
Итого 36 или же 0x24.

Но здесь есть одна ловушка, вы можете подумать, что сейчас адрес здоровья будет равен: 00223344 + 12 + 8 + 36 = 00223400. Но это на самом деле не так, по причине того, как сказал ранее, когда мы считали оффсеты для указателей, то мы находили не сам адрес структуры, а адрес УКАЗАТЕЛЯ, в котором расположен адрес начала структуры. Значит мы должны будем где то в коде своей программы, найдя указатель, должны считать его содержимое,
8.png
а именно адрес другой структуры и уже к нем прибавить оффсет, чтобы найти другой такой же указатель на другую структуру или переменную.
Еще иллюстрация:
10.png
Как видим, если мы прибавим к базовому адресу оффсет для указателя level, то получим адрес самого указателя и в ячейки по этому адресу будет лежать адрес начала структуры Level.

Для полноты картины:
11.png
То есть мы поэтапно идем от модуля игры к адресу указателя и в этой ячейки адреса уже находим адрес другой структуры и так дальше пока не дойдем до нужной нам структуры. Чаще всего путей добраться до нужной структуры может быть много, еще это называют глубиной указателей. Допустим мы могли добраться до структуры игрока через структуру интерфейса, ведь он тоже чаще всего связан с игроком. Даже могло выйти так, что он окажется короче и будет надежнее держаться после обновления игры. Ведь разработчик может добавить новое поле в структуру Level и уже те оффсеты, которые мы посчитали будут недействительный и приводить вообще непонятно к чему. Еще нужно учесть, что оффсеты не долго не живут, обычно при среднем обновлении игры, их приходится заново вычислять сканером памяти. Конечно есть более другие методы поиска этих оффсетов, называется поиск по сигнатурам – они более надежны, но пока эту тему отложим для будущих статей.

По началу это будет воспринять сложно, но это нормально. Если чувствуете себя неуверенно, перечитайте через час – полтора. Мозгу надо время на переваривание информации!

Практическая часть… Наконец-то!

P.S. Нашим подопытным будет тот же пациент Assault Cube!

Теперь, когда мы базово разобрались что же такое на самом деле виртуальная память, структуры данных, объекты, смещения(оффсеты), указатели. Думаю можем приступить к поиску этих самых оффсетов и указателей. Для начала полностью проведем рефакторинг нашего проекта из прошой части, если не сделаем этого сейчас, то в будущем будет сложно поддерживать наш проект. Напишем функцию, которая будет делать запрос при помощи WinApi к ОС, чтобы узнать базовый адрес модуля игры. И под конец все это испытаем и убедимся, что найденные нами оффсеты работают даже после перезапуска игры!

Надежный план, как швейцарские часы!

Итак весь наш план будет таков:

  • Найдем при помощи сканера Cheat Engine, те самые оффсеты.
  • Объявим функцию GetProcessID, которая будет получать ID запущенного процесса в системе. Только в этот раз будем находить ID не по названию окна игры, а по названию исполняемого файла / модуля.
  • Объявим функцию GetModuleBaseAddress, которая будет делать запрос при помощи WinApi к ОС на получение базового адреса запущенного модуля в памяти.
  • Объявим функцию FindTargetAddress, которая будет вычислять при помощи базового адреса и оффсетов искомый адрес чего-то, например патронов, жизней и т.д.
  • Внесем изменения в шаблоны функций memWrite и memRead, для корректной их работы уже в новом нашем проекте.
  • Испытаем на практике написанную нами программу.
Поиск оффсетов, указателей…

Ну что же, как всегда запускаем наш сканер Cheat Engine и подключаемся к процессу игры Assault Cube.

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

Для начала найдем адрес здоровья:

13.png

Теперь, чтобы вычислить, те самые оффсеты, указатели нам нужно кликнуть ПКМ на этот адрес и выбираем “Pointer scan for this address” или же “Сканирование указателя по этому адресу”. Далее у нас появляется окошко на нем нажимаем “ОК”. Пока про тонкую настройку поиска указателей не будем говорить.


15.png

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


16.png

Как выделено на картине мы имеем 3 цвета, это:

  • Красный это название модуля(ac_client.exe) + некий оффсет, как говорили в теории мы нашли оффсет в самом модуле и если прибавим его к адресу модуля, то вероятно придем к первой некой структуре, возможно это будет наш Level. Можете посмотреть это визуально на картинке под названием спойлера “Полная структура”.
  • Зеленый это глубина указателей при сканировании.
  • Желтый это те самые оффсеты(смещения). Их видим для примера 3 начиная от 0. Счет в программировании в большинстве случаев начинается с 0.
Значит, если мы узнаем адрес модуля и прибавим к нему оффсет, который выделен красным, то узнаем адрес указателя. А если потом прочитаем значение, которое хранит указатель, то получим адрес следующей структуры, может тот же Level. И до тех пор, пока не придем к искомому адресу здоровья.

Иногда одного сканирования недостаточно, ведь здесь аж целых 26790 указателей. Многие из них возможно не будут рабочими после перезапуска игры. Для этого нужно проделать действия выше 2-3 раза перезапустив игру.

Покажу это на видео для экономии времени:

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

17.png

Желтыми отмечены указатели, которые остались работоспособными после перезапуска игры.

Значит они более надежны! Красные как видим поменяли значение на рандомный мусор.

Два раза кликнем теперь по любому из них(желтые):
18.png

Как видим внутри расположены зеленым наши те самые оффсеты, которые приведут к нужному адресу в структуре!
Теперь тоже самое проделайте и с адресом патронов и сохраните конечный результат в таблице!
Теперь осталось переписать наш проект!
Следуя плану составленному выше:

Вот полный исходный код нашего проекта:
C++:
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>

using namespace std;


DWORD GetProcessID(const wchar_t* targetNameProcess)
{
    DWORD procID = 0;
    HANDLE handleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (handleSnap != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32 procEntry;
        procEntry.dwSize = sizeof(procEntry);

        if (Process32First(handleSnap, &procEntry))
        {
            do
            {
                if (!_wcsicmp(procEntry.szExeFile, targetNameProcess))
                {
                    procID = procEntry.th32ProcessID;
                    cout << "break" << endl;
                    break;
                }
            } while (Process32Next(handleSnap, &procEntry));
        }
    }
    CloseHandle(handleSnap);
    return procID;
}

uintptr_t GetModuleBaseAddress(DWORD procID, const wchar_t* moduleName)
{
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procID);
    uintptr_t baseAddr = 0;

    if (hSnap != INVALID_HANDLE_VALUE)
    {
        MODULEENTRY32 modEntry;
        modEntry.dwSize = sizeof(modEntry);
        if (Module32First(hSnap, &modEntry))
        {
            do
            {
                if (!_wcsicmp(modEntry.szModule, moduleName))
                {
                    baseAddr = (uintptr_t)modEntry.modBaseAddr;
                    break;
                }
            } while (Module32Next(hSnap, &modEntry));
        }

    }
    CloseHandle(hSnap);
    return baseAddr;
}

uintptr_t FindTargetAddress(HANDLE handleProc, uintptr_t baseAddr, vector<unsigned int>offsets)
{
    uintptr_t address = baseAddr;
    for (unsigned int i = 0; i < offsets.size(); ++i)
    {
        ReadProcessMemory(handleProc, (BYTE*)address, &address, sizeof(address), 0);
        address += offsets[i];
    }
    return address;
}

template<class dataType>
void memWrite(HANDLE targetProc, DWORD address, dataType value)
{
    WriteProcessMemory(targetProc, (PVOID)address, &value, sizeof(dataType), 0);
}

template <class dataType>
dataType memRead(HANDLE targetProc, DWORD address)
{
    dataType buffer;
    ReadProcessMemory(targetProc, (PVOID)address, &buffer, sizeof(dataType), 0);
    return buffer;
}



int main() {

    setlocale(LC_ALL, "Russian");

    DWORD processID = GetProcessID(L"ac_client.exe");
    cout << "ProccesID: " << processID << endl;

    uintptr_t baseAddress = GetModuleBaseAddress(processID,L"ac_client.exe");

    uintptr_t DynamicPtrAddress = baseAddress + 0x00183828;

    vector<unsigned int> offsets_ammo = { 0x8, 0xB74, 0x30, 0x744 };

    cout << "Dynamic ptr address:" << hex << DynamicPtrAddress << endl;

    HANDLE handleProcess = OpenProcess(PROCESS_ALL_ACCESS, false, processID);

    uintptr_t addresses_ammo = FindTargetAddress(handleProcess, DynamicPtrAddress, offsets_ammo);

    cout << "addresses_ammo: " << hex << addresses_ammo << endl;
 
    cout <<"Патронов было: "<< dec << memRead<int>(handleProcess, addresses_ammo)<<endl;

    memWrite<int>(handleProcess, addresses_ammo, 1000);

    cout << "Патронов стало: " << dec << memRead<int>(handleProcess, addresses_ammo)<<endl;

    CloseHandle(handleProcess);
    cin.get();
    return 0;
}

Разбор кода...
Подключаем нужные библиотеки и заголовочные файлы:
C++:
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>

Далее объявляем функцию GetProcessID, которая принимает в качестве параметра строку targetNameProcess, представляющую имя целевого процесса, и возвращает идентификатор (ID) процесса (process ID), если процесс с таким именем был найден. В противном случае возвращается 0.
C++:
DWORD GetProcessID(const wchar_t* targetNameProcess)
{
    // Идентификатор процесса, который будет возвращен
    DWORD procID = 0;

    // Создаем снимок текущих процессов в системе
    HANDLE handleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // Проверяем, удалось ли создать снимок
    if (handleSnap != INVALID_HANDLE_VALUE)
    {
        // Инициализируем структуру для хранения информации о процессе
        PROCESSENTRY32 procEntry;
        procEntry.dwSize = sizeof(procEntry);

        // Начинаем перебор процессов в снимке
        if (Process32First(handleSnap, &procEntry))
        {
            do
            {
                // Сравниваем имя текущего процесса с целевым именем
                if (!_wcsicmp(procEntry.szExeFile, targetNameProcess))
                {
                    // Если найдено совпадение, сохраняем ID процесса и выходим из цикла
                    procID = procEntry.th32ProcessID;
                    cout << "break" << endl;
                    break;
                }
            } while (Process32Next(handleSnap, &procEntry));
        }
    }

    // Закрываем дескриптор снимка процессов
    CloseHandle(handleSnap);

    // Возвращаем ID найденного процесса (или 0, если не найден)
    return procID;
}

Если перейдете в диспетчер задач и во вкладке “Подробности” , увидите те самые запущенные процессы в системе. Этот код делает “Скриншот” на данный момент запущенных процессов и ищет в них искомый нами процесс.
19.png

К примеру процесс ac_client.exe имеет у меня ID 9952.

Дальше объявим функцию GetModuleBaseAddress, которая принимает идентификатор процесса (procID) и имя модуля (moduleName) и возвращает базовый адрес загруженного модуля в указанном процессе. Если модуль с указанным именем не найден, возвращается 0.
C++:
uintptr_t GetModuleBaseAddress(DWORD procID, const wchar_t* moduleName)
{
    // Дескриптор снимка модулей процесса
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procID);
 
    // Инициализация базового адреса модуля
    uintptr_t baseAddr = 0;

    // Проверка, удалось ли создать снимок модулей
    if (hSnap != INVALID_HANDLE_VALUE)
    {
        // Структура для хранения информации о модуле
        MODULEENTRY32 modEntry;
        modEntry.dwSize = sizeof(modEntry);

        // Начинаем перебор модулей в снимке
        if (Module32First(hSnap, &modEntry))
        {
            do
            {
                // Сравниваем имя текущего модуля с целевым именем
                if (!_wcsicmp(modEntry.szModule, moduleName))
                {
                    // Если найдено совпадение, сохраняем базовый адрес модуля и выходим из цикла
                    baseAddr = (uintptr_t)modEntry.modBaseAddr;
                    break;
                }
            } while (Module32Next(hSnap, &modEntry));
        }
    }

    // Закрываем дескриптор снимка модулей
    CloseHandle(hSnap);

    // Возвращаем базовый адрес найденного модуля (или 0, если модуль не найден)
    return baseAddr;
}

Теперь функция FindTargetAddress, которая принимает дескриптор процесса (handleProc), базовый адрес (baseAddr) и вектор смещений (offsets) или же проще сказать массив/список смещений. Функция пытается найти целевой адрес, выполняя последовательные чтения памяти с использованием указанных смещений.То есть эта функция будет идти от начала адреса модуля до искомого нами адреса в структуре.
C++:
uintptr_t FindTargetAddress(HANDLE handleProc, uintptr_t baseAddr, vector<unsigned int> offsets)
{
    // Инициализация адреса с базовым адресом
    uintptr_t address = baseAddr;

    // Итерация по вектору смещений
    for (unsigned int i = 0; i < offsets.size(); ++i)
    {
        // Чтение значения по текущему адресу
        ReadProcessMemory(handleProc, (BYTE*)address, &address, sizeof(address), 0);

        // Добавление текущего смещения к адресу
        address += offsets[i];
    }

    // Возвращение найденного адреса
    return address;
}

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

Функция memWrite, которая служит для записи значения в память процесса. Функция использует функцию WriteProcessMemory, которая позволяет записывать данные в виртуальную память другого процесса.
C++:
template<class dataType>
void memWrite(HANDLE targetProc, DWORD address, dataType value)
{
    WriteProcessMemory(targetProc, (PVOID)address, &value, sizeof(dataType), 0);
}

функцию memRead, которая служит для чтения данных из виртуальной памяти процесса. Эта функция также является шаблонной и использует ReadProcessMemory для выполнения операции чтения.
C++:
template <class dataType>
dataType memRead(HANDLE targetProc, DWORD address)
{
    dataType buffer;
    ReadProcessMemory(targetProc, (PVOID)address, &buffer, sizeof(dataType), 0);
    return buffer;
}

Когда с функциями закончили, осталась главная функция main:

Разрешим для начала кириллицу в консоли для удобства.
C++:
setlocale(LC_ALL, "Russian");

Дальше вызовем функцию GetProccesID, и передадим ему название модуля в данном случае ac_client.exe. Результат сохраним в переменную processID. И выведем этот айди в консоли.
C++:
DWORD processID = GetProcessID(L"ac_client.exe");
cout << "ProccesID: " << processID << endl;

Теперь вызовем функцию GetModuleBaseAddress для получения базового адреса модуля, результат запишем в переменную baseAddress. Дальше объявим переменную DynamicPtrAddress. Значением будет сам базовый адрес и смещение для этого модуля.
C++:
uintptr_t DynamicPtrAddress = baseAddress + 0x00183828;
20.png

Дальше объявим беззнаковый вектор offsets. В нем будем хранить массив оффсетов.
C++:
vector<unsigned int> offsets_ammo = { 0x8, 0xB74, 0x30, 0x744 };
вам нужно подставить свои оффсеты, которые нашли по видео.

Теперь выведем сам адрес относительно базового модуля. Для информативности.
C++:
cout << "Dynamic ptr address:" << hex << DynamicPtrAddress << endl;

Получаем хэндл и права на этот процесс.
C++:
HANDLE handleProcess = OpenProcess(PROCESS_ALL_ACCESS, false, processID);

Давайте вызовем функцию FindTargetAddres и передаем в параметры сам хэндл процесса, адрес относительно базового модуля, и сам массив оффсетов. Результатом будет конечный адрес того значения, что искали. Допустим адрес патронов.
C++:
uintptr_t addresses_ammo = FindTargetAddress(handleProcess, DynamicPtrAddress, offsets);

Сделаем вывод этого адреса в hex формате для информативности.
C++:
cout << "addresses_ammo: " << hex << addresses_ammo << endl;

Теперь как всегда сначала прочитаем значение по найденному значению при помощи функции memRead. И выведем результат.
C++:
cout <<"Патронов было: "<< dec << memRead<int>(handleProcess, addresses_ammo)<<endl;

далее вызываем функцию записи в память memWrite и передаем ему хэндл, сам адрес по которому нужно записать, и значение для записи.
C++:
memWrite<int>(handleProcess, addresses_ammo, 1000);

Теперь снова прочитаем новое значение при помощи memRead.
C++:
cout << "Патронов стало: " << dec << memRead<int>(handleProcess, addresses_ammo)<<endl;

21.png

Поздравляю! Вы очень молодцы! Все получилось, все прошло по плану! Попробуйте несколько раз перезапустить игру и снова проверить работоспособность ваших оффсетов!

Соглашусь статья очень получилась объемной, даже сам не представлял. Она отняла очень много сил и времени. Последние строчки еле дописывал… Я попытался все передать подробно, показать что и как. Если у вас остались какие нибудь вопросы по данной статье – смело задавайте в обсуждении. Все разберем и решим!

Всех благодарю за чтение! Если я пропустил ошибку или что то не разобрал по данной теме, прощу дополнить в комментариях.

Если хотите отблагодарить меня чашечкой кофе – реквизиты в подписи.

Всех с наступающим Новым Годом! Пожелаю, чтобы ваши профиты выросли в разы, а то и сотни иксов. А у новичков появились первые бабки $. До следующей части!
Ваш Unseen!
 
Последнее редактирование:
Как всегда лучший :smile10:
Благодарю за поддержку. Твоя статья тоже в ударе )) решил эту тему поглубже разобрать.
 
Хороший мануал, спасибо за старание. Вопрос! Будет мануал на примере экшн игры?
Привет. Да будет. Сейчас стараюсь не брать для примеров сложные игры. Ещё предстоит много чего изучить.
 
Очень хорошо и доступно написано! Жду такую же интересную статью про обход популярных античитов:)
Думаю мы все ближе и ближе к этим темам :) благодарю за поддержку
 
1713104531072.png
1713104554839.png


Попытался сделать с хп тоже самое что и с патронами однако при запуске изменяются только патроны. Причём если менять значение в Cheat Engine то хп меняется. Почему так?
 
Посмотреть вложение 82511Посмотреть вложение 82512 Попытался сделать с хп тоже самое что и с патронами однако при запуске изменяются только патроны. Причём если менять значение в Cheat Engine то хп меняется. Почему так?

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


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх