Приватная статья, часть первая:
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Играть в игры любят все, но это гораздо интереснее, когда у тебя имеется нескончаемый запас патронов и здоровья. Чтобы обзавестись и тем и другим, можно погуглить читы и трейнеры для твоей любимой игры. Но как быть, если их еще не разработали? Написать самому! Обладая навыками реверс‑инжиниринга и программирования, сделать это намного проще, чем кажется.
ВЫБОР ИГРЫ
Для начала определимся с игрой. Мой выбор пал на
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(далее HLD). Если ты планируешь поэкспериментировать с коммерческой игрой, обрати внимание на сайт
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, а также на игры
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Так как для написания этой статьи я буду использовать коммерческую игру, мне нужно удостовериться, что лицензионное соглашение (EULA) позволяет это делать.
Начав установку и внимательно прочитав текст EULA, я убедился, что в нем явно запрещается написание и распространение только тех читов и трейнеров, которые мешают работе сервиса, а в нашем случае ничего подобного не планируется. Поэтому смело продолжаем установку.
ПОИСК ЗНАЧЕНИЙ
Для поиска значений, которые будет изменять чит, мы станем использовать
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(далее CE).Запустим игру и в настройках игры выберем оконный режим — нам нужно, чтобы на экране помещалось еще что‑то, кроме игры.
Как видим, в оконном режиме отсутствует панель заголовка, с помощью которой мы могли бы перетаскивать окно игры по экрану. Чтобы исправить эту неприятность, откроем отладчик
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, а именно его 32-битную версию (x32dbg) и запустим под ним HLD.Поставим брейк‑пойнты на функции CreateWindowExA и CreateWindowExW, которые отвечают за создание окна. Найти их можно на вкладке Symbols, выбрав библиотеку user32.dll.
Видим, что наше окно создается с параметром dwStyle, имеющим значение WS_POPUP = 0x80000000.
Поменяем это значение на WS_OVERLAPPED = 0x00000000.
И вот результат: теперь мы можем перемещать окно.
После того как мы настроили окно игры с помощью отладчика, ненадолго отложим его. Чтобы найти нужные нам значения в Cheat Engine, разберемся с теорией.
ЧТО ТАКОЕ СТАТИЧЕСКИЙ АДРЕС
Статический адрес — это адрес, который изменяется предсказуемо по отношению к модулю, которому он принадлежит. Если переменная глобальная, то можно найти ее в сегменте данных.Статические адреса указываются в формате [module+offset]. Например, в library.dll мы могли обнаружить значение по адресу 0x700004C0 (base = 0x70000000, offset = 0x4C0). Поскольку library.dll может перемещаться и ее базовый адрес загрузки будет меняться, чтобы получить доступ к нашему значению, мы не используем этот адрес напрямую. Вместо этого возьмем адрес [library.dll + 0x4C0]. Следовательно, когда library.dll загружается по базовому адресу 0x10000000, [library.dll + 0x4C0] дает нам 0x100004C0 и у нас появится доступ к нашему значению.
Если же переменная локальная, то искать нужно в стеке. Для этого получаем TebBaseAddress определенного потока, а затем второй указатель из этой структуры (FS:[0x04] или GS:[0x08], в зависимости от разрядности процесса), которая содержит вершину стека. TebBasePointer может быть получен с помощью NtQueryInformationThread (если это 64-битный процесс) или же с помощью Wow64GetThreadSelectorEntry (если это 32-битный процесс в 64-битной системе).
ПОИСК ПОКАЗАТЕЛЕЙ ЗДОРОВЬЯ
Запускаем Cheat Engine и подключаемся к процессу игры.Так как мы не знаем, в каком типе хранится показатель здоровья, выставляем следующие параметры для первого сканирования.
Далее продолжаем сканирование, не забывая при этом терять hp (показатель здоровья) в игре. Делаем мы это для того, чтобы отслеживать изменения значения hp в памяти игры через CE, а также уменьшать значение в поиске для следующих сканирований. Делать мы это будем до тех пор, пока не будет достигнуто адекватное количество значений в окне CE. Адекватное количество значений в данном случае — это такое количество адресов, проверка которых займет максимум минут пять.
Мне приглянулись вот эти два адреса, которые я добавил в нижнее окно двойным щелчком мыши на них. Приглянулись они мне в первую очередь потому, что значения по этим адресам среди всех остальных имеют наибольший тип — double. Всегда нужно проверять от большего типа к меньшему. То есть сначала проверяем адреса, хранящие тип double, затем float, после integer и так далее. Более подробно о размере типов данных можно прочитать в
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Если мы поменяем значение по адресу 0x36501940, то на экране появится полоса здоровья, но его количество не поменяется.
Если теперь мы поменяем значение по адресу 0x36501A30, то на экране появится полоса hp и значение изменится. Это значит, что мы нашли адрес, в котором хранится значение здоровья в игре. Значение хранится в формате double (стандарт
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
).Дадим название найденным нами адресам: hp_bar и hp соответственно. Однако, как я уже рассказывал в разделе, посвященном статическим адресам, найденный нами адрес будет бесполезен после того, как мы выйдем в меню или перезапустим игру.
Поиск статического адреса для индикатора здоровья
Для дальнейшего поиска статического адреса вернемся к отладчику. В окне дампа переходим по ранее полученному адресу 0x36501A30, в котором хранится значение hp.Ставим по адресу 0x36501A34 аппаратный брейк‑пойнт на запись и теряем в игре здоровье. Брейк‑пойнт срабатывает, и мы видим, что новое значение hp берется из регистра EDI. Это значение является первым параметром текущей функции.
Выйдя из этой функции, проследим, откуда она получает свой первый параметр. Мы увидим, что передаваемый параметр — это возвращаемое значение функции по адресу 0x003EFCE9.
Поставим брейк‑пойнт на вызов функции по адресу 0x003EFCE9, а дальше продолжим отладку, пока не остановимся на ее вызове. Зайдя внутрь функции, выполняем ее до конца. Как только мы достигнем адреса 0x00F88E19, мы увидим, что регистр EAX хранит адрес значения hp. Очевидно, что в этой функции происходит доступ к нашему адресу через арифметику с указателями для структур, а именно через прибавление к указателю смещений и дальнейшего его разыменования. Более подробно об этом можно прочитать
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
. Нам нужно будет повторно пройтись по этой функции, чтобы узнать, через какой адрес и смещения она получает адрес значения hp.После того как мы узнали адрес 0x353F9BB0, из которого получается адрес значения hp, начинаем выходить из функций. При этом внимательно отслеживаем, что передается им в качестве параметров. Спустя пару выходов мы наткнемся на следующее.
Мы нашли статический адрес! Если посмотреть его расположение в памяти, он находится в секции .data.
Зная все смещения, добавим их в CE, нажав Add Address Manually.
ПОИСК ЗНАЧЕНИЯ ЧИСЛА ПАТРОНОВ
Теперь приступим к поиску значения числа патронов (ammo). Первое сканирование делаем с такими же параметрами поиска, как когда мы искали здоровье.В данном случае мы смогли найти лишь одно значение, и это значение полосы, которая показывает число боеприпасов.
В игре этот индикатор не появился. В отличие от полосы здоровья, он отображается только после нажатия на кнопку E или во время выстрелов.
Поиск статического адреса для ammo
Мы понимаем, что показания индикаторов в игре всегда сравниваются с фактическими. Если одна из полос показывает не то, что нужно, ее длина изменяется. Поэтому возвращаемся к отладчику и начинаем с аппаратного брейк‑пойнта на запись по адресу 0x365014С4. Как видим по комментариям, эта функция уже нам встречалась.По аналогии с поиском hp, выходим из функции.
Так как мы уже знаем, что индикатор должен получать значение где‑то раньше, нам придется пролистать окно дизассемблера выше, пока мы не увидим функцию, предположительно получающую фактическое значение ammo.
Мы видим, что в этой функции мы уже были, а это значит, что она тоже получает значение, но уже ammo — 365014E0. Только какое‑то оно странное.
Добавив это «странное» значение в Cheat Engine, а потом изменив его, к примеру, на 100, мы увидим, что на экране появится индикатор патронов и его значение поменяется. Значит, мы нашли адрес, в котором хранится значение ammo в игре.
Зная все смещения от статического адреса к адресу значений ammo, добавим их в CE, нажав Add Address Manually.
Скорее всего, боеприпасы в HLD представляют собой заряд энергии и поэтому хранятся в процентах, ведь при их поиске через отладчик можно было увидеть строки, содержащие слово energy. Которое намекает на то, как будет выглядеть значение в памяти. Например, игра от одной небезызвестной польской компании хранила патроны в памяти вместе, а для игрока показывала раздельно: как рожок, так и количество оставшихся патронов, поэтому при их поиске не удавалось найти каждое из значений, а нужно было искать их сумму.
ПРОВЕРКА ПОЛУЧЕННОГО СТАТИЧЕСКОГО АДРЕСА
Чтобы проверить, правильно ли мы определили адреса, нужно выйти в меню игры и вернуться к игровому процессу или же перезапустить игру.Проверка для HP
Так выглядит наша cheat table для hp.А вот так она выглядит после перезапуска игры.
Проверка для ammo
Так выглядит наша cheat table для ammo.А вот так она выглядит после перезапуска игры.
КАК БУДЕТ ВЫГЛЯДЕТЬ НАШ УКАЗАТЕЛЬ В C++
В нашем чите доступ к найденным адресам значений будет таким.
C++:
static_addr = (DWORD)GetModuleHandle(0);
static_addr = *(DWORD*)(static_addr + 0x255AF150);
static_addr = *(DWORD*)(static_addr);
static_addr = *(DWORD*)(static_addr + 0xD48);
static_addr = *(DWORD*)(static_addr + 0x0C);
static_addr = (DWORD*)(static_addr + 0xC4);
static_addr = *(DWORD*)(*static_addr + 0x08);
static_addr = *(DWORD*)(static_addr + 0x44);
static_addr = *(DWORD*)(static_addr + 0x10);
drifter_hp = (double*)(DWORD*)*(DWORD*)(static_addr + 0x1FD8);
drifter_ammo = (double*)(DWORD*)*(DWORD*)(static_addr + 0x268C);
НАПИСАНИЕ ТРЕЙНЕРА
По принципу действия читы можно разделить на две группы: внутренние и внешние. Внешние читы — это отдельное приложение, запущенное в системе в виде процесса. Внутренние читы обычно реализованы как динамическая библиотека, внедряемая в процесс игры.Мы будем писать внутренний чит, поэтому нам понадобится не только сама библиотека, но и инжектор, который внедрит нашу библиотеку в процесс игры. Инжектор получит список процессов, найдет процесс игры, выделит в ней память, в которую запишет наш внутренний чит, а после создаст удаленный поток внутри игры для выполнения кода нашего чита.
Injector
Код нашего инжектора выглядит следующим образом.
C++:
#include <windows.h>
#include <tlhelp32.h>
// Имя внедряемой dll
const char* dll_path = "internal_trainer_hld.dll";
int main(void) {
HANDLE process;
void* alloc_base_addr;
HMODULE kernel32_base;
LPTHREAD_START_ROUTINE LoadLibraryA_addr;
HANDLE thread;
HANDLE snapshot = 0;
PROCESSENTRY32 pe32 = { 0 };
DWORD exitCode = 0;
pe32.dwSize = sizeof(PROCESSENTRY32);
// Получение снапшота текущих процессов
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(snapshot, &pe32);
do {
// Мы хотим работать только с процессом HyperLightDrifter
if (wcscmp(pe32.szExeFile, L"HyperLightDrifter.exe") == 0) {
// Во-первых, нам нужно получить дескриптор процесса, чтобы использовать его для следующих вызовов
process = OpenProcess(PROCESS_ALL_ACCESS, true, pe32.th32ProcessID);
// Чтобы не повредить память, выделим дополнительную память для хранения нашего пути к DLL
alloc_base_addr = VirtualAllocEx(process, NULL, strlen(dll_path) + 1, MEM_COMMIT, PAGE_READWRITE);
// Записываем путь к нашей DLL в память, которую мы только что выделили внутри игры
WriteProcessMemory(process, alloc_base_addr, dll_path, strlen(dll_path) + 1, NULL);
// Создаем удаленный поток внутри игры, который будет выполнять LoadLibraryA
// К этому вызову LoadLibraryA мы передадим полный путь к нашей DLL, которую мы прописали в игру
kernel32_base = GetModuleHandle(L"kernel32.dll");
LoadLibraryA_addr = (LPTHREAD_START_ROUTINE)GetProcAddress(kernel32_base, "LoadLibraryA");
thread = CreateRemoteThread(process, NULL, 0, LoadLibraryA_addr, alloc_base_addr, 0, NULL);
// Чтобы убедиться, что наша DLL внедрена, мы можем использовать следующие два вызова для синхронизации
WaitForSingleObject(thread, INFINITE);
GetExitCodeThread(thread, &exitCode);
// Наконец, освобождаем память и очищаем дескрипторы процесса
VirtualFreeEx(process, alloc_base_addr, 0, MEM_RELEASE);
CloseHandle(thread);
CloseHandle(process);
break;
}
// Перебор процессов из снапшота
} while (Process32Next(snapshot, &pe32));
return 0;
}
DLL
Наша библиотека будет состоять из следующих модулей:- главный модуль (dllmain.cpp);
- модуль хуков (directx_hook.cpp и directx_hook.h);
- модуль обратных вызовов (directx_hook_callbacks.h);
- модуль работы с памятью (memory.h).
Главный модуль
При загрузке нашей библиотеки через функцию LoadLibraryA создаетcя поток, в котором будет происходить вызов двух функций. Первая установит хук, а вторая отрисует меню нашего чита и изменит ранее найденные значения через нажатие клавиш.
C++:
#include "directx_hook.h"
double full_health = 6;
double full_ammo = 100;
DWORD static_addr = 0;
DWORD *drifter = 0;
// Получаем player class
void get_player_class() {
if (drifter == NULL) {
static_addr = (DWORD)GetModuleHandle(0);
static_addr = *(DWORD*)(static_addr + 0x255AF150);
static_addr = *(DWORD*)static_addr;
static_addr = *(DWORD*)(static_addr + 0xD48);
static_addr = *(DWORD*)(static_addr + 0x0C);
drifter = (DWORD*)(static_addr + 0xC4);
}
}
// Отрисовываем наше меню
void draw_menu(directx_hook* hook) {
hook->draw_text(10, 70, D3DCOLOR_ARGB(255, 255, 255, 255), "Full health press F1");
hook->draw_text(10, 90, D3DCOLOR_ARGB(255, 255, 255, 255), "Full ammo press F2");
}
// Изменяем по нажатию клавиш значения hp и ammo
void run(directx_hook* hook) {
double* drifter_hp;
double* drifter_ammo;
draw_menu(hook);
get_player_class();
if (*drifter) {
static_addr = *(DWORD*)(*drifter + 0x08);
static_addr = *(DWORD*)(static_addr + 0x44);
static_addr = *(DWORD*)(static_addr + 0x10);
drifter_hp = (double*)(DWORD*)*(DWORD*)(static_addr + 0x1FD8);
drifter_ammo = (double*)(DWORD*)*(DWORD*)(static_addr + 0x268C);
if (GetAsyncKeyState(VK_F1) & 1) {
*drifter_hp = full_health;
}
if (GetAsyncKeyState(VK_F2) & 1) {
*drifter_ammo = full_ammo;
}
}
}
void injected_thread() {
// Добавляем нашу функцию, которая будет вызываться при inline hook
directx_hook::get_instance()->add_callback(&run);
// Начинаем работу по установке inline hook
directx_hook::get_instance()->initialize();
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)injected_thread, 0, 0, 0);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Модуль хуков
Это модуль, в котором будет выполняться перехват функции
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
— она отвечает за отрисовку сцен. Это необходимо для того, чтобы отрисовывать наше меню.Хуки
Есть множество
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, но мы будем использовать самый простой из этого списка — byte patching/inline hook. Выбор объясняется просто: наша игра поддерживает только одиночный режим, а другие хуки служат для обхода античитов в многопользовательских играх и нам попросту не нужны.
C++:
#pragma once
#include <windows.h>
#include <stdint.h>
#include <d3d9.h>
#include <d3dx9.h>
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
typedef HRESULT(WINAPI* _endscene)(LPDIRECT3DDEVICE9 pDevice);
class directx_hook;
typedef void (*_callback_run)(directx_hook* hook);
class directx_hook {
private:
static directx_hook* instance;
static uint8_t* orig_endscene_code;
static uint32_t endscene_addr;
static LPDIRECT3DDEVICE9 hooked_device;
static bool hook_ready_pre, hook_ready;
_callback_run callback_run;
LPD3DXFONT font;
void set_hooks();
uint32_t locate_endscene();
public:
static _endscene orig_endscene;
static directx_hook* get_instance() {
if (!directx_hook::instance) {
directx_hook::instance = new directx_hook();
}
return directx_hook::instance;
}
static void delete_instance() {
if (directx_hook::instance) {
delete directx_hook::instance;
directx_hook::instance = NULL;
}
}
void initialize();
void add_callback(_callback_run cb);
void draw_text(uint32_t x, uint32_t y, D3DCOLOR color, const char* text);
uint32_t init_hook_callback(LPDIRECT3DDEVICE9 device);
HRESULT WINAPI endscene_hook_callback(LPDIRECT3DDEVICE9 pDevice);
};
В остальных модулях мы выполним следующие действия:
- создадим временное устройство Direct3D и получим его таблицы VF, чтобы найти адрес функции EndScene;
- установим временный хук на функцию EndScene;
- при выполнении хука выполним обратный вызов и передадим ему адрес устройства, которое использовалось для вызова функции EndScene. Затем удалим хук и восстановим нормальное выполнение функции EndScene;
- выполним нашу функцию run;
- вернемся ко второму пункту.
C++:
#include "directx_hook.h"
#include "directx_hook_callbacks.h"
#include "memory.h"
directx_hook* directx_hook::instance = NULL;
uint8_t* directx_hook::orig_endscene_code = NULL;
uint32_t directx_hook::endscene_addr = NULL;
LPDIRECT3DDEVICE9 directx_hook::hooked_device = NULL;
_endscene directx_hook::orig_endscene = NULL;
bool directx_hook::hook_ready = false;
bool directx_hook::hook_ready_pre = false;
void directx_hook::initialize() {
// Проверяем, находится ли в адресном пространстве d3d9.dll
while (!GetModuleHandleA("d3d9.dll")) {
Sleep(10);
}
// Получаем адрес функции endscene
directx_hook::endscene_addr = this->locate_endscene();
// Проверяем, успешно ли получен адрес EndScene
// Если да, то устанавливаем наш inline hook
if (directx_hook::endscene_addr) {
directx_hook::orig_endscene_code = hook_jmp(directx_hook::endscene_addr, (uint32_t)&endscene_trampoline);
}
// Ждем
while (!directx_hook::hook_ready_pre) {
Sleep(10);
}
// Сигнализируем, что наш хук готов
directx_hook::hook_ready = true;
}
// Заменить присвоением
// Как только наш хук будет установлен
void directx_hook::add_callback(_callback_run cb) {
if (!directx_hook::hook_ready)
callback_run = cb;
}
// Обертка для отрисовки текста
void directx_hook::draw_text(uint32_t x, uint32_t y, D3DCOLOR color, const char* text) {
RECT rect;
rect.left = x + 1;
rect.top = y + 1;
rect.right = rect.left + 1000;
rect.bottom = rect.top + 1000;
this->font->DrawTextA(NULL, text, -1, &rect, 0, color);
}
uint32_t directx_hook::init_hook_callback(LPDIRECT3DDEVICE9 device) {
directx_hook::hooked_device = device;
while (directx_hook::orig_endscene_code == NULL) {}
unhook_jmp(directx_hook::endscene_addr, orig_endscene_code);
D3DXCreateFont(directx_hook::hooked_device, 15, 0, FW_BOLD, 1, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Arial", &this->font);
this->set_hooks();
directx_hook::hook_ready_pre = true;
return directx_hook::endscene_addr;
}
HRESULT WINAPI directx_hook::endscene_hook_callback(LPDIRECT3DDEVICE9 pDevice) {
HRESULT result;
callback_run(this);
result = orig_endscene(pDevice);
this->set_hooks();
return result;
}
void directx_hook::set_hooks() {
uint32_t ret;
// Устанавливаем хук
ret = hook_vf((uint32_t)directx_hook::hooked_device, 42, (uint32_t)&my_endscene);
if (ret != (uint32_t)&my_endscene) {
*(uint32_t*)&directx_hook::orig_endscene = ret;
}
}
uint32_t directx_hook::locate_endscene() {
LPDIRECT3D9 pD3D;
D3DPRESENT_PARAMETERS d3dpp;
LPDIRECT3DDEVICE9 pd3dDevice;
uint32_t endscene_addr;
// Cоздаем временный объект D3D9 и проходимся по виртуальной таблице, чтобы получить адрес метода EndScene
pD3D = Direct3DCreate9(D3D_SDK_VERSION);
if (!pD3D) {
return 0;
}
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.hDeviceWindow = GetForegroundWindow();
HRESULT result = pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, d3dpp.hDeviceWindow, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &pd3dDevice);
if (FAILED(result) || !pd3dDevice) {
pD3D->Release();
return 0;
}
// Получаем адрес, который хранится в rdata
endscene_addr = get_vf((uint32_t)pd3dDevice, 42);
// Очищаем
pD3D->Release();
pd3dDevice->Release();
return endscene_addr;
}
Модуль обратных вызовов
Это модуль, в котором реализована функция inline hook — endscene_trampoline, a также функции init_endscene, на которую хук передаст управление. Она, в свою очередь, запустит функцию init_hook_callback для установки хука. А функция my_endscene вызовет функцию endscene_hook_callback, которая отвечает за запуск функции run.
C++:
#include "directx_hook.h"
DWORD WINAPI init_endscene(LPDIRECT3DDEVICE9 device_addr) {
return directx_hook::get_instance()->init_hook_callback(device_addr);
}
HRESULT WINAPI my_endscene(LPDIRECT3DDEVICE9 pDevice) {
return directx_hook::get_instance()->endscene_hook_callback(pDevice);
}
// Наш inline hook
__declspec(naked) void endscene_trampoline() {
__asm {
MOV EAX, DWORD PTR SS:[ESP + 0x4]
PUSH EAX
CALL init_endscene
JMP EAX
}
}
Модуль работы с памятью
Функция Direct3DCreate9 возвращает указатель на интерфейс IDirect3D9, который содержит таблицу виртуальных функций (далее VMT). Среди них по индексу 42 располагается нужная нам функция EndScene. Поэтому напишем несколько функций, которые позволят получить VMT, а также писать, читать и изменять права доступа памяти. Более подробно о таблице виртуальных функций рассказано
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.
C++:
#pragma once
#include <windows.h>
#include <stdint.h>
// Чтение из памяти
template<typename T>
T read_memory(uint32_t address) {
return *((T*)address);
}
// Запись в память
template<typename T>
void write_memory(uint32_t address, T value) {
*((T*)address) = value;
}
template<typename T>
uint32_t protect_memory(uint32_t address, uint32_t prot) {
DWORD old_prot;
VirtualProtect((LPVOID)address, sizeof(T), prot, &old_prot);
return old_prot;
}
// Получение адреса EndScene в VMT
uint32_t get_vf(uint32_t class_inst, uint32_t func_idx) {
uint32_t vf_table;
uint32_t hook_addr;
vf_table = read_memory<uint32_t>(class_inst);
hook_addr = vf_table + func_idx * sizeof(uint32_t);
return read_memory<uint32_t>(hook_addr);
}
// Получение адреса EndScene из VMT
uint32_t hook_vf(uint32_t class_inst, uint32_t func_idx, uint32_t new_func) {
uint32_t vf_table;
uint32_t hook_addr;
uint32_t old_prot;
uint32_t orig_func;
vf_table = read_memory<uint32_t>(class_inst);
hook_addr = vf_table + func_idx * sizeof(uint32_t);
old_prot = protect_memory<uint32_t>(hook_addr, PAGE_READWRITE);
orig_func = read_memory<uint32_t>(hook_addr);
write_memory<uint32_t>(hook_addr, new_func);
protect_memory<uint32_t>(hook_addr, old_prot);
return orig_func;
}
// Установка хука
unsigned char* hook_jmp(uint32_t hook, uint32_t new_func) {
uint32_t new_offest;
uint32_t old_prot;
uint8_t* originals = new uint8_t[5];
// Вычисляем смещение до нашего inline hook
new_offest = new_func - hook - 5;
// Чтобы не произошло краша, так как все виртуальные функции
// хранятся в rdata, изменяем права доступа по адресу функции
old_prot = protect_memory<uint8_t[5]>(hook, PAGE_EXECUTE_READWRITE);
// Сохраняем оригинальный адрес
for (uint8_t i = 0; i < 5; i++) {
originals[i] = read_memory<uint8_t>(hook + i);
}
// Перезаписываем нашим кодом
write_memory<uint8_t>(hook, 0xE9);
write_memory<uint32_t>(hook + 1, new_offest);
// Восстанавливаем прежние права
protect_memory<uint8_t[5]>(hook + 1, old_prot);
return originals;
}
// Снимаем хук
void unhook_jmp(uint32_t hook, uint8_t* originals) {
uint32_t old_prot;
// Чтобы не произошло краша, изменяем права доступа по адресу функции
old_prot = protect_memory<uint8_t[5]>(hook, PAGE_EXECUTE_READWRITE);
// Записываем прежний адрес
for (unsigned int i = 0; i < 5; i++) {
write_memory<uint8_t>(hook + i, originals[i]);
}
// Восстанавливаем прежние права
protect_memory<uint8_t[5]>(hook + 1, old_prot);
delete[] originals;
}
ПРОВЕРКА РАБОТОСПОСОБНОСТИ
Запускаем наш чит, получаем урон, тратим боезапас, нажимаем F1-F2 и видим, как значения hp и ammo становятся максимальными. Значит, чит работает!ВЫВОДЫ
С помощью Cheat Engine мы научились искать фактическое значение той переменной, которую мы хотим модифицировать. Затем с помощью отладчика мы нашли статические адреса для значений, определенных с помощью CE, при этом разобрались, как работает игровой движок. Например, есть общая функция для получения значений игрока, таких как hp и ammo, есть общая функция для записи изменений этих значений. С этими знаниями поиск других значений будет гораздо проще и быстрее. А написанный нами код можно использовать как основу, базу для читов к другим играм.Несколько полезных ссылок по теме взлома игр и написанию читов:
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки