Показать сообщение отдельно
Старый 30.09.2014, 16:06   #1
dark
Читодел
Аватар для dark
OFFLINE
Регистрация: 09.04.2007
Сообщений: 965
Благодарностей:
36,577 всего
Мнения: + 37721
Репутация: 116394
Отправить сообщение для dark с помощью ICQ Отправить сообщение для dark с помощью Skype™

Как сделать чит (пакетчик) на примере авто-спама для Blood and Soul.


А точнее в статье описан способ реализации читов на основе использования функций игрового клиента. А если ещё точнее, то описан метод создания пакетчика на примере спамера :)

Недавно получил заказ на создание авто-спамилки для игры Blood and Soul с сохранением моего права на его распространение. Штука не особо востребованная, поэтому решил рассказать вам на примере реализации спамера о довольно интересном методе создания читов для игр. Ну и в целом статья поможет вам понять ход мыслей программиста при создании нужного чита.
Ссылки на инструменты я дам в конце статьи.

Итак, задача - Научиться программным способом постить в чат свои сообщения. Защита игрового клиента не даёт использовать кликеры, поэтому этот вариант сразу опустим. Можно, конечно, рыть защиту или просто заюзать мой обход для неё - но в контексте статьи эти варианты не интересны. Первым вариантом реализации поставленной задачи в голову приходит, разумеется, "пакетный" способ. Иными словами мы можем разобрать сетевой протокол клиент-серверного взаимодействия BS и отталкиваясь от этого генерить нужный пакет для посылки сообщения в чат. Как его слать? Можно через WPF, можно сделать DLL, которая бы хукала какую-либо wsock API для определения сокета и просто в цикле слала бы в сокет ваш пакет. Но это уже второе дело, сейчас главное найти вариант хотя бы вручную постить сообщения в чат без мышки и клавы :)
Для начала пробуем определить какими пакетами осуществляется пост сообщений в чат. Для этого нам требуется перехватить исходящие пакеты игрового клиента. Как? Вариантов много - начиная со сниффера, заканчивая API-шпионами и дебаггерами. Я выбрал третий вариант, т.к. у него просто больше возможностей чем у остальных. Из дебаггеров я выбрал, разумеется, ollydbg. Аттачимся к игре и ставим бряк на send():



Далее постим в чат, например, "aaaa", после чего срабатывает наш breakpoint:



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

Код:
общий чат:
BB A9 CB D5 A6 0F FB 76 38 00 00 00 00 00 00 00  | .......v8.......
FF FF FF FF 00 00 00 00 04 00 61 00 61 00 61 00  | ..........a.a.a.
61 00 61 00 61 00 61 00 61 00 61 00 61 00 61 00  | a.a.a.a.a.a.a.a.
61 00 61 00 61 00 61 00 61 00 00 00 	 	 | a.a.a.a.a....

BB A9 CB D5 A6 0F FB 76 38 00 00 00 00 00 00 00  | .......v8.......
FF FF FF FF 00 00 00 00 04 00 62 00 62 00 62 00  | ..........b.b.b.
62 00 62 00 62 00 62 00 62 00 62 00 62 00 62 00  | b.b.b.b.b.b.b.b.
62 00 62 00 62 00 62 00 62 00 00 00 	  	 | b.b.b.b.b....

B4 A9 CB C1 A6 0F FB 76 2C 00 00 00 00 00 00 00  | .......v,.......
FF FF FF FF 00 00 00 00 04 00 64 00 77 00 64 00  | ..........d.w.d.
77 00 64 00 77 00 64 00 77 00 64 00 77 00 00 00  | w.d.w.d.w.d.w...

Релогин:
B1 A9 CB CD A6 0F FB 76 20 00 00 00 00 00 00 00  | .......v .......
FF FF FF FF 00 00 00 00 04 00 61 00 61 00 61 00  | ..........a.a.a.
61 00 00 00 01  				 | a....	

BE A9 CB CD A6 0F FB 76 20 00 00 00 00 00 00 00  | .......v .......
FF FF FF FF 00 00 00 00 04 00 61 00 61 00 61 00  | ..........a.a.a.
61 00 00 00 D1  				 | a....

локация:
B4 A9 CB D5 A6 0F FB 76 38 00 00 00 00 00 00 00  | .......v8.......
FF FF FF FF 00 00 00 00 05 00 61 00 61 00 61 00  | ..........a.a.a.
61 00 61 00 61 00 61 00 61 00 61 00 61 00 61 00  | a.a.a.a.a.a.a.a.
61 00 61 00 61 00 61 00 61 00 00 00 	  	 | a.a.a.a.a....
Меняя тип чата, длину текста и перелогиниваясь мы можем по пакету понять, какой байт за что отвечает. Исходя из этого набора пакетов я делаю следующие выводы на примере последнего пакета:
B4 A9 CB D5 A6 0F FB 76 38 00 00 00 00 00 00 00 FF FF FF FF 00 00 00 00 05 00 61 00 61 00 61 00 61 00 .......
B4 A9 CB D5 - неизвестная составляющая, которая в рамках сессии периодически меняется.
A6 0F FB 76 - не меняется. Видимо, ID персонажа или аккаунта - для нас не важно, главное, что константа.
38 00 00 00 - если посмотреть на второй скриншот, то видно, что это длина пакета минус 4. Это нам говорит о том, что первые 4 байта идут как бы отдельно от тела пакета и несут иной функциональный смысл.
00 00 00 00 FF FF FF FF 00 00 00 00 - константы. Не известно что, но нам впрочем и не важно.
05 - это тип чата! Видно, что в пакете для общего чата и локации этот байт отличается
00 61 00 61 00 61 00 61 ... - это сам текст в юникоде.
Вот тут мы сталкиваемся с главной проблемой, которая весьма усложняет поставленную задачу. Мы не знаем как генерируются первые 4 байта и для чего они нужны. Вероятно, они призваны защитить игру как раз от инжекта чужих пакетов. Что делать дальше? У нас два варианта - либо копать игру дебаггером в поисках места, где генерируется эти 4 байта, либо искать другой вариант решения задачи. Проблема первого варианта в том, что в алгоритме генерации этих 4 байт могут быть забиты такие величины, из-за которых наши пакеты внесут ассинхронизацию генерации этих байт между сервером и клиентом. Сложно, да? :) Ну допустим этот код генериться через каждые 3 отосланных пакета. Клиент и сервер абсолютно точно знают, когда его генерировать снова. Если мы вдруг шлём свой пакет в обход клиента, то клиент о посылке знать не будет, а сервер будет - отсюда последствия в виде ассинхронизации и дисконнекта. Вроде понятно объяснил.. Чтобы избежать подобных вероятных проблем, можно не генерировать свой код, а воспользоваться непосредственно клиентской функцией, которая его генерирует! Как найти эту самую функцию? Точно также, через дебаггер. Я не зря выше писал о причине использования дебаггера вместо сниффера и апи-шпиона.
Итак, снова брякаемся на send() и смотрим стек (тот скриншот №2). Первый же адрес в нём - это адрес возврата в функцию, которая вызвала send(). Точнее олька нам подсказывает, что это функция-член (или метод) ThreadSend класса XClient. Далее отпускаем и ищем этот метод в vEngine.dll, брякаем:



Шлём сообщение в чат и что мы видим? Бряк не срабатывает. Чисто логически это означает, что данный метод зациклен (если исключить варианты защиты от бряков). Да и по его названию можно понять, что это поток, который берёт из некой кучи пакеты и отправляет их в сокет. Побегав по этому методу бряками моя версия подтверждается.
Может показаться, что мы в тупике, потому что мы не знаем какая функция или метод кладёт сгенеренные пакеты в кучу. На самом деле логично предположить, что адрес этой кучи должен быть константным. Снова пару раз брякаемся на send() и видим, что отсылаемый пакет меняется, но всегда лежит по одному и тому же адресу!
В нашем случае это 0x05280264 (смотрите скриншот №2). Дальше всё элементарно! Ставим бряк на запись в память по этому адресу и пишем в чат:



Вуаля! Мы попадаем в метод AddMsg, в котором пакет записывается в эту самую очередь (буду называть её "кучей"). Этот метод в свою очередь вызывается методом Send класса XClient. Брякаемся на нём и постим любимые "aaaaa" в чат:



И вот мы внутри Send, смотрим его аргументы и понимаем, что дело в шляпе. Функция Send принимает два аргумента: отсылаемый пакет и его размер, после чего просто добавляет его в очередь. Если внимательнее посмотрите на пакет, то заметите, что в нём отсутствуют те первые 4 байта, значение которых мы хотели определить. По всей видимости они добавляются к пакету в методе ThreadSend. Но это для нас уже не важно, потому что мы можем слать пакеты через этот самый Send, а неопознанные байты будут добавлены методом ThreadSend.

Теперь перейдём к реализации. Я решил описать этот момент, т.к. там есть пара подводных камней. Первый из которых - это согласование вызовов. Метод Send должен вызываться как __thiscall, ибо это функция-член класса XClient. Подробнее об этом читайте в MSDN. Мы это легко можем определить по коду. Вот как вызывается Send:
Код:
CPU Disasm
Address   Hex dump          Command                                                         Comments
0082B398  ***179;. 8B55 D0        MOV EDX,DWORD PTR SS:[LOCAL.12]
0082B39B  ***179;. 52             PUSH EDX                                                        ; ***218;Arg2 => [LOCAL.12]
0082B39C  ***179;. 8B45 D4        MOV EAX,DWORD PTR SS:[LOCAL.11]                                 ; ***179;
0082B39F  ***179;. 50             PUSH EAX                                                        ; ***179;Arg1 => [LOCAL.11]
0082B3A0  ***179;. 8B4D CC        MOV ECX,DWORD PTR SS:[LOCAL.13]                                 ; ***179;
0082B3A3  ***179;. 83C1 60        ADD ECX,60                                                      ; ***179;
0082B3A6  ***179;. E8 F5DCBDFF    CALL client.004090A0                                            ; ***179;
0082B3AB  ***179;. 8BC8           MOV ECX,EAX                                                     ; ***179;
0082B3AD  ***179;. FF15 DCA49900  CALL DWORD PTR DS:[<&vEngine.?Send@XClient@vEngine@@QAEHPAXK@Z> ; ***192;vEngine.?Send@XClient@vEngine@@QAEHPAXK@Z
Оба аргумента передаются через стек, а вот через регистр ECX передаётся указатель на свой класс. Так какое согласование следует использовать нам? Я пошёл путём использования __fastcall. Данное соглашение о вызовах говорит компилятору о том, что первые два аргумента должны передаваться через регистр ECX (да, да!) и EDX. А остальные через стек. Получается, что прототип функции Send мы можем представить вот так:
Код:
int (__fastcall *vEngine__XClient__Send)(void *This, void *_EDX, void *Src, int Size);
В EDX можно передавать мусор, а вот указатель на класс, к сожалению, должен быть заполнен. Как его найти? Можно тупо через Cheat Engine найти его расположение через указатели, можно хукнуть какой-нибудь из методов этого класса и просто сохранять значение ECX для дальнейшего использования. Тут уже сами, т.к. задача тривиальна.

В итоге код, который шлёт пакет, занимает всего несколько строчек:
Код:
HMODULE hModule = GetModuleHandle("vEngine.dll");
(FARPROC &)vEngine__XClient__Send = (FARPROC)GetProcAddress(hModule, "?Send@XClient@vEngine@@QAEHPAXK@Z");
vEngine__XClient__Send((void *)thisAddr, (void *)0, (void *)packet, size);
Всё гениальное - просто. Точно также в других в играх вы можете обходить, например, шифрование траффика. Обратите внимание, что этим способом вы можете слать любой игровой пакет! Просто в контексте моей задачи я затронул только чат.
Мало того, используя описанный подход, вы можете использовать любую игровую функцию. Допустим, хотите из своего кода атаковать моба с неограниченной скоростью? Не вопрос - точно так же брякаетесь на методе Send и далее по описанному выше сценарию ищите ту функцию, которая отвечает за атаку и вызываете её с нужной вам скоростью. Огромный плюс этого способа написания читов заключается в том, что игровые обновления очень редко на него влияют. Используемая выше функция Send вообще врядли когда-либо будет изменена, таким образом этот пакетчик (и спамилка) будут работать вечно.

Ни у результат:



Используемые инструменты в статье:
OllyDbg
TotalInjector

Ну и кому интересна тематика, рекомендую предыдущую читерскую статью по обходу игровых защит - https://www.rf-cheats.ru/forum/showthread.php?t=201477

Последний раз редактировалось dark; 30.09.2014 в 17:06.

Cheats Development | Создание читов на заказ

Я не оказываю услуги гаранта!
База данных кидал: blacklist.rf-cheats.ru
Обязательно проверяйте человека через чёрный список прежде чем совершать с ним сделку.
 
Ответить с цитированием
Сказали спасибо:
LasQa (15.03.2018), Armyyan (19.02.2016), MbIKoLa (25.10.2015), Big Bad Sensey (04.08.2015), FullZver (25.03.2015), Алексей 5e3 (06.02.2015), Romb!k (02.11.2014), MoXxX (30.10.2014), Dannger (13.10.2014), Qolt (08.10.2014), Rangris (01.10.2014), Paladin (01.10.2014), ТуЗеМеЦ (01.10.2014), rat (01.10.2014), Артём612 (30.09.2014), Shov (30.09.2014), Vyronys (30.09.2014)