Реверс-інжиніринг і чіти

Тренер і internal-модуль на C++

Кросплатформний зовнішній тренер та бібліотека, що завантажується всередину гри на Windows, Linux і macOS.

Тренер і internal-модуль на C++

🎯 Мета розділу

  • Зрозуміти будову тренера: прикладна логіка окремо від системних викликів.
  • Прочитати код Process для Windows, Linux і macOS.
  • Запустити сканування, читання, запис і режим god у грі-мішені.
  • Зрозуміти, чим internal-модуль відрізняється від зовнішнього тренера.
  • Завантажити власну динамічну бібліотеку в гру на Windows, Linux та macOS.
  • Знайти структуру гравця зсередини процесу за сигнатурою.

🔑 Ключові терміни

  • Зовнішній (external) тренер: окрема програма, яка працює з пам'яттю гри через виклики ОС.
  • Абстракція: спільний інтерфейс, за яким ховаються різні реалізації.
  • Internal-модуль: код, що виконується в адресному просторі гри.
  • DLL / .so / .dylib: динамічна бібліотека Windows / Linux / macOS.
  • Інжекція: завантаження бібліотеки в чужий процес.
  • Сигнатура: впізнаваний набір байтів, за яким знаходимо структуру чи код.

Зовнішній тренер на C++: один код для трьох ОС

Тренер виконує три дії: отримує доступ до процесу, читає або пише байти, обходить області пам'яті. На кожній ОС вони виконуються різними викликами, тому код розділено: клас Process має однаковий інтерфейс, а в трьох файлах process_win.cpp, process_linux.cpp, process_mac.cpp лежать реалізації. CMake компілює всі три, а директиви #if defined(...) залишають активним лише відповідний.

Структура проєкту

Повний код — у labs/reverse-engineering/trainer/. Файл main.cpp не містить жодного системного виклику: він працює лише через Process.

Сканер

Для кожної записуваної області тренер зчитує її цілком у буфер і проходить по ньому з кроком 4 байти. Крок у 4 байти використовується, бо int32_t зазвичай вирівняний за адресою, кратною 4.

for (const Region& r : p.writable_regions()) {
    buf.resize(r.size);
    if (!p.read(r.start, buf.data(), r.size)) continue;
    for (size_t i = 0; i + 4 <= r.size; i += 4) {
        int32_t v;
        std::memcpy(&v, &buf[i], 4);
        if (v == value) printf("0x%llx\n", r.start + i);
    }
}

memcpy замість приведення вказівника потрібен, щоб уникнути порушення вирівнювання та правил псевдонімів. Читання за один виклик на область швидше, ніж по 4 байти.

Команди

КомандаДія
trainer scan <pid> <int>вивести всі адреси зі значенням
trainer read <pid> <addr> [float]прочитати значення
trainer write <pid> <addr> <int>записати ціле
trainer chain <pid> <Game*>пройти ланцюжок і показати HP, броню, набої
trainer god <pid> <Game*>кожні 50 мс виставляти HP=999 і набої=99

Запуск на трьох ОС

Запустіть термінал від адміністратора, якщо гра запущена від адміністратора. Інакше достатньо звичайного.

trainer.exe scan 12345 100
Тренер із режимом god змінює значення кожні 50 мс. Гра покаже HP=999, поки тренер працює. Зупинка тренера (Ctrl+C) повертає звичайну поведінку, але змінені значення залишаються до наступного оновлення.
Проєкт тренера компілюється на macOS, а гра-мішень збирається й запускається. Реалізації для Linux і Windows написані за документацією API, але на цих ОС ще не запускалися: якщо виникне помилка доступу або компіляції, перевірте умови з розділів «Процеси…» та «Інструменти».

Internal-чіт: бібліотека всередині процесу гри

Зовнішній тренер звертається до гри через виклики ОС, кожне читання проходить через ядро. Бібліотека, завантажена в процес, живе в тому самому адресному просторі й бачить пам'ять гри напряму. Вона може читати вказівники, викликати функції гри та міняти код без ReadProcessMemory.

ExternalInternal
Де виконуєтьсяокремий процесвсередині гри
Швидкість доступувиклик ядра на кожне читанняпрямий доступ до пам'яті
Виклик функцій грискладнопрямо за адресою
Складність запускупростапотрібна інжекція
Легше виявитианалізом дескрипторів процесуаналізом списку модулів

Як бібліотека потрапляє в процес

Змінна LD_PRELOAD змушує завантажувач підвантажити бібліотеку першою:

LD_PRELOAD=$PWD/internal/build/libinternal.so ./target-game/build/target_game

Код цього модуля на Windows не перевірявся на цій машині: інжектор написаний за документацією WinAPI.

Код модуля

Модуль викликається під час завантаження (конструктор у Linux/macOS, DllMain у Windows) і запускає окремий потік. Потім потік знаходить гравця й підтримує значення.

__attribute__((constructor)) static void on_load() { start(); }   // Linux, macOS
// Windows: DllMain -> case DLL_PROCESS_ATTACH -> start();

Через std::thread робота виконується не в потоці завантажувача: DllMain на Windows не повинен чекати. Для зв'язку з пам'яттю гри використано той самий клас Process, але для власного PID, тож код однаковий для всіх ОС.

Пошук гравця за сигнатурою

Ланцюжок вказівників (розділ «Ланцюжки вказівників») потребує відомої адреси Game*. Зсередини процесу простіше знайти структуру за її вмістом: ім'я Hero, потім health == 100 на зміщенні 16 та armor == 50 на зміщенні 20.

for (size_t i = 0; i + 36 <= r.size; i += 16) {
    if (std::memcmp(&buf[i], "Hero\0", 5) != 0) continue;
    int32_t hp, armor;
    std::memcpy(&hp, &buf[i + 16], 4);
    std::memcpy(&armor, &buf[i + 20], 4);
    if (hp == 100 && armor == 50) return r.start + i;
}

Крок 16 використано, бо new вирівнює об'єкти за 16 байтів. Сигнатура має бути унікальною; одного імені мало, тому додано перевірку значень.

Результат на macOS

Збірка бібліотеки та запуск гри з нею:

DYLD_INSERT_LIBRARIES=... ./target_game
[Hero] HP=100 armor=50 ammo=30/30 score=0 pos=(10.0, 20.0)
> [internal] Player знайдено: 0x102199890
[Hero] HP=999 armor=50 ammo=99/30 score=0 pos=(11.5, 19.5)

Вивід отримано на macOS (Apple Silicon) зі збіркою гри, зробленою за інструкцією з розділу «Інструменти». Адреса гравця щоразу інша.

Internal-модуль виконується з правами гри. Помилка в ньому, наприклад невірний вказівник, завершує гру. У зовнішньому тренері така помилка завершує лише тренер.
Інжектувати можна лише у власні процеси та в ігри, які ви можете запускати локально й офлайн. Інжекція в онлайн-ігри з антічітом розпізнається і блокується.
Copyright © 2026