Как стать автором
Обновить

Заставляем Red Alert 3 играть по локальной сети через Интернет

image

Однажды меня очень вдохновила статья на хабре Реверс-инжиниринг полёта Бэтмена. Так она понравилась, что время от времени я её перечитывал, как интересную историю о том, как автор, не имея исходного кода, смог найти и исправить баг в большой игре. Конечно, я и до этого слышал про реверс-инжиниринг, но на примере игры история выглядела более красочной и увлекательной.

И вот спустя пять лет мне самому выпал шанс попробовать себя в роли обратного разработчика. Проблема возникла с игрой Red Alert 3, когда я захотел поиграть в неё по сети в режиме «LAN over the Internet». Для этого игрокам нужно подключиться к одной виртуальной сети.

image

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

image

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

Я пробовал открывать порты в обоих маршрутизаторах, подключать компьютеры к интернету напрямую, отключать Брандмауэр Windows, переустанавливать игру с одного образа. Но результат был тем же. Я решил посмотреть в Wireshark какие именно запросы и куда отправляет игра на нашем виртуальном интерфейсе. Игра примерно раз в секунду отправляла по 8 UDP-сообщений на порты с 8086 по 8093. Но данные в сообщениях были зашифрованы, поэтому увидеть, что именно одна запущенная игра пытается сообщить другой, было нельзя.

image

При этом после нажатия на кнопку «Подключиться» в игре, в Wireshark не отображались сетевые сообщения другого вида, а только всё те же периодические восемь сообщений на порты 8086-8093. Я подумал, что в сообщении должен содержаться IP-адрес игрока-хоста, к которому игра должна отправить иной сетевой запрос в момент нажатия на кнопку подключения к корпоративной игре. Наверное, в сообщении указан IP-адрес физического интерфейса компьютера игрока (192.168.1.X), подумал я. Ведь в те года в корпоративном режиме играли через реальную локальную сеть, и не исключено, что игра не была рассчитана на работу в окружении нескольких сетевых интерфейсов.

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

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

image

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

Отлаживая свой код, я заметил, что те сообщения, которые я отправлял из своей программы, моментально имели эффект на втором компьютере (делал это на своём компьютере и ноутбуке, подключенных к той же виртуальной сети). Это были сообщения об игроке находящемся в лобби, о новой запущенной карте, сообщения чата. Отличием в сообщениях было то, что я свои отправлял, указывая точный IP-адрес получателя, а игра же отправляла их на широковещательный адрес, т.е. 255.255.255.255.

Так я и узнал о существующей проблеме в стеке TCP/IP ОС Windows, которая возникает при использовании глобального широковещательного адреса при наличии нескольких сетевых интерфейсов в системе (Problems With LAN Game Announcements and Broadcasts on Windows). Предложенные воркэраунды не помогли и я решил, что нужно в коде самой игры заменить широковещательный адрес IP-адресом оппонента в виртуальной сети.

Конечно, этот адрес хранится где-то в файле программы и его можно было бы найти обычным Hex-редактором, но проблема в том, что данная комбинация байт (FF FF FF FF) очень популярна и встречается в файле игры более двух тысяч раз. Разумеется, перебрать все из них не представляется возможным.

image

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

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

Код программы
using System;
using System.IO;
using System.Linq;
using System.Net;

namespace RA3LanFix
{
    class Program
    {
        static void Main(string[] args)
        {
            string gameDataDir = Path.Combine(Environment.CurrentDirectory, "Data");

            try
            {
                if (!Directory.Exists(gameDataDir))
                {
                    Console.WriteLine("Программа должна быть запущена из папки с игрой");
                    return;
                }

                var gameFilePath = Path.Combine(gameDataDir, "ra3_1.12.game");
                if (!File.Exists(gameFilePath))
                {
                    Console.WriteLine($"Не удалось найти файл по пути {gameFilePath}");
                    return;
                }

                byte[] ipBytes;

                Console.WriteLine("Введите IP-адрес игрока-оппонента");

                while(true)
                {
                    var address = Console.ReadLine();
                    try
                    {
                        ipBytes = IPAddress.Parse(address).GetAddressBytes().Reverse().ToArray();
                        break;
                    }
                    catch
                    {
                        Console.WriteLine("Введите корректный IP-адрес");
                    }
                }

                using (Stream stream = File.Open(gameFilePath, FileMode.Open))
                {
                    stream.Position = 2349393;
                    stream.Write(ipBytes, 0, ipBytes.Length);

                    stream.Position = 2351178;
                    stream.Write(ipBytes, 0, ipBytes.Length);
                }

                Console.WriteLine("Успешно");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            finally
            {
                Console.ReadKey();
            }
        }
    }
}


image

Игра после этого стала такой же отзывчивой, как и в реальной локальной сети, и мы наконец смогли насладиться долгожданной партией игры.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.