Анализ сетевого протокола и безопасности

Техническая документация 4-этапной архитектуры взаимодействия клиента Doomsday: Last Survivors: от обхода WAF Anubis на веб-шлюзе до бинарного стриминга GameServer (TCP/Protobuf/ZLIB).

🛡 Исследование защитных механизмов и статус «античита»

Технический вывод анализа бинарных файлов:

В коде клиента Doomsday.exe, движка Unity и сборки GameAssembly.dll полностью отсутствуют сторонние античит-системы (EasyAntiCheat, BattlEye, NetEase Yidun, Tencent ACE, Denuvo, VMProtect). Игра работает на открытом бинарном сокете.

Ранее возникавшие гипотезы о наличии агрессивного античита были вызваны локальными сбоями перехвата Winsock:

Технические детали: доказательства отсутствия драйверов и модулей защиты

Проверка запущенных процессов и загруженных драйверов показала отсутствие:

  • Отсутствуют драйверы фильтрации сетевых пакетов (нет EasyAntiCheat.sys, BEDrive.sys, ACE-Base.sys).
  • Отсутствует перехват вызовов NtOpenProcess / NtReadVirtualMemory со стороны игры.
  • Фреймворк igg_framework взаимодействует напрямую с портами игрового кластера через стандартную библиотеку Python socket, не требуя даже запуска игрового клиента!
Технические детали: устройство WAF Anubis и генерация сессионных кук

Портал авторизации accounts.igg.com защищён проприетарным WAF Anubis (Cloudflare / Turnstile fork), который валидирует клиента перед выдачей формы ввода пароля:

  • Кука techaro.lol-anubis-auth-auth: JWT-токен с алгоритмом подписи EdDSA. Содержит поля action: "CHALLENGE", идентификатор челленджа и правило политики. При первом GET-запросе к странице логина WAF отдаёт HTML с JS-вычислением Proof-of-Work.
  • Кука PHPSESSID: Сессионная кука бэкенда PHP, связывающая состояние сессии с WAF токеном.
  • Кука _account_sso_token: Выдаётся при успешном вызове POST /login/ajax_login с полями email и password=md5(pass). Имеет срок жизни 30 суток (lifecycle=2592000).

Сниппет из core/login.py, реализующий валидацию WAF и сохранение сессии:

# Эмуляция браузерного контекста с WAF-куками для accounts.igg.com headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...", "Origin": "https://accounts.igg.com", "Referer": "https://accounts.igg.com/login?lang=rus&game_id=11200799071", "X-Requested-With": "XMLHttpRequest" } cookies = { "techaro.lol-anubis-auth-auth": anubis_cookie, "PHPSESSID": php_sess_id } # Пароль всегда хешируется в MD5 нижнего регистра перед отправкой payload = { "account": email, "password": hashlib.md5(password.encode("utf-8")).hexdigest(), "game_id": 11200799071 } resp = requests.post("https://accounts.igg.com/login/ajax_login", data=payload, headers=headers, cookies=cookies)

🔗 Архитектура сквозного протокола авторизации

Этап Протокол и хост Входные данные Результат этапа
1. Passport Web HTTPS accounts.igg.com Email + MD5(Password) + WAF Cookie PassportToken (JWT, живет 1800 сек)
2. API Gateway HTTPS gw-dls-na-login.igg.com PassportToken + HMAC X-GPC-* AccessKey (JWT, 3 дня) и SSOToken
3. LoginServer TCP Binary 188.43.58.101:9030 Кадр 0x279d (AccessKey, Udid, GameId) Кадр 0x279e (LoginSession, адрес кластера)
4. GameServer TCP Binary 204.141.172.24:12065 Кадр 0x279f (LoginSession, Hardware UDID) Кадр 0x27a0 (Успех) → Кадр 0x27a1 (Вход в мир)

🕑 Спецификация токенов авторизации и жизненный цикл (TTL)

Система авторизации IGG построена по многоуровневой каскадной модели. Каждый токен имеет строго ограниченную зону ответственности и срок жизни. Пока действует игровой токен (AccessKey), обращение к веб-порталу IGG и API Gateway не требуется — бот входит на игровой сервер напрямую за доли секунды.

Токен / Артефакт Кем выдается Формат Срок жизни (TTL) Назначение и использование Правило обновления
_account_sso_token accounts.igg.com (Cookie) JWT (HS256) 30 суток (2 592 000 с) Долгоживущая сессия веб-аккаунта в браузере / curl. Обновляется при полном вводе логина/пароля через Cold Login.
PassportToken accounts.igg.com/login JWT (HS256) 30 минут (1 800 с) Веб-паспорт персонажа для обращения к API Gateway. Запрашивается заново через Cold Login только когда истекает AccessKey.
SSOToken Kong Gateway (/api/v1/auth/sso) JWT (RS256) 3 суток (259 200 с) Сквозная единая авторизация игрока в сервисах IGG. Генерируется шлюзом в связке с AccessKey в обмен на PassportToken.
AccessKey Kong Gateway (/api/v1/auth/game_access) HMAC-токен 3 суток (259 200 с) Главный игровой токен для входа на LoginServer (кадр 0x279d). Переиспользуется во всех сессиях. Запрос к Gateway выполняется лишь раз в 3 дня.
LoginSession LoginServer (TCP 0x279e) HEX (32 символа) 60–120 сек (одноразовый) Временный тикет для авторизации на ноде GameServer (кадр 0x279f). Генерируется динамически при каждом входе в игру по действующему AccessKey.
GameSession GameServer (TCP Socket) Сокет TCP Время сессии Сетевой канал игрового процесса. Удерживается Heartbeat-пингом 0x2716 каждые 25 сек. Закрывается при logout.
Дамп кадра MsgCL2GSLoginRequest (0x279f) и структура Protobuf
// Protobuf схема сообщения MsgCL2GSLoginRequest (опкод 0x279f) message MsgCL2GSLoginRequest { uint64 PlayerId = 1; // Идентификатор персонажа игрока uint64 Account = 2; // Идентификатор аккаунта string LoginSession = 3; // Сессионный токен от LoginServer (32 символа hex) bool Certification = 5; // Флаг верификации bool Adult = 6; // Флаг совершеннолетия uint32 ConfigVersion = 8; // Версия конфигурации string Udid = 9; // Аппаратный идентификатор ПК string ClientVersion = 10; // Версия клиента игры (например "1.60.0") }
Технические детали: криптографический протокол подписи Kong API Gateway (HMAC-SHA256, X-GPC-*)

Все запросы к шлюзу gw-dls-na-login.igg.com подписываются кастомной схемой аутентификации Kong API Gateway. Запросы без валидной подписи отсекаются с HTTP 403 Forbidden.

  • X-GPC-Auth-Timestamp: UNIX-время в секундах (допустимое расхождение ±300 с).
  • X-GPC-Auth-Nonce: Уникальная случайная строка (UUIDv4 или 16 байт hex) для предотвращения атак повторного воспроизведения (Replay Attack).
  • X-GPC-Auth-AppId: Идентификатор клиента игры (11200799071).
  • X-GPC-Auth-Signature: Вычисленный дайджест HMAC-SHA256 в hex-формате.

Алгоритм канонизации строки подписи (String-To-Sign):

def calc_gpc_signature(method: str, path: str, params: dict, headers: dict, secret: str) -> str: # 1. Сортировка параметров запроса по алфавиту ключей sorted_params = "&".join(f"{k}={to_lower_pct(quote(str(v)))}" for k, v in sorted(params.items())) # 2. Канонизация URI canonical_uri = path.rstrip("/") # 3. Формирование строки подписи: METHOD + \n + URI + \n + PARAMS + \n + NONCE + \n + TIMESTAMP sign_str = f"{method.upper()}\n{canonical_uri}\n{sorted_params}\n{headers['X-GPC-Auth-Nonce']}\n{headers['X-GPC-Auth-Timestamp']}" # 4. HMAC-SHA256 хеширование с секретным клиентским ключом signature = hmac.new(secret.encode("utf-8"), sign_str.encode("utf-8"), hashlib.sha256).hexdigest() return signature
Технические детали: кадрирование потока TCP, структура фрейма DLS и распаковка ZLIB (0x04b3)

В отличие от HTTP, сетевой протокол Doomsday передается по «сырому» TCP-сокету (Raw TCP Stream). Стек не гарантирует совпадение границ системных вызовов recv() с границами сообщений, поэтому фреймворк реализует буферизованное кадрирование:

// Бинарный заголовок фрейма протокола DLS (4 байта, Little-Endian): ┌─────────────────────────┬─────────────────────────┬─────────────────────────────────────┐ │ Payload Length (u16LE) │ Opcode (u16LE) │ Protobuf Payload (N байт) │ │ (2 байта) │ (2 байта) │ (Length байт) │ └─────────────────────────┴─────────────────────────┴─────────────────────────────────────┘
  • Склейка пакетов (TCP Packing): Один вызов recv(65536) может вернуть несколько кадров подряд. Функция split_stream_to_frames последовательно считывает length, отрезает фреймы и сдвигает указатель буфера.
  • Фрагментация (TCP Fragmentation): Если в буфере накопилось меньше 4 + length байт, цикл не удаляет хвост, а ожидает следующую порцию данных из сокета.
  • Сжатые контейнеры 0x04b3 (MsgServerPacketBundle): Сервер упаковывает множественные обновления состояния в архив ZLIB (DEFLATE). Фреймворк считывает длину несжатых данных, распаковывает архив через zlib.decompress() и рекурсивно парсит полученный поток байт как серию независимых фреймов DlsFrame.
Технические детали: парсинг адреса LoginServer (0x279e), формула портов кластера и разделение UDID

Критический этап авторизации — правильный переход от LoginServer к GameServer. Здесь решены две ключевые проблемы протокола:

  • Вложенный Protobuf поля 2 в ответе 0x279e: Поле net_address_raw не содержит двоеточия : в ASCII, а представляет собой вложенное сообщение с тегами:
    Tag 1 (Varint): внутренний порт игрового узла (например, 11005);
    Tag 2 (String): внутренний IP-адрес игрового узла (например, "10.246.40.6").
  • Формула внешнего порта: Игровой кластер защищен NAT/Edge шлюзом. Внешний порт рассчитывается со смещением:
    external_port = in_port + 1060 (например: 11005 + 1060 = 12065).
    Попытка подключиться к базовому узлу 12064 с токеном, выписанным для узла 12065, вызывает фатальную ошибку 1007 KEcloginSessionTimeout.
  • Разделение ролей UDID (предотвращение ошибок 1013 и 1007):
    web_udid (48 символов): Передаётся в API Gateway и кадре 0x279d на LoginServer. Передача аппаратного UDID на LoginServer приводит к ErrorCode 1013.
    hardware_udid (40 символов SHA-1): Передаётся в кадре 0x279f на GameServer. Передача веб-UDID на GameServer приводит к ErrorCode 1007.

🔄 Спецификация ключевых опкодов DLS

Опкод Hex Направление Имя Protobuf сообщения Назначение
10006 0x2716 C → S MsgCL2GSHeartBeatRequest Пинг для поддержания активности TCP-сессии (каждые 25–30 сек)
10007 0x2717 S → C MsgGS2CLHeartBeatReply Ответ сервера на пинг (Heartbeat ACK)
10141 0x279d C → S MsgCL2LSLoginRequest Запрос сессии и адреса игрового кластера на LoginServer 9030
10142 0x279e S → C MsgLS2CLLoginReply Ответ LoginServer с токеном LoginSession и портом кластера
10143 0x279f C → S MsgCL2GSLoginRequest Авторизация сокета на GameServer 12060+
10144 0x27a0 S → C MsgGS2CLLoginReply Подтверждение авторизации канала (билд сервера, ErrorCode)
10145 0x27a1 C → S MsgCL2GSEnterGameRequest Запрос входа в игровой мир
10146 0x27a2 S → C MsgGS2CLEnterGameReply Подтверждение входа в мир
10481 0x28f1 C → S MsgCl2GscollectResourceRequest Сбор ресурсов указанного типа (Type=2..5: нефть, еда, дерево, сталь) со всех ферм/вышек базы сразу
10482 0x28f2 S → C MsgGs2ClcollectResourceReply Ответ с собранным количеством (Value: uint64, ErrorCode: int32)
10117 0x2785 C → S MsgCL2GSCollectResourceRequest Точечный сбор ресурсов по списку идентификаторов зданий базы
10118 0x2786 S → C MsgGS2CLCollectResourceReply Результат сбора ресурсов базы по списку зданий
10630 0x2986 C → S MsgCL2GSGuildGiftClaimAllRequest Забор всех доступных подарков альянса
10631 0x2987 S → C MsgGS2CLGuildGiftClaimAllReply Результат сбора подарков альянса
1203 0x04b3 S → C MsgServerPacketBundle Сжатый ZLIB контейнер с серией внутренних пакетов DlsFrame
Технические детали: динамический парсер и сериализатор Protobuf без компиляции .proto файлов

В отличие от стандартного компилятора protoc, требующего статических *_pb2.py файлов на каждую команду, фреймворк реализует динамический кодек (модули core/game.py и core/opcodes.py) на основе бинарного стандарта Google Protocol Buffers:

  • Парсинг тегов (Tag & WireType): Каждый байт-маркер поля декодируется операциями field_number = tag >> 3 и wire_type = tag & 0x7.
  • Поддержка Wire Types:
    WireType 0 (Varint): декодируется 7-битным сдвигом decode_varint() (int32, int64, bool, enums, ErrorCode);
    WireType 2 (Length-delimited): строки UTF-8, вложенные Protobuf-сообщения и массивы байт;
    WireType 1 (64-bit fixed) и WireType 5 (32-bit fixed): IEEE-754 числа и фиксированные int.
  • Универсальное извлечение ErrorCode: Сервер Doomsday во всех ответах помещает код ошибки в тег 99. Метод lr.check_error_code(reply) автоматически валидирует тег 99 без необходимости знания остальной структуры ответа.
  • Автоматическая сериализация именованных аргументов: Вызов lr.send_and_wait("KMsgCl2GscollectResourceRequest", Type=2) берет метаданные из data/opcodes.json (номер поля 1, тип enum), преобразует Python-значения в бинарные varint/байты и собирает готовый payload кадра.