dwa83
01.05.2012, 21:02
Попробую собрать воедино разбросанную по форуму информацию и применить её к созданию конкретной простенькой программы-бота. Я не говорю в этой теме как НУЖНО делать, а всего лишь показываю, как МОЖНО сделать. Расчитана данная статья на программистов, использующих среду Borland C++ Builder(так как пример будет реализован именно в ней), но думаю что и другим пригодится. Всю статью разбил на несколько частей, так как объём получился не маленький, вероятность чего, кстати, ожидалась. Могут присутствовать какие-либо ошибки, так как я не гуру программирования. Замечания и указания на ошибки ожидаются.
АДРЕСА И ОФСЕТЫ СЛЕДУЕТ ЗАМЕНИТЬ НА НОВЫЕ
Вообще почти все действия бота совершаются в зависимости от полученных из клиента данных, например бот должен начать хилить себя, когда считанное из клиента значение ХП меньше определённого значения. Итак, для начала нам нужно научиться получать различные данные из клиента. Для этого понадобится напрямую работать памятью клиента игры. Чтобы работать с памятью нужно сначала получить доступ к ней, то есть открыть для работы процесс клиента. Для этих целей существует функция OpenProcess, которая открывает существующий объект процесса. Посмотрим на параметры, которые получает эта функция, и что она нам возвращает:
HANDLE OpenProcess(
DWORD dwDesiredAccess, // флажок доступа
BOOL bInheritHandle, // параметр дескриптора наследования
DWORD dwProcessId // идентификатор процесса
);
dwDesiredAccess Устанавливает уровень доступа к объекту процесса, мы будем использовать значение PROCESS_ALL_ACCESS, оно означает все возможные права доступа для объекта процесса.
bInheritHandle будем выставлять в false, это будет значить, что дескриптор не может наследоваться.
dwProcessId Идентификатор процесса, который открывается. Его нам ещё предстоит найти.
Возвращаемое значения типа HANDLE - дескриптор процесса, который мы открыли(если открытие не удалось, функция вернёт NULL).
Итак определим переменную hProc типа HANDLE и PID типа DWORD (PID - Process ID, идентификатор процесса, параметр, который имеет любой загруженный в память процесс).
HANDLE hProc;
DWORD PID;
Функция открытия теперь будет выглядеть так
hProc = OpenProcess(PROCESS_ALL_ACCESS,false,PID);
Всё бы ничего, но параметр PID нам не известен. Будем его искать. Нам известен заголовок окна процесса "Perfect World" и имя самого процесса "elementclient.exe". Будем искать PID по имени процесса. В этом нам поможет библиотека Tlhelp32.h, поэтому включим её в наш проект строчкой
#include <Tlhelp32.h>
Можно конечно обойтись и без неё, но не будем заморачиваться. Напишем функцию, которая будет возвращать PID процесса по его имени.
DWORD PIDByProcName(AnsiString ProcessName)
{
PROCESSENTRY32 ProcessEntry;
HANDLE pHandle;
DWORD pid;
pHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
ProcessEntry.dwSize = sizeof(ProcessEntry);
pid=0;
bool loop=Process32First(pHandle, &ProcessEntry);
while (loop)
{
AnsiString nm=ProcessEntry.szExeFile;
if (nm==ProcessName)
{
pid = ProcessEntry.th32ProcessID;
CloseHandle(pHandle);
break;
}
loop=Process32Next(pHandle, &ProcessEntry);
}
return pid;
}
Вообще нужно бы создать отдельный модуль с именем например Client и писать туда всё что касается именно работы с клиентом, для нормальной структуризации нашего проекта, и затем объявить этот модуль в главном модуле проекта. В нем описать класс CLIENT(или структуру, кому что по вкусу), и вышеописанную функцию прописать внутри класса, чтобы можно было в главном модуле указать например
CLIENT client;
и далее в программе вызвать функцию определения PID, например так
DWORD PID = client.PIDByProcName("elementclient.exe");
Вроде бы всё выглядит аккуратно и красиво.
Вообще давайте сделаем отступление(касательно программирования в среде с++ builder) и определим сразу свои действия. Определим сразу, какие структуры у нас будут в программе использоваться, и создадим для каждой свой модуль, в котором они будут описаны и реализованы, а потом мы будем подключать к проекту нужный модуль. Итак структуры:
BOT - структура будет содержать все действия, связанные с работой бота, в этой структуре будет указана структура типа CLIENT, тоесть к модулю Bot будет подключаться модуль Client, а уже модуль Bot будем включать в главный модуль проекта.
Позже реализуем структуру READER, которая будет отвечать за работу с памятью клиента. Основной её функцией, следуя из названия, будет считывание значений из клиента.
Если вы думаете, что так заморачиваться не стоит, то вы поменяете своё мнение, когда функциональность вашего бота солидно вырастет, и вы начнёте как котёнок искать концы в клубке своих же функций.
Модуль в среде с++ builder состоит из заголовочного файла *.h , в котором наш класс и его внутренности просто объявлены, а так же файла *.срр, в котором идёт уже реализация того, что мы описали в *.h. Нужно не забыть прописывать в файле *.cpp #include "*.h", для того, чтобы наша реализация "видела" объявление того, что собственно мы пишем(хотя при добавлении модуля это происходит автоматически). Итак давайте создадим новый проект(у вас наверное уже создан) и добавим к нему 2 новых модуля.
File->New->Unit
и сохраним их под именами Client и Bot.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
При добавлении модуля среда автоматически создаёт 2 одноимённых пустых(почти) файла с расширениями cpp и h, автоматически подключает их к проекту(но не связывает друг с другом инклудами), а так же сразу инклюдит в файле *.cpp (реализации модуля) файл *.h (заголовочный файл модуля, в нём будут только объявления функций/классов). Напоминаю, что переключения между файлом реализации и описания в редакторе происходит правым кликом по закладке модуля и выборе пункта Open source/header file. Также это можно сделать в нижней части окна редактора нажатием на закладки.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
В модуле Client в заголовочном файле Client.h опишем нашу структуру(далее мы будем её по надобности усложнять). Также не забываем включить сюда модули vcl.h и Tlhelp32.h
#include <vcl.h>
#include <Tlhelp32.h>
#ifndef ClientH
#define ClientH
struct CLIENT
{
DWORD pid; // Идентификатор Процесса
DWORD PIDByProcName(AnsiString ProcessName); // определение pid по имени процесса
};
#endif
Пока что такая простенькая структура. Переключимся на реализацию модуля(файл Client.cpp) и пишем там реализацию функции PIDByProcName.
#include "Client.h"
DWORD CLIENT::PIDByProcName(AnsiString ProcessName)
{
PROCESSENTRY32 ProcessEntry;
HANDLE pHandle;
DWORD pid;
pHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
ProcessEntry.dwSize = sizeof(ProcessEntry);
pid=0;
bool loop=Process32First(pHandle, &ProcessEntry);
while (loop)
{
AnsiString nm=ProcessEntry.szExeFile;
if (nm==ProcessName)
{
pid = ProcessEntry.th32ProcessID;
CloseHandle(pHandle);
break;
}
loop=Process32Next(pHandle, &ProcessEntry);
}
return pid;
}
Теперь переходим к модулю Bot и в Bot.h обьявляем структуру нашего бота. Внутри структуры объявляем структуру типа CLIENT. Также не забываем проверит, подключен ли Client.h, чтобы в этом модуле было известно, что представляет структура типа CLIENT.
#include "Client.h"
#ifndef BotH
#define BotH
// Структура бота
struct BOT
{
CLIENT client;
};
#endif
В файле реализации пока писать нечего, достаточно объявления, всё реализовано в других модулях.
Теперь у нас есть всё, чтобы открыть доступ к процессу. Давайте сначала напишем функцию инициализации, в которой будет происходить вся подготовка. Вызывать мы её будем при запуске бота. Переходим к объявлению структуры Client и описываем в структуре новую функцию Init().
struct CLIENT
{
DWORD pid; // Идентификатор Процесса
DWORD PIDByProcName(AnsiString ProcessName); // определение pid по имени процесса
void Init(); // инициализация
};
Перейдём к реализации в Client.cpp и добавим нашу функцию.
void CLIENT::Init()
{
pid=PIDByProcName("elementclient.exe");
}
На этом этапе можно проверить что у нас получилось. В главном модуле Unit1.cpp подключим модуль Bot, и обьявим экземпляр нашего бота.
#include "Bot.h"
BOT bot;
Затем кинем на форму два компонента, Button и Label. В событии создания формы вызовем функцию инициализации.
bot.client.Init(); // инициализация
В событии клика по кнопке напишем:
Label1->Caption=bot.client.pid; // выводим PID процесса клиента игры
Теперь запускаем PW и затем нашу программу. Нажмём на кнопку и мы должны будем увидеть PID процесса игры. Можно сравнить его со значением в диспетчере задач и убедиться в правильности. Как видим всё получилось правильно.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Теперь создадим ещё один модуль и сохраним под именем Reader. Он будет отвечать за считывание значений из клиента. В нём опишем и реализуем нашу структуру ридера. Перейдём к редактированию файла Reader.h
#include <vcl.h>
#ifndef ReaderH
#define ReaderH
struct READER
{
DWORD pid; // PID
};
#endif
Не забываем подключить vcl.h, иначе компилятор скажет "DWORD? не, не слышал". Пока что вот такая маленькая структура. Давайте "прикрутим" её к структуре Client. Добавим в неё ещё одну строчку.
READER get; // Ридер значений из клиента
В начале не забудем подключить новый созданный нами модуль. Строчку #include <vcl.h> можно убрать, так как она включена в Reader.h, который в свою очередь подключим здесь. Получилось так:
#include "Reader.h"
#include <Tlhelp32.h>
#ifndef ClientH
#define ClientH
struct CLIENT
{
public:
DWORD pid; // Идентификатор Процесса
READER get; // Ридер значений из клиента
DWORD PIDByProcName(AnsiString ProcessName); // определение pid по имени процесса
void Init(); // инициализация
};
#endif
Также давайте в функции инициализации отдадим ридеру копию значения PID. Он тоже будет его использовать.
void CLIENT::Init()
{
pid=PIDByProcName("elementclient.exe");
get.pid=pid;
}
Теперь будем учить нашу структуру "читальщика" памяти делать то, для чего она и нужна. Опишем в структуре ридера новую функцию, которая будет считывать четырёхбайтное значение, находящееся по определённому адресу. Обзовём её к примеру Read_32 и в качестве параметра будем давать ей адрес, по которому она считает значение и вернёт нам. Переходим к редактированию модуля Reader в описании структуры добавим обьявление функции
struct READER
{
DWORD pid; // PID
DWORD Read_32(DWORD addr);
};
Наконец то мы дошли, туда куда шли, а именно к открытию процесса и работе с ним. Пишем реализацию.
DWORD READER::Read_32(DWORD addr)
{
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ; // открываем процесс
DWORD value;
ReadProcessMemory(hProcess,(void*)addr,&value,4,0); // считываем значение по заданному адресу
CloseHandle(hProcess); // закроем процесс
return value; // вернём считанное значение
}
ReadProcessMemory считывает байты из памяти указанного процесса в указанное место
1 параметр - hProcess это хэндл процесса, который вернёт функция OpenProcess
2 параметр - это указатель на начало памяти, с которой считываются данные
3 параметр - это указатель на место, куда мы эти данные считываем
4 параметр - количество байт, которое нужно считать
5 параметр - здесь указывается переменная-счётчик, в которую будет помещаться количество байт реально считанных данных(будем считать, что у нас будет считываться без ошибок, потому его мы не указываем и провирять это количество не будем)
Теперь давайте проверим как наш ридер справляется со своей задачей, попробуем считать прямо с нулевого адреса, с адреса начала процесса
в событие ранее добавленной кнопки напишем для теста
bot.client.Init();
Label1->Caption=bot.client.get.Read_32(0);
Запустим и нажмём на кнопку. Как видим считалось какое то значение которое валяется по адресу самого начала памяти клиента.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Ну считали мы его, только нафиг оно нам нужно? Давайте считывать нужные значения, например HP нашего персонажа. Для этого мы должны знать адрес, по которому это значение лежит. Заглянем в темку РУОФФ Адреса и оффсеты ([Ссылки могут видеть только зарегистрированные и активированные пользователи]) на форуме и посмотрим что там есть. Находим там такую вот вещь
Структура перса (HostPlayer Struct)
BaseAdress +0x1C +0x34
+494 HP, dword /Жизненная сила/
Что же это такое? Это цепочка смещений до структуры персонажа в памяти клиента. Так как адрес начала структуры игрока меняется мы не можем просто указать адрес этой структуры и считать оттуда, но есть такие значения, которые постоянно находятся по одному и тому же адресу, такому как BaseAdress и GameAdress, а так же есть неизменные смещения от начала какой либо структуры до полей этой структуры.
Вот пример, у нас есть 2 структуры, одна является одним из полей другой и создаётся динамически, первая так же в программе создаётся динамически
str1
{
int value1;
int value2;
};
str2
{
int byaka1;
str1* byaka2;
};
str2* buka // обьявим в программе указатель buka на структуру типа str2 и далее динамически создадим
При динамическом выделении памяти по указалелю byaka2 на структуру типа str1, адрес по которому эта память выделится не будет всегда один и тот же, но мы сможем узнать этот адрес, посмотрев значение указателя byaka2. Он и будет содержать значение адреса. Итак допустим в нашей программе структура str2 обьявлена как
str2* buka;
Тоесть тоже создаётся динамически, НО указатель на эту структуру всегда на одном и том же месте от начала прогаммы, допустим по адресу BaseAdress. Допустим в нашей прграмме этот BaseAdress равен 0x8. Это значит что указатель на динамическую структуру buka всегда находится по адресу со смещением от начала программы на 0x8. Считываем оттуда значение(а тут лежит buka - указатель на начало структуры типа str2, по сути её адрес), получаем адрес структуры. В начале структуры лежит значение int byaka1, занимающее 4 байта, а дальше лежит указатель на ещё одну динамическую структуру byaka2, следовательно цепочка оффсетов будет выглядеть так - BaseAddres + 0x4. В этой динамической структуре в свою очередь находится обычное значение int value2, судя по структуре оно находится на 4 байта дальше от начала структуры(а в начале лежит int value1 которое занимает эти 4 байта). Следовательно чтобы добраться до значения value2 нужно пройтись по цепочке смещений BaseAdress + 0x4 + 0x4.
То есть считав значение по BaseAdress и прибавив к нему определённое(тоже неизменное) смещение, мы получим адрес начала какой либо структуры в памяти.
Допустим по адресу BaseAdress у нас лежит значение 0x111, мы считываем его, прибавляем следующее значение из цепочки 0x1C, получаем 0x111+0x1C=12D. Считываем значение по этому адресу, там лежит к примеру 0x222. К этому значению прибавляем следующее смещение из цепочки 0x34, 0x222 + 0x34 = 0x256. Теперь если считать по этому адресу значение, это значение будет представлять из себя адрес начала структура персонажа. Считаем и получим к примеру 0x333, это будет найденный адрес начала структуры перса. +494 - это смещение от начала этой структуры до того места, где лежит HP. Итак если взять адрес начала структуры перса (в нашем примере 0x333) и прибавить к нему смещение до ХП получим 0x333 + 0x494, получим уже адрес не начала структуры а места где в структуре лежит ХП. Можем взять его по этому адресу и считать.
Так как в структуре персонажа находится много значений, для каждого своё смещение, разделим задачу на 2 функции, первая будет возвращать адрес начала структуры перса. а вторая именно значение ХП. Обьявим эти 2 функции в нашем ридере. В Reader.h добавим в класс обьявления двух новых функций
DWORD PersStruct(); // возвращает адрес начала структуры перса
int myHP(); // возвращает значение ХП
Сделаем ещё одно короткое отступление. Для всех оффсетов давайте сделаем отдельный модуль, где их и укажем. Позже можно будет их вынести в файл и программно считывать оттуда. Это на случай, если после обновления клиента оффсеты поменяются, тогда не предётся перекомпилировать прогу а просто в файле указа ть новые значения.
Итак, опять же создадим новый модуль с именем Offsets и объявим в Offsets.h нужные значения.
#define BA 0xA571E0 // base adress
#define D_GA 0x1C // смещение до GA
#define PERS_STRUCT 0x34
#define MY_HP 0x494
подключим его в файле Reader.h и получим следующее
//---------------------------------------------------------------------------
#include <vcl.h>
#include "Offsets.h"
#ifndef ReaderH
#define ReaderH
struct READER
{
DWORD pid; // PID
DWORD Read_32(DWORD addr);
DWORD PersStruct();
int myHP();
};
#endif
Итак оффсеты видны в модуле ридера, продолжим написание двух новых функций. Мы их уже обьявили, теперь добавим их тела в Reader.cpp
DWORD READER::PersStruct()
{
DWORD buff;
buff = Read_32(BA);
buff = Read_32(buff+D_GA);
return Read_32(buff+PERS_STRUCT);
}
int READER::myHP()
{
return Read_32(PersStruct()+MY_HP);
}
Ну вот и всё, значения можно считывать. Для полноты давайте объявим и напишем ещё пару функций, например получения уровня и текущего значения MP.
Добавим в модуль с оффсетами 2 новых
#define MY_MP 0x498
#define MY_LEVEL 0x48C
а в модуль ридера 2 новые функции
int READER::myMP()
{
return Read_32(PersStruct()+MY_MP);
}
int READER::myLevel()
{
return Read_32(PersStruct()+MY_LEVEL);
}
Посмотрим на результат нашей работы
Разместим ещё 2 элемента Label на нашей форме а в обработчике кнопки перепишем
bot.client.Init();
Label1->Caption=bot.client.get.myHP();
Label2->Caption=bot.client.get.myMP();
Label3->Caption=bot.client.get.myLevel();
запустим PW зайдём за какого либо персонажа в игру, запустим нашу прогу, нажмём кнопку и проверим считанные значения.
Как видим всё считывается верно.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Теперь сделаем считывание данных какого-нибудь моба.
Всё по аналогии, но не совсем. Идём в тему с оффсетами и видим целых 2 цепочки смещений
BA +0x1C +0x1C +0x24 +0x14 Count, dword /Количество/
BA +0x1C +0x1C +0x24 +0x18 +(i*0x4) +0x4 /i = 0 - 0x300/
Mоб не один, как наш персонаж, их много, и в памяти клиента структуры мобов расположены массивом, причём этих массивов два, упорядоченный и неупорядоченный.
Эти две цепочки смещений и приводят нас к данным этих массивов. Мы будем использовать неупорядоченный. В упорядоченном количество записей меняется так как количество мобов вокруг меняется, и номер одного и того же моба в этом массиве может изменяться со временем. Это для некоторых не очень удобно. Но у несортированного массива своя фишка. Один и тот же моб всегда в нём занимает одно и то же положение по порядку. Массив расчитан на 768(0x300 hex) мобов, но ведь вокруг нет столько, поэтому некоторые места пустуют. Причём существующие мобы расположены не по порядку в нём, а могут быть раскиданны по всему массиву.
Итак берём вторую цепочку смещений, она для несортированного массива мобов.
Прочитав адреса по цепочке BA +0x1C +0x1C +0x24 +0x18 +(i*0x4) +0x4 мы получим адрес структуры моба с номером i. Если вместо адреса структуры мы получили 0, значит моба в этом месте массива нет и место пустует. Напишем в ридере новую функцию, которая будет возвращать адрес структуры моба по номеру(по аналогии с адресом структуры перса, но у перса номера не было.
Добавим в модуль с оффсетами смещения до структуры моба а так же смещения до некоторых параметров моба (BA +0x1C уже добавлены, 0x1C определено у нас как D_GA, это смещение до GameAdress, который кстати тоже всегда в одном месте. (i*0x4) и +0x4 не будем выносить в оффсеты, но вы можете это сделать)
// Смещения до структуры моба
#define M_D1 0x1C // первое смещение
#define M_D2 0x24 // второе смещение
// структура моба BA + D_GA + M_D1 + M_D2 +
#define M_STRUCT 0x18
// параметры моба M_STRUCT+
#define MOB_X 0x03C
#define MOB_Z 0x040
#define MOB_Y 0x044
#define MOB_TYPE 0x0B4
#define MOB_WID 0x120
#define MOB_DIST 0x284
Оффсеты добавили, возьмемся за функции. Перед классом ридера опишем вспомогательную структуру, которая будет представлять координаты. Перед обьявлением READER напишем
struct COORDS
{
float x;
float y;
float z;
};
Значения координат редко когда нужны по отдельности(разве что кроме высоты), поэтому будем объединять их в одну структуру. Далее пропишем нужные нам новые функции внутри класса ридера
DWORD MobStruct(int nom);
DWORD mobType(int nom);
DWORD mobWID(int nom);
float mobDist(int nom);
COORDS mobCoords(int nom);
и приступим к написанию их "внутренностей" в Reader.cpp
DWORD READER::MobStruct(int nom)
{
DWORD buff;
buff = Read_32(BA);
buff = Read_32(buff+D_GA);
buff = Read_32(buff+M_D1);
buff = Read_32(buff+M_D2);
buff = Read_32(buff+M_STRUCT);
buff = Read_32(buff+nom*0x4);
if (buff!=0) return Read_32(buff+0x4); // если значение не 0, значит этот моб существует, вернём адрес его структуры
return 0; //иначе вернём 0
}
DWORD READER::mobType(int nom)
{
DWORD buff=MobStruct(nom);
if (buff!=0) return Read_32(buff+MOB_TYPE); // если функция вернула не 0, значит моб существует, считываем его тип
return 0; // нет этого моба, вернём 0 вместо типа
}
DWORD READER::mobWID(int nom)
{
DWORD buff=MobStruct(nom);
if (buff!=0) return Read_32(buff+MOB_WID);
return 0;
}
Дистанция и координаты являются дробными значениями(float). Допишем в ридере ещё одну функцию Read_float по аналогии с Read_32
Вообще можно было воспользоваться Read_32 и возвращаемое значение DWORD просто преобразовать в float, но всё же напишем отдельную функцию для этого.
обьявим её в классе, а в реализации напишем
float READER::Read_float(DWORD addr)
{
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
float value;
ReadProcessMemory(hProcess,(void*)addr,&value,4,0);
CloseHandle(hProcess);
return value;
}
Продолжим писать наши функции считывания данных конкретного моба
float READER::mobDist(int nom)
{
DWORD buff=MobStruct(nom);
if (buff!=0) return Read_float(buff+MOB_DIST);
return 0;
}
COORDS READER::mobCoords(int nom)
{
COORDS value; value.x=0; value.y=0; value.z=0;
DWORD buff=MobStruct(nom);
if (buff!=0)
{
value.x = Read_float(buff+MOB_X);
value.y = Read_float(buff+MOB_Y);
value.z = Read_float(buff+MOB_Z);
}
return value;
}
Стоит сообщить о том, что NPC, чужие петы и свой вызванный пет тоже расположенны в этом же массиве и считаются "мобом" но у них значение типа разное.
Давайте инициализацию перенесём в событие создания окна, а вывод в Label зделаем внутри таймета с интервалом 200мс(чтобы не тыркать по кнопке), кнопку наверное можно удалить, если что добавим снова))
void __fastcall TForm1::FormCreate(TObject *Sender)
{
bot.client.Init();
Timer1->Enabled=true; // таймер изначально отключен. включается после инициализации
}
Теперь разместим на форме компонент Timer укажем ему интервал 200(по умолчанию отключим его, включим после инициализации) и в событии напишем поиск ближайшего моба и вывод его координат в добавленные ранее три компонента Label
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
int nom=-1; // номер ближайшего моба в массиве
float Dist=100000; // дистанция для сравнения, изначально установим в очень большую
for (int i=0; i<768; i++) // пройдёмся по всему списку мобов в клиенте, i - номер моба в списке
{
if ( bot.client.get.mobWID(i)!=0) // если место в списке занято мобом
{
if ( bot.client.get.mobType(i)==6) // если это действительно моб (6 - моб, 7 - NPC, 9 - пет)
{
if ( bot.client.get.mobDist(i)<Dist) // если дистанция меньше чем у предыдущего проверенного
{
Dist=bot.client.get.mobDist(i); // запомним дистанцию для следующего сравнения в списке
nom=i; // запомним номер моба в списке(если более близких мобов не будет, то это и будет номер ближайшего)
}
}
}
}
if (nom!=-1) // -1 будет только в том случае, когда вокруг вообще ни одного моба
{
COORDS crd = bot.client.get.mobCoords(nom);
Label1->Caption="x - " + FloatToStr(crd.x);
Label2->Caption="y - " + FloatToStr(crd.y);
Label3->Caption="z - " + FloatToStr(crd.z);
}
}
Запускаем игру, приходим персонажем в место, где водятся какие-нибудь безобидные мобы встаём рядом с одним из них и запускаем нашу прогу. Что мы видим?
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Ерунда какая-то? Координаты совсем не сходятся. На самом деле всё считалось верно. именно в таком виде координаты и хранятся в клиенте а возле миникарты отображаются преобразованными в привычный игроку вид. Формулы, по которым происходит преобразование известны и довольно просты. Давайте для себя напишем вспомогательные функции, которые преобразовывают клиентский вариант координат в игровые и обратно. Давайте поместим их в модуле Bot перед обьявлением класса бота. Вообще в файлах *.h реализация не пишется, ну да ладно, мы же не какие-нибудь Петры Митричевы.
// из клиентских в игровые
float GameX(float px){return(px+4000)/10;}
float GameY(float py){return(py+5500)/10;}
float GameZ(float pz){return pz/10;}
// из игровых в клиентские
float ProcX(float gx){return(gx*10)-4000;}
float ProcY(float gy){return(gy*10)-5500;}
float ProcZ(float gz){return gz*10;}
Немного перепишем в таймере вывод в Label
Label1->Caption="x - " + FloatToStr(GameX(crd.x));
Label2->Caption="y - " + FloatToStr(GameY(crd.y));
Label3->Caption="z - " + FloatToStr(GameZ(crd.z));
Запускаем, проверяем.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Ура, всё сходится. Ну почти(так как мы стоим не точно по координатам моба, да и клиент округляет координаты).
Думаю на этом раздел связанный со считыванием данных можно закончить.
В прошлом разделе мы учились считывать различные значения из клиента Perfect World. Но ведь ежу понятно, что на одном считывании данных бота не построишь, хоть целое ведро их насчитывай. Да и в самой игре их прекрасно видно. Нужно научить нашу прогу заставлять клиент выполнять какие-либо действия. Вот мы и подошли к инжектам.
Давайте попробуем понять, что же такое инжект. Инжект - это инъекция кода в сторонний процесс. Нам понадобится это для того, чтобы наш кусок кода работал прямо в клиенте. На примере выделения моба в таргет попробуем разобраться.
Гдето в недрах клиента зашита функция, которая выделяет моба в таргет. Она получает в качестве параметра WID моба. Что такое WID? Как помните из прошлого раздела, один из параметров, значение которого мы считывали, был WID моба. WID - это WorldID, то есть мировой идентификатор. Он есть во множестве структур, в структуре персонажа, мобов, лута и т.п. Представляет из себя число типа DWORD, которое уникально для всех обьектов в Perfect World, нет двух обьёктов, у которых был бы одинаковый WID. Если бросить из инвентаря два раза по монетке и умудриться попасть ими в одни и те же координаты, а затем поднимать, они не поднимутся вместе и считаются двумя разными кучками. Хоть и структуры в памяти у них одинаковые, но WID у них разный.
Пример: Допустим в клиенте есть функция
void Kill_a_kitten(int kitty)
{
Terminate(kitty);
}
а так же
CatUnderMouse(), которая возвращает WID элемента, по которому мы щёлкнули мышкой
и где-то в программе их вызовы
int cat=CatUnderMouse(); // получаем WID
Kill_a_kitten(cat); // вызываем функцию с параметром
Мы щёлкаем мышкой по мобу, клиент находит этого моба в списке, получает его WID и вызывает функцию выделения его в таргет. WID моба мы получаем програмно, поэтому без всяких щелчков можем передать этот параметр клиентской функции, и произойдёт выделение. Нам нужно вот этот кусок
int cat=CatUnderMouse(); // получаем WID
Kill_a_kitten(cat); // вызываем функцию с параметром
скопировать с клиента, модифицировать и запихать в клиент обратно. Под модификацией я предпологаю просто подставление нужного нам WID
int cat=WID; // получаем WID
Kill_a_kitten(cat); // вызываем функцию с параметром
(Но это как пример, а не реально существующая в клиенте функция.)
Как это сделать? Мы опять же получаем доступ к процессу клиента, выделяем в нём дополнительную память, копируем в эту память нашу копию куска клиентского кода в виде функции и вызываем её. Эта функция в свою очередь начинает выполняться в клиенте и вызывает уже клиентскую родную функцию (в примере - Kill_a_kitten) с нашим параметром, а не вычисленным внутри клиента. Но клиент ведь уже откомпилирован и кода мы посмотреть не можем? Можем, но только если декомпилируем и посмотрим его в командах ассемблера. Затем мы можем найти нужный нам кусок кода, из которого идёт вызов клиентской функции таргета, скопировать его, подставить свой параметр, в программе написать функцию с этим куском кода(ассемблерные вставки в коде программы ещё никто не отменял). Затем скопировать эту нашу функцию в клиент и заставить работать. Теперь следовало бы вам почитать вот эту темку от Dinmaite
"Поиск инжектов" или "Наш код в чужом процессе" ([Ссылки могут видеть только зарегистрированные и активированные пользователи])
Давайте уже вернёмся к нашему боту и оснастим его модулем, который будет инжектить различные функции в клиент
Создадим новый модуль и сохраним его под именем Injector. Опишем в нём структуру INJECTOR
// Структура инжектора
struct INJECTOR
{
DWORD pid; // Идентификатор Процесса
void* pFunction; // Указатель на память для функций
void* pParams; // Указатель на память для параметров
};
Далее подключим этот модуль к модулю Client и в классе CLIENT добавим новое поле типа INJECTOR
#include "Reader.h"
#include "Injector.h"
#include <Tlhelp32.h>
#ifndef ClientH
#define ClientH
struct CLIENT
{
public:
DWORD pid; // Идентификатор Процесса
READER get; // Ридер значений
INJECTOR inject; // Инжектор
DWORD PIDByProcName(AnsiString ProcessName); // определение pid по имени процесса
void Init(); // Инициализация
};
#endif
Далее в функции Init добавим ещё одну строчку, в которой мы запишем в структуру инжектора копию значения PID (это значение инжектору для работы тоже нужно)
void CLIENT::Init()
{
pid=PIDByProcName("elementclient.exe");
get.pid=pid;
inject.pid=pid;
}
Теперь давайте определимся вот с чем. Нам нужно будет выделять в клиенте память под наши функции. Можно поступить двумя способами.
1)Перед каждым инжектом выделять память, инжектить нашу функцию а после окончания её работы освобождать память.
2)Выделить достаточное количество памяти в клиенте один раз в начале работы нашей программы и не освобождать её пока бот работает.
Я буду использовать второй вариант, так как с первым у меня были проблемы. Спустя определённое время работы бота память переставала выделяться. Я правда не в курсе почему такое происходило, потому не стал разбираться и пошёл более простым путём. Итак, будем выделять память один раз, значит будем делать это при инициализации в функции Init класса CLIENT. Допишем в неё такие строчки
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
DWORD* pfunc = (DWORD*)VirtualAllocEx(hProcess,NULL,511,MEM_COMMI T,PAGE_READWRITE);
inject.pFunction = pfunc;
inject.pParams = pfunc+64;
CloseHandle(hProcess);
C первой строчкой всё понятно, мы это уже разбирали.
VirtualAllocEx выделяет память в процессе. Параметры такие
1 параметр - это дескриптор процесса, его мы знаем как получить
2 параметр - желательный стартовый адрес для выделение памяти, мы указываем NULL, тогда функция сама решит, где лучше выделить
3 параметр - размер выделяемой памяти в байтах. 511 байт нам будет больше чем достаточно
4 параметр - тип распределения памяти, указываем MEM_COMMIT, в этом случае память реально выделится и очистится от мусора
5 параметр - тип защиты памяти, указываем PAGE_READWRITE, этим мы говорим, что хотим получить доступ на чтение и запись
Функция возвращает указатель на начало выделенной памяти. Если вернёт NULL, значит выделение не удалось. Мы будем хранить этот указатель в переменной внутри класса INJECT. Далее мы будем использовать этот указатель чтобы копировать нашу функцию в клиент по этому адресу. Так же мы здесь определяем указатель на память выделенную под параметры нашей функции. Замечу здесь вот что. Я выделяю один кусок памяти и использую одну его часть для размещения функции а вторую часть для размещения параметров. Указатель на память для параметров я получаю так - к адресу начала выделенной памяти прибавляю 64 (64*DWORD=64*4=256) тоесть на 256 байт дальше от начала области памяти. Получается что в начале блока памяти будет распологаться функция, а ровно с середины этого блока параметры.
После выделения памяти и получения указателей мы закрываем хэндл процесса.
Добавим в структуру инжектора описание функции, которая будет инжектить в клиент различные функции(по сути сам инжектор и есть)
BYTE InjectAndExecute(void *Func, void* Params);
получает как параметры указатель на функцию, которую надо инжектить, и указатель на данные параметров для этой функции.
Затем напишем реализацию этой функции в Injector.cpp
BYTE INJECTOR::InjectAndExecute(void *Func, void* Params)
{
HANDLE hProcThread;
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
if (!hProcess) return 0; // выйдем из функции если не удалось открыть процесс
WriteProcessMemory(hProcess,pFunction,Func,250,NUL L);
WriteProcessMemory(hProcess,pParams,Params,250,NUL L);
hProcThread = CreateRemoteThread(hProcess,NULL,NULL,(LPTHREAD_ST ART_ROUTINE)pFunction,pParams,NULL,NULL);
if(hProcThread==INVALID_HANDLE_VALUE) return 0; // не удалось создать поток
WaitForSingleObject(hProcThread,INFINITE); // ожидаем завершения работы потока
CloseHandle(hProcThread); // закрываем хэндл нашего потока
CloseHandle(hProcess); // закрываем хэндл процесса
return 1; // успешная инъекция и выполнение кода
}
Опять же не забудем в файле Injector.h подключить модуль vcl.h, так же подключим модуль с оффсетами
#include "Offsets.h"
#include <vcl.h>
#ifndef InjectorH
#define InjectorH
#include <vcl.h>
#ifndef InjectorH
#define InjectorH
// Структура инжектора
struct INJECTOR
{
DWORD pid; // Идентификатор Процесса
void* pFunction; // Указатель на память для функций
void* pParam; // Указатель на память для параметров
BYTE InjectAndExecute(void *Func, void* Params);
};
#endif
Пояснения к новой функции
Сначала открываем процесс для работы, как мы уже делали. PID у нас давно найден и хранится в переменной. Затем мы в ранее уже выделенную память копируем инжектируемую функцию и в следующей строчке параметры для неё
WriteProcessMemory(hProcess,pFunction,Func,250,NUL L);
WriteProcessMemory(hProcess,pParams,Params,250,NUL L);
Мы копируем по 250 байт из места, где находится наша функция и так же параметры. Правда не указывая размеры этих данных мы так же скопируем хвостики, из не нужных нам данных, но пока не будем заморачиваться на счёт этого, далее мы модернизируем нашу функцию и избавимся от этого "недостатка".
WriteProcessMemory получает в качестве параметров хэндл процесса, в который будет писать, указатель на область памяти в которую будет писать, указатель на память откуда будет брать данные и размер этих данных.
Функция CreateRemoteThread создает поток, который запускается в виртуальном адресном пространстве другого процесса.
HANDLE CreateRemoteThread(
HANDLE hProcess, // дескриптор процесса
LPSECURITY_ATTRIBUTES lpThreadAttributes,// дескриптор защиты (SD)
SIZE_T dwStackSize, // размер начального стека
LPTHREAD_START_ROUTINE lpStartAddress, // функция потока
LPVOID lpParameter, // параметры функции потока
DWORD dwCreationFlags, // параметры создания
LPDWORD lpThreadId // идентификатор потока
);
Этой функцией ма создаём поток в котором будет выполняться наша функция. Любая программа может иметь несколько потоков, которые выполняются параллельно (ведь Винда - это многозадачная операционка). То есть у клиента есть основной поток, мы создаём в нём ещё один поток в котором выполняется наша функция и считается частью клиента. После создания потока код в нём выполнится, выполнит какие то действия и окончив свою работу сообщит о завершении. В строчке
WaitForSingleObject(hProcThread,INFINITE);
мы ожидаем завершение созданного нами потока после чего наша программа выполняется дальше.
Итак, инжектор функций у нас есть, но самой функции для инжекта нет. Давайте её создадим. Будем писать функцию таргета моба. Надеюсь указанную тему от Dinmaite вы читали. В файле Injector.cpp напишем нашу функцию. Просто отдельная функция не входящая ни в какую структуру.
void Target_THREAD(DWORD* WID)
{
DWORD Id = *WID;
__asm
{
MOV EDI, Id // Помещаем WID в регистр EDI
MOV EBX, 0x00630790 // Помещаем адрес клиентской функции таргета
MOV EAX,DWORD PTR DS:[BA] //
PUSH EDI // ; /Arg1
MOV ECX,DWORD PTR DS:[EAX+0x20] // ; |
ADD ECX,0x0EC // ; |
CALL EBX // ; \elementc.00606A70
}
}
Как видим здесь присутствует ассемблерная вставка, которая по сути является копией куска кода клиента, из которого вызывается клиентская функция выделения моба. Но WID подставляем мы сами.
Теперь добавим в структуру INJECTOR функцию, которая будет вызывать функцию-инжектор с нужными параметрами. Обьявим её в структуре инжектора
void TargetMob(DWORD wid);
и напишем реализацию
void INJECTOR::TargetMob(DWORD wid)
{
InjectAndExecute(&Target_THREAD,&wid);
}
Здесь функции-инжектору передаётся адрес нашей отдельной функции и адрес параметра для неё(WID). А далее функция-инжектор копирует её и параметры в выделенную в клиенте память и создаёт из неё поток. Наша функция выполнится в клиенте. Проверим работоспособность? Разместим на форме кнопку. При щелчке на ней бедет происходить выделение ближайшего моба. Изменим ранее написанный нами в таймере код, чтобы он получал не координаты а WID ближайшего моба. Из таймера уберём и вставим в кнопку.
DWORD wid=0; // WID ближайшего моба в массиве
float Dist=100000; // дистанция для сравнения, изначально установим в очень большую
for (int i=0; i<768; i++) // пройдёмся по всему списку мобов в клиенте, i - номер моба в списке
{
if ( bot.client.get.mobWID(i)!=0) // если место в списке занято мобом
{
if ( bot.client.get.mobType(i)==6) // если это действительно моб (6 - моб, 7 - NPC, 9 - пет)
{
if ( bot.client.get.mobDist(i)<Dist) // если дистанция меньше чем у предыдущего проверенного
{
Dist=bot.client.get.mobDist(i); // запомним дистанцию для следующего сравнения в списке
wid=bot.client.get.mobWID(i); // запомним WID моба в списке(если более близких мобов не будет, то это и будет WID ближайшего)
}
}
}
}
bot.client.inject.TargetMob(wid); // инжект выделения моба
Войдём в игру, придём на место где есть какие-нибудь мобы. Запустим нашу программу и нажмём кнопку. Если вылета клиента не случилось, значит мы всё сделали правильно и ближайший моб выделится. У меня всё нормально прошло, и после клика по кнопке моб выделился.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Итак, с инжектами мы разобрались. Давайте немного модернизируем наш инжектор. Сейчас у нас происходит инжект функции таргета, которая вместе с параметром записывается в адресное пространство клиента. В новом потоке она начинает работать берёт параметр находящийся в этом же куске памяти но на 256 байт дальше. Затем этот параметр использует в ассемблерной вставке, где происходит вызов клиентской функции. Давайте всё это упростим. Мы будем инжектить не целую функцию а уже готовый кусок кода с уже проставленными параметрами. Передавать параметр в функцию станет не нужно. Этот кусок кода будет представлять из себя просто массив чисел(ведь любая программа в памяти так и выглядит). Нам нужен всего лиш этот кусок кода
MOV EDI, Id // Помещаем WID в регистр EDI
MOV EBX, 0x00630790 // Помещаем адрес клиентской функции таргета
MOV EAX,DWORD PTR DS:[BA] //
PUSH EDI // ; /Arg1
MOV ECX,DWORD PTR DS:[EAX+0x20] // ; |
ADD ECX,0x0EC // ; |
CALL EBX // ; \elementc.00606A70
Нам нужно преобразовать его в набор цыфр и поместить в массив. Кстати, сюда нужно добавить последнюю строчку
RETN
Эта команда ассемблера производит возврат в место откуда этот кусок кода вызван. Аналогично return в функциях с++.
Для того чтобы узнать как этот кусок кода выглядит в цыфрах будем использовать программу OllyDbg. Я думаю она уже у вас есть.
Запустим Olly. Подключимся к любому процессу. Я для этого запустил паинт и подключился к нему. В меню File выбираем Attach.. и в списке выбираем например mspaint. В окне просмотра кода листаем в самый низ, видим память заполненную нулями. Будем ею пользоваться(надеюсь процесс мы не подвесим)).
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Теперь щёлкаем 2 раза по любой из этих строчек, появится окно редактирования кода процесса. Начинаем вводить строки из ассемблерного кода, указанного выше(или просто копировать их туда). После ввода первой сразу появится запрос следующей строчки кода. Вместо неизвестных параметров будем писать просто цыфры. Вместо Id - 11111111, вместо BA - 22222222. Хотя BA нам известно, но давайте вынесем на случай его дальнейшего изменения(хотя после обновлений клиента данный код тоже может измениться и придётся массив составлять заново). Итак что же у нас получилось после ввода
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
То что выделенно - это и есть наш код так сказать в числах. Выделим наши строчки, нажмём на них правой кнопкой, в появившемся меню выберем Edit> и далее Binary copy.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Так мы скопируем последовательность этих самых чисел. Вставим их пока что куда-нибудь в блокнот, пусть полежат. А мы займёмся функцией
void INJECTOR::TargetMob(DWORD wid)
{
InjectAndExecute(&Target_THREAD,&wid);
}
Обьявим в ней массив типа char и размером с количество байт нашего кода. Массив сразу инициируем нашими кодами, точнее не кодами а символами, которые имеют эти коды. Так как числа шестнадцатеричные, то нужно указывать перед ними x а так же символ \ который скажет, что это не число а символ, кодом которого является это число. То есть у символа \xBF - код BF(hex). Так же здесь определим переменную в которой укажем размер массива. Инициируем массив строчкой из наших символов(коды которых и являются этими числами).
char code[28]="\xBF\x11\x11\x11\x11\xBB\x90\x07\x63\x00\xA1\x22\x 22\x22\x22\x57\x8B\x48\x20\x81\xC1\xEC\x00\x00\x00 \xFF\xD3\xC3";
int len=28;
Теперь смотрим на массив и видим числа 11 11 11 11 и 22 22 22 22, которые мы указывали вместо неизвестных параметров. Первый - это WID, второй это BA. Давайте сразу эти числа запишем в массив.
DWORD ba=BA;
memcpy(code+1,&wid,4); // WID в массиве распологается начиная с первой позиции от начала(начало - нулевая позиция)
memcpy(code+11,&ba,4); // а BA начиная с 11ой
Функцию инжектора мы переделаем, и вызов её будет выглядеть так
InjectAndExecute(code,len);
Как параметры передаются указатель на массив нашего кода и его длинна.
Теперь наша функция выделения в таргет выглядит так
void INJECTOR::TargetMob(DWORD wid)
{
char code[28]="\xBF\x11\x11\x11\x11\xBB\x90\x07\x63\x00\xA1\x22\x 22\x22\x22\x57\x8B\x48\x20\x81\xC1\xEC\x00\x00\x00 \xFF\xD3\xC3";
DWORD ba=BA;
memcpy(code+1,&wid,4); // WID в массиве распологается начиная с первой позиции от начала(начало - нулевая позиция)
memcpy(code+11,&ba,4); // а BA начиная с 11ой
InjectAndExecute(code,28);
}
Функцию Target_THREAD можно выкинуть, она нам больше не нужна. Мы копируем в клиент не её, а массив кода(её аналог, но попроще).
Теперь переделаем инжектор так, чтобы он инжектил этот массив в клиент без всяких параметров, ведь параметры мы уже прописали прямо в код.
BYTE INJECTOR::InjectAndExecute(char* code, int len)
{
HANDLE hProcThread;
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
if (!hProcess) return 0;
WriteProcessMemory(hProcess,pFunction,code,len,NUL L);
//WriteProcessMemory(hProcess,pParams,Params,250,NUL L); параметры нам больше не нужны, удалим эту строчку
// pParam заменим на NULL, так как праметров у нас теперь нет
hProcThread = CreateRemoteThread(hProcess,NULL,NULL,(LPTHREAD_ST ART_ROUTINE)pFunction,NULL,NULL,NULL);
if(hProcThread==INVALID_HANDLE_VALUE) return 0;
WaitForSingleObject(hProcThread,INFINITE);
CloseHandle(hProcThread);
CloseHandle(hProcess);
return 1;
}
Давайте запустим нашу программу и попробуем нажать на кнопку на форме. Инжект прошёл успешно и ближайший моб удачно выделился.
Приведу некоторые другие инжекты
// Использование скилла по ID
void INJECTOR::Skill(DWORD id)
{
char fdata[39]="\x60\x6A\xFF\x6A\x00\x6A\x00\x68\x00\x00\x00\x00\x 8B\x0D\x00\x00\x00\x00\x8B\x89\x00\x00\x00\x00\x8B \x89\x00\x00\x00\x00\xB8\x00\x00\x00\x00\xFF\xD0\x 61\xC3";
DWORD func=F_SKILL;
DWORD ba=BA;
DWORD dga=D_GA;
DWORD ps=PERS_STRUCT;
memcpy(fdata+8,&id,4);
memcpy(fdata+14,&ba,4);
memcpy(fdata+20,&dga,4);
memcpy(fdata+26,&ps,4);
memcpy(fdata+31,&func,4);
InjectFunction(fdata,39);
}
// Перемещение в координаты
// самая длинная и страшная функция)))
void INJECTOR::Move(float x, float y, float z, int walkmode)
{
char fdata[117]="\x60\xA1\x00\x00\x00\x00\x8B\xB0\x11\x11\x11\x11\x 8B\x8E\x22\x22\x22\x22\x6A\x01\xBB\x33\x33\x33\x33 \xFF\xD3\x89\xC7\x8D\x44\xE4\x0C\x50\x68\x44\x44\x 44\x44\x89\xF9\xBB\x55\x55\x55\x55\xFF\xD3\x8B\x8E \x66\x66\x66\x66\x6A\x00\x6A\x01\x57\x6A\x01\xBB\x 77\x77\x77\x77\xFF\xD3\xA1\x88\x88\x88\x88\x8B\x80 \x99\x99\x99\x99\x8B\x80\xAA\xAA\xAA\xAA\x8B\x40\x 30\x8B\x48\x04\xB8\xBB\xBB\xBB\xBB\x89\x41\x20\xB8 \xCC\xCC\xCC\xCC\x89\x41\x24\xB8\xDD\xDD\xDD\xDD\x 89\x41\x28\x61\xC3";
DWORD func1=F_MOVE1;
DWORD func2=F_MOVE2;
DWORD func3=F_MOVE3;
DWORD ga=GA;
DWORD ps=PERS_STRUCT;
DWORD maa=MY_ACTION_ARRAY;
DWORD wmode=1; if (walkmode==0) wmode=0;
memcpy(fdata+2,&ga,4);
memcpy(fdata+8,&ps,4);
memcpy(fdata+14,&maa,4);
memcpy(fdata+21,&func1,4);
memcpy(fdata+35,&wmode,4);
memcpy(fdata+42,&func2,4);
memcpy(fdata+50,&maa,4);
memcpy(fdata+62,&func3,4);
memcpy(fdata+69,&ga,4);
memcpy(fdata+75,&ps,4);
memcpy(fdata+81,&maa,4);
memcpy(fdata+92,&x,4);
memcpy(fdata+100,&z,4);
memcpy(fdata+108,&y,4);
InjectFunction(fdata,117);
}
// Подбор лута/шахты по WID
void INJECTOR::GetLoot(DWORD wid, BYTE type)
{
char fdata[29]="\x60\x8B\x0D\x00\x00\x00\x00\x8B\x89\x11\x11\x11\x 11\x6A\x22\x68\x33\x33\x33\x33\xBB\x44\x44\x44\x44 \xFF\xD3\x61\xC3";
DWORD func=F_GET_LOOT;
DWORD ga=GA;
DWORD prs=PERS_STRUCT;
BYTE typ=0; if (type==2) typ=1;
memcpy(fdata+3,&ga,4);
memcpy(fdata+9,&prs,4);
fdata[14]=typ;
memcpy(fdata+16,&wid,4);
memcpy(fdata+21,&func,4);
InjectFunction(fdata,29);
}
// Открытие диалога с НПС по WID
void INJECTOR::OpenDialog(DWORD wid)
{
char fdata[30]="\x60\x8B\x15\x00\x00\x00\x00\x8B\x4A\x20\x68\x11\x 11\x11\x11\x81\xC1\xEC\x00\x00\x00\xBB\x22\x22\x22 \x22\xFF\xD3\x61\xC3";
DWORD func=F_DIALOG;
DWORD ba=BA;
memcpy(fdata+3,&ba,4);
memcpy(fdata+11,&wid,4);
memcpy(fdata+22,&func,4);
InjectFunction(fdata,30);
}
F_SKILL, F_MOVE1, F_MOVE2, F_MOVE3, F_GET_LOOT - это адреса соответствующих функций вызываемых в клиенте, их я вынес в модуль Offsets
Ну вот вроде бы с нижектами мы закончили.
В прошлом разделе мы разбирались с инжектами. Теперь давайте разберёмся с пакетами.
Сервер Perfect World взаимодействует с клиентом посредством пакетов, получает и отсылает их. Пакет это последовательность данных, получив которые сервер распознаёт их и в зависимости от того, что это за данные, делает какие то действия у себя. Вспомним к примеру нашу клиентскую функцию выделения моба. В результате её работы, в клиенте формируется пакет(последовательность чисел) определённой длинны, затем вызывается функция, которая этот пакет отправляет серверу. Сервер его получает, расшифровывает и видит что это пакет выделения моба с определённым WID. Выделяет его, и отсылает клиенту ответ, мол выделил. Клиент получает этот пакет и отображает выделение моба(ну и не только отображает). Так же как и различные внутренние функции клиента(как например таргет моба) мы можем использовать и функцию отправки пакета. Нужно передать ей как параметры адрес начала готового пакета и его длинну. Пакет мы готовим сами а не клиент. Итак, мы скопируем с клиента кусок ассемблерного кода, который вызывает клиентскую функцию отправки пакета. Будем подставлять в него свои параметры (такие как адрес готового пакета в памяти и его длинну) а потом будем его инжектить в клиент. Таким образом этот кусок кода, работая в клиенте, заставит клиентскую функцию отправки пакета отправить наш пакет серверу. Получается так что при инжекте функций действий пакет составлял сам клиент и отправлял его серверу. А при инжекте функции посылки пакета, пакет формируется нами пропуская использование клиентских функций, выполняющих какие то действия. В классе инжектора опишем новую функцию, которая будет отвечать за инжект кода, вызывающего клиентскую функцию отправки пакета с нашими параметрами. Напишем её реализацию. Не буду размусоливать и сразу укажу тут массив байт, который представляет собой кусок кода из клиента, вызывающего функцию отправки пакета.
// Инъекция и отправка пакета
BYTE INJECTOR::SendPacket(PACKET* pack)
{
HANDLE hProcThread;
char fdata[29]="\x60\x8B\x0D\x00\x00\x00\x00\x8B\x49\x20\x68\x11\x 11\x11\x11\x68\x22\x22\x22\x22\xB8\x33\x33\x33\x33 \xFF\xD0\x61\xC3";
int lenfunc=29;
DWORD func=F_SEND_PACKET;
DWORD ba=BA;
DWORD len=pack->len;
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
if (!hProcess) return 0;
WriteProcessMemory(hProcess,pParams,pack->Bytes,len,NULL); // инжектим данные пакета
DWORD addr=DWORD(pParams); // возьмём адрес расположения данных нашего пакета
memcpy(fdata+3,&ba,4);
memcpy(fdata+11,&len,4);
memcpy(fdata+16,&addr,4); // запишем адрес расположения пакета прямо в массив кода
memcpy(fdata+21,&func,4);
WriteProcessMemory(hProcess,pFunction,fdata,lenfun c,NULL); // инжектим наш код
hProcThread = CreateRemoteThread(hProcess,NULL,NULL,(LPTHREAD_ST ART_ROUTINE)pFunction,NULL,NULL,NULL);
if(hProcThread==INVALID_HANDLE_VALUE) // не удалось создать поток
{
CloseHandle(hProcess);
return 0;
}
WaitForSingleObject(hProcThread, INFINITE); // ожидаем завершения работы потока
CloseHandle(hProcThread); // освобождаем
CloseHandle(hProcess);
return 1; // успешная инъекция и выполнение кода
}
F_SEND_PACKET - адрес функции посылки пакета, адрес этот известен и указан в теме с Оффсетами.
Здесь мы инжектим кроме самой функции ещё и данные. Данные эти и будут являться пакетом. Опишем вспомогательную структуру пакета в начале модуля перед описанием структуры инжектора
// Структура пакета
struct PACKET
{
int len; // длина
BYTE Bytes[60]; // данные пакета
};
Функция SendPacket(PACKET* pack) получает указатель на структуру пакета, который мы подготовили для отправки.
Давайте перепишем нашу функцию выделения моба, чтобы выделение происходило отправкой пакета серверу.
Функция выглядела вот так
void INJECTOR::TargetMob(DWORD wid)
{
char code[28]="\xBF\x11\x11\x11\x11\xBB\x90\x07\x63\x00\xA1\x22\x 22\x22\x22\x57\x8B\x48\x20\x81\xC1\xEC\x00\x00\x00 \xFF\xD3\xC3";
int len=28;
DWORD ba=BA;
memcpy(code+1,&wid,4);
memcpy(code+11,&ba,4);
InjectAndExecute(code,len);
}
перепишем её
void INJECTOR::TargetMob(DWORD wid)
{
PACKET pack; // структура пакета
pack.len=6; // длина данных пакета
char Packet[6] = "\x02\x00\x01\x00\x00\x00"; // массив данных пакета
memcpy(pack.Bytes,Packet,pack.len); // скопируем в структуру пакета данные массива
memcpy(pack.Bytes+2,&wid,4); // последние четыре байта - WID выделяемого моба
SendPacket(&pack); // инжектим
}
Запускаем нашу программу, жмём на кнопочку и смотрим - ближайший моб выделился. Значит всё прошло хорошо, и пакет удачно отправился серверу.
На этом о пакетах всё. Укажу ещё некоторые функции, которые могут понадобиться для работы бота.
// Убрать таргет
void INJECTOR::TargetOff()
{
PACKET pack;
pack.len=2;
pack.Bytes[0]='\x08';
pack.Bytes[1]='\x00';
SendPacket(&pack);
}
// Использование скилла по ID. Нужно быть на расстоянии действия скилла
void INJECTOR::Skill2(DWORD skillid, DWORD mobwid)
{
PACKET pack;
pack.len=12;
char Packet[12] = "\x29\x00\x2B\x01\x00\x00\x00\x01\xF9\x16\x10\x80";
memcpy(pack.Bytes,Packet,pack.len);
memcpy(pack.Bytes+2,&skillid,4);
memcpy(pack.Bytes+8,&mobwid,4);
SendPacket(&pack);
}
// Включение/выключение полёта
void INJECTOR::Fly(DWORD FlyID)
{
PACKET pack;
pack.len=10;
char Packet[10] = "\x28\x00\x01\x01\x0C\x00\xF7\x31\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
memcpy(pack.Bytes+6,&FlyID,4);
SendPacket(&pack);
}
// Вызов пета из ячейки с номером..
void INJECTOR::CallPet(int nom)
{
PACKET pack;
pack.len=6;
char Packet[6] = "\x64\x00\x01\x00\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
pack.Bytes[2]=nom;
SendPacket(&pack);
}
// Использование предмета в инвентаре
void INJECTOR::UseItem(DWORD id, BYTE cell)
{
PACKET pack;
pack.len=10;
char Packet[10] = "\x28\x00\x00\x01\x0D\x00\xAD\x21\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
pack.Bytes[4] = cell; // номер ячейки
memcpy(pack.Bytes+6,&id,4);
SendPacket(&pack);
}
// Продажа из инвентаря
void INJECTOR::Sell(BYTE nomcell, DWORD kol, DWORD ID)
{
PACKET pack;
pack.len=26;
char Packet[26] = "\x25\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\x00\x 00\x00\xFF\xFF\x00\x00\xFF\x00\x00\x00\xD0\x07\x00 \x00";
memcpy(pack.Bytes,Packet,pack.len);
pack.Bytes[18] = nomcell; // 18 - номер ячейки
memcpy(pack.Bytes+22,&kol,4); // 22-25 - количество
memcpy(pack.Bytes+14,&ID,4); // 14-17 - ID предмета
SendPacket(&pack);
}
// Покупка у НПС
void INJECTOR::Buy(BYTE NPCcell, DWORD kol, DWORD ID)
{
PACKET pack;
pack.len=50;
char Packet[50] = "\x25\x00\x01\x00\x00\x00\x28\x00\x00\x00\x00\x00\x 00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x 00\xA5\x21\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
pack.Bytes[42] = NPCcell; // 30 - номер ячейки
memcpy(pack.Bytes+46,&kol,4); // 34-27 - количество
memcpy(pack.Bytes+38,&ID,4); // 26-29 - ID предмета
SendPacket(&pack);
}
// Обычная атака пета
void INJECTOR::PetAttack(DWORD wid)
{
PACKET pack;
pack.len=11;
char Packet[11] = "\x67\x00\x00\x00\x00\x80\x01\x00\x00\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
memcpy(pack.Bytes+2,&wid,4); // 4 байта, начиная с 2 - WID монстра
SendPacket(&pack);
}
// Ремонт у НПС
void INJECTOR::Repair()
{
PACKET pack;
pack.len=16;
char Packet[16] = "\x25\x00\x03\x00\x00\x00\x06\x00\x00\x00\xFF\xFF\x FF\xFF\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
SendPacket(&pack);
}
// Воскрешение в ближгород
void INJECTOR::Res()
{
PACKET pack;
pack.len=2;
char Packet[2] = "\x04\x00";
memcpy(pack.Bytes,Packet,pack.len);
SendPacket(&pack);
}
// Режим пета Охрана
void INJECTOR::PetGuard()
{
PACKET pack;
pack.len=14;
char Packet[14] = "\x67\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x 00\x00";
memcpy(pack.Bytes,Packet,pack.len);
SendPacket(&pack);
}
// Включение скилла, используемого петом автоматически, как активного
void INJECTOR::PetDefSkill(DWORD skillid)
{
PACKET pack;
pack.len=14;
char Packet[14] = "\x67\x00\x00\x00\x00\x00\x05\x00\x00\x00\xF1\x02\x 00\x00";
memcpy(pack.Bytes,Packet,pack.len);
memcpy(pack.Bytes+10,&skillid,4);
SendPacket(&pack);
}
Продолжение... ([Ссылки могут видеть только зарегистрированные и активированные пользователи])
АДРЕСА И ОФСЕТЫ СЛЕДУЕТ ЗАМЕНИТЬ НА НОВЫЕ
Вообще почти все действия бота совершаются в зависимости от полученных из клиента данных, например бот должен начать хилить себя, когда считанное из клиента значение ХП меньше определённого значения. Итак, для начала нам нужно научиться получать различные данные из клиента. Для этого понадобится напрямую работать памятью клиента игры. Чтобы работать с памятью нужно сначала получить доступ к ней, то есть открыть для работы процесс клиента. Для этих целей существует функция OpenProcess, которая открывает существующий объект процесса. Посмотрим на параметры, которые получает эта функция, и что она нам возвращает:
HANDLE OpenProcess(
DWORD dwDesiredAccess, // флажок доступа
BOOL bInheritHandle, // параметр дескриптора наследования
DWORD dwProcessId // идентификатор процесса
);
dwDesiredAccess Устанавливает уровень доступа к объекту процесса, мы будем использовать значение PROCESS_ALL_ACCESS, оно означает все возможные права доступа для объекта процесса.
bInheritHandle будем выставлять в false, это будет значить, что дескриптор не может наследоваться.
dwProcessId Идентификатор процесса, который открывается. Его нам ещё предстоит найти.
Возвращаемое значения типа HANDLE - дескриптор процесса, который мы открыли(если открытие не удалось, функция вернёт NULL).
Итак определим переменную hProc типа HANDLE и PID типа DWORD (PID - Process ID, идентификатор процесса, параметр, который имеет любой загруженный в память процесс).
HANDLE hProc;
DWORD PID;
Функция открытия теперь будет выглядеть так
hProc = OpenProcess(PROCESS_ALL_ACCESS,false,PID);
Всё бы ничего, но параметр PID нам не известен. Будем его искать. Нам известен заголовок окна процесса "Perfect World" и имя самого процесса "elementclient.exe". Будем искать PID по имени процесса. В этом нам поможет библиотека Tlhelp32.h, поэтому включим её в наш проект строчкой
#include <Tlhelp32.h>
Можно конечно обойтись и без неё, но не будем заморачиваться. Напишем функцию, которая будет возвращать PID процесса по его имени.
DWORD PIDByProcName(AnsiString ProcessName)
{
PROCESSENTRY32 ProcessEntry;
HANDLE pHandle;
DWORD pid;
pHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
ProcessEntry.dwSize = sizeof(ProcessEntry);
pid=0;
bool loop=Process32First(pHandle, &ProcessEntry);
while (loop)
{
AnsiString nm=ProcessEntry.szExeFile;
if (nm==ProcessName)
{
pid = ProcessEntry.th32ProcessID;
CloseHandle(pHandle);
break;
}
loop=Process32Next(pHandle, &ProcessEntry);
}
return pid;
}
Вообще нужно бы создать отдельный модуль с именем например Client и писать туда всё что касается именно работы с клиентом, для нормальной структуризации нашего проекта, и затем объявить этот модуль в главном модуле проекта. В нем описать класс CLIENT(или структуру, кому что по вкусу), и вышеописанную функцию прописать внутри класса, чтобы можно было в главном модуле указать например
CLIENT client;
и далее в программе вызвать функцию определения PID, например так
DWORD PID = client.PIDByProcName("elementclient.exe");
Вроде бы всё выглядит аккуратно и красиво.
Вообще давайте сделаем отступление(касательно программирования в среде с++ builder) и определим сразу свои действия. Определим сразу, какие структуры у нас будут в программе использоваться, и создадим для каждой свой модуль, в котором они будут описаны и реализованы, а потом мы будем подключать к проекту нужный модуль. Итак структуры:
BOT - структура будет содержать все действия, связанные с работой бота, в этой структуре будет указана структура типа CLIENT, тоесть к модулю Bot будет подключаться модуль Client, а уже модуль Bot будем включать в главный модуль проекта.
Позже реализуем структуру READER, которая будет отвечать за работу с памятью клиента. Основной её функцией, следуя из названия, будет считывание значений из клиента.
Если вы думаете, что так заморачиваться не стоит, то вы поменяете своё мнение, когда функциональность вашего бота солидно вырастет, и вы начнёте как котёнок искать концы в клубке своих же функций.
Модуль в среде с++ builder состоит из заголовочного файла *.h , в котором наш класс и его внутренности просто объявлены, а так же файла *.срр, в котором идёт уже реализация того, что мы описали в *.h. Нужно не забыть прописывать в файле *.cpp #include "*.h", для того, чтобы наша реализация "видела" объявление того, что собственно мы пишем(хотя при добавлении модуля это происходит автоматически). Итак давайте создадим новый проект(у вас наверное уже создан) и добавим к нему 2 новых модуля.
File->New->Unit
и сохраним их под именами Client и Bot.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
При добавлении модуля среда автоматически создаёт 2 одноимённых пустых(почти) файла с расширениями cpp и h, автоматически подключает их к проекту(но не связывает друг с другом инклудами), а так же сразу инклюдит в файле *.cpp (реализации модуля) файл *.h (заголовочный файл модуля, в нём будут только объявления функций/классов). Напоминаю, что переключения между файлом реализации и описания в редакторе происходит правым кликом по закладке модуля и выборе пункта Open source/header file. Также это можно сделать в нижней части окна редактора нажатием на закладки.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
В модуле Client в заголовочном файле Client.h опишем нашу структуру(далее мы будем её по надобности усложнять). Также не забываем включить сюда модули vcl.h и Tlhelp32.h
#include <vcl.h>
#include <Tlhelp32.h>
#ifndef ClientH
#define ClientH
struct CLIENT
{
DWORD pid; // Идентификатор Процесса
DWORD PIDByProcName(AnsiString ProcessName); // определение pid по имени процесса
};
#endif
Пока что такая простенькая структура. Переключимся на реализацию модуля(файл Client.cpp) и пишем там реализацию функции PIDByProcName.
#include "Client.h"
DWORD CLIENT::PIDByProcName(AnsiString ProcessName)
{
PROCESSENTRY32 ProcessEntry;
HANDLE pHandle;
DWORD pid;
pHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
ProcessEntry.dwSize = sizeof(ProcessEntry);
pid=0;
bool loop=Process32First(pHandle, &ProcessEntry);
while (loop)
{
AnsiString nm=ProcessEntry.szExeFile;
if (nm==ProcessName)
{
pid = ProcessEntry.th32ProcessID;
CloseHandle(pHandle);
break;
}
loop=Process32Next(pHandle, &ProcessEntry);
}
return pid;
}
Теперь переходим к модулю Bot и в Bot.h обьявляем структуру нашего бота. Внутри структуры объявляем структуру типа CLIENT. Также не забываем проверит, подключен ли Client.h, чтобы в этом модуле было известно, что представляет структура типа CLIENT.
#include "Client.h"
#ifndef BotH
#define BotH
// Структура бота
struct BOT
{
CLIENT client;
};
#endif
В файле реализации пока писать нечего, достаточно объявления, всё реализовано в других модулях.
Теперь у нас есть всё, чтобы открыть доступ к процессу. Давайте сначала напишем функцию инициализации, в которой будет происходить вся подготовка. Вызывать мы её будем при запуске бота. Переходим к объявлению структуры Client и описываем в структуре новую функцию Init().
struct CLIENT
{
DWORD pid; // Идентификатор Процесса
DWORD PIDByProcName(AnsiString ProcessName); // определение pid по имени процесса
void Init(); // инициализация
};
Перейдём к реализации в Client.cpp и добавим нашу функцию.
void CLIENT::Init()
{
pid=PIDByProcName("elementclient.exe");
}
На этом этапе можно проверить что у нас получилось. В главном модуле Unit1.cpp подключим модуль Bot, и обьявим экземпляр нашего бота.
#include "Bot.h"
BOT bot;
Затем кинем на форму два компонента, Button и Label. В событии создания формы вызовем функцию инициализации.
bot.client.Init(); // инициализация
В событии клика по кнопке напишем:
Label1->Caption=bot.client.pid; // выводим PID процесса клиента игры
Теперь запускаем PW и затем нашу программу. Нажмём на кнопку и мы должны будем увидеть PID процесса игры. Можно сравнить его со значением в диспетчере задач и убедиться в правильности. Как видим всё получилось правильно.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Теперь создадим ещё один модуль и сохраним под именем Reader. Он будет отвечать за считывание значений из клиента. В нём опишем и реализуем нашу структуру ридера. Перейдём к редактированию файла Reader.h
#include <vcl.h>
#ifndef ReaderH
#define ReaderH
struct READER
{
DWORD pid; // PID
};
#endif
Не забываем подключить vcl.h, иначе компилятор скажет "DWORD? не, не слышал". Пока что вот такая маленькая структура. Давайте "прикрутим" её к структуре Client. Добавим в неё ещё одну строчку.
READER get; // Ридер значений из клиента
В начале не забудем подключить новый созданный нами модуль. Строчку #include <vcl.h> можно убрать, так как она включена в Reader.h, который в свою очередь подключим здесь. Получилось так:
#include "Reader.h"
#include <Tlhelp32.h>
#ifndef ClientH
#define ClientH
struct CLIENT
{
public:
DWORD pid; // Идентификатор Процесса
READER get; // Ридер значений из клиента
DWORD PIDByProcName(AnsiString ProcessName); // определение pid по имени процесса
void Init(); // инициализация
};
#endif
Также давайте в функции инициализации отдадим ридеру копию значения PID. Он тоже будет его использовать.
void CLIENT::Init()
{
pid=PIDByProcName("elementclient.exe");
get.pid=pid;
}
Теперь будем учить нашу структуру "читальщика" памяти делать то, для чего она и нужна. Опишем в структуре ридера новую функцию, которая будет считывать четырёхбайтное значение, находящееся по определённому адресу. Обзовём её к примеру Read_32 и в качестве параметра будем давать ей адрес, по которому она считает значение и вернёт нам. Переходим к редактированию модуля Reader в описании структуры добавим обьявление функции
struct READER
{
DWORD pid; // PID
DWORD Read_32(DWORD addr);
};
Наконец то мы дошли, туда куда шли, а именно к открытию процесса и работе с ним. Пишем реализацию.
DWORD READER::Read_32(DWORD addr)
{
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ; // открываем процесс
DWORD value;
ReadProcessMemory(hProcess,(void*)addr,&value,4,0); // считываем значение по заданному адресу
CloseHandle(hProcess); // закроем процесс
return value; // вернём считанное значение
}
ReadProcessMemory считывает байты из памяти указанного процесса в указанное место
1 параметр - hProcess это хэндл процесса, который вернёт функция OpenProcess
2 параметр - это указатель на начало памяти, с которой считываются данные
3 параметр - это указатель на место, куда мы эти данные считываем
4 параметр - количество байт, которое нужно считать
5 параметр - здесь указывается переменная-счётчик, в которую будет помещаться количество байт реально считанных данных(будем считать, что у нас будет считываться без ошибок, потому его мы не указываем и провирять это количество не будем)
Теперь давайте проверим как наш ридер справляется со своей задачей, попробуем считать прямо с нулевого адреса, с адреса начала процесса
в событие ранее добавленной кнопки напишем для теста
bot.client.Init();
Label1->Caption=bot.client.get.Read_32(0);
Запустим и нажмём на кнопку. Как видим считалось какое то значение которое валяется по адресу самого начала памяти клиента.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Ну считали мы его, только нафиг оно нам нужно? Давайте считывать нужные значения, например HP нашего персонажа. Для этого мы должны знать адрес, по которому это значение лежит. Заглянем в темку РУОФФ Адреса и оффсеты ([Ссылки могут видеть только зарегистрированные и активированные пользователи]) на форуме и посмотрим что там есть. Находим там такую вот вещь
Структура перса (HostPlayer Struct)
BaseAdress +0x1C +0x34
+494 HP, dword /Жизненная сила/
Что же это такое? Это цепочка смещений до структуры персонажа в памяти клиента. Так как адрес начала структуры игрока меняется мы не можем просто указать адрес этой структуры и считать оттуда, но есть такие значения, которые постоянно находятся по одному и тому же адресу, такому как BaseAdress и GameAdress, а так же есть неизменные смещения от начала какой либо структуры до полей этой структуры.
Вот пример, у нас есть 2 структуры, одна является одним из полей другой и создаётся динамически, первая так же в программе создаётся динамически
str1
{
int value1;
int value2;
};
str2
{
int byaka1;
str1* byaka2;
};
str2* buka // обьявим в программе указатель buka на структуру типа str2 и далее динамически создадим
При динамическом выделении памяти по указалелю byaka2 на структуру типа str1, адрес по которому эта память выделится не будет всегда один и тот же, но мы сможем узнать этот адрес, посмотрев значение указателя byaka2. Он и будет содержать значение адреса. Итак допустим в нашей программе структура str2 обьявлена как
str2* buka;
Тоесть тоже создаётся динамически, НО указатель на эту структуру всегда на одном и том же месте от начала прогаммы, допустим по адресу BaseAdress. Допустим в нашей прграмме этот BaseAdress равен 0x8. Это значит что указатель на динамическую структуру buka всегда находится по адресу со смещением от начала программы на 0x8. Считываем оттуда значение(а тут лежит buka - указатель на начало структуры типа str2, по сути её адрес), получаем адрес структуры. В начале структуры лежит значение int byaka1, занимающее 4 байта, а дальше лежит указатель на ещё одну динамическую структуру byaka2, следовательно цепочка оффсетов будет выглядеть так - BaseAddres + 0x4. В этой динамической структуре в свою очередь находится обычное значение int value2, судя по структуре оно находится на 4 байта дальше от начала структуры(а в начале лежит int value1 которое занимает эти 4 байта). Следовательно чтобы добраться до значения value2 нужно пройтись по цепочке смещений BaseAdress + 0x4 + 0x4.
То есть считав значение по BaseAdress и прибавив к нему определённое(тоже неизменное) смещение, мы получим адрес начала какой либо структуры в памяти.
Допустим по адресу BaseAdress у нас лежит значение 0x111, мы считываем его, прибавляем следующее значение из цепочки 0x1C, получаем 0x111+0x1C=12D. Считываем значение по этому адресу, там лежит к примеру 0x222. К этому значению прибавляем следующее смещение из цепочки 0x34, 0x222 + 0x34 = 0x256. Теперь если считать по этому адресу значение, это значение будет представлять из себя адрес начала структура персонажа. Считаем и получим к примеру 0x333, это будет найденный адрес начала структуры перса. +494 - это смещение от начала этой структуры до того места, где лежит HP. Итак если взять адрес начала структуры перса (в нашем примере 0x333) и прибавить к нему смещение до ХП получим 0x333 + 0x494, получим уже адрес не начала структуры а места где в структуре лежит ХП. Можем взять его по этому адресу и считать.
Так как в структуре персонажа находится много значений, для каждого своё смещение, разделим задачу на 2 функции, первая будет возвращать адрес начала структуры перса. а вторая именно значение ХП. Обьявим эти 2 функции в нашем ридере. В Reader.h добавим в класс обьявления двух новых функций
DWORD PersStruct(); // возвращает адрес начала структуры перса
int myHP(); // возвращает значение ХП
Сделаем ещё одно короткое отступление. Для всех оффсетов давайте сделаем отдельный модуль, где их и укажем. Позже можно будет их вынести в файл и программно считывать оттуда. Это на случай, если после обновления клиента оффсеты поменяются, тогда не предётся перекомпилировать прогу а просто в файле указа ть новые значения.
Итак, опять же создадим новый модуль с именем Offsets и объявим в Offsets.h нужные значения.
#define BA 0xA571E0 // base adress
#define D_GA 0x1C // смещение до GA
#define PERS_STRUCT 0x34
#define MY_HP 0x494
подключим его в файле Reader.h и получим следующее
//---------------------------------------------------------------------------
#include <vcl.h>
#include "Offsets.h"
#ifndef ReaderH
#define ReaderH
struct READER
{
DWORD pid; // PID
DWORD Read_32(DWORD addr);
DWORD PersStruct();
int myHP();
};
#endif
Итак оффсеты видны в модуле ридера, продолжим написание двух новых функций. Мы их уже обьявили, теперь добавим их тела в Reader.cpp
DWORD READER::PersStruct()
{
DWORD buff;
buff = Read_32(BA);
buff = Read_32(buff+D_GA);
return Read_32(buff+PERS_STRUCT);
}
int READER::myHP()
{
return Read_32(PersStruct()+MY_HP);
}
Ну вот и всё, значения можно считывать. Для полноты давайте объявим и напишем ещё пару функций, например получения уровня и текущего значения MP.
Добавим в модуль с оффсетами 2 новых
#define MY_MP 0x498
#define MY_LEVEL 0x48C
а в модуль ридера 2 новые функции
int READER::myMP()
{
return Read_32(PersStruct()+MY_MP);
}
int READER::myLevel()
{
return Read_32(PersStruct()+MY_LEVEL);
}
Посмотрим на результат нашей работы
Разместим ещё 2 элемента Label на нашей форме а в обработчике кнопки перепишем
bot.client.Init();
Label1->Caption=bot.client.get.myHP();
Label2->Caption=bot.client.get.myMP();
Label3->Caption=bot.client.get.myLevel();
запустим PW зайдём за какого либо персонажа в игру, запустим нашу прогу, нажмём кнопку и проверим считанные значения.
Как видим всё считывается верно.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Теперь сделаем считывание данных какого-нибудь моба.
Всё по аналогии, но не совсем. Идём в тему с оффсетами и видим целых 2 цепочки смещений
BA +0x1C +0x1C +0x24 +0x14 Count, dword /Количество/
BA +0x1C +0x1C +0x24 +0x18 +(i*0x4) +0x4 /i = 0 - 0x300/
Mоб не один, как наш персонаж, их много, и в памяти клиента структуры мобов расположены массивом, причём этих массивов два, упорядоченный и неупорядоченный.
Эти две цепочки смещений и приводят нас к данным этих массивов. Мы будем использовать неупорядоченный. В упорядоченном количество записей меняется так как количество мобов вокруг меняется, и номер одного и того же моба в этом массиве может изменяться со временем. Это для некоторых не очень удобно. Но у несортированного массива своя фишка. Один и тот же моб всегда в нём занимает одно и то же положение по порядку. Массив расчитан на 768(0x300 hex) мобов, но ведь вокруг нет столько, поэтому некоторые места пустуют. Причём существующие мобы расположены не по порядку в нём, а могут быть раскиданны по всему массиву.
Итак берём вторую цепочку смещений, она для несортированного массива мобов.
Прочитав адреса по цепочке BA +0x1C +0x1C +0x24 +0x18 +(i*0x4) +0x4 мы получим адрес структуры моба с номером i. Если вместо адреса структуры мы получили 0, значит моба в этом месте массива нет и место пустует. Напишем в ридере новую функцию, которая будет возвращать адрес структуры моба по номеру(по аналогии с адресом структуры перса, но у перса номера не было.
Добавим в модуль с оффсетами смещения до структуры моба а так же смещения до некоторых параметров моба (BA +0x1C уже добавлены, 0x1C определено у нас как D_GA, это смещение до GameAdress, который кстати тоже всегда в одном месте. (i*0x4) и +0x4 не будем выносить в оффсеты, но вы можете это сделать)
// Смещения до структуры моба
#define M_D1 0x1C // первое смещение
#define M_D2 0x24 // второе смещение
// структура моба BA + D_GA + M_D1 + M_D2 +
#define M_STRUCT 0x18
// параметры моба M_STRUCT+
#define MOB_X 0x03C
#define MOB_Z 0x040
#define MOB_Y 0x044
#define MOB_TYPE 0x0B4
#define MOB_WID 0x120
#define MOB_DIST 0x284
Оффсеты добавили, возьмемся за функции. Перед классом ридера опишем вспомогательную структуру, которая будет представлять координаты. Перед обьявлением READER напишем
struct COORDS
{
float x;
float y;
float z;
};
Значения координат редко когда нужны по отдельности(разве что кроме высоты), поэтому будем объединять их в одну структуру. Далее пропишем нужные нам новые функции внутри класса ридера
DWORD MobStruct(int nom);
DWORD mobType(int nom);
DWORD mobWID(int nom);
float mobDist(int nom);
COORDS mobCoords(int nom);
и приступим к написанию их "внутренностей" в Reader.cpp
DWORD READER::MobStruct(int nom)
{
DWORD buff;
buff = Read_32(BA);
buff = Read_32(buff+D_GA);
buff = Read_32(buff+M_D1);
buff = Read_32(buff+M_D2);
buff = Read_32(buff+M_STRUCT);
buff = Read_32(buff+nom*0x4);
if (buff!=0) return Read_32(buff+0x4); // если значение не 0, значит этот моб существует, вернём адрес его структуры
return 0; //иначе вернём 0
}
DWORD READER::mobType(int nom)
{
DWORD buff=MobStruct(nom);
if (buff!=0) return Read_32(buff+MOB_TYPE); // если функция вернула не 0, значит моб существует, считываем его тип
return 0; // нет этого моба, вернём 0 вместо типа
}
DWORD READER::mobWID(int nom)
{
DWORD buff=MobStruct(nom);
if (buff!=0) return Read_32(buff+MOB_WID);
return 0;
}
Дистанция и координаты являются дробными значениями(float). Допишем в ридере ещё одну функцию Read_float по аналогии с Read_32
Вообще можно было воспользоваться Read_32 и возвращаемое значение DWORD просто преобразовать в float, но всё же напишем отдельную функцию для этого.
обьявим её в классе, а в реализации напишем
float READER::Read_float(DWORD addr)
{
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
float value;
ReadProcessMemory(hProcess,(void*)addr,&value,4,0);
CloseHandle(hProcess);
return value;
}
Продолжим писать наши функции считывания данных конкретного моба
float READER::mobDist(int nom)
{
DWORD buff=MobStruct(nom);
if (buff!=0) return Read_float(buff+MOB_DIST);
return 0;
}
COORDS READER::mobCoords(int nom)
{
COORDS value; value.x=0; value.y=0; value.z=0;
DWORD buff=MobStruct(nom);
if (buff!=0)
{
value.x = Read_float(buff+MOB_X);
value.y = Read_float(buff+MOB_Y);
value.z = Read_float(buff+MOB_Z);
}
return value;
}
Стоит сообщить о том, что NPC, чужие петы и свой вызванный пет тоже расположенны в этом же массиве и считаются "мобом" но у них значение типа разное.
Давайте инициализацию перенесём в событие создания окна, а вывод в Label зделаем внутри таймета с интервалом 200мс(чтобы не тыркать по кнопке), кнопку наверное можно удалить, если что добавим снова))
void __fastcall TForm1::FormCreate(TObject *Sender)
{
bot.client.Init();
Timer1->Enabled=true; // таймер изначально отключен. включается после инициализации
}
Теперь разместим на форме компонент Timer укажем ему интервал 200(по умолчанию отключим его, включим после инициализации) и в событии напишем поиск ближайшего моба и вывод его координат в добавленные ранее три компонента Label
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
int nom=-1; // номер ближайшего моба в массиве
float Dist=100000; // дистанция для сравнения, изначально установим в очень большую
for (int i=0; i<768; i++) // пройдёмся по всему списку мобов в клиенте, i - номер моба в списке
{
if ( bot.client.get.mobWID(i)!=0) // если место в списке занято мобом
{
if ( bot.client.get.mobType(i)==6) // если это действительно моб (6 - моб, 7 - NPC, 9 - пет)
{
if ( bot.client.get.mobDist(i)<Dist) // если дистанция меньше чем у предыдущего проверенного
{
Dist=bot.client.get.mobDist(i); // запомним дистанцию для следующего сравнения в списке
nom=i; // запомним номер моба в списке(если более близких мобов не будет, то это и будет номер ближайшего)
}
}
}
}
if (nom!=-1) // -1 будет только в том случае, когда вокруг вообще ни одного моба
{
COORDS crd = bot.client.get.mobCoords(nom);
Label1->Caption="x - " + FloatToStr(crd.x);
Label2->Caption="y - " + FloatToStr(crd.y);
Label3->Caption="z - " + FloatToStr(crd.z);
}
}
Запускаем игру, приходим персонажем в место, где водятся какие-нибудь безобидные мобы встаём рядом с одним из них и запускаем нашу прогу. Что мы видим?
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Ерунда какая-то? Координаты совсем не сходятся. На самом деле всё считалось верно. именно в таком виде координаты и хранятся в клиенте а возле миникарты отображаются преобразованными в привычный игроку вид. Формулы, по которым происходит преобразование известны и довольно просты. Давайте для себя напишем вспомогательные функции, которые преобразовывают клиентский вариант координат в игровые и обратно. Давайте поместим их в модуле Bot перед обьявлением класса бота. Вообще в файлах *.h реализация не пишется, ну да ладно, мы же не какие-нибудь Петры Митричевы.
// из клиентских в игровые
float GameX(float px){return(px+4000)/10;}
float GameY(float py){return(py+5500)/10;}
float GameZ(float pz){return pz/10;}
// из игровых в клиентские
float ProcX(float gx){return(gx*10)-4000;}
float ProcY(float gy){return(gy*10)-5500;}
float ProcZ(float gz){return gz*10;}
Немного перепишем в таймере вывод в Label
Label1->Caption="x - " + FloatToStr(GameX(crd.x));
Label2->Caption="y - " + FloatToStr(GameY(crd.y));
Label3->Caption="z - " + FloatToStr(GameZ(crd.z));
Запускаем, проверяем.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Ура, всё сходится. Ну почти(так как мы стоим не точно по координатам моба, да и клиент округляет координаты).
Думаю на этом раздел связанный со считыванием данных можно закончить.
В прошлом разделе мы учились считывать различные значения из клиента Perfect World. Но ведь ежу понятно, что на одном считывании данных бота не построишь, хоть целое ведро их насчитывай. Да и в самой игре их прекрасно видно. Нужно научить нашу прогу заставлять клиент выполнять какие-либо действия. Вот мы и подошли к инжектам.
Давайте попробуем понять, что же такое инжект. Инжект - это инъекция кода в сторонний процесс. Нам понадобится это для того, чтобы наш кусок кода работал прямо в клиенте. На примере выделения моба в таргет попробуем разобраться.
Гдето в недрах клиента зашита функция, которая выделяет моба в таргет. Она получает в качестве параметра WID моба. Что такое WID? Как помните из прошлого раздела, один из параметров, значение которого мы считывали, был WID моба. WID - это WorldID, то есть мировой идентификатор. Он есть во множестве структур, в структуре персонажа, мобов, лута и т.п. Представляет из себя число типа DWORD, которое уникально для всех обьектов в Perfect World, нет двух обьёктов, у которых был бы одинаковый WID. Если бросить из инвентаря два раза по монетке и умудриться попасть ими в одни и те же координаты, а затем поднимать, они не поднимутся вместе и считаются двумя разными кучками. Хоть и структуры в памяти у них одинаковые, но WID у них разный.
Пример: Допустим в клиенте есть функция
void Kill_a_kitten(int kitty)
{
Terminate(kitty);
}
а так же
CatUnderMouse(), которая возвращает WID элемента, по которому мы щёлкнули мышкой
и где-то в программе их вызовы
int cat=CatUnderMouse(); // получаем WID
Kill_a_kitten(cat); // вызываем функцию с параметром
Мы щёлкаем мышкой по мобу, клиент находит этого моба в списке, получает его WID и вызывает функцию выделения его в таргет. WID моба мы получаем програмно, поэтому без всяких щелчков можем передать этот параметр клиентской функции, и произойдёт выделение. Нам нужно вот этот кусок
int cat=CatUnderMouse(); // получаем WID
Kill_a_kitten(cat); // вызываем функцию с параметром
скопировать с клиента, модифицировать и запихать в клиент обратно. Под модификацией я предпологаю просто подставление нужного нам WID
int cat=WID; // получаем WID
Kill_a_kitten(cat); // вызываем функцию с параметром
(Но это как пример, а не реально существующая в клиенте функция.)
Как это сделать? Мы опять же получаем доступ к процессу клиента, выделяем в нём дополнительную память, копируем в эту память нашу копию куска клиентского кода в виде функции и вызываем её. Эта функция в свою очередь начинает выполняться в клиенте и вызывает уже клиентскую родную функцию (в примере - Kill_a_kitten) с нашим параметром, а не вычисленным внутри клиента. Но клиент ведь уже откомпилирован и кода мы посмотреть не можем? Можем, но только если декомпилируем и посмотрим его в командах ассемблера. Затем мы можем найти нужный нам кусок кода, из которого идёт вызов клиентской функции таргета, скопировать его, подставить свой параметр, в программе написать функцию с этим куском кода(ассемблерные вставки в коде программы ещё никто не отменял). Затем скопировать эту нашу функцию в клиент и заставить работать. Теперь следовало бы вам почитать вот эту темку от Dinmaite
"Поиск инжектов" или "Наш код в чужом процессе" ([Ссылки могут видеть только зарегистрированные и активированные пользователи])
Давайте уже вернёмся к нашему боту и оснастим его модулем, который будет инжектить различные функции в клиент
Создадим новый модуль и сохраним его под именем Injector. Опишем в нём структуру INJECTOR
// Структура инжектора
struct INJECTOR
{
DWORD pid; // Идентификатор Процесса
void* pFunction; // Указатель на память для функций
void* pParams; // Указатель на память для параметров
};
Далее подключим этот модуль к модулю Client и в классе CLIENT добавим новое поле типа INJECTOR
#include "Reader.h"
#include "Injector.h"
#include <Tlhelp32.h>
#ifndef ClientH
#define ClientH
struct CLIENT
{
public:
DWORD pid; // Идентификатор Процесса
READER get; // Ридер значений
INJECTOR inject; // Инжектор
DWORD PIDByProcName(AnsiString ProcessName); // определение pid по имени процесса
void Init(); // Инициализация
};
#endif
Далее в функции Init добавим ещё одну строчку, в которой мы запишем в структуру инжектора копию значения PID (это значение инжектору для работы тоже нужно)
void CLIENT::Init()
{
pid=PIDByProcName("elementclient.exe");
get.pid=pid;
inject.pid=pid;
}
Теперь давайте определимся вот с чем. Нам нужно будет выделять в клиенте память под наши функции. Можно поступить двумя способами.
1)Перед каждым инжектом выделять память, инжектить нашу функцию а после окончания её работы освобождать память.
2)Выделить достаточное количество памяти в клиенте один раз в начале работы нашей программы и не освобождать её пока бот работает.
Я буду использовать второй вариант, так как с первым у меня были проблемы. Спустя определённое время работы бота память переставала выделяться. Я правда не в курсе почему такое происходило, потому не стал разбираться и пошёл более простым путём. Итак, будем выделять память один раз, значит будем делать это при инициализации в функции Init класса CLIENT. Допишем в неё такие строчки
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
DWORD* pfunc = (DWORD*)VirtualAllocEx(hProcess,NULL,511,MEM_COMMI T,PAGE_READWRITE);
inject.pFunction = pfunc;
inject.pParams = pfunc+64;
CloseHandle(hProcess);
C первой строчкой всё понятно, мы это уже разбирали.
VirtualAllocEx выделяет память в процессе. Параметры такие
1 параметр - это дескриптор процесса, его мы знаем как получить
2 параметр - желательный стартовый адрес для выделение памяти, мы указываем NULL, тогда функция сама решит, где лучше выделить
3 параметр - размер выделяемой памяти в байтах. 511 байт нам будет больше чем достаточно
4 параметр - тип распределения памяти, указываем MEM_COMMIT, в этом случае память реально выделится и очистится от мусора
5 параметр - тип защиты памяти, указываем PAGE_READWRITE, этим мы говорим, что хотим получить доступ на чтение и запись
Функция возвращает указатель на начало выделенной памяти. Если вернёт NULL, значит выделение не удалось. Мы будем хранить этот указатель в переменной внутри класса INJECT. Далее мы будем использовать этот указатель чтобы копировать нашу функцию в клиент по этому адресу. Так же мы здесь определяем указатель на память выделенную под параметры нашей функции. Замечу здесь вот что. Я выделяю один кусок памяти и использую одну его часть для размещения функции а вторую часть для размещения параметров. Указатель на память для параметров я получаю так - к адресу начала выделенной памяти прибавляю 64 (64*DWORD=64*4=256) тоесть на 256 байт дальше от начала области памяти. Получается что в начале блока памяти будет распологаться функция, а ровно с середины этого блока параметры.
После выделения памяти и получения указателей мы закрываем хэндл процесса.
Добавим в структуру инжектора описание функции, которая будет инжектить в клиент различные функции(по сути сам инжектор и есть)
BYTE InjectAndExecute(void *Func, void* Params);
получает как параметры указатель на функцию, которую надо инжектить, и указатель на данные параметров для этой функции.
Затем напишем реализацию этой функции в Injector.cpp
BYTE INJECTOR::InjectAndExecute(void *Func, void* Params)
{
HANDLE hProcThread;
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
if (!hProcess) return 0; // выйдем из функции если не удалось открыть процесс
WriteProcessMemory(hProcess,pFunction,Func,250,NUL L);
WriteProcessMemory(hProcess,pParams,Params,250,NUL L);
hProcThread = CreateRemoteThread(hProcess,NULL,NULL,(LPTHREAD_ST ART_ROUTINE)pFunction,pParams,NULL,NULL);
if(hProcThread==INVALID_HANDLE_VALUE) return 0; // не удалось создать поток
WaitForSingleObject(hProcThread,INFINITE); // ожидаем завершения работы потока
CloseHandle(hProcThread); // закрываем хэндл нашего потока
CloseHandle(hProcess); // закрываем хэндл процесса
return 1; // успешная инъекция и выполнение кода
}
Опять же не забудем в файле Injector.h подключить модуль vcl.h, так же подключим модуль с оффсетами
#include "Offsets.h"
#include <vcl.h>
#ifndef InjectorH
#define InjectorH
#include <vcl.h>
#ifndef InjectorH
#define InjectorH
// Структура инжектора
struct INJECTOR
{
DWORD pid; // Идентификатор Процесса
void* pFunction; // Указатель на память для функций
void* pParam; // Указатель на память для параметров
BYTE InjectAndExecute(void *Func, void* Params);
};
#endif
Пояснения к новой функции
Сначала открываем процесс для работы, как мы уже делали. PID у нас давно найден и хранится в переменной. Затем мы в ранее уже выделенную память копируем инжектируемую функцию и в следующей строчке параметры для неё
WriteProcessMemory(hProcess,pFunction,Func,250,NUL L);
WriteProcessMemory(hProcess,pParams,Params,250,NUL L);
Мы копируем по 250 байт из места, где находится наша функция и так же параметры. Правда не указывая размеры этих данных мы так же скопируем хвостики, из не нужных нам данных, но пока не будем заморачиваться на счёт этого, далее мы модернизируем нашу функцию и избавимся от этого "недостатка".
WriteProcessMemory получает в качестве параметров хэндл процесса, в который будет писать, указатель на область памяти в которую будет писать, указатель на память откуда будет брать данные и размер этих данных.
Функция CreateRemoteThread создает поток, который запускается в виртуальном адресном пространстве другого процесса.
HANDLE CreateRemoteThread(
HANDLE hProcess, // дескриптор процесса
LPSECURITY_ATTRIBUTES lpThreadAttributes,// дескриптор защиты (SD)
SIZE_T dwStackSize, // размер начального стека
LPTHREAD_START_ROUTINE lpStartAddress, // функция потока
LPVOID lpParameter, // параметры функции потока
DWORD dwCreationFlags, // параметры создания
LPDWORD lpThreadId // идентификатор потока
);
Этой функцией ма создаём поток в котором будет выполняться наша функция. Любая программа может иметь несколько потоков, которые выполняются параллельно (ведь Винда - это многозадачная операционка). То есть у клиента есть основной поток, мы создаём в нём ещё один поток в котором выполняется наша функция и считается частью клиента. После создания потока код в нём выполнится, выполнит какие то действия и окончив свою работу сообщит о завершении. В строчке
WaitForSingleObject(hProcThread,INFINITE);
мы ожидаем завершение созданного нами потока после чего наша программа выполняется дальше.
Итак, инжектор функций у нас есть, но самой функции для инжекта нет. Давайте её создадим. Будем писать функцию таргета моба. Надеюсь указанную тему от Dinmaite вы читали. В файле Injector.cpp напишем нашу функцию. Просто отдельная функция не входящая ни в какую структуру.
void Target_THREAD(DWORD* WID)
{
DWORD Id = *WID;
__asm
{
MOV EDI, Id // Помещаем WID в регистр EDI
MOV EBX, 0x00630790 // Помещаем адрес клиентской функции таргета
MOV EAX,DWORD PTR DS:[BA] //
PUSH EDI // ; /Arg1
MOV ECX,DWORD PTR DS:[EAX+0x20] // ; |
ADD ECX,0x0EC // ; |
CALL EBX // ; \elementc.00606A70
}
}
Как видим здесь присутствует ассемблерная вставка, которая по сути является копией куска кода клиента, из которого вызывается клиентская функция выделения моба. Но WID подставляем мы сами.
Теперь добавим в структуру INJECTOR функцию, которая будет вызывать функцию-инжектор с нужными параметрами. Обьявим её в структуре инжектора
void TargetMob(DWORD wid);
и напишем реализацию
void INJECTOR::TargetMob(DWORD wid)
{
InjectAndExecute(&Target_THREAD,&wid);
}
Здесь функции-инжектору передаётся адрес нашей отдельной функции и адрес параметра для неё(WID). А далее функция-инжектор копирует её и параметры в выделенную в клиенте память и создаёт из неё поток. Наша функция выполнится в клиенте. Проверим работоспособность? Разместим на форме кнопку. При щелчке на ней бедет происходить выделение ближайшего моба. Изменим ранее написанный нами в таймере код, чтобы он получал не координаты а WID ближайшего моба. Из таймера уберём и вставим в кнопку.
DWORD wid=0; // WID ближайшего моба в массиве
float Dist=100000; // дистанция для сравнения, изначально установим в очень большую
for (int i=0; i<768; i++) // пройдёмся по всему списку мобов в клиенте, i - номер моба в списке
{
if ( bot.client.get.mobWID(i)!=0) // если место в списке занято мобом
{
if ( bot.client.get.mobType(i)==6) // если это действительно моб (6 - моб, 7 - NPC, 9 - пет)
{
if ( bot.client.get.mobDist(i)<Dist) // если дистанция меньше чем у предыдущего проверенного
{
Dist=bot.client.get.mobDist(i); // запомним дистанцию для следующего сравнения в списке
wid=bot.client.get.mobWID(i); // запомним WID моба в списке(если более близких мобов не будет, то это и будет WID ближайшего)
}
}
}
}
bot.client.inject.TargetMob(wid); // инжект выделения моба
Войдём в игру, придём на место где есть какие-нибудь мобы. Запустим нашу программу и нажмём кнопку. Если вылета клиента не случилось, значит мы всё сделали правильно и ближайший моб выделится. У меня всё нормально прошло, и после клика по кнопке моб выделился.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Итак, с инжектами мы разобрались. Давайте немного модернизируем наш инжектор. Сейчас у нас происходит инжект функции таргета, которая вместе с параметром записывается в адресное пространство клиента. В новом потоке она начинает работать берёт параметр находящийся в этом же куске памяти но на 256 байт дальше. Затем этот параметр использует в ассемблерной вставке, где происходит вызов клиентской функции. Давайте всё это упростим. Мы будем инжектить не целую функцию а уже готовый кусок кода с уже проставленными параметрами. Передавать параметр в функцию станет не нужно. Этот кусок кода будет представлять из себя просто массив чисел(ведь любая программа в памяти так и выглядит). Нам нужен всего лиш этот кусок кода
MOV EDI, Id // Помещаем WID в регистр EDI
MOV EBX, 0x00630790 // Помещаем адрес клиентской функции таргета
MOV EAX,DWORD PTR DS:[BA] //
PUSH EDI // ; /Arg1
MOV ECX,DWORD PTR DS:[EAX+0x20] // ; |
ADD ECX,0x0EC // ; |
CALL EBX // ; \elementc.00606A70
Нам нужно преобразовать его в набор цыфр и поместить в массив. Кстати, сюда нужно добавить последнюю строчку
RETN
Эта команда ассемблера производит возврат в место откуда этот кусок кода вызван. Аналогично return в функциях с++.
Для того чтобы узнать как этот кусок кода выглядит в цыфрах будем использовать программу OllyDbg. Я думаю она уже у вас есть.
Запустим Olly. Подключимся к любому процессу. Я для этого запустил паинт и подключился к нему. В меню File выбираем Attach.. и в списке выбираем например mspaint. В окне просмотра кода листаем в самый низ, видим память заполненную нулями. Будем ею пользоваться(надеюсь процесс мы не подвесим)).
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Теперь щёлкаем 2 раза по любой из этих строчек, появится окно редактирования кода процесса. Начинаем вводить строки из ассемблерного кода, указанного выше(или просто копировать их туда). После ввода первой сразу появится запрос следующей строчки кода. Вместо неизвестных параметров будем писать просто цыфры. Вместо Id - 11111111, вместо BA - 22222222. Хотя BA нам известно, но давайте вынесем на случай его дальнейшего изменения(хотя после обновлений клиента данный код тоже может измениться и придётся массив составлять заново). Итак что же у нас получилось после ввода
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
То что выделенно - это и есть наш код так сказать в числах. Выделим наши строчки, нажмём на них правой кнопкой, в появившемся меню выберем Edit> и далее Binary copy.
[Ссылки могут видеть только зарегистрированные и активированные пользователи]
Так мы скопируем последовательность этих самых чисел. Вставим их пока что куда-нибудь в блокнот, пусть полежат. А мы займёмся функцией
void INJECTOR::TargetMob(DWORD wid)
{
InjectAndExecute(&Target_THREAD,&wid);
}
Обьявим в ней массив типа char и размером с количество байт нашего кода. Массив сразу инициируем нашими кодами, точнее не кодами а символами, которые имеют эти коды. Так как числа шестнадцатеричные, то нужно указывать перед ними x а так же символ \ который скажет, что это не число а символ, кодом которого является это число. То есть у символа \xBF - код BF(hex). Так же здесь определим переменную в которой укажем размер массива. Инициируем массив строчкой из наших символов(коды которых и являются этими числами).
char code[28]="\xBF\x11\x11\x11\x11\xBB\x90\x07\x63\x00\xA1\x22\x 22\x22\x22\x57\x8B\x48\x20\x81\xC1\xEC\x00\x00\x00 \xFF\xD3\xC3";
int len=28;
Теперь смотрим на массив и видим числа 11 11 11 11 и 22 22 22 22, которые мы указывали вместо неизвестных параметров. Первый - это WID, второй это BA. Давайте сразу эти числа запишем в массив.
DWORD ba=BA;
memcpy(code+1,&wid,4); // WID в массиве распологается начиная с первой позиции от начала(начало - нулевая позиция)
memcpy(code+11,&ba,4); // а BA начиная с 11ой
Функцию инжектора мы переделаем, и вызов её будет выглядеть так
InjectAndExecute(code,len);
Как параметры передаются указатель на массив нашего кода и его длинна.
Теперь наша функция выделения в таргет выглядит так
void INJECTOR::TargetMob(DWORD wid)
{
char code[28]="\xBF\x11\x11\x11\x11\xBB\x90\x07\x63\x00\xA1\x22\x 22\x22\x22\x57\x8B\x48\x20\x81\xC1\xEC\x00\x00\x00 \xFF\xD3\xC3";
DWORD ba=BA;
memcpy(code+1,&wid,4); // WID в массиве распологается начиная с первой позиции от начала(начало - нулевая позиция)
memcpy(code+11,&ba,4); // а BA начиная с 11ой
InjectAndExecute(code,28);
}
Функцию Target_THREAD можно выкинуть, она нам больше не нужна. Мы копируем в клиент не её, а массив кода(её аналог, но попроще).
Теперь переделаем инжектор так, чтобы он инжектил этот массив в клиент без всяких параметров, ведь параметры мы уже прописали прямо в код.
BYTE INJECTOR::InjectAndExecute(char* code, int len)
{
HANDLE hProcThread;
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
if (!hProcess) return 0;
WriteProcessMemory(hProcess,pFunction,code,len,NUL L);
//WriteProcessMemory(hProcess,pParams,Params,250,NUL L); параметры нам больше не нужны, удалим эту строчку
// pParam заменим на NULL, так как праметров у нас теперь нет
hProcThread = CreateRemoteThread(hProcess,NULL,NULL,(LPTHREAD_ST ART_ROUTINE)pFunction,NULL,NULL,NULL);
if(hProcThread==INVALID_HANDLE_VALUE) return 0;
WaitForSingleObject(hProcThread,INFINITE);
CloseHandle(hProcThread);
CloseHandle(hProcess);
return 1;
}
Давайте запустим нашу программу и попробуем нажать на кнопку на форме. Инжект прошёл успешно и ближайший моб удачно выделился.
Приведу некоторые другие инжекты
// Использование скилла по ID
void INJECTOR::Skill(DWORD id)
{
char fdata[39]="\x60\x6A\xFF\x6A\x00\x6A\x00\x68\x00\x00\x00\x00\x 8B\x0D\x00\x00\x00\x00\x8B\x89\x00\x00\x00\x00\x8B \x89\x00\x00\x00\x00\xB8\x00\x00\x00\x00\xFF\xD0\x 61\xC3";
DWORD func=F_SKILL;
DWORD ba=BA;
DWORD dga=D_GA;
DWORD ps=PERS_STRUCT;
memcpy(fdata+8,&id,4);
memcpy(fdata+14,&ba,4);
memcpy(fdata+20,&dga,4);
memcpy(fdata+26,&ps,4);
memcpy(fdata+31,&func,4);
InjectFunction(fdata,39);
}
// Перемещение в координаты
// самая длинная и страшная функция)))
void INJECTOR::Move(float x, float y, float z, int walkmode)
{
char fdata[117]="\x60\xA1\x00\x00\x00\x00\x8B\xB0\x11\x11\x11\x11\x 8B\x8E\x22\x22\x22\x22\x6A\x01\xBB\x33\x33\x33\x33 \xFF\xD3\x89\xC7\x8D\x44\xE4\x0C\x50\x68\x44\x44\x 44\x44\x89\xF9\xBB\x55\x55\x55\x55\xFF\xD3\x8B\x8E \x66\x66\x66\x66\x6A\x00\x6A\x01\x57\x6A\x01\xBB\x 77\x77\x77\x77\xFF\xD3\xA1\x88\x88\x88\x88\x8B\x80 \x99\x99\x99\x99\x8B\x80\xAA\xAA\xAA\xAA\x8B\x40\x 30\x8B\x48\x04\xB8\xBB\xBB\xBB\xBB\x89\x41\x20\xB8 \xCC\xCC\xCC\xCC\x89\x41\x24\xB8\xDD\xDD\xDD\xDD\x 89\x41\x28\x61\xC3";
DWORD func1=F_MOVE1;
DWORD func2=F_MOVE2;
DWORD func3=F_MOVE3;
DWORD ga=GA;
DWORD ps=PERS_STRUCT;
DWORD maa=MY_ACTION_ARRAY;
DWORD wmode=1; if (walkmode==0) wmode=0;
memcpy(fdata+2,&ga,4);
memcpy(fdata+8,&ps,4);
memcpy(fdata+14,&maa,4);
memcpy(fdata+21,&func1,4);
memcpy(fdata+35,&wmode,4);
memcpy(fdata+42,&func2,4);
memcpy(fdata+50,&maa,4);
memcpy(fdata+62,&func3,4);
memcpy(fdata+69,&ga,4);
memcpy(fdata+75,&ps,4);
memcpy(fdata+81,&maa,4);
memcpy(fdata+92,&x,4);
memcpy(fdata+100,&z,4);
memcpy(fdata+108,&y,4);
InjectFunction(fdata,117);
}
// Подбор лута/шахты по WID
void INJECTOR::GetLoot(DWORD wid, BYTE type)
{
char fdata[29]="\x60\x8B\x0D\x00\x00\x00\x00\x8B\x89\x11\x11\x11\x 11\x6A\x22\x68\x33\x33\x33\x33\xBB\x44\x44\x44\x44 \xFF\xD3\x61\xC3";
DWORD func=F_GET_LOOT;
DWORD ga=GA;
DWORD prs=PERS_STRUCT;
BYTE typ=0; if (type==2) typ=1;
memcpy(fdata+3,&ga,4);
memcpy(fdata+9,&prs,4);
fdata[14]=typ;
memcpy(fdata+16,&wid,4);
memcpy(fdata+21,&func,4);
InjectFunction(fdata,29);
}
// Открытие диалога с НПС по WID
void INJECTOR::OpenDialog(DWORD wid)
{
char fdata[30]="\x60\x8B\x15\x00\x00\x00\x00\x8B\x4A\x20\x68\x11\x 11\x11\x11\x81\xC1\xEC\x00\x00\x00\xBB\x22\x22\x22 \x22\xFF\xD3\x61\xC3";
DWORD func=F_DIALOG;
DWORD ba=BA;
memcpy(fdata+3,&ba,4);
memcpy(fdata+11,&wid,4);
memcpy(fdata+22,&func,4);
InjectFunction(fdata,30);
}
F_SKILL, F_MOVE1, F_MOVE2, F_MOVE3, F_GET_LOOT - это адреса соответствующих функций вызываемых в клиенте, их я вынес в модуль Offsets
Ну вот вроде бы с нижектами мы закончили.
В прошлом разделе мы разбирались с инжектами. Теперь давайте разберёмся с пакетами.
Сервер Perfect World взаимодействует с клиентом посредством пакетов, получает и отсылает их. Пакет это последовательность данных, получив которые сервер распознаёт их и в зависимости от того, что это за данные, делает какие то действия у себя. Вспомним к примеру нашу клиентскую функцию выделения моба. В результате её работы, в клиенте формируется пакет(последовательность чисел) определённой длинны, затем вызывается функция, которая этот пакет отправляет серверу. Сервер его получает, расшифровывает и видит что это пакет выделения моба с определённым WID. Выделяет его, и отсылает клиенту ответ, мол выделил. Клиент получает этот пакет и отображает выделение моба(ну и не только отображает). Так же как и различные внутренние функции клиента(как например таргет моба) мы можем использовать и функцию отправки пакета. Нужно передать ей как параметры адрес начала готового пакета и его длинну. Пакет мы готовим сами а не клиент. Итак, мы скопируем с клиента кусок ассемблерного кода, который вызывает клиентскую функцию отправки пакета. Будем подставлять в него свои параметры (такие как адрес готового пакета в памяти и его длинну) а потом будем его инжектить в клиент. Таким образом этот кусок кода, работая в клиенте, заставит клиентскую функцию отправки пакета отправить наш пакет серверу. Получается так что при инжекте функций действий пакет составлял сам клиент и отправлял его серверу. А при инжекте функции посылки пакета, пакет формируется нами пропуская использование клиентских функций, выполняющих какие то действия. В классе инжектора опишем новую функцию, которая будет отвечать за инжект кода, вызывающего клиентскую функцию отправки пакета с нашими параметрами. Напишем её реализацию. Не буду размусоливать и сразу укажу тут массив байт, который представляет собой кусок кода из клиента, вызывающего функцию отправки пакета.
// Инъекция и отправка пакета
BYTE INJECTOR::SendPacket(PACKET* pack)
{
HANDLE hProcThread;
char fdata[29]="\x60\x8B\x0D\x00\x00\x00\x00\x8B\x49\x20\x68\x11\x 11\x11\x11\x68\x22\x22\x22\x22\xB8\x33\x33\x33\x33 \xFF\xD0\x61\xC3";
int lenfunc=29;
DWORD func=F_SEND_PACKET;
DWORD ba=BA;
DWORD len=pack->len;
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,false,pid) ;
if (!hProcess) return 0;
WriteProcessMemory(hProcess,pParams,pack->Bytes,len,NULL); // инжектим данные пакета
DWORD addr=DWORD(pParams); // возьмём адрес расположения данных нашего пакета
memcpy(fdata+3,&ba,4);
memcpy(fdata+11,&len,4);
memcpy(fdata+16,&addr,4); // запишем адрес расположения пакета прямо в массив кода
memcpy(fdata+21,&func,4);
WriteProcessMemory(hProcess,pFunction,fdata,lenfun c,NULL); // инжектим наш код
hProcThread = CreateRemoteThread(hProcess,NULL,NULL,(LPTHREAD_ST ART_ROUTINE)pFunction,NULL,NULL,NULL);
if(hProcThread==INVALID_HANDLE_VALUE) // не удалось создать поток
{
CloseHandle(hProcess);
return 0;
}
WaitForSingleObject(hProcThread, INFINITE); // ожидаем завершения работы потока
CloseHandle(hProcThread); // освобождаем
CloseHandle(hProcess);
return 1; // успешная инъекция и выполнение кода
}
F_SEND_PACKET - адрес функции посылки пакета, адрес этот известен и указан в теме с Оффсетами.
Здесь мы инжектим кроме самой функции ещё и данные. Данные эти и будут являться пакетом. Опишем вспомогательную структуру пакета в начале модуля перед описанием структуры инжектора
// Структура пакета
struct PACKET
{
int len; // длина
BYTE Bytes[60]; // данные пакета
};
Функция SendPacket(PACKET* pack) получает указатель на структуру пакета, который мы подготовили для отправки.
Давайте перепишем нашу функцию выделения моба, чтобы выделение происходило отправкой пакета серверу.
Функция выглядела вот так
void INJECTOR::TargetMob(DWORD wid)
{
char code[28]="\xBF\x11\x11\x11\x11\xBB\x90\x07\x63\x00\xA1\x22\x 22\x22\x22\x57\x8B\x48\x20\x81\xC1\xEC\x00\x00\x00 \xFF\xD3\xC3";
int len=28;
DWORD ba=BA;
memcpy(code+1,&wid,4);
memcpy(code+11,&ba,4);
InjectAndExecute(code,len);
}
перепишем её
void INJECTOR::TargetMob(DWORD wid)
{
PACKET pack; // структура пакета
pack.len=6; // длина данных пакета
char Packet[6] = "\x02\x00\x01\x00\x00\x00"; // массив данных пакета
memcpy(pack.Bytes,Packet,pack.len); // скопируем в структуру пакета данные массива
memcpy(pack.Bytes+2,&wid,4); // последние четыре байта - WID выделяемого моба
SendPacket(&pack); // инжектим
}
Запускаем нашу программу, жмём на кнопочку и смотрим - ближайший моб выделился. Значит всё прошло хорошо, и пакет удачно отправился серверу.
На этом о пакетах всё. Укажу ещё некоторые функции, которые могут понадобиться для работы бота.
// Убрать таргет
void INJECTOR::TargetOff()
{
PACKET pack;
pack.len=2;
pack.Bytes[0]='\x08';
pack.Bytes[1]='\x00';
SendPacket(&pack);
}
// Использование скилла по ID. Нужно быть на расстоянии действия скилла
void INJECTOR::Skill2(DWORD skillid, DWORD mobwid)
{
PACKET pack;
pack.len=12;
char Packet[12] = "\x29\x00\x2B\x01\x00\x00\x00\x01\xF9\x16\x10\x80";
memcpy(pack.Bytes,Packet,pack.len);
memcpy(pack.Bytes+2,&skillid,4);
memcpy(pack.Bytes+8,&mobwid,4);
SendPacket(&pack);
}
// Включение/выключение полёта
void INJECTOR::Fly(DWORD FlyID)
{
PACKET pack;
pack.len=10;
char Packet[10] = "\x28\x00\x01\x01\x0C\x00\xF7\x31\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
memcpy(pack.Bytes+6,&FlyID,4);
SendPacket(&pack);
}
// Вызов пета из ячейки с номером..
void INJECTOR::CallPet(int nom)
{
PACKET pack;
pack.len=6;
char Packet[6] = "\x64\x00\x01\x00\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
pack.Bytes[2]=nom;
SendPacket(&pack);
}
// Использование предмета в инвентаре
void INJECTOR::UseItem(DWORD id, BYTE cell)
{
PACKET pack;
pack.len=10;
char Packet[10] = "\x28\x00\x00\x01\x0D\x00\xAD\x21\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
pack.Bytes[4] = cell; // номер ячейки
memcpy(pack.Bytes+6,&id,4);
SendPacket(&pack);
}
// Продажа из инвентаря
void INJECTOR::Sell(BYTE nomcell, DWORD kol, DWORD ID)
{
PACKET pack;
pack.len=26;
char Packet[26] = "\x25\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\x00\x 00\x00\xFF\xFF\x00\x00\xFF\x00\x00\x00\xD0\x07\x00 \x00";
memcpy(pack.Bytes,Packet,pack.len);
pack.Bytes[18] = nomcell; // 18 - номер ячейки
memcpy(pack.Bytes+22,&kol,4); // 22-25 - количество
memcpy(pack.Bytes+14,&ID,4); // 14-17 - ID предмета
SendPacket(&pack);
}
// Покупка у НПС
void INJECTOR::Buy(BYTE NPCcell, DWORD kol, DWORD ID)
{
PACKET pack;
pack.len=50;
char Packet[50] = "\x25\x00\x01\x00\x00\x00\x28\x00\x00\x00\x00\x00\x 00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x 00\xA5\x21\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
pack.Bytes[42] = NPCcell; // 30 - номер ячейки
memcpy(pack.Bytes+46,&kol,4); // 34-27 - количество
memcpy(pack.Bytes+38,&ID,4); // 26-29 - ID предмета
SendPacket(&pack);
}
// Обычная атака пета
void INJECTOR::PetAttack(DWORD wid)
{
PACKET pack;
pack.len=11;
char Packet[11] = "\x67\x00\x00\x00\x00\x80\x01\x00\x00\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
memcpy(pack.Bytes+2,&wid,4); // 4 байта, начиная с 2 - WID монстра
SendPacket(&pack);
}
// Ремонт у НПС
void INJECTOR::Repair()
{
PACKET pack;
pack.len=16;
char Packet[16] = "\x25\x00\x03\x00\x00\x00\x06\x00\x00\x00\xFF\xFF\x FF\xFF\x00\x00";
memcpy(pack.Bytes,Packet,pack.len);
SendPacket(&pack);
}
// Воскрешение в ближгород
void INJECTOR::Res()
{
PACKET pack;
pack.len=2;
char Packet[2] = "\x04\x00";
memcpy(pack.Bytes,Packet,pack.len);
SendPacket(&pack);
}
// Режим пета Охрана
void INJECTOR::PetGuard()
{
PACKET pack;
pack.len=14;
char Packet[14] = "\x67\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x 00\x00";
memcpy(pack.Bytes,Packet,pack.len);
SendPacket(&pack);
}
// Включение скилла, используемого петом автоматически, как активного
void INJECTOR::PetDefSkill(DWORD skillid)
{
PACKET pack;
pack.len=14;
char Packet[14] = "\x67\x00\x00\x00\x00\x00\x05\x00\x00\x00\xF1\x02\x 00\x00";
memcpy(pack.Bytes,Packet,pack.len);
memcpy(pack.Bytes+10,&skillid,4);
SendPacket(&pack);
}
Продолжение... ([Ссылки могут видеть только зарегистрированные и активированные пользователи])