Pull to refresh

У «Казаков» секретов нет

Reading time 4 min
Views 37K
image

Думаю, многие из читателей с добрым словом вспомнят серию игр «Казаки», многочасовые баталии, военные хитрости и бесподобное звуковое сопровождение — отличная стратегия своего времени.

Спустя 15 лет они вернулись, и теперь уже в режиме онлайн, о проблемах и уязвимостях новой версии и пойдет речь в данной статье.

Дисклеймер


Все исследования проводились исключительно в благих целях, правообладатель и разработчик был заведомо уведомлен о найденных проблемах, статья несет поучительных характер и не призывает к активной эксплуатации описанных проблем.

Структура сетевых пакетов


Все началось с простого вопроса — «как шифруются сетевые данные?», после отлова первого же пакета ответ стал очевиден — «никак». Никаких xor операций, никаких подписей, честные и правдивые байты.

В этот же момент (после осознания простоты возможного анализа), было принято решение идти дальше и попытаться воссоздать примитивную работу сервера (вход, регистрация, восстановление пароля), для этого требовалось понять:

  1. Что представляет из себя «шапка» пакета
  2. Как происходит сериализация и десериализация данных
  3. Где хранятся данные о серверах (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 00

VersionStringSize: 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), способный общаться с оригинальным клиентом, за час с небольшим он безупречно мог:

  1. Обработать запрос входа
  2. Обработать запрос регистрации
  3. Обработать запрос восстановления пароля
  4. Обработать приватное и публичное чат сообщение
  5. Выдать список пользователей на сервере, а так же лобби

Столь быстрая реализация бодрила лучше любого кофе и было решено потратить остаток ночи на работу с лобби, а именно:

  1. Создание / обновление / удаление
  2. Синхронизация пользователей (цвет флага, страна и пр.)
  3. Открытие / закрытие слотов создателем

С первым же пунктом начались не совсем понятные (по началу) проблемы:

Получилось отловить и разложить три пары (запрос клиента и ответ сервера) пакетов — создание, обновление (что включало в себя и удаление), а так же вход в публичное лобби.

Именно последнее и вызывало головные боли, вход в публичное лобби проходил на «ура», а вот в приватное не совсем, было не ясно, где искать введенный пользователем пароль, что-бы проверить верность данных, ведь пакет один — как для входа в публичное, так и для входа в приватное лобби, и он содержит лишь шапку и одно целое (идентификатор сессии создателя лобби), за объяснениями пришлось лезть «под капот», но не привыкшие к результату компиляции Delphi кода глаза ничего толкового так и не нашли.

В конечном итоге стало очевидным — сервер никак не обрабатывает пароли лобби, от слова «совсем», а значит в теории было возможным зайти в любое лобби и на оригинальном сервере, ведь клиент получает пароль в чистом виде.

Теория была доказана в три шага:

Получение пароля нужной комнаты
image

Проверка связи
image

Реакция пользователей
image

Все верно — приватное == публичное.

Остальные пункты повестки ночи прошли относительно без особых затруднений, синхронизация пользователей — простое зеркало, от одного пользователя ко всем участникам лобби, закрытие слота (например, что-бы выгнать участника из лобби) вызвало небольшое беспокойство, сервер принимает запрос на закрытие слота, если слот был занят участником — выгоняет его, посылая оповещение, но, если игнорировать пакет на стороне клиента — визуально мы остаемся в лобби, не теряя связь, вызывает ли это проблемы с запуском игры у оставшихся участников — хороший вопрос, ответ на который получить в такое время суток не удалось.

Так же не малое беспокойство вызвало и выдача имени ПК создателя лобби, зачем оно вообще выдается участникам, если создатель != хост — вопрос, на который еще предстоит ответить.

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

Подводя итоги


Любой программист, которому приходилось доводить сетевое приложение до публики так или иначе ощутил на себе главное правило: никогда не доверять клиенту.

Каждое действие клиента должно иметь одобрение сервера, иначе есть существенный риск, если и не загубить весь проект, то заложить в него мины, которые будут взрываться в самое неожиданное время, хороший пример, но плохой сетевой работы — игра MU Online, ей более десяти лет, а тривиальные проблемы клонирования игровых предметов (с помощью манипуляций пакетами) привели к тому, что пришлось отключить функционал персонального магазина.

Отдельная тема для дискуссий это сохранность персональных данных, как можно понять из описанных примеров: такой подход, передачи чистых байтов, а особенно строк — большой грех для любой компании работающей в информационной сфере, прямой путь в ад к краже аккаунтов.

Пример на GitHub
Tags:
Hubs:
+46
Comments 14
Comments Comments 14

Articles