Анализ сетевого протокола и безопасности
Техническая документация 4-этапной архитектуры взаимодействия клиента Doomsday: Last Survivors: от обхода WAF Anubis на веб-шлюзе до бинарного стриминга GameServer (TCP/Protobuf/ZLIB).
🛡 Исследование защитных механизмов и статус «античита»
Технический вывод анализа бинарных файлов:
В коде клиентаDoomsday.exe, движка Unity и сборки GameAssembly.dll полностью отсутствуют сторонние античит-системы (EasyAntiCheat, BattlEye, NetEase Yidun, Tencent ACE, Denuvo, VMProtect). Игра работает на открытом бинарном сокете.
Ранее возникавшие гипотезы о наличии агрессивного античита были вызваны локальными сбоями перехвата Winsock:
- Краши при инжекте Frida: Падения происходили исключительно из-за попытки хука низкоуровневых функций
ws2_32.dll!sendиws2_32.dll!recvсо смещением контекста потоков Windows APC, а не из-за античит-драйверов. - Чистый PE-заголовок: Бинарный файл
GameAssembly.dllне содержит упакованных секций или виртуализации кода. Экспортирует стандартный набор вызовов Unity IL2CPP. - Где на самом деле сосредоточена защита:
- WAF Anubis (на
accounts.igg.com): Web Application Firewall, фильтрующий запросы к веб-порталу авторизации по кукамtecharo.lol-anubis-auth-auth. Полностью воспроизведен вcore/login.py. - HMAC-SHA256 подписи Gateway (
X-GPC-*): Защита API Gateway Kong от подделки HTTP-запросов обмена токенов. Алгоритм полностью реверсирован и реализован.
- WAF Anubis (на
Технические детали: доказательства отсутствия драйверов и модулей защиты
Проверка запущенных процессов и загруженных драйверов показала отсутствие:
- Отсутствуют драйверы фильтрации сетевых пакетов (нет
EasyAntiCheat.sys,BEDrive.sys,ACE-Base.sys). - Отсутствует перехват вызовов
NtOpenProcess/NtReadVirtualMemoryсо стороны игры. - Фреймворк
igg_frameworkвзаимодействует напрямую с портами игрового кластера через стандартную библиотеку Pythonsocket, не требуя даже запуска игрового клиента!
Технические детали: устройство 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 и сохранение сессии:
🔗 Архитектура сквозного протокола авторизации
| Этап | Протокол и хост | Входные данные | Результат этапа |
|---|---|---|---|
| 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
Технические детали: криптографический протокол подписи 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):
Технические детали: кадрирование потока TCP, структура фрейма DLS и распаковка ZLIB (0x04b3)
В отличие от HTTP, сетевой протокол Doomsday передается по «сырому» TCP-сокету (Raw TCP Stream). Стек не гарантирует совпадение границ системных вызовов recv() с границами сообщений, поэтому фреймворк реализует буферизованное кадрирование:
- Склейка пакетов (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 кадра.