[Из песочницы] У «Казаков» секретов нет
Думаю, многие из читателей с добрым словом вспомнят серию игр «Казаки», многочасовые баталии, военные хитрости и бесподобное звуковое сопровождение — отличная стратегия своего времени.
Спустя 15 лет они вернулись, и теперь уже в режиме онлайн, о проблемах и уязвимостях новой версии и пойдет речь в данной статье.
Дисклеймер
Все исследования проводились исключительно в благих целях, правообладатель и разработчик был заведомо уведомлен о найденных проблемах, статья несет поучительных характер и не призывает к активной эксплуатации описанных проблем.
Структура сетевых пакетов
Все началось с простого вопроса — «как шифруются сетевые данные?», после отлова первого же пакета ответ стал очевиден — «никак». Никаких xor операций, никаких подписей, честные и правдивые байты.
В этот же момент (после осознания простоты возможного анализа), было принято решение идти дальше и попытаться воссоздать примитивную работу сервера (вход, регистрация, восстановление пароля), для этого требовалось понять:
- Что представляет из себя «шапка» пакета
- Как происходит сериализация и десериализация данных
- Где хранятся данные о серверах (IP, порт)
После отлова очередного десятка пакетов «шапка» стала яркой и весьма четкой:
struct NetPacketHeader {
unsigned int Size; // размер чистых (без учета самой шапки) данных пакета
unsigned char Direction; // уникальный идентификатор пакета
unsigned char Mode; // предположительно описывает режим передачи пакета - броуд или приват
unsigned int SessionId0; // идентификатор пользователя, выданный сервером при входе
unsigned int SessionId1; // тоже самое, но для цели, например: приватного чата
};
Тем же путем были выявлены основные черты сериализации:
- Запись и чтение выполняется с помощью специальных классов (изначальная догадка была о том, что это просто приведенные структуры)
- Строки бывают двух типов — малые и большие, размер строки указывается перед ее началом, у первых он — один байт, у вторых — два байта
- Числовые данные записываются как есть (плавающие встретить не удалось)
- Имеются специальные блоки, заполненные строго нулями, служат разделителем: 4 байта — конец массива, 6 байт — конец записи (изначально казалось, что это смещения структуры от компилятора)
А список серверов был найден обычным поиском по названию (data\resources\servers.dat), хранителем данных оказался читабельный скрипт собственного производства.
По завершению этих шагов разложение пакетов начало сводиться только к усидчивости и внимательности, например:
3D 00 00 00 9A 01 00 00 00 00 00 00 00 00 07 31 2E 30 2E 30 2E 37 05 31 2E 32 2E 33 14 61 61 61 61 61 61 61 61 61 61 40 67 6D 61 69 6C 2E 63 6F 6D 0A 61 61 61 61 61 61 61 61 61 61 0E 39 30 30 30 2D 38 30 30 30 2D 35 30 30 30
Size: 3D 00 00 00 (всего в пакете 75 байт, но 61 байт является телом, а 14 других шапкой)
Direction: 9A
Mode: 01
SessionId0: 00 00 00 00
SessionId1: 00 00 00 00VersionStringSize: 07
VersionString: 31 2E 30 2E 30 2E 37 (1.0.0.7)
UpdateStringSize: 05
UpdateString: 31 2E 32 2E 33 (1.2.3)
EmailStringSize: 14
EmailString: 61 61 61 61 61 61 61 61 61 61 40 67 6D 61 69 6C 2E 63 6F 6D (aaaaaaaaaa@gmail.com)
PasswordStringSize: 0A
PasswordString: 61 61 61 61 61 61 61 61 61 61 (aaaaaaaaaa)
GameKeyStringSize: 0E
GameKeyString: 39 30 30 30 2D 38 30 30 30 2D 35 30 30 30 (9000–8000–5000)
Таков пакет запроса входа на сервер от клиента.
Работа с лобби
За короткий промежуток времени был сделан примитивный сервер (на основе asio), способный общаться с оригинальным клиентом, за час с небольшим он безупречно мог:
- Обработать запрос входа
- Обработать запрос регистрации
- Обработать запрос восстановления пароля
- Обработать приватное и публичное чат сообщение
- Выдать список пользователей на сервере, а так же лобби
Столь быстрая реализация бодрила лучше любого кофе и было решено потратить остаток ночи на работу с лобби, а именно:
- Создание / обновление / удаление
- Синхронизация пользователей (цвет флага, страна и пр.)
- Открытие / закрытие слотов создателем
С первым же пунктом начались не совсем понятные (по началу) проблемы:
Получилось отловить и разложить три пары (запрос клиента и ответ сервера) пакетов — создание, обновление (что включало в себя и удаление), а так же вход в публичное лобби.
Именно последнее и вызывало головные боли, вход в публичное лобби проходил на «ура», а вот в приватное не совсем, было не ясно, где искать введенный пользователем пароль, что-бы проверить верность данных, ведь пакет один — как для входа в публичное, так и для входа в приватное лобби, и он содержит лишь шапку и одно целое (идентификатор сессии создателя лобби), за объяснениями пришлось лезть «под капот», но не привыкшие к результату компиляции Delphi кода глаза ничего толкового так и не нашли.
В конечном итоге стало очевидным — сервер никак не обрабатывает пароли лобби, от слова «совсем», а значит в теории было возможным зайти в любое лобби и на оригинальном сервере, ведь клиент получает пароль в чистом виде.
Теория была доказана в три шага:
Все верно — приватное == публичное.
Остальные пункты повестки ночи прошли относительно без особых затруднений, синхронизация пользователей — простое зеркало, от одного пользователя ко всем участникам лобби, закрытие слота (например, что-бы выгнать участника из лобби) вызвало небольшое беспокойство, сервер принимает запрос на закрытие слота, если слот был занят участником — выгоняет его, посылая оповещение, но, если игнорировать пакет на стороне клиента — визуально мы остаемся в лобби, не теряя связь, вызывает ли это проблемы с запуском игры у оставшихся участников — хороший вопрос, ответ на который получить в такое время суток не удалось.
Так же не малое беспокойство вызвало и выдача имени ПК создателя лобби, зачем оно вообще выдается участникам, если создатель != хост — вопрос, на который еще предстоит ответить.
По окончанию ночи удалось дойти до входа в игру, из десяти попыток синхронизации геймплея успехом завершилась лишь одна, и та была успешна лишь от части, один игрок не получает данных о действиях другого, что вызывает асинхронизацию и приводит к спешному ступору игры, а значит работы еще много.
Подводя итоги
Любой программист, которому приходилось доводить сетевое приложение до публики так или иначе ощутил на себе главное правило: никогда не доверять клиенту.
Каждое действие клиента должно иметь одобрение сервера, иначе есть существенный риск, если и не загубить весь проект, то заложить в него мины, которые будут взрываться в самое неожиданное время, хороший пример, но плохой сетевой работы — игра MU Online, ей более десяти лет, а тривиальные проблемы клонирования игровых предметов (с помощью манипуляций пакетами) привели к тому, что пришлось отключить функционал персонального магазина.
Отдельная тема для дискуссий это сохранность персональных данных, как можно понять из описанных примеров: такой подход, передачи чистых байтов, а особенно строк — большой грех для любой компании работающей в информационной сфере, прямой путь в ад к краже аккаунтов.
→ Пример на GitHub