PDA

Просмотр полной версии : [Статья] Внедрение и модификация кода (C++ Builder)


BritishColonist
27.09.2011, 20:37
Привет, друзья. Это моя первая статья на zhyk.ru.
Сегодня поговорим о нескольких способах внедрения своего кода в приложение и об изменении стандартного кода.
Учтите, что статья рассчитана на новичков. Знания, полученные из этой статьи могут служить как и просто для самообразования, так и для разработки, скажем, бота.

"]Что нам потребуется: любой язык программирования (я буду использовать C++ Builder, входящий в пакет Rad Studio 2010 Architect), любой отладчик (я использую OllyDbg) и для удобства Cheat Engine (либо другой его аналог).
Издеваться будем над Perfect World (руоф, амфибии). Однако учтите, что модификация игрового клиента запрещена и может стать причиной блокировки аккаунта.

Существует несколько способов внедрения кода. Я использую один из этих двух: DLL Injection (внедряем целую библиотеку, в которой создаём обычный поток CreateThread. В потоке будет работать наша читерская функция) и CreateRemoteThread (создание удалённого потока в приложении-жертве, который запускает код, который мы туда и запишем). Сразу говорю, что оба способа вполне применимы, но более "шустрый" способ это создание удалённого потока (с DLL больше геморроя: приходится перезапускать приложение/освобождать DLL, что отнимает много времени + создание потока сложнее "заметить"). О плюсах и минусах этих техник мы поговорим ниже.

Часть #1.
"]Какой бы способ Вы не выбрали, придётся пройти такой этап, как поиск адресов, оффсетов и прочего. А в этом нам здорово поможет Cheat Engine.
Вот его основное окно:
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Правда, красавец? :)

