Всем, привет!
В этой части я хочу рассказать про поиск объектов в игре. А так же про перехват функций.
И так. Начнём
Запустим игру и выберем то что хотим перехватить. Например у нас с какой-то частотой падает солнце, запустим Cheat Engine, и попробуем найти массив где находятся солнце в игре.
Например мы на экране видим 1 солнце в Cheat Engine ищем 1, потом когда солнца будет 2шт, нужно отсеять уже 2, и так далее пока не будет минимальное количество адресов.
Вот мы нашли 2 адреса в памяти игры, попробуем узнать функцию которая записывает в этот адрес в памяти. Показывать как это найти в Cheat Engine, я не буду, так как в интернете полно видео уроков как пользоваться Cheat Engine.
Первый статический, а значит что после перезапуска игры, этот адрес не изменится. Попробуем с него начать.
И видим инкрементирование и декрементирование когда солнце пропадает, очень похоже на то что нам нужно.
Копируем адрес инструкции инкрементирования, и переходим в IDA Pro. Для того чтобы понять что там происходит. Запустим под отладчиком, и поставим breakpoint чтобы узнать какая функция вызывает sub_476100. Смотрим на скриншот.
Это функция sub_477CB0. Смотрим где она вызывается.
Пробуем найти где именно, для этого просто выйдем из функции нажав сочетание клавиш Ctrl + F7. И вызвали её из sub_467970. С параметрами
v18 = sub_477CB0(*(_DWORD *)(*(_DWORD *)(v23 + 2368) + 8), *(_DWORD *)(v4 + 8), 0.0, 0.0, 0);Code: C++
На что очень сомнительно чтобы последние параметры были 0. Попробуем через Cheat Engine, занопить вызов sub_477CB0.
Код:
.text:00467BD3 fst [esp+28h+var_24] ; float
.text:00467BD7 fstp [esp+28h+var_28] ; float
.text:00467BDA call sub_477CB0
.text:00467BDF mov esi, eax
.text:00467BE1 push ebp
.text:00467BE2 mov ecx, esi
.text:00467BE4 call sub_4779A0
И мы увидели что создалось солнце, и через 1-2 сек игра крашнулась. А нам интересен был факт того что солнце создалось, а значит это не та функция что нам нужна.
Берем второй адрес и точно так же делаем все с ним.
И видим что здесь немного другие инструкции. Но не важно, выбираем первую когда солнце создается. И переходим в IDA Pro.
Видим что выполняются они в функции sub_420D90, теперь пробуем найти откуда эта функция вызывается, все в точности как и с прошлым вариантом.
Нашли sub_40F400, а эта функция вызывается из sub_4163D0. И видим.
Свернуть ↑
int __usercall sub_4163D0@<eax>(int a1@<esi>)
{
int result; // eax@1
bool v2; // zf@25
int v3; // eax@26
int v4; // edi@26
int v5; // ebx@26
int v6; // eax@28
int v7; // ecx@28
int v8; // eax@28
result = *(_DWORD *)(a1 + 21860);
if ( result != 1
&& result != 3
&& result != 5
&& result != 6
&& result != 8
&& !*(_BYTE *)(a1 + 22052)
&& *(_DWORD *)(a1 + 22044) <= 0
&& *(_DWORD *)(a1 + 22040) < 0 )
{
result = *(_DWORD *)(*(_DWORD *)(a1 + 164) + 2328);
if ( result != 19
&& result != 42
&& result != 71
&& result != 72
&& result != 23
&& result != 43
&& result != 50
&& result != 31 )
{
result = sub_456CA0();
if ( !(_BYTE)result )
{
result = sub_456DC0();
if ( !(_BYTE)result )
{
result = sub_456C80();
if ( !(_BYTE)result )
{
result = sub_41EC10();
if ( !(_BYTE)result )
{
result = *(_DWORD *)(a1 + 21916);
if ( result != 13 && (result != 1 && result != 2 || *(_DWORD *)(a1 + 212)) )
{
v2 = (*(_DWORD *)(a1 + 21840))-- == 1;
if ( v2 )
{
v3 = sub_5FF2A0(550);
++*(_DWORD *)(a1 + 21844);
v4 = v3 + 100;
v5 = 950;
if ( 10 * *(_DWORD *)(a1 + 21844) + 425 <= 950 )
v5 = 10 * *(_DWORD *)(a1 + 21844) + 425;
v6 = sub_5FF2A0(275);
v7 = *(_DWORD *)(a1 + 164);
*(_DWORD *)(a1 + 21840) = v5 + v6;
v8 = 4;
if ( *(_DWORD *)(v7 + 2328) == 37 )
v8 = 6;
result = sub_40F400((_DWORD *)a1, v4, 60, v8, 0);
}
}
}
}
}
}
}
}
return result;
}Code: C++
Свернуть ↑Развернуть ↓
Пробуем сделать выход из функции sub_4163D0, заменив первый байт на retn.
И видим что солнышко больше никогда не создастся. Ура мы нашли функцию которая создает солнце. А если быть более точным это функция sub_40F400.
Давайте попробуем проанализировать что это за функция.
int __thiscall sub_40F400(_DWORD *this, int a2, int a3, int a4, int a5)Code: C++
Видим что первый параметр это указатель на объект который управляет созданием солнц в игре. Остальные параметры не понятные, знаем что последний равен 0, а3 равен 60, а параметр а4 принимает значение 4 или 6.
А второй параметр получается после вызова функции с передачей аргумента 550, и прибавлением к результату 100. Очень похоже на рандом
но сказать окончательно не могу.
v3 = sub_5FF2A0(550);
++*(_DWORD *)(a1 + 21844);
v4 = v3 + 100;Code: C++
И так что мы имеем в плане этого класса который управляет солнцами. Некий класс в котором есть функция sub_40F400(int, int, int, int)
Например класс у нас такой.
class Creator
{
public:
void sub_40F400(int, int, int, int);
}Code: C++
Попробуем реализовать это в нашей DLL.
И так чтобы работала та функция которая нам нужна нам нужно указать её адрес в памяти. Смотрим код ниже
void Creator::CreateSun(int a2, int a3, int a4, int a5)
{
typedef void(__thiscall *t)(Creator*, int, int, int, int);
t f = (t)0x0040F400;
f(this, a2, a3, a4, a5);
}Code: C++
Всё класс объекта создан теперь осталось найти адрес этого объекта в памяти игры. Для этого вернемся в IDA Pro. И найдем какую-то функцию которая использует соглашение о вызове __cdecl, __stdcall.
Чуть не забыл о соглашениях, подробно можно о них почитать вот
здесь
Почему именно __cdecl, __stdcall, а все потому что эти функции проще всего перехватить. Так как аргументы функции передаются через стек, а не через регистры.
Хотя я покажу в следующих частях, как перехватить функцию где соглашение о вызове не позволяет это просто сделать. Например __usercall это последствия оптимизации программы/игры.
Помним что функция sub_40F400 вызывается в sub_4163D0, ищем где вызывается sub_4163D0. В функции sub_4181E0, но здесь все еще __thiscall, идем дальше sub_418600, тоже самое, дальше...
Код:
.rdata:006E4D8C dd offset sub_549210
.rdata:006E4D90 dd offset sub_418600
.rdata:006E4D94 dd offset sub_549930
Видим что функция sub_418600 виртуальная, а значит ищем чуть выше указатель на деструктор, а там где он и будет сам конструктор
Совсем рядом находится начало
Код:
.rdata:006E4D38 off_6E4D38 dd offset sub_40AF10 ; DATA XREF: sub_40A3C0+3D
.rdata:006E4D38 ; sub_40AF40+1F
.rdata:006E4D3C dd offset sub_547BB0
А именно off_6E4D38 ищем где оно применяется. И видим применение его в функции
int __thiscall sub_40A3C0(_DWORD *this, int a2)
v2 = this;
v51 = 0;
sub_54BE80(a2);
v58 = 0;
*(_DWORD *)(a2 + 160) = &off_6F4000;
*(_DWORD *)a2 = &off_6E4D38; //Вот оно
*(_DWORD *)(a2 + 160) = &off_6E4E50;
*(_DWORD *)(a2 + 168) = 0;
*(_DWORD *)(a2 + 172) = 0;Code: C++
То есть а2, и есть указатель на наш объект. Но соглашение все еще __thiscall. Идем дальше и видим что функция sub_40A3C0, вызывается в sub_4528B0
Свернуть ↑
int __thiscall sub_4528B0(_DWORD *this)
{
_DWORD *v1; // edi@1
void *v2; // eax@1
int v3; // eax@2
int v4; // ST10_4@4
int v5; // ST0C_4@4
v1 = this;
sub_452640();
v2 = operator new(0x57D8u);
if ( v2 )
v3 = sub_40A3C0(v1, (int)v2);
else
v3 = 0;
v4 = v1[49];
v5 = v1[48];
v1[538] = v3;
(*(void (__thiscall **)(int, _DWORD, _DWORD, int, int))(*(_DWORD *)v3 + 160))(v3, 0, 0, v5, v4);
(*(void (__stdcall **)(_DWORD))(*(_DWORD *)v1[200] + 12))(v1[538]);
(*(void (__stdcall **)(_DWORD))(*(_DWORD *)v1[200] + 48))(v1[538]);
return (*(int (__stdcall **)(_DWORD))(*(_DWORD *)v1[200] + 32))(v1[538]);
}Code: C++
Свернуть ↑Развернуть ↓
Сразу на будущее хочу сказать что размер нашего объекта равен
0x57D8u, тогда и напишем это в нашем классе
Свернуть ↑
class Creator
{
Creator() {};
public:
void CreateSun(int a2, int a3, int a4, int a5);
/* 000 */ virtual ~Creator() {}; // Помним что внутри объекта есть виртуальные функции, а значит по адресу 0000, лежит таблица виртуальных функций
/* 004 */
/* 0004 */ unsigned long __uUnkValue0004[0x15F6 - 1]; // 0x57D8u делим на 4 так как мы используем тип long,
/* 57D8 */ //а 0x57D8u размер в байтах. -1 потому что первые 4 байта это указатель на таблицу виртуальных функций
};
CompileTimeSizeCheck(Creator, 0x57D8);Code: C++
Свернуть ↑Развернуть ↓
Свернуть ↑
#include "stdafx.h"
#include "Creator.h"
void Creator::CreateSun(int a2, int a3, int a4, int a5)
{
typedef void(__thiscall *t)(Creator*, int, int, int, int);
t f = (t)0x0040F400;
f(this, a2, a3, a4, a5);
}Code: C++
Свернуть ↑Развернуть ↓
и видим что
CompileTimeSizeCheck(Creator, 0x57D8); не подсвечивается красным, для примера просто измените значение размера.
Продолжаем поиск __cdecl, __stdcall. И видим что функция sub_4528B0, вызывается из sub_452B30. Идем дальше sub_452820, и дальше
Поиск очень долгая и нудная работа. Для того чтобы не писать здесь, я сделал видео о том как я нашел эту функцию.
Видео
И так вот мы нашли эту функцию
int __stdcall sub_40D840(signed int a1)
Пробуем её заменить на свою, для того чтобы определить объект который создает солнце. Смотрим на код.
Свернуть ↑
#pragma once
class Creator
{
Creator() {};
public:
static int __stdcall sub_40D840(Creator* pCreator);
void CreateSun(int a2, int a3, int a4, int a5);
/* 000 */ virtual ~Creator() {}; // Помним что внутри объекта есть виртуальные функции, а значит по адресу 0000, лежит таблица виртуальных функций
/* 004 */
/* 0004 */ unsigned long __uUnkValue0004[0x15F6 - 1]; // 0x57D8u делим на 4 так как мы используем тип long,
/* 57D8 */ //а 0x57D8u размер в байтах. -1 потому что первые 4 байта это указатель на таблицу виртуальных функций
};
extern Creator* g_Creator;
CompileTimeSizeCheck(Creator, 0x57D8);Code: C++
Свернуть ↑Развернуть ↓
Свернуть ↑
#include "stdafx.h"
#include "Creator.h"
Creator* g_Creator = NULL;
void Creator::CreateSun(int a2, int a3, int a4, int a5)
{
typedef void(__thiscall *t)(Creator*, int, int, int, int);
t f = (t)0x0040F400;
f(this, a2, a3, a4, a5);
}
int __stdcall Creator::sub_40D840(Creator* pCreator)
{
typedef int(__stdcall *t)(Creator*);
t f = (t)0x0040D840;
g_Creator = pCreator;
return f(pCreator);
}Code: C++
Свернуть ↑Развернуть ↓
Осталось только перезаписать вызов нашей функции в игре. Находим адрес где вызывается sub_40D840.
Код:
.text:00452B47 push eax
.text:00452B48 call sub_40D840
.text:00452B4D mov dword ptr [esi+9A8h], 0
Делается это в нашей FixGame::Initialize вот так.
void FixGame::Initialize(void)
{
BYTE bytes[] = { 0xB1, 0x01, 0x90 };
WriteMemoryBYTES(0x005D14BD, bytes, 3);
WriteInstructionCall(0x00452B48, (UINT)Creator::sub_40D840);
}Code: C++
Теперь мы можем управлять нашим объектом, из любого места в DLL. Например вот так:
if (g_Creator)
g_Creator->CreateSun(100, 60, 4, 0);Code: C++
Круто ведь, да?)
Можем когда угодно создать солнце)
Например можем по нажатию кнопки, или еще после чего-то) Это уже в пределах вашей фантазии)
Теперь я покажу как в игре, расширить объект, например создадим переменную(объект, любого другого класса) в которой будет хранится количество солнц, созданные нами с помощью нашей DLL.
Создадим класс CreatorEx.
Свернуть ↑
#pragma once
class CreatorEx
{
public:
CreatorEx();
~CreatorEx();
void Increment(void);
unsigned long GetCountSuns(void) const;
private:
unsigned long dwMyCountSuns;
bool bInit;
};
CompileTimeSizeCheck(CreatorEx, 0x0008);Code: C++
Свернуть ↑Развернуть ↓
Свернуть ↑
#include "stdafx.h"
#include "CreatorEx.h"
CreatorEx::CreatorEx()
: bInit(false)
{
this->dwMyCountSuns = 0;
this->bInit = true;
}
CreatorEx::~CreatorEx()
{
}
void CreatorEx::Increment(void)
{
if (this->bInit)
this->dwMyCountSuns++;
}
unsigned long CreatorEx::GetCountSuns(void) const
{
return this->dwMyCountSuns;
}Code: C++
Свернуть ↑Развернуть ↓
Добавляем вконец переменную CreatorEx.
class Creator
{
Creator() {};
public:
static int __stdcall sub_40D840(Creator* pCreator);
void CreateSun(int a2, int a3, int a4, int a5);
/* 000 */ virtual ~Creator() {}; // Помним что внутри объекта есть виртуальные функции, а значит по адресу 0000, лежит таблица виртуальных функций
/* 004 */
/* 0004 */ unsigned long __uUnkValue0004[0x15F6 - 1]; // 0x57D8u делим на 4 так как мы используем тип long,
//а 0x57D8u размер в байтах. -1 потому что первые 4 байта это указатель на таблицу виртуальных функций
/* 57D8 */ CreatorEx objCreatorEx;
/* 57E0 */
};
extern Creator* g_Creator;
CompileTimeSizeCheck(Creator, 0x57E0);Code: C++
Очевидно что размер изменился, заставим теперь игру выделить память под нашу переменную.
Вспоминаем место где мы видели размер нашего класса, а это было в функции sub_4528B0.
Код:
.text:004528CE push 57D8h ; size_t
.text:004528D3 call ??2@YAPAXI@Z ; operator new(uint)
.text:004528D8 add esp, 4
Вот это место, нам нужно заменить push
57D8h , на свой. Давайте это и сделаем.
Код:
004528CE 68 D8 57 00 00 E8 FA 75 24 00 83 C4 04 89 44 24 h+W..ш·u$.Г-.ЙD$
Нам нужно записать новый размер по адресу
004528CE + 1, потому что
68 - это
push, его мы не трогаем.
Действуем.
void FixGame::Initialize(void)
{
BYTE bytes[] = { 0xB1, 0x01, 0x90 };
WriteMemoryBYTES(0x005D14BD, bytes, 3);
WriteMemoryDWORD(0x004528CF, sizeof(Creator)); // patch new size
WriteInstructionCall(0x00452B48, (UINT)Creator::sub_40D840);
}Code: C++
Всё теперь мы заставили игру выделить для нас память, и нам осталось лишь вызвать конструктор нашего объекта.
Поправим инициализацию, объекта.
int __stdcall Creator::sub_40D840(Creator* pCreator)
{
typedef int(__stdcall *t)(Creator*);
t f = (t)0x0040D840;
new (&pCreator->objCreatorEx) CreatorEx();
g_Creator = pCreator;
return f(pCreator);
}Code: C++
Теперь у нас создано и наше расширение, и мы можем вызывает его функции.
Например вот так.
DWORD dwCountSuns;
if (g_Creator)
{
dwCountSuns = g_Creator->objCreatorEx.GetCountSuns();
if (dwCountSuns == 0)
{
g_Creator->CreateSun(100, 60, 4, 0);
g_Creator->objCreatorEx.Increment();
}
}Code: C++
Но нам же так же нужно и вызвать деструктор, это я уже покажу в следующей статье А на этом все
Жду ваши вопросы
Так же сделал репозиторий с исходным кодом проекта.
GitHub
Если нужен исходник:
PlantsVsZombies.rar
1 часть
2 часть
3 часть