Пошук даних у пам'яті: сканування, структури, ланцюжки вказівників
Пошук даних у пам'яті: сканування, структури, ланцюжки вказівників
🎯 Мета розділу
- Знайти адресу змінної в чужому процесі, не знаючи вихідного коду.
- Зрозуміти алгоритм «перше сканування → зміна → повторне сканування».
- Змінити значення і побачити результат у грі.
- Зрозуміти, що знайдена адреса здоров'я — це початок структури гравця плюс зміщення.
- Обчислювати зміщення полів із визначення структури та перевіряти їх у пам'яті.
- Відновлювати розкладку невідомої структури за кількома знайденими полями.
- Зрозуміти, чому адреса
healthзмінюється після кожного запуску гри. - Знайти вказівник, який веде до структури гравця, і побудувати ланцюжок.
- Читати ланцюжок у тренері так, щоб він працював після перезапуску.
🔑 Ключові терміни
- Перше сканування: пошук усіх адрес із заданим значенням.
- Наступне сканування: фільтрація попереднього списку після зміни значення.
- Кандидат: адреса, яка досі відповідає всім зробленим перевіркам.
- Зміщення (offset): відстань у байтах від початку структури до поля.
- Вирівнювання (alignment): вимога розміщувати поле за адресою, кратною його розміру.
- База об'єкта: адреса початку структури, відносно якої рахуються зміщення.
- Динамічна адреса: адреса об'єкта, створеного під час роботи (
new), змінюється між запусками. - Статична адреса: адреса глобальної змінної, стала відносно бази модуля.
- Ланцюжок вказівників (pointer chain): база → читаємо вказівник → додаємо зміщення → читаємо наступний вказівник.
Сканування пам'яті: знаходимо здоров'я гравця
У грі-мішені здоров'я дорівнює 100. У процесі в сотні мегабайтів пам'яті число 100 може траплятися тисячі разів, тому одного сканування замало. Розв'язок — змінювати значення в грі й залишати лише ті адреси, які змінилися так само.
Алгоритм
Перше сканування
Шукаємо всі 4-байтові цілі, рівні поточному значенню (100).
Зміна в грі
Командою d отримуємо удар: здоров'я стає 85.
Наступне сканування
З попереднього списку лишаємо адреси, де тепер 85. Повторюємо, поки лишається кілька кандидатів або один.
Зміна значення
Записуємо за знайденою адресою число 999 і перевіряємо, що в грі HP=999.
Практика зі своїм сканером
У трьох терміналах: у першому запущена гра, у другому тренер, у третьому — обмін командами. Спочатку знаходимо PID гри у її першому рядку виводу.
Команда uniq -d виводить адреси, що є в обох файлах: за цими адресами спершу було 100, а після удару стало 85. Наш сканер зберігає лише адреси, тому порівняння списків замінює повторне читання значень.
armor у цього гравця ніколи не буде 100 на старті, а інші змінні в бібліотеках можуть). Повторення циклу «зміна → скан» з іншим значенням (70, 55) прибирає випадкові збіги.Якщо після кількох ітерацій лишилася одна адреса, записуємо значення:
sudo ./build/trainer write 66038 0x... 999
У грі наступний вивід показує HP=999. Гра сама нічого не помітила: вона довіряє пам'яті.
Те саме в Cheat Engine і scanmem
- Відкрийте Cheat Engine, натисніть значок комп'ютера й оберіть процес
target_game.exe. - У полі Value введіть
100, тип 4 Bytes, натисніть First Scan. - У грі отримайте удар. У Cheat Engine введіть
85і натисніть Next Scan. - Двічі клацніть знайдену адресу, щоб додати її в нижню таблицю, змініть Value на 999.
sudo scanmem $(pgrep target_game)
> 100
> 85 # після удару в грі
> list
> set 999
x, y) зберігаються у форматі IEEE 754, а не як цілі. Для них у сканері потрібен тип Float, а точний збіг працює рідко: використовуйте «змінилося» / «збільшилося».float, 8 байтів) або в зашифрованому вигляді. Спробуйте інші типи сканування або «невідоме початкове значення» (unknown initial value).Структури в пам'яті: від однієї адреси до всього гравця
У розділі про сканування знайшлась адреса health. Але гравець — це структура, в якій поруч лежать броня, координати та зброя. Якщо ми знаємо базу структури, усі її поля доступні за сталими зміщеннями.
Розкладка структури гравця
Структура гри-мішені:
struct Weapon { int32_t ammo; int32_t magazine; float damage; };
struct Player {
char name[16]; // +0
int32_t health; // +16
int32_t armor; // +20
float x; // +24
float y; // +28
Weapon weapon; // +32
};
Компілятор розміщує поля одне за одним, додаючи порожні байти для вирівнювання. У цій структурі всі поля чотирибайтові, тому проміжків нема.

Якщо health знайдено за адресою 0x10215da80, база гравця дорівнює 0x10215da80 − 16 = 0x10215da70, а armor лежить на 0x10215da84. У гру-мішень вбудовано перевірку static_assert(offsetof(Player, health) == 16 ...), тож зміщення в тренері й у цьому розділі збігаються зі збіркою.
Перевірка в налагоджувачі
Зміщення ми розрахували за кодом. У реверс-інжинірингу коду немає, тому розкладку перевіряють у пам'яті. Дамп 48 байтів від бази гравця в LLDB:
Числа 1092616192 і 1101004800 — це біти float, показані як ціле. Тому поруч зі health легко побачити пару «кілька малих цілих, потім великі числа»: великі числа свідчать про координати.
--format f, щоб побачити float: memory read --size 4 --count 2 --format f 0x10215da88. У GDB — x/2f. У Cheat Engine — «Dissect data/structures».Як відновити невідому структуру
Знайти одне поле
Скануванням знаходимо health (розділ «Сканування пам'яті»). Це перша точка.
Знайти сусідні поля
Дивимось у дамп навколо: броня зазвичай поряд зі здоров'ям, далі координати, зброя. Змінюємо в грі броню або набої й спостерігаємо, яке число поряд змінилося.
Записати розкладку
Складаємо таблицю «зміщення → призначення → тип». У реальних іграх вона росте поступово, і її зберігають у власній struct з порожніми полями-заповнювачами (char pad[0x40]).
Перевірити на іншому об'єкті
Знаходимо другого гравця (ворога) і перевіряємо, що з тими ж зміщеннями отримуємо розумні значення.
char pad_0010[0x30]; int32_t health;.new, тому його адреса змінюється при кожному запуску. Стала лише відстань health від початку структури. Як знайти початок щоразу, розглянуто в наступному розділі.Ланцюжки вказівників: як знайти гравця після перезапуску
Після перезапуску гри health опиняється за іншою адресою, і збережена адреса вказує на сміття. Але десь у пам'яті лежить вказівник на структуру Player, і сам цей вказівник зберігається у Game, який, у свою чергу, доступний за сталим шляхом. Задача — знайти цей шлях.

Пошук вказівника на структуру
Ми знаємо базу гравця: health − 16. Вказівник на гравця — це 8 байтів, значення яких дорівнює цій адресі. Тому шукаємо в пам'яті 8-байтове число Player*.
Обчислити базу
Адреса health мінус зміщення (16) дає базу гравця, наприклад 0x10215da70.
Знайти вказівники на базу
Сканування з типом 8 bytes і значенням бази. У Cheat Engine — шестнадцяткове значення, у scanmem — 0x10215da70. Знайдена адреса — поле Game::player.
Піднятися на рівень вище
База об'єкта Game = адреса знайденого поля мінус 8 (зміщення player у Game). Шукаємо вказівник на неї, поки не дійдемо до адреси всередині самого виконуваного файлу.
Запам'ятати ланцюжок
Записуємо: [база_модуля + X] + 0x8 → Player*, Player* + 0x10 → health.
У нашій грі-мішені Game теж створюється через new, тому його вказівника не існує в глобальних даних. Це повторює ситуацію в реальних іграх, де ланцюжок часто закінчується глобальною змінною localPlayer. Щоб відтворити такий кінець ланцюжка, використаємо адресу Game*, яку гра друкує з параметром --show-addr, і розглянемо, що з нею робить тренер.
Тренер із ланцюжком
Команда chain приймає PID і адресу Game*, читає вказівник Player* і виводить поля. Вона працює правильно після перезапуску гри, якщо передати нову Game*:
uintptr_t player = 0;
p.read_value(game + OFF_GAME_PLAYER, player); // Player* із Game::player
int32_t hp;
p.read_value(player + OFF_HEALTH, hp); // health
Ключовий момент — вказівник читається щоразу заново: тренер не запам'ятовує абсолютну адресу гравця, а проходить ланцюжок. Режим god робить це в циклі кожні 50 мс:
god адреса Game* береться з виводу --show-addr — це підказка, що замінює крок «піднятися до статичної адреси». У реальній грі ланцюжок закінчується в секції даних виконуваного файлу, і тоді замість абсолютної адреси використовують база_модуля + зміщення.Базова адреса модуля
Статична адреса записується як module+offset, бо сам модуль завантажується за випадковою базою (ASLR).
CreateToolhelp32Snapshot з TH32CS_SNAPMODULE, поле modBaseAddr для потрібного .exe. Cheat Engine показує такі адреси як game.exe+17E0A8./proc/<pid>/maps: перший рядок із назвою виконуваного файлу дає початок модуля.vmmap <pid> або lldb: команда image list -o -f показує зсув виконуваного файлу.Коли ланцюжок «рветься»
- Гравець створюється заново після смерті або переходу на нову мапу, його
Player*змінюється. Ланцюжок усе одно коректний, якщо читати вказівник щоразу. - Ланцюжок веде до тимчасового об'єкта (наприклад, буфера) і ламається після кадру. Лікує повторне сканування з іншої ситуації та порівняння знайдених ланцюжків: правильний повторюється.
- Оновлення гри змінило зміщення. Ланцюжок знаходять заново за описаним вище алгоритмом.
Основи: пам'ять, процеси та інструменти
Що таке реверс-інжиніринг, правові межі, адресний простір процесу, доступ до чужої пам'яті та інструменти для Windows, Linux і macOS.
Тренер і internal-модуль на C++
Кросплатформний зовнішній тренер та бібліотека, що завантажується всередину гри на Windows, Linux і macOS.