Ещё не придумали, что мы будем делать? А я подскажу. Попробуем отображать названия ресурсов на максимально большом расстоянии? ;)
Запускаем клиент Perfect World и Cheat Engine, заходим на любой сервер любым персонажем (при разработке рекомендуется использовать ненужных левых персонажей на ненужных левых аккаунтах. Так, на всякий случай). Бежим к любому ресурсу.
Я создал друльку и нашёл "Высохший древесный корень":
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Из самых банальных основ информатики мы знаем, что 1 - это "да", а 0 - "нет". Пользуясь этим, попробуем найти значение видимости надписи данного ресурса (1 - надпись видна, 0 - не видна). Убеждаемся, что мы подошли близко к ресурсу и его название отображается, идём в Cheat Engine, открываем процесс игры и ищем значение 1 (тип 1 byte):
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Слева видим наши адреса. Здесь показаны все адреса, по которым на момент поиска было значение 1.
Теперь отсеим значения (кнопка Next Scan, справа от той, что была для поиска): отойдём от ресурса, чтобы его название пропало, в Cheat Engine значение меняем на 0 и жмём Next Scan:
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Затем снова подбегаем, чтобы название ресурса отобразилось. Отсеиваем по числу 1. И так далее до тех пор, пока адресов слева не останется совсем мало (в идеале - 1 адрес).
Когда адрес останется один, сделайте на нём двойной щелчок мышью, он появится в списке внизу. Отсюда он никуда не денется. Теперь нам интересно узнать, ЧТО меняет данный адрес (какие команды взаимодействуют с этим адресом памяти, записывая по нему значения). Жмём правой кнопкой мыши на адресе, затем в контекстном меню выбираем пункт Find out what writes to this address. Cheat Engine скажет, что необходимо использовать отладчик, жмём Yes.
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Появится окно отладчика. Подбежим к ресурсу, отойдём от него (чтобы название появилось и пропало), вернёмся в Cheat Engine.
Здесь мы видим, какие ассемблерные инструкции получают доступ к адресу памяти:
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Красным цветом я выделил то, что выполняется, когда название отображается, а синим - когда название невидимо. Логично предположить, что нас не устраивает именно вторая команда (потому что первая пишет 01, т.е. делает надпись видимой, а вторая пишет bl. bl - это регистр, типа переменная. вероятно, что bl = 0).
В правом нижнем уголке всплывшего окошка с инструкциями нажмём кнопочку Stop (это прекратит отслеживать команды). Выделим обе команды (важна лишь вторая, но первая пусть будет на всякий случай), нажмём Ctrl + C, сохраним себе код в блокнотик (рекомендую Notepad++). Окошко отладчика можно закрыть, Cheat Engine - тоже.
Запускаем OllyDbg и присоединяем его к процессу игры. Не стоит пугаться интерфейса Olly.
Нажимаем "File" -> "Attach". В списке ищем окно Perfect World и жмём "Attach". Как только в центре экрана появится куча кода, нажмите F9. Это продолжит нормальную работу процесса (OllyDbg при подключении к процессу останавливает его). Если процесс будет заморожен в течение длительного времени, очень вероятен вылет с сервера.
Смотрим нашу вторую запись в блокноте: 005E9854 - 88 9E 69010000 - mov [esi+00000169],bl.
Красненькое - адрес команды в EXE-файле. Синенькое - байты (опкод, то бишь оператор + операнды, то бишь то, с чем работает опкод). Зелёное - сама команда ассемблера в читабельном виде.
Копируем адрес, в OllyDbg нажимаем сочетание клавиш Ctrl + G, вставляем адрес туда, жмём Ok, либо Enter.
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Красненьким на скриншоте (вверху) я выделил важный момент: мы сейчас просматриваем адресное пространство не клиента, а библиотеки ntdll. Поэтому просто повторяем операцию (Ctrl + G, вставляем адрес, Ok).
Мы попали на выделенный адрес:
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Я добавил комментарии справа от кода. RED и BLUE это наши адреса (они, как оказалось, расположены рядом). Видите ли, мы не можем просто добавить прямо здесь свой код. Эффективнее и изящнее будет замена оригинального кода, но при этом возникает сложность: размер заменяющих команд должен соответствовать размеру заменяемых команд. Давайте обратим внимание на команды типа Jxx, где xx - различные вариации. J - это переход к адресу. Посредством таких переходов осуществляются проверки, циклы, switch и т.д.
Дальше всё зависит от Вашей фантазии. Я вижу два простых решения, вот одно из них.
Как мы видим, рядом у нас есть две команды типа Jxx, переходящих на адрес второй (синей) команды.
Первая: 005E9844 75 16 JNZ SHORT (STD)Ele.005E985C
Вторая: 005E9852 74 08 JE SHORT (STD)Ele.005E985C
JNZ (Jump if Not Zero) - грубо говоря, переход, если последняя операция НЕ установила ZF (Zero Flag - флаг нуля). Т.е. если ZF = 1, переход не срабатывает.
JE (Jump if Equal) - переход, если сравниваемые предыдущей командой операнды равны.
Кстати, (STD)Ele. - это типа имя модуля (в данном случае имя Exe-файла), в адресном пространстве которого мы сейчас сидим.
Поскольку вторая команда пихает по нашему адресу единицу (т.е. заставляет название ресурса отображаться), а нам нужно, чтобы наименование ресурса отрисовывалось всегда, то логично предположить, что можно изменить код так, чтобы переходы срабатывали всегда, а не при выполнении проверок.
В общем, нажимаем дважды на строке кода, расположенной по первому из приведённых выше адресов с переходами, откроется окошечко, куда вводим новую команду.
Безусловный переход (переход без проверок) это JMP. Так что меняем код.
Было: JNZ SHORT 005E985C Стало: JMP SHORT 005E985C.
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Смотрим окно игры. Если игра не вылетела, то скорей всего всё сделали правильно:
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]

Второй вариант, ведущий к аналогичному результату. Выше по коду я заметил использование константы (и добавил в этой строке комментарий "intresting const"). При этом используются операции с префиксом F (Float - работа с дробными величинами, то бишь с числами с плавающей точкой). Логично предположить, что там должна лежать дистанция прорисовки этих названий (и тогда те JNZ и JE проверяют, находится ли ресурс на должной дистанции). Почему константа? [123456] - получение значения по адресу 123456. Адрес указан явно (число), стало быть, значение лежит по постоянному адресу (и этот адрес как раз указан). Выделяем строку и OllyDbg уже сообщает нам, что там лежит, а лежит там 20.0 (пишется это прямо под окошечком с кодом). Откроем контекстное меню на этой строке, выберем: "Follow in dump" -> "Memory address". Активным при этом станет окошечко, находящееся под окошечком с кодом.
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
В нём по умолчанию отображаются байты в шестнадцатеричном виде. Откроем контекстное меню и изменим тип на Float (32 bit):
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
Как видим, там действительно лежит значение 20.0. Жмём на нём правой кнопкой мыши, выбираем пункт "Modify". Просто вбиваем новое значение и идём в игру смотреть результат.
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
К сожалению вариант #2 привёл к побочным эффектам, т.к. константа использовалась не только для имён ресурсов, но и для имён монстров, а так же для лимита отдаления камеры (зато мы сделали некий Zoom Hack ;D). Вообще, можно найти другую большую константу и заменить адрес в коде выше (там, где стоит комментарий "intresting const").
В общем, я остановлюсь на первом варианте и верну всё, как было в нём. Можно переходить к программированию, однако есть ещё один способ сделать готовый хак - прямо сейчас сохранить всё в Exe.
Выделяем строчку с изменённым переходом, щёлкаем правой кнопкой мыши, выбираем "Copy to executable" -> "Selection", всплывёт окно, которое следует просто закрыть, затем спросят, действительно ли мы хотим сохранить файл, нажимаем "Да" и выбираем, как назвать и куда положить новый файл. Закрываем всё, запускаем новый Exe и проверяем. Всё должно быть нормально. Способ неудобен тем, что такой чит нельзя отключить и что при обновлениях Ваш файл (если он назван ElementClient.exe) может быть заменён патчером (а если назван иначе, однако Вы будете пользоваться им, то могут возникнуть баги).

Часть #2.
"]В общем, приступим к кодингу. Начнём с техники DLL Injection, а именно - создадим свою DLL.
Напоминаю, что я использую C++ Builder. Запускаем его, жмём "File" -> "New" -> "Other" -> "Dynamic-link Library".
Снимаем галочку с пункта "Use VCL", оставляем галочку "Multi Threaded" (хотя я без понятия, повлияет ли это на возможность создавать потоки (а мы будем), поэкспериментируйте сами, если хотите).
Удаляем огромнейший комментарий, нам его читать не нужно. Где угодно в коде создаём функцию с любым названием (я назову "Hack") - это будет функция чита, обрабатывающая нажатия кнопок. В основной функции DLL (int WINAPI DllEntryPoint) делаем запуск потока нашей функции Hack. Вот, как получилось у меня:

//---------------------------------------------------------------------------
#include <windows.h>
//---------------------------------------------------------------------------
void Hack() // наша функция
{
while(1) // вечный цикл в ней
{
Sleep(50); // пауза в 50 мс для уменьшения нагрузки от вечного цикла
// тут будет обработчик нажатий клавиш
}
}
//---------------------------------------------------------------------------
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
if(reason==DLL_PROCESS_ATTACH) // существует несколько возможных причин
{ // запуска функции DllEntryPoint
// нас интересует присоединение к процессу (DLL_PROCESS_ATTACH)
CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)Hac k,NULL,NULL,NULL);
// создаём поток функции Hack
}
return 1;
}
//-----------------------------------------------------------------

Нам потребуется проверять, зажата ли клавиша в данный момент + активно ли окно игры, вот две функции, которые нам помогут в этом (вставьте где-нибудь над функцией Hack):

//---------------------------------------------------------------------------
bool KeyPressed(BYTE key)
{
return ((GetAsyncKeyState(key)&(1<<16))!=0);
// проверяем старший бит. если он не равен 0, то клавиша зажата в текущий момент
}
//---------------------------------------------------------------------------
bool IsGameWindowActive()
{
HWND hWnd = GetForegroundWindow(); // получаем активное окно
if(!hWnd) // на всякий случай проверка на то, что окно получено
return false;

DWORD pId = 0;
GetWindowThreadProcessId(hWnd,&pId); // получаем процесс найденного окна
return (pId==GetCurrentProcessId()); // если Id этого процесса равен Id текущего процесса
// ..возвращаем true
//-----------------------------------------------------------------

Перейдём собственно к патчингу, то бишь к замене оригинального кода на свой. Сразу стоит взять на заметку, что придётся помимо записи в адреса памяти использовать VirtualProtect (чтобы менять атрибуты доступа к памяти. дело в том, что исполняемый код всегда защищён и при модификации приложение вылетит с ошибкой). Посмотрим на байты наших переходов до и после замены:
005E9844 75 16 JNZ SHORT 005E985C
005E9844 EB 16 JMP SHORT 005E985C
Красным выделен байткод операций. Нам повезло; это забавно, но замены одного байта достаточно (75 -> EB). Вот как мы поступим:


state = !state; // это переключатель состояния чита (вкл/выкл)
VirtualProtect((void*)0x5E9844,1,PAGE_EXECUTE_READ WRITE,&dwOldProtect); // получаем привилегии записи на 1 байт, начиная от 0x5E9844
// флаги старых привилегий помещаются в переменную dwOldProtect
if(state) // если чит включён
{
*(BYTE*)0x5E9844 = 0xEB; // превращаем JNZ в JMP
}
else // в противном случае
{
*(BYTE*)0x5E9844 = 0x75; // делаем обратную операцию
}
VirtualProtect((void*)0x5E9844,1,dwOldProtect,NULL ); // возвращаем атрибуты защиты из переменной dwOldProtect


Я буду использовать запись лаконичнее (но смысл остаётся такой же):

state=!state;
VirtualProtect((void*)0x5E9844,1,PAGE_EXECUTE_READ WRITE,&dwOldProtect);
*(BYTE*)0x5E9844 = state ? 0xEB : 0x75; // if (true) 0xEB; else 0x75
VirtualProtect((void*)0x5E9844,1,dwOldProtect,NULL );

Собираем код по кусочкам, вот, что получилось у меня:

//---------------------------------------------------------------------------
#include <windows.h>
//---------------------------------------------------------------------------
bool KeyPressed(BYTE key)
{
return ((GetAsyncKeyState(key)&(1<<16))!=0);
}
//---------------------------------------------------------------------------
bool IsGameWindowActive()
{
HWND hWnd = GetForegroundWindow();
if(!hWnd)
return false;

DWORD pId = 0;
GetWindowThreadProcessId(hWnd,&pId);
return (pId==GetCurrentProcessId());
}
//---------------------------------------------------------------------------
void Hack()
{
bool state = false; // состояние чита
DWORD dwOldProtect; // старые атрибуты защиты

while(1)
{
Sleep(50);
if(KeyPressed(VK_F2)) // если нажали клавишу F2,
{
if(IsGameWindowActive()) // проверяем, активно ли окно игры
{
state=!state; // переключаем состояние чита
VirtualProtect((void*)0x5E9844,1,PAGE_EXECUTE_READ WRITE,&dwOldProtect);
*(BYTE*)0x5E9844 = state ? 0xEB : 0x75; // патчим в соответствии с состоянием чита
VirtualProtect((void*)0x5E9844,1,dwOldProtect,NULL );
}
do{ // ждём, пока клавишу F2 не отпустят. если не делать этого, то чит будет
Sleep(50); // ..быстро включаться и выключаться, не давая возможности нормально
}while(KeyPressed(VK_F2)); // ..управлять его состоянием
}
}
}
//---------------------------------------------------------------------------
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
if(reason==DLL_PROCESS_ATTACH)
{
CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)Hac k,NULL,NULL,NULL);
}
return 1;
}
//---------------------------------------------------------------------------

Собственно, осталось только откомпилировать. Если никаких ошибок не возникло, то всё сделано правильно, можете тестировать результат, пользуясь любым инжектором библиотек (просто внедрите библиотеку в игру; в игре нажмите F2). Удачного сбора поликов ;)
Если хотите, чтобы Ваша библиотека могла работать на других компьютерах (а не только на тех, где установлен C++ Builder), то нужно настроить проект: в правом верхнем углу открываем свиток "Build Configurations", выбираем в нём "Release". Затем жмём "Project" -> "Options". В разделе "C++ Linker" ставим False в пунктах "Dynamic RTL" и "Generate import library". Если Вам понадобилось использовать VCL (при создании DLL Вы не снимали галочку "Use VCL"), то следует зайти также в раздел "Packages" и убрать там галочку "Build with runtime packages".
Компилируем. Размер результирующего DLL-файла увеличится (при использовании VCL - значительно увеличится). Рекомендуется сжать библиотеку через ASPack, UPX или прочую подобную программу.

Часть #3.
"]Пришло время способа удалённого потока - CreateRemoteThread. В чём соль: выделяем в процессе-жертве немного памяти, копируем туда функцию из адресного пространства нашего процесса, запускаем функцию в процессе-жертве. Удобно то, что мы можем передать в функцию параметры, а при особом желании (и усердии) - даже получить возвращаемое значение. Способ применим при так называемых инжектах, то бишь при эмуляции оригинальных функций клиента игры. Особое удобство в том, что, попадая в адресное пространство процесса-жертвы, мы получаем прямой доступ к этому пространству, т.е. можем обращаться к адресам памяти в процессе-жертве напрямую, через указатели. Главный недостаток способа - зачастую функции должны быть написаны преимущественно на ассемблере (однако решение есть, решение есть вообще всегда. В данном случае оно довольно сложное).
В общем, за дело. Создаём новый проект в C++ Builder: "File" -> "New" -> "VCL Forms Application". Кидаем на форму кнопку (компонент "TButton" либо его аналоги).
Теперь перейдём к исходному коду приложения для этого нажмите в правом верхнем углу Builder'а на элемент списка с названием "*.cpp", где * - название по умолчанию, либо Ваше заданное название проекта. Всё показано на скриншоте:
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
.cpp - исходный код приложения.
.dfm - форма, т.е. внешний вид окна будущей программы.

Добавим вверх исходника инклуд:
#include <tlhelp32.h>
С его помощью мы будем искать процесс игры (Вы можете использовать любой другой способ, например GetWindowThreadProcessId(FindWindow(NULL,"Perfect World"));).
Ознакомимся с некоторыми полезными функциями:
//---------------------------------------------------------------------------
bool GetProcessId(DWORD* pId, AnsiString ExeName) // получение идентификатора процесса по его имени
{
HANDLE hProcessSnap;
PROCESSENTRY32 ProcEntry32 = {0};
AnsiString asName = "";

hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
if(hProcessSnap==INVALID_HANDLE_VALUE)
{
*pId=0;
return false;
}
ProcEntry32.dwSize = sizeof(PROCESSENTRY32);
if(Process32First(hProcessSnap,&ProcEntry32))
{
do {
asName = ProcEntry32.szExeFile;
if((asName.UpperCase())==ExeName.UpperCase())
{
*pId = ProcEntry32.th32ProcessID;
CloseHandle(hProcessSnap);
return true;
}
ProcEntry32.dwSize = sizeof(PROCESSENTRY32);
Process32Next(hProcessSnap,&ProcEntry32);
} while(hProcessSnap!=INVALID_HANDLE_VALUE);
}
CloseHandle(hProcessSnap);
*pId=0;
return false;
}
//---------------------------------------------------------------------------
bool IsGameVersionValid(HANDLE hProc) // проверка версии клиента
{ // применять к процессам, открытым через OpenProcess
if(hProc==INVALID_HANDLE_VALUE)
return false;

DWORD ShouldBeEqualToGA;
ReadProcessMemory(hProc,(void*)BA,&ShouldBeEqualToGA,4,NULL);
ShouldBeEqualToGA+=0x1C;
return (ShouldBeEqualToGA==GA);
}
//---------------------------------------------------------------------------
Не забудьте описать где-нибудь вверху BA и GA. У меня это define:
#define BA 0x00A5B90C
#define GA 0x00A5BFCC
Так можно будет быстро перегнать версию, под которую должна работать программа. Своеобразная защита от ошибок при работе со старыми (либо слишком новыми) клиентами игры.
Вообще, мой дефайн выглядит вот так:
//---------------------------------------------------------------------------
#define BA 0x00A5B90C // для поддержки определённой версии игры
#define GA 0x00A5BFCC
//---------------------------------------------------------------------------
#define INJECT_OK 0x00 // для инжектора функций, о котором речь пойдёт дальше
#define INJECT_NO_PROCESS 0x01
#define INJECT_NO_ACCESS 0x02
#define INJECT_BAD_VERSION 0x03
#define INJECT_THREAD_FAIL 0x04
//---------------------------------------------------------------------------
Вам стоит описать у себя так же.
Для тех, кто не знает, что такое define: define, он же макрос, служит своеобразной ассоциацией между двумя выражениями. В нашем случае, везде, где встретится выражение BA, компилятор автоматически поменяет его на 0x00A5B90C. Похоже на константы, но не нужно указывать тип и можно подставить что угодно; вот пример:
#define Sqr(x) ((x)*(x)) // функция возводит в квадрат подставленное значение
if(Sqr(15)==225)
return true;
Или вот ещё пример:
#define w(x) while(x)
w(1)
{
// вечный цикл
}
Теперь собственно код инжектора:
BYTE InjectAndExecute(void* Func, void* Params)
{
DWORD pId;
if(!GetProcessId(&pId,"elementclient.exe"))
return INJECT_NO_PROCESS; // нет такого процесса

HANDLE hProc;
HANDLE hProcThread;
void* pFunction;
void* pParams;

hProc = OpenProcess(PROCESS_ALL_ACCESS,false,pId);
if(hProc==INVALID_HANDLE_VALUE) // не удалось открыть процесс
return INJECT_NO_ACCESS;

if(!IsGameVersionValid(hProc))
{
CloseHandle(hProc);
return INJECT_BAD_VERSION; // не та версия игры
}


// занимаем место под функцию и параметры, получаем указатели на начало занятой для них памяти
pFunction = VirtualAllocEx(hProc,NULL,4096,MEM_COMMIT,PAGE_REA DWRITE);
pParams = VirtualAllocEx(hProc,NULL,256,MEM_COMMIT,PAGE_READ WRITE);
// копируем функцию в адресное пространство игры
WriteProcessMemory(hProc,pFunction,Func,4096,NULL) ;
WriteProcessMemory(hProc,pParams,Params,256,NULL);

// запускаем функцию с параметрами
hProcThread = CreateRemoteThread(hProc,NULL,NULL,(LPTHREAD_START _ROUTINE)pFunction,pParams,NULL,NULL);
if(hProcThread==INVALID_HANDLE_VALUE) // не удалось создать поток
{
VirtualFreeEx(hProc,pFunction,4096,MEM_RELEASE);
VirtualFreeEx(hProc,pParams,256,MEM_RELEASE);
CloseHandle(hProc);
return INJECT_THREAD_FAIL;
}

WaitForSingleObject(hProcThread,INFINITE); // ожидаем завершения работы потока
CloseHandle(hProcThread); // освобождаем память

VirtualFreeEx(hProc,pFunction,4096,MEM_RELEASE); // стираем из процесса нашу функцию
VirtualFreeEx(hProc,pParams,256,MEM_RELEASE); // и параметры
CloseHandle(hProc); // закрывает открытый хендл процесса (OpenProcess)
return INJECT_OK; // успешная инъекция и выполнение кода
}

Какой код можно внедрять таким способом? Исключительно такой, который не будет использовать любые функции и любые переменные, недоступные в самой функции. В их числе находятся и все Kernel-функции (даже LoadLibrary!). Выкрутиться безусловно можно, но это уже будет Вашим домашним заданием :D
Теперь собственно к кускам кода, которые мы будем внедрять. Такие кусочки следует формировать по результатам долгих просиживаний в OllyDbg. Здесь поможет соседняя тема от Dinmaite "Поиск инжектов" или "Наш код в чужом процессе" ([Ссылки могут видеть только зарегистрированные и активированные пользователи]).
Какие советы по поиску инжектов могу дать я:
1. Составляйте свою базу прямиком в OllyDbg путём добавления к коду комментариев и закладок (Shift+Alt+<цифра от 0 до 9> - создание закладки на выделенном адреса, Alt+<цифра> - переход к закладке).
2. Если видите такой код:
mov ecx,<тут любой код>
call 0x123456
то скорее всего ecx содержит начало структуры в этом можно убедиться, поставив BP (BreakPoint) на этот mov и глянув, чему равен ecx. А вызываемая функция в таком случае, вероятно, является членом класса.
3. Постоянно ставьте BP на команды push. Так передаются параметры функциям.
4. Составьте в CheatEngine таблицу с часто используемыми значениями (например, начало структуры игрока). В дальнейшем будет удобно быстро сверять данные из CheatEngine с регистрами. Зачем таблицу? Потому что такие адреса не статические, CheatEngine будет отображать данные по указателям с определёнными смещениями, что, несомненно, очень удобно.
5. Если видите незнакомое значение, пихаемое в стек через push (если не знаете, как это значение рассчитано), то проследите выше по коду, как это значение получается. Рано или поздно найдётся статический адрес, из которого это значение берётся, либо более знакомый нам адрес, который мы получать умеем. Нам главное понять, что это такое и/или понять, откуда это взять.
6. Не бойтесь исследовать функцию глубже (переходить на внутренние вызовы других функций и т.д.).
7. Используйте блокнот, куда будете записывать интересные Вам адреса/коды. В голове всё хранить не удастся :D
8. Приобретите/скачайте какую-нибудь литературу по ассемблеру. Я, например, пользуюсь книгой "Assembler", 2-е издание (издательский дом Питер, 2010 г.), автор В.И. Юров. Добротно написано. Ну так вот, важно изучить (либо иметь всегда под рукой) всевозможные вариации команд переходов, а так же справку по всем командам, чтобы мы без проблем могли получать информацию о незнакомых командах.

Приведённые советы могут немного облегчить жизнь начинающим, однако в любом случае всегда придётся полагаться на собственную логику и внимание, а так же зачастую проводить много времени в отладчике, экспериментируя. Всё в Ваших руках.

Короче, что мне удалось раскопать при написании этой статьи (сам впервые занимался этим):
//---------------------------------------------------------------------------
void __stdcall Target_THREAD(DWORD* WorldIdentifier) // функция выделения монстра/игрока/нипа
{
DWORD Id = *WorldIdentifier; // идентификатор того, что хотим выделить (WID)
__asm
{
pushad // сохраняем все регистры, чтобы не "повредить" стек
mov edx,0x00606A70
mov edi,Id
mov eax,dword ptr ds:[BA]
push edi
mov ecx,dword ptr ds:[eax+0x20]
add ecx,0xEC
call edx
popad // возвращаем регистры на место
}
}
//---------------------------------------------------------------------------
void __stdcall Attack_THREAD() // обычная атака выделенной цели
{ // если выделена недопустимая цель, ничего не происходит
__asm
{
pushad
mov edx,0x0044FE60 // адрес вызываемой функции
mov ecx,dword ptr ds:[BA]
mov ecx,dword ptr ds:[ecx+0x1C]
mov ecx,dword ptr ds:[ecx+0x20] // структуру игрока пихаем в ecx
push -1 // понятия не имею, что это за параметры
push 0 // они просто взяты из OllyDbg
push 0
push 0
call edx
popad
}
}
//---------------------------------------------------------------------------

Обратите внимание на ключевое слово __stdcall. В функциях-инжектах оно необходимо.
Итак, у нас есть код, который мы получили из процесса игры, путём копания клиента. Мы не будем использовать его напрямую, потому как находится он в адресном пространстве нашего процесса и не имеет прямого доступа к памяти игры. Мы будем пользоваться инжектором, да.
Но для начала мы создадим класс игрока, чтобы впоследствии не заморачиваться с функциями, написанными на ассемблере, а просто писать короткие и красивые "Player->AttackSelectedTarget();".
Вот как получилось у меня (обратите внимание на то, как работать с нашим кодом инжектов):
class CHostPlayer
{
public:
bool SelectSomething(DWORD GlobalId)
{
return (InjectAndExecute(&Target_THREAD,&GlobalId)==INJECT_OK);
// передаём ссылку на параметр
}
bool AttackSelectedTarget()
{
return (InjectAndExecute(&Attack_THREAD,NULL)==INJECT_OK);
// параметр не нужен - передаём NULL
}
} *HostPlayer; // создаём указатель на экземпляр класса

В обработчик нажатия нашей кнопочки (чтобы создать его, перейдите к форме, затем сделайте двойной клик мышью по кнопке) добавим вызов функции из класса:
"][Ссылки могут видеть только зарегистрированные и активированные пользователи]
void __fastcall TForm1::Button1Click(TObject *Sender)
{
HostPlayer->AttackSelectedTarget();
}

Попробуйте скомпилировать и нажать на кнопку (предварительно выделив монстра в игре). Персонаж должен побежать в атаку. Инжект выделения следует прикрутить к данным о монстрах, полученным из игры (действительно, Вы же не знаете, какой ID у определённого монстра).
//---------------------------------------------------------------------------
#include <vcl.h>
#include <tlhelp32.h>
#pragma hdrstop
#include "source.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
#define BA 0x00A5B90C
#define GA 0x00A5BFCC
//---------------------------------------------------------------------------
#define INJECT_OK 0x00
#define INJECT_NO_PROCESS 0x01
#define INJECT_NO_ACCESS 0x02
#define INJECT_BAD_VERSION 0x03
#define INJECT_THREAD_FAIL 0x04
//---------------------------------------------------------------------------
bool GetProcessId(DWORD* pId, AnsiString ExeName)
{
HANDLE hProcessSnap;
PROCESSENTRY32 ProcEntry32 = {0};
AnsiString asName = "";

hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
if(hProcessSnap==INVALID_HANDLE_VALUE)
{
*pId=0;
return false;
}
ProcEntry32.dwSize = sizeof(PROCESSENTRY32);
if(Process32First(hProcessSnap,&ProcEntry32))
{
do {
asName = ProcEntry32.szExeFile;
if((asName.UpperCase())==ExeName.UpperCase())
{
*pId = ProcEntry32.th32ProcessID;
CloseHandle(hProcessSnap);
return true;
}
ProcEntry32.dwSize = sizeof(PROCESSENTRY32);
Process32Next(hProcessSnap,&ProcEntry32);
} while(hProcessSnap!=INVALID_HANDLE_VALUE);
}
CloseHandle(hProcessSnap);
*pId=0;
return false;
}
//---------------------------------------------------------------------------
bool IsGameVersionValid(HANDLE hProc)
{
if(hProc==INVALID_HANDLE_VALUE)
return false;

DWORD ShouldBeEqualToGA;
ReadProcessMemory(hProc,(void*)BA,&ShouldBeEqualToGA,4,NULL);
ShouldBeEqualToGA+=0x1C;
return (ShouldBeEqualToGA==GA);
}
//---------------------------------------------------------------------------
BYTE InjectAndExecute(void* Func, void* Params)
{
DWORD pId;
if(!GetProcessId(&pId,"elementclient.exe"))
return INJECT_NO_PROCESS; // нет такого процесса

HANDLE hProc;
HANDLE hProcThread;
void* pFunction;
void* pParams;

hProc = OpenProcess(PROCESS_ALL_ACCESS,false,pId);
if(hProc==INVALID_HANDLE_VALUE) // не удалось открыть процесс
return INJECT_NO_ACCESS;

if(!IsGameVersionValid(hProc))
return INJECT_BAD_VERSION; // не та версия игры

pFunction = VirtualAllocEx(hProc,NULL,4096,MEM_COMMIT,PAGE_REA DWRITE);
pParams = VirtualAllocEx(hProc,NULL,256,MEM_COMMIT,PAGE_READ WRITE);
WriteProcessMemory(hProc,pFunction,Func,4096,NULL) ;
WriteProcessMemory(hProc,pParams,Params,256,NULL);

hProcThread = CreateRemoteThread(hProc,NULL,NULL,(LPTHREAD_START _ROUTINE)pFunction,pParams,NULL,NULL);
if(hProcThread==INVALID_HANDLE_VALUE) // не удалось создать поток
{
VirtualFreeEx(hProc,pFunction,4096,MEM_RELEASE);
VirtualFreeEx(hProc,pParams,256,MEM_RELEASE);
CloseHandle(hProc);
return INJECT_THREAD_FAIL;
}

WaitForSingleObject(hProcThread,INFINITE); // ожидаем завершения работы потока
CloseHandle(hProcThread); // освобождаем память

VirtualFreeEx(hProc,pFunction,4096,MEM_RELEASE);
VirtualFreeEx(hProc,pParams,256,MEM_RELEASE);
CloseHandle(hProc);
return INJECT_OK;
}
//---------------------------------------------------------------------------
void __stdcall Target_THREAD(DWORD* WorldIdentifier)
{
DWORD Id = *WorldIdentifier;
__asm
{
pushad
mov edx,0x00606A70
mov edi,Id
mov eax,dword ptr ds:[BA]
push edi
mov ecx,dword ptr ds:[eax+0x20]
add ecx,0xEC
call edx
popad
}
}
//---------------------------------------------------------------------------
void __stdcall Attack_THREAD()
{
__asm
{
pushad
mov edx,0x0044FE60
mov ecx,dword ptr ds:[BA]
mov ecx,dword ptr ds:[ecx+0x1C]
mov ecx,dword ptr ds:[ecx+0x20]
push -1
push 0
push 0
push 0
call edx
popad
}
}
//---------------------------------------------------------------------------
class CHostPlayer
{
public:
bool SelectSomething(DWORD GlobalId)
{
return (InjectAndExecute(&Target_THREAD,&GlobalId)==INJECT_OK);
}
bool AttackSelectedTarget()
{
return (InjectAndExecute(&Attack_THREAD,NULL)==INJECT_OK);
}
} *HostPlayer;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
HostPlayer->AttackSelectedTarget();
}
//---------------------------------------------------------------------------



"]На этом, пожалуй, всё, что я хотел донести до читателя. Если что-то не выходит, не нужно отчаиваться, Вы всегда можете задать вопрос в этой теме, и я постараюсь дать ответ. Не стоит думать, что приведённые здесь примеры являются идеальными/единственно верными; всё всегда зависит от Вашей фантазии и Ваших же знаний. Надеюсь, Вы узнали что-то новое для себя, потратив время на прочтение.
Статья предоставлена здесь сугубо в образовательных целях, я не призываю никого модифицировать/ломать клиент Perfect World.
Благодарности:
Dinmaite - за помощь в моих вопросах.
Читателю - за усердие при прочтении (ведь Вы дошли до этого места!).


27.09.2011 © BritishColonist
специально для zhyk.ru

Jok3r666
01.10.2011, 10:17
Все шикарно описано, наверно не мало времени угробил. Молодец!!!

~BUSTER~
12.10.2013, 00:54
Достаточно интересный материал, хотелось бы узнать где прочитать про такие системные функции как GetProcessId, OpenProcess, CloseHandlе, GetAsyncKeyState и другие. Дело в том, что большинство начальных учебников описывают лишь различные примитивные алгоритмы. Поэтому был бы рад инфе о более насыщеной книге.

Dinmaite
12.10.2013, 01:19
GetProcessId
[Ссылки могут видеть только зарегистрированные и активированные пользователи]