Pull to refresh

Заставляем 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

Игра после этого стала такой же отзывчивой, как и в реальной локальной сети, и мы наконец смогли насладиться долгожданной партией игры.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.