Интро
Продолжаем предыдущую тему:
Таб-таргет для любых серверов. Мы уже нашли окружающие персонажа объекты, объекты с умениями и инвентарем, функцию выбора объекта в таргет, и функцию использования скиллов и предметов. Теперь перейдем к следующему этапу — обходу шифрования пакетов на примере сервера cerberus-games. Итоговая тулза будет работать и на других серверах.
На гифке мы перехватываем исходящие пакеты, отправляем сообщение в чат, бафаемся через пакеты, а затем отслеживаем входящие пакеты.
Содержание- Зашифрованный и незашифрованный пакет
- Поиск функции шифрования исходящих пакетов
- Хукаем функцию шифрования пакетов
- Работа с входящими пакетами (спойлер: они не шифруются)
- Итоговая тулза
Зашифрованный и незашифрованный пакет
Сначала перехватим незашифрованный пакет, чтобы понять, как он устроен.
- Запускаем клиент RF Online на сервере без шифрования (я экспериментирую на RF Altruism, также можно запустить свой сервер локально).
- Запускаем x64dbg, аттачимся и ставим брейкпоинт на функцию send из ws2_32.dll
Example
- Пишем в чат "123" — срабатывает брейкпоинт. Функция send принимает в аргументах буфер, который нужно отправить. Находим его — там лежит искомый пакет.
Example
- То же самое мы увидим, если перехватим этот пакет в Wireshark. Буфер, который передается в send, соответствует данным в пакете стека протоколов TCP/IP.
Example
Имеем такой пакет:
0f 00 09 00 02 02 03 31 32 33 00 04 00 00 00- 0f - длина пакета
- 02 02 - тип пакета
- 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++
Код:
typedef void(__thiscall* _SendNetMessage)(void* thisClass, DWORD zero1, const char* packetTypePtr, const char* packetContentPtr, DWORD length, DWORD zero2);
Важное замечание: эта функция используется почти для всех действий в игре, таких как передвижение, использование умений и предметов, передачи предметов, отправки сообщений в чат и др. Раз в несколько секунд клиент также шлет пакет на синхронизацию, и он отправляется через другую функцию. Ее также можно найти, повторив шаги выше, если вам нужно.
Хукаем функцию шифрования пакетов
Хукать функцию нужно для того, чтобы логировать исходящие пакеты и понимать, какие пакеты может отправить клиент. При вызове
SendNetMessage в стеке будут лежать аргументы — тип пакета, данные и их длина. Нам нужно перенаправить исполнение программы в наш заинжекченный код с помощью команды
jmp, вывести значения аргументов в консоль или интерфейс программы и вернуть исполнение обратно в функцию.
Будем хукаться в этом месте, так как команда jmp имеет длину 5 байт.
Код:
0052EB91 | 8BEC | mov ebp,esp
0052EB93 | 83EC 18 | sub esp,18
Example
Для того, чтобы хукнуться я использую такой класс
Код на 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++
Код:
void __declspec(naked) sendHookFunc() {
__asm {
mov thisPTR, ecx
mov teax, eax; backup registers
mov tebx, ebx
mov tecx, ecx
mov tedx, edx
mov tesi, esi
mov tedi, edi
mov tebp, ebp
mov tesp, esp; end of backup
mov eax, [esp + 0xC]
mov outgoingTypePtr, eax
mov eax, [esp + 0x10]
mov outgoingContentPtr, eax
mov eax, [esp + 0x14]
mov outgoingContentLen, eax
}
printSendBufferToLog();
__asm {
mov eax, teax; start to restore registers
mov ebx, tebx
mov ecx, tecx
mov edx, tedx
mov esi, tesi
mov edi, tedi
mov ebp, tebp
mov esp, tesp; end of restore
mov ebp, esp
sub esp, 0x18
jmp[jmpBackAddrSendNetMessage]
}
}
В этой функции мы сохраняем указатель на
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.
Ссылка на скачивание в этом сообщении:
https://www.rf-cheats.ru/forum/showp...5&postcount=28