Продолжаем предыдущую тему: Таб-таргет для любых серверов. Мы уже нашли окружающие персонажа объекты, объекты с умениями и инвентарем, функцию выбора объекта в таргет, и функцию использования скиллов и предметов. Теперь перейдем к следующему этапу — обходу шифрования пакетов на примере сервера cerberus-games. Итоговая тулза будет работать и на других серверах.
На гифке мы перехватываем исходящие пакеты, отправляем сообщение в чат, бафаемся через пакеты, а затем отслеживаем входящие пакеты.
Содержание
Зашифрованный и незашифрованный пакет
Поиск функции шифрования исходящих пакетов
Хукаем функцию шифрования пакетов
Работа с входящими пакетами (спойлер: они не шифруются)
Итоговая тулза
Зашифрованный и незашифрованный пакет
Сначала перехватим незашифрованный пакет, чтобы понять, как он устроен.
Запускаем клиент RF Online на сервере без шифрования (я экспериментирую на RF Altruism, также можно запустить свой сервер локально).
Запускаем x64dbg, аттачимся и ставим брейкпоинт на функцию send из ws2_32.dll Example
Пишем в чат "123" — срабатывает брейкпоинт. Функция send принимает в аргументах буфер, который нужно отправить. Находим его — там лежит искомый пакет. Example
То же самое мы увидим, если перехватим этот пакет в Wireshark. Буфер, который передается в send, соответствует данным в пакете стека протоколов TCP/IP. Example
03 31 32 33 00 04 00 00 00 - данные. В данном случае 03 — длина сообщения, 31 32 33 — "123".
Забегая вперед, нам будут важны только тип и данные. То, что идет в начале, будет рассчитываться функциями игры самостоятельно.
Теперь проделаем то же самое на Церберусе, где шифрование есть. Ставим брейкпоинт на send и пишем "123" в чат. Видим непонятный набор данных, не похожий на пакет, который мы перехватили выше. Example
Поиск функции шифрования исходящих пакетов
Ставим снова брейкпоинт на send и триггерим его, отправив сообщение в чат. Делаем это несколько раз и следим, как меняется адрес буффера. Example
Замечаем, что адреса всегда похожи, и иногда новый буфер может перетирать старый. Переходим к одному из этих адресов в памяти, убираем брейкпоинт и начинаем спамить сообщения в чат. В результате видим, что буфер при отправке сообщения всегда лежит в какой-то области из фиксированного куска памяти. На гифке значения в памяти меняются только когда я пишу сообщения в чат. Example
Ставим брейкпоинт на запись на какой-то из адресов (или на несколько), по которым может произойти запись, и начинаем спамить в чат, пока он не сработает. В итоге мы окажемся внутри RFOClient.vmp.dll Example
RFOClient.vmp.dll - это DLL с кастомной логикой клиента Церберуса. В оригинальном rf_online.bin хукаются необходимые функции и выполнение перенаправляется в данную DLL. Т.к. при каждом старте она инжектится заново, адреса в ней после рестарта клиента съезжают.
Начинаем выполнять возврат из функции, пока не окажемся в rf_online.bin (не забудьте перед этим деактивировать поставленные брейкпоинты, чтобы они снова не сработали). В итоге находим место, где была изменена дефолтная логика. Example
Ставим брейкпоинт на push eax и проверяем, что лежит по этому адресу. Находим что-то похожее на незашифрованный пакет: кажется, что данные зашифрованы, а тип — нет. Example
Возобновляем выполнение программы и следим за памятью. Новые пакеты добавляются после предыдущих. Example
Ставим брейкпоинт на запись где-нибудь после текущего пакета, чтобы определить, где подготавливаются данные (важно при этом попасть именно в зашифрованные данные, а не в тип пакета). После срабатывания опять окажемся в RFOClient.vmp.dll. Example
Выполняем возврат из функции, пока не окажемся в rf_online.bin. В итоге попадаем в функцию LoadSendMsg (название подсказал Cutter). Example
Делаем возврат из функции и снова оказываемся в RFOClient.vmp.dll Example
Снова делаем возврат пока не окажемся в rf_online.bin и наконец находим искомую функцию — SendNetMessage. На скриншоте справа подписаны аргументы, которые передаются в обертку над LoadSendMsg из RFOClient.vmp.dll. Example
Ставим брейкпоинт на начало функции и убеждаемся, что в функцию передаются незашифрованный тип и данные для пакета. Example
В итоге мы нашли функцию SendNetMessage, с помощью которой можно отправлять свои пакеты. Функция имеет такую сигнатуру: Код на C++
Важное замечание: эта функция используется почти для всех действий в игре, таких как передвижение, использование умений и предметов, передачи предметов, отправки сообщений в чат и др. Раз в несколько секунд клиент также шлет пакет на синхронизацию, и он отправляется через другую функцию. Ее также можно найти, повторив шаги выше, если вам нужно.
Хукаем функцию шифрования пакетов
Хукать функцию нужно для того, чтобы логировать исходящие пакеты и понимать, какие пакеты может отправить клиент. При вызове SendNetMessage в стеке будут лежать аргументы — тип пакета, данные и их длина. Нам нужно перенаправить исполнение программы в наш заинжекченный код с помощью команды jmp, вывести значения аргументов в консоль или интерфейс программы и вернуть исполнение обратно в функцию.
Будем хукаться в этом месте, так как команда jmp имеет длину 5 байт.
Для того, чтобы хукнуться я использую такой класс Код на C++
Код:
#pragma once
class Hook {
void* tToHook;
std::unique_ptr<char[]> oldOpcodes;
int tLen;
public:
Hook(void* toHook, void* ourFunct, int len) : tToHook(toHook), oldOpcodes(nullptr), tLen(len){
if (len < 5) {
return;
}
DWORD curProtection;
VirtualProtect(toHook, len, PAGE_EXECUTE_READWRITE, &curProtection);
oldOpcodes = std::make_unique<char[]>(len);
if (oldOpcodes != nullptr) {
for (int i = 0; i < len; ++i) {
oldOpcodes[i] = ((char*)toHook)[i];
}
}
memset(toHook, 0x90, len);
DWORD relativeAddress = ((DWORD)ourFunct - (DWORD)toHook) - 5;
*(BYTE*)toHook = 0xE9;
*(DWORD*)((DWORD)toHook + 1) = relativeAddress;
VirtualProtect(toHook, len, curProtection, &curProtection);
}
~Hook() {
if (oldOpcodes != nullptr) {
DWORD curProtection;
VirtualProtect(tToHook, tLen, PAGE_EXECUTE_READWRITE, &curProtection);
for (int i = 0; i < tLen; ++i) {
((char*)tToHook)[i] = oldOpcodes[i];
}
VirtualProtect(tToHook, tLen, curProtection, &curProtection);
}
}
};
Первый аргумент в конструкторе - адрес для хука, второй - адрес функции, в которую нужно перенаправить исполнение, третий - количество заменяемых байтов. То есть в нашем случае его нужно вызвать так:
Hook sendHook{ (void*)0x0052EB91, reinterpret_cast<void*>(ourCustomFunction), 5 };
В деструкторе возвращается исходный код, чтобы при закрытии нашей программы игра работала как прежде.
Сама функция, в которую будет перенаправляться выполнение, может выглядеть так: Код на C++
В этой функции мы сохраняем указатель на this, значения регистров и аргументы в переменные. Затем выполняем с этими данными необходимые действия (printSendBufferToLog()). После этого восстанавливаем значения регистров, выполняем команды, перезаписанные jmp (mov ebp, esp и sub esp, 0x18), и возвращаем исполнение в исходную функцию.
В итоге, после хука пролог функции будет выглядеть так (с добавленным нашим jmp). Example
Более подробно про перенаправление можно почитать здесь: и
Работа с входящими пакетами
Тут все проще, так как входящие пакеты на Церберусе не шифруются. Чтобы убедиться в этом, можно поставить брейкпоинт на recv из ws2_32.dll и нажать execute till return, чтобы в буфер записался контент из сокета. Example
Единственная проблема здесь — непонятно, где начинается и заканчивается пакет. Начало мне удалось найти: если поставить access-брейкпоинт на буфер и проанализировать, где происходит чтение из него, то можно найти функцию AnalysisZoneServerPacket. Example
В нее передаются два аргумента — тип пакета (в данном случае 4 байта) и контент, но нет длины. В итоге в хуке я просто логирую тип и 64 байта из контента.
Итоговая тулза
Сделано на основе такой же тулзы для Теры:
Поддерживает
Логирование исходящих и входящих пакетов
Отправку пакетов
Отправку последовательности пакетов, предварительно загруженных из файла
Выгрузгу логов
Работает на всех популярных серверах (независимо от того, есть ли шифрование или нет).
В архиве два файла: RF Proxy.exe — инжектор (методом Manual Mapping) и rfproxy.dll. Вы можете использовать любой другой инжектор, например, встроенный в Cheat Engine.
Скрытый текст (вы должны войти под своим логином или зарегистрироваться и иметь 300 сообщение(ий)):
У вас нет прав чтобы видеть скрытый текст, содержащейся здесь.
Чтобы его прочитать, вы должны заработать сообщения. Как это сделать бесплатно, описано в правилах форума. Как купить доступ, описано в Платных услугах.
Попробовал, но возникла проблема с указателями на стек. В твоем примере мы вызываем jmpBackAddrSendNetMessage как __thiscall, поэтому перед выполнением функции нужно подготовить стек, а эти команды мы перетерли jmp-ом.
Компилятор сам разберется, видя соглашения о вызовах, единственное что тебе нужно это создать трамплин скопировав в него пролог , затем jmp на 0x0052EB90 + 6. А сам трамплин
уже вызываешь как __thiscall функцию
Трамплин лучше динамически создавать, например доработав класс. А так тебе придется под каждый хук добавлять отдельную naked функцию... Желательно бы еще и дизассемблер длин прицепить, чтобы количество байт в прологе просчитывалось автоматически.
А еще лучше воспользоваться готовым решением типа
Добавлено через 45 минут
асм вставки надо вообще избегать, пользоваться ими только где это действительно необходимо. В x64 тебе пришлось бы колхозить через .asm файл, а это то еще веселье)
Последний раз редактировалось Dino; 19.11.2024 в 21:46.
Причина: Добавлено сообщение
Разрешите подушнить. Если планируешь использовать этот класс в других проектах, то стоит-таки проверять возвращаемое значение, часто античиты хукают VirtualProtect, вернее NtProtectVirtualMemory
Diantro +400 постов. Отличная статья по реверсу вообще в принципе любых игр.
Добавлено через 2 минуты
Цитата:
Сообщение от Diantro
Единственная проблема здесь — непонятно, где начинается и заканчивается пакет.
А в чём проблема? Чёто я не помню у себя таких запар.
Последний раз редактировалось dark; 20.11.2024 в 10:45.
Причина: Добавлено сообщение
Я не оказываю услуги гаранта!
База данных кидал: blacklist.rf-cheats.ru
Обязательно проверяйте человека через чёрный список прежде чем совершать с ним сделку.