Как я восстановил LAN-кооператив в Neverwinter Nights 2 на Switch

Прокомментировать Просмотры: 6

У нас с супругой зародилась традиция: по вечерам залипать в кооперативные проекты, каждый со своей консоли Nintendo Switch. После прохождения Divinity захотелось чего-то аналогичного, и выбор пал на вечную классику — Neverwinter Nights 2 (оригинальную версию 2006 года). Запустив игру, кликнули «Мультиплеер» и закономерно уткнулись в системное требование авторизоваться в аккаунте Nintendo и приобрести подписку NSO.

Поскольку наши консоли модифицированы (используется Atmosphere), официальный онлайн отпадает сам собой. Самое любопытное, что в ПК-версии NWN2 всегда присутствовали стандартная игра по локальной сети и прямое подключение по IP-адресу. Мне стало чертовски интересно проверить лень разработчиков: остался ли этот сетевой функционал в порте для Switch или его безжалостно вырезали.

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

Готовый патч доступен в репозитории: https://github.com/AnDyQnn/nwn2-switch-lan

Патч предназначен исключительно для консолей с кастомной прошивкой Atmosphere. Он не открывает доступ к официальному онлайну Nintendo, а лишь активирует локальную сеть, которая изначально заложена в игру.

Инструменты

  • Дамп игры, полученный через утилиту DBI (базовый файл плюс обновление v65536).

  • hactool / nstool в комплекте с персональными prod.keys для извлечения исполняемого файла exefs/main (NSO).

  • Python и библиотека capstone для реализации дизассемблирования. Полноценной IDA под рукой не оказалось, но для поставленной задачи с лихвой хватило пары компактных скриптов примерно на 50 строк, которые Claude оперативно написал на лету.

  • Claude Code — терминальный ИИ-помощник, способный автономно выполнять команды, анализировать логи и писать код.

  • Atmosphere: директория exefs_patches для применения IPS-патчей кода, а также LayeredFS (atmosphere/contents//romfs) для подмены ресурсов.

  • Автономный gdbstub от Atmosphere вкупе с aarch64-none-elf-gdb (из состава Arm GNU Toolchain), который стал главным действующим лицом во второй половине нашего расследования.

Распаковка NSO

Исполняемый файл main внутри формата NSO сжато алгоритмом LZ4 по сегментам. Проще всего развернуть его в монолитный образ, где смещения точно соответствуют виртуальным адресам внутри модуля:

import struct, lz4.block as L

d = open('exefs/main', 'rb').read()
flags = struct.unpack_from('

Дальнейший анализ производился непосредственно над main.bin: импорты считываются из цепочки MOD0 → .dynamic → DT_SYMTAB/DT_JMPREL, вызовы системного SDK маппятся посредством PLT-заглушек (конструкции вида adrp x16 / ldr x17 / br x17), а текстовые строки отыскиваются по комбинации инструкций adrp + add. Из этих кирпичиков Claude за пару минут собрал подобие «карманной IDA» — простейший дизассемблер, автоматически подписывающий вызовы системных функций и строковые константы.


Слой 1. «Требуется учетная запись Nintendo»

В таблице импортов ожидаемо обнаружились следующие вызовы:

nn::nifm::IsNetworkAvailable()
nn::account::EnsureNetworkServiceAccountAvailable(UserHandle const&)
nn::account::EnsureNetworkServiceAccountIdTokenCacheAsync(...)

Функция EnsureNetworkServiceAccountAvailable и порождает то самое назойливое системное окно авторизации. Её вызывает функция-поллер, ожидающая код состояния 0 (успех), 1 (ошибка) либо 2 (процесс выполнения):

0x3ccaac  bl   nifm::IsNetworkAvailable
0x3ccab0  tbz  w0, #0, 0x3ccae4
0x3ccab4  ldr  x8, [x19, #0x150]          ; <- патчим здесь
0x3ccab8  mov  x0, sp
...
0x3ccacc  bl   account::EnsureNetworkServiceAccountAvailable

Суть исправления: при наличии сетевого подключения сразу отрапортовать об успехе.

0x3ccab4  mov  w0, wzr
0x3ccab8  b    0x3cca80                   ; переход на эпилог с возвратом нуля

Тест на консоли показал закономерный результат: старое окно пропало, но тут же всплыло новое: «К сожалению, сетевые службы в настоящий момент недоступны». Классическое подземелье — открываешь створку, а за ней притаился мимик.

Слой 2. «Онлайн-проверки не завершены»

Текст этого сообщения был локализован по смещению строки (strref 235397) в файле dialog.tlk, а сам адрес в бинарнике нашелся по связке инструкций mov w1, #0x9785 / movk w1, #3, lsl #16. Обработчик нажатия на кнопку «Мультиплеер» верифицирует, пройдены ли «все сетевые этапы платформы»:

0x3c8e40  ldp  x8, x9, [x0, #0x48]        ; массив статусов проверки
...                                        ; все ли флаги установлены в done?

Поскольку эта функция задействована более чем в дюжине сценариев (включая браузер серверов и инициализацию соединения), проще и надежнее пропатчить её возвращаемое значение:

0x3c8e40  mov  w0, #1
0x3c8e44  ret

Слой 3. Фотонный барьер и токен Nintendo

Далее активируемый пункт «Мультиплеер» обращается к облачному сервису матчмейкинга Photon (от компании Exit Games), причем интерфейсный экран мультиплеера разблокируется исключительно по коллбэку успешной авторизации. Для входа клиент передает Photon аутентификационный токен Nintendo вида token=, который сервер валидирует на стороне вендора. Обойти эту проверку силами одного клиента физически невозможно — сервер Photon просто не примет поддельную заглушку.

Однако для чисто локальной игры (LAN) Photon вовсе не требуется. Успешный коллбэк делает лишь одно — открывает требуемый графический интерфейс по его имени. Следовательно, мы можем форсировать открытие экрана напрямую, попутно инициируя системный сетевой запрос (nifm::SubmitNetworkRequest), без которого сетевые сокеты на Switch попросту не инициализируются:

; StartMultiplayer, обходим вызов connect(Photon)
0x622b44  ldr  x8, [x22]                  ; синглтон менеджера платформы
0x622b48  ldr  x0, [x8, #0x48]
0x622b4c  bl   0x3cc9cc                   ; pctl::TryBeginFreeCommunication + nifm::SubmitNetworkRequest
0x622b64  adrp x8, ...                    ; указатель на GUI
0x622b6c  ldr  x0, [x8]
0x622b70  sub  x1, x29, #0x10             ; имя целевого экрана из аргумента скрипта
0x622b74  bl   0x678dc4                   ; функция OpenScreen
0x622b78  b    0x622be4                   ; завершение и очистка

Меню мультиплеера успешно распахнулось, но нужных пунктов «Локальная сеть» и «Прямое подключение» в нем не оказалось.

Слой 4. Интерфейсные ограничения: «только для PC»

Элементы интерфейса в NWN2 описываются стандартными XML-документами в директории romfs. Искомые ограничения обнаружились немедленно:



Используя механизм LayeredFS (по пути atmosphere/contents/01008E601E03C000/romfs/ui/default/enhanced/), мы внедрили модифицированные XML-файлы:

  • сняли запрет платформенного скрытия (hideonplatforms) с функции Direct Connect;

  • принудительно активировали вкладку LAN_TAB, назначив ее дефолтной;

  • интегрировали в интерфейс недостающую кнопку LAN.

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

Слой 5. Клиент и сервер все еще завязаны на Photon

Проверить, действительно ли хост ожидает входящие подключения, можно с обычного ПК. Семейство движков NWN/NWN2 ищет локальные серверы путем отправки широковещательного пакета BNES на UDP-порт 5121:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
s.sendto(b'BNES\x00\x14\x00', ('192.168.1.15', 5121))
try:
    print(s.recvfrom(4096))
except ConnectionResetError:
    print('closed')   # ICMP port unreachable — порт закрыт, никто не слушает

Результат: ответ closed. Сервер де-факто молчит. Началось расследование механизма выбора сетевого протокола. Была локализована функция инициализации сессии, поддерживающая три режима:

; CreateGameAndConnect(..., mode)
cmp  w24, #2   -> вызов netlayer.SetPhoton()
cmp  w24, #1   -> вызов netlayer.SetUDP(port)
     0         -> одиночная игра

Режим вычислялся как выражение 1 + ini["Server Options"]["GameSpy Enabled"]:

0x650470  cmp  w0, #0
0x650474  mov  w8, #1
0x650478  cinc w4, w8, ne        ; принудительно -> mov w4, #1

Параллельно в серверном модуле (servercore) тип сетевого стека определялся тернарным условием Photon ? Photon : UDP(Game Port):

0x8a4368  tbz  w19, #0, 0x8a4374 ; патчим в безусловный переход -> b 0x8a4374 (всегда UDP)
0x8a436c  bl   SetPhoton
0x8a4374  mov  w1, w21           ; игровой порт, по умолчанию 5121
0x8a4378  bl   SetUDP

Обе точки были успешно пропатчены (плюс аналогичный участок при загрузке сетевых сохранений). Однако порт 5121 упорно оставался недоступным.

Слой 6. Отладка через GDB на «живой» консоли

Строить дальнейшие догадки по статическому коду стало бесперспективно — потребовалось тяжелое вооружение. Мы попытались запустить игру в эмуляторе Eden, но сталкивались либо с ограничением в 60 FPS, либо с черным экраном, так что от этой идеи пришлось отказаться. К счастью, прошивка Atmosphere содержит встроенный gdbstub, который активируется всего двумя строчками в конфигурационном файле atmosphere/config/system_settings.ini:

[atmosphere]
enable_standalone_gdbstub=u8!0x1
enable_htc=u8!0x0

После перезагрузки консоль начинает слушать TCP-порт 22225, к которому без проблем коннектится стандартный отладчик aarch64-none-elf-gdb:

(gdb) target extended-remote 192.168.1.15:22225
(gdb) info os processes
...
139        NWN2_EE
(gdb) attach 139
(gdb) monitor get info
Modules:
  0x76ac005000 - 0x76acb26fff nwmain.nss

Трудность заключалась в наличии ASLR (рандомизации памяти), из-за чего базовый адрес модуля менялся при каждом запуске. Эту проблему Claude решил, написав скрипт, считывающий актуальную базу через команду monitor get info и генерирующий для GDB набор «тихих» брейкпоинтов с выводом регистров и автоматическим продолжением выполнения (continue). Производительность практически не падала, а консольные логи пролили свет на происходящее:

break *0x76ac44aa80
commands
silent
printf "HIT Netlayer.SetUDP port=%lx lr=%lx\n", $x1, $x30
continue
end

Первый же тестовый прогон расставил все точки над i:

HIT CreateGameAndConnect mode=0        <- игра считает, что запущена одиночная кампания?!
HIT ServerNet UDP path port=1401       <- порт 5121 передан верно, патч сработал
HIT Netlayer.SetUDP port=1401
HIT Netlayer.SetUDP exit               <- мгновенный выход без создания сокета

Выяснилось, что обе функции обращаются к определенному полю глобального объекта приложения, отвечающему за «режим мультиплеера» (находится по смещению app+0x2C). На оригинальном Switch это значение устанавливалось в процессе авторизации через Photon, которую мы безжалостно вырезали. В результате флаг оставался нулевым, движок воспринимал сессию как соло-игру, а процедура SetUDP молча прерывала работу. Проблема решается добавлением одной инструкции записи в нашем перехватчике нажатия кнопки:

0x622b50  adrp x8, 0x3402000
0x622b54  add  x8, x8, #0xf0
0x622b58  ldr  x8, [x8]           ; получаем указатель на app
0x622b5c  mov  w9, #1
0x622b60  str  w9, [x8, #0x2c]    ; принудительно устанавливаем multiplayer = 1

Повторный прогон выдал обнадеживающий результат:

HIT CreateGameAndConnect mode=1
HIT UdpInit port=1401
HIT UdpInit OK

А проверка с ПК:

REPLY b'BNERU\x01\x14\x00\x07Pipiska'
REPLY b'BNXR...1100_west_harbor'

Сервер откликнулся по локальной сети, сообщив свое имя и текущий модуль. Победа!

Слой 7. «Имя игрока уже занято»

Вторая консоль успешно обнаружила сервер, но в момент коннекта соединение обрывалось:

Сервер обнаружен. Выполняется подключение...
Ошибка связи: указанное имя игрока уже используется.

Хотя в настройках профилей были указаны разные никнеймы, мы развернули отладку одновременно на обеих консолях (пара сеансов GDB параллельно) и пошагово прошли всю цепочку рукопожатия сетевого протокола NWN: BNCS → BNCR → BNVS → BNVR, удерживая хост на одном устройстве и подключаясь со второго.

SRV HIT BNVS handler conn=1
SRV HIT Validate ret w0=6        ; код 6 соответствует строке 10289 в tlk: «имя уже используется»

Анализ функции проверки нового участника сессии:

0x447900  bl   0x4472c0          ; проверка наличия игрока с идентичным логином
0x447904  cmp  w0, #1
0x447908  b.ne 0x44793c          ; меняем на безусловный переход -> b 0x44793c
0x44790c  mov  w21, #6

Выяснилось, что логин на Switch генерируется не из никнейма профиля, а считывается из общего для всех консолей внутреннего буфера сетевого слоя. Из-за этого система считала попытку подключения клонированием текущего игрока. Снятие этого ограничения позволило обоим устройствам успешно пройти валидацию (Validate ret w0=0). Жена благополучно зашла на сервер, и мы наконец встретились в одной игровой локации.

Итоговый патч

Кастомная прошивка Atmosphere поддерживает формат патчей IPS32. Смещения в нем рассчитываются как оригинальный адрес в NSO плюс сдвиг заголовка 0x100:

def ips32(recs):
    out = b'IPS32'
    for off, data in sorted(recs.items()):
        out += struct.pack('>I', off + 0x100) + struct.pack('>H', len(data)) + data
    return out + b'EEOF'

Каждая модификация перед сборкой верифицируется скриптом: исходные байты по целевому адресу должны в точности соответствовать ожидаемым инструкциям, в противном случае сборка прерывается, что защищает от применения патча к чужой версии игры. Имя файла формируется на основе build ID: atmosphere/exefs_patches/nwn2_lan/3554C29951DDA41FE0AF61D3888DC3BB019764CA.ips.

  • 0x3ccab4 — отказ от вызова системного окна авторизации Nintendo

  • 0x3c8e40 — принудительный флаг успешного завершения онлайн-проверок

  • 0x622b44 — запуск мультиплеера в обход Photon, инициализация nifm и установка сетевого флага

  • 0x650478 — привязка создания новой сетевой игры к протоколу UDP

  • 0x5e8fc4 — аналогичная привязка для загрузки сетевых сохранений

  • 0x8a4368 — принудительный запуск сервера на UDP-порту 5121

  • 0x447908 — отключение блокировки дублирующихся логин-имен

Бонус: графические настройки

В процессе ковыряния в файлах выяснилось, где именно порт для Switch хранит параметры графики: конфигурационный файл nwn2.ini располагается непосредственно в папке с сохранениями. Оказалось, что настройки сильно занижены разработчиками:

NumberOfTextureMipLevelsToSkip=1   ; текстуры принудительно сжаты вполовину разрешения!
TextureMaxAnisoTropy=2
FarShadows=0
CharacterDropShadows=0
FarClipPlaneModifier=0.700000
WaterReflections=0
MultiSampleType=0

Мы выкрутили все ползунки на максимум: полные текстуры без пропуска мип-мапов, 16-кратная анизотропная фильтрация, детальные тени с картами 2048, 4x MSAA, HDR-рендеринг, отражения на воде и дальность прорисовки 100%. Консоль справляется с этим без малейших просадок — игра 2006 года для нее не нагрузка, а визуальный ряд преобразился кардинально. Готовый пресет настроек выложен в репозитории в директории extras/.

Нерешенные нюансы

  • Вкладка LAN в браузере серверов пока остается пустой: широковещательные пакеты в рамках инфраструктуры Switch не доходят до адресата. Тем не менее, прямое подключение по IP работает безупречно, чего нам вполне достаточно.

  • После единичного входа в меню мультиплеера игра удерживает активированный сетевой режим вплоть до перезапуска.

  • Возможность играть через официальный интернет (Photon) мы намеренно не восстанавливали, так как задачи такой не стояло.

Каково это — заниматься реверсом с помощью ИИ

Честный взгляд со стороны человека, который до этого никогда серьезно не занимался обратной разработкой.

Что удалось на славу:

  • Claude самостоятельно писал и выполнял скрипты: распаковывал NSO, сканировал бинарники на наличие строк и точек вызова SDK, собирал IPS-патчи и генерировал точки останова с учетом ASLR. Мне оставалось лишь анализировать готовые результаты, дописывать нужные фрагменты кода и координировать дальнейшие шаги.

  • Когда я поделился ссылкой на форумное обсуждение сетевого стека NWN, ассистент мгновенно сопоставил теорию с кодом: пакет BNES, порт 5121 и валидация на ПК-клиенте.

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

С чем возникли трудности:

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

  • Не обошлось без казусов: во время очередной «уборки» мусора скрипт случайно удалил дамп базы данных игры. Пришлось заново тратить время на повторный дамп 21 ГБ. Пару раз я тратил часы на проверки гипотез, которых можно было избежать.

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

Резюме: статический анализ прекрасно показывает, что заложено в бинарнике, но на вопрос почему эта чертовщина не работает отвечает исключительно живой отладчик. Встроенный в Atmosphere gdbstub для таких задач — настоящий подарок судьбы.

Заключение

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

Патч, подробный README и графический пресет доступны по ссылке: https://github.com/AnDyQnn/nwn2-switch-lan (лицензия MIT). Если у кого-то установлена иная версия игры, смело создавайте Issues: портировать правки под новый билд дело нехитрое, все ключевые адреса приведены выше.

В сухом остатке: Neverwinter Nights 2 на прошитых консолях Switch снова полноценно функционирует по локальной сети. Один игрок поднимает сервер, второй присоединяется по IP-адресу, а учетная запись Nintendo и платные подписки больше не нужны. Именно так мы сейчас и проходим кампанию вдвоем.

 

Источник

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов