0,0
рейтинг
16 января в 19:42

Разработка → Добавляем в игру мультиплеер с помощью Node.JS и Frida. Часть 1 tutorial recovery mode



На Хабре уже есть пару упоминаний об инструменте Frida («Frida-node или немножко странного кода», «Точки соприкосновения JavaScript и Reverse Engineering»). В одной статье уже упоминается использование Frida на практике, однако почти везде инструмент используют как фреймворк для реверс-инжиниринга и исследования функционала программ (может даже взлом).
 
Я же хочу рассказать о процессе превращения одной любимой для меня однопользовательской игрушки в полноценную, многопользовательскую.
 
Сразу хочу предупредить: в подобном процессе я почти новичок, поэтому не удивлюсь, если в меня полетят гнилые помидоры от гуру системного программирования. С другой стороны я надеюсь, что моя статья позволит начать использовать Frida (и не только) другим новичкам, а из гневных комментариев гуру я почерпну что-то полезное для себя. Также продолжать написание статей (при положительных оценках конечно) я буду прямо в процессе разработки мультиплеера.
 
Дано:
Node.Js + Frida + frida-node
Игра Street Legal Racing: Redline
SLRR: java pack
 

Установка Frida


С момента выпуска первой версии frida-node прошло много времени. Модуль обзавелся своими бинарниками Frida, поэтому установку можно просто расписать по шагам:
  1. Скачиваем и устанавливаем Node.Js (на момент написания статьи 5.4.1)
  2. При установке не забываем ткнуть в галочку установки npm
  3. Создаем в нужном нам месте директорию с нужным именем (для проекта), запускаем в ней консоль и вводим npm install frida-node
  4. Надеюсь, установка прошла удачно.

Что с игрой




Небольшая история игры.

Сама игра была выпущена в 2003 году компанией Activision. Игру разработала Венгерская компания Invictus Games. Случилось так, что игра, толи из-за плохого маркетинга, толи из за явного несоответствия интересам аудитории, не взлетела. Несмотря на это, у игры образовалось несколько сообществ фанатов, скорее всего из-за особенностей
геймплея: Автомобиль можно практически полностью разбирать и собирать, менять и настраивать детали, очень реалистичная (на тот момент) физика, деформация кузова, и поведение на дороге. Несколько раз сообщество пыталось заполучить исходные коды игры, подписывая петиции и отправляя их в Invictus. Разработчики отказывались их передавать, ссылаясь на проблемы с правами и компанией Activision, которой в данный момент принадлежат права. Несмотря на это, как-то случилось так, что в сеть утекло часть java кода игры. В саму игру встроена какая-то старая и урезанная версия JVM (Java Virtual Machine, даже без поддержки throw-catch), полностью отсутствуют функции для работы с сетью, а файлы сохраняются и открываются только во встроенном в игру формате.

На данный момент нашим земляком, с ником RAXAT был выпущен неофициальный патч 2.3.0LE, который стал
стандартом для этой игры. На базе этого патча я и решил добавить в игру поддержку мультиплеера.
 

Неудачные и удачные попытки





Сразу хочу заметить, что я не первый из сообщества, кто захотел реализовать мультиплеер в игре. Также я сам сделал несколько попыток добавить нужные функции в игру. Изначально я делал это с использованием dll, написанной на Delphi, которая инжектировалась в процесс. Такой метод работал, но добавлять что-либо в модуль было достаточно трудоемкой задачей.

На помощь пришел инструмент Frida, который позволяет внедрять в процесс JavaScript движок V8, и работать с процессом «изнутри». Самой важной задачей по началу являлось добавить обмен данными между JVM игры и внешним процессом Node.js.
 
Код проекта валяется на Github: https://github.com/lailune/SLRRMultiplayer однако представляет из себя исключительно тестовую версию, и то, что я имею на данный момент.
 
Ссылку на игру не привожу по понятным причинам.
 
Скрипт, загружаемый внутрь процесса будет называться injectScript.js. Название самого скрипта приложения не важно, я назвал его app.js.
 
Наш скрипт будет запускать бинарник игры, и передавать pid процесса модулю Frida для внедрения скрипта.
 
var frida = require('frida');
var spawn = require('child_process').spawn;
 
var injectScript = fs.readFileSync('injectScript.js', "utf8");
var workingDir = 'C:/SLRR/'; //Да простят меня боги за абсолютный путь
 
//Переходим в директорию игры и запускаем её
process.chdir(workingDir);
var gameProcess = spawn(
            workingDir + 'StreetLegal_Redline.exe',
            [], {
                        stdio: 'inherit'
            });
 
//Передаем id процесса «куда следует»
AttachHook(gameProcess.pid);

 
Сама функция AttachHook содержит весь код инициализации Frida:
  1. Подключаемся к процессу
  2. Загружаем наш внутренний скрипт во внедренный V8 (в это время V8 проверяет скрипт на ошибки и компилирует его в байт код)
  3. Ставим обработчик сообщений из скрипта
  4. Запускаем скрипт, и выводим сообщение об успехе или ошибке.

 
function AttachHook(pid) {
            frida.attach(pid)
                        .then(function (session) {
                                   return session.createScript(injectScript);
                        })
                        .then(function (script) {
                                   script.events.listen('message',function (message, data) {
                                               handleMessage(script, message.payload.name, message.payload.data);
                                   });
                                   script.load()
                                               .then(function () {
                                                           console.log('Hook script injected.');
                                               })
                                               .catch(function(error) {
                                                           console.log('Hook Error:', error.message);
                                               });
                        })
}

 
В случае прихода какой-либо «посылки» (payload) из нашего внедренного скрипта мы должны его обработать по своему. Для этого есть функция handleMessage, в которой, правда, пока реализован только прием информации о позиции игрока в виртуальном пространстве.
 
//Глобальный объект расположения в пространстве
var pos = {x: 0.0, y: 0.0, z: 0.0, sy: 0.0, sp: 0.0, sr: 0.0, angle: 0.0};
 
function handleMessage(script, type, data) {
            if (type == "POS") {
                        var tmp = data.split(';');
                        pos.x=tmp[0];
                        pos.y=tmp[1];
                        pos.z=tmp[2];
            }
}

 
Теперь мы можем распоряжаться этими данными так, как нам захочется, например: передать их на сервер.
 

injectScript.js


Способ передачи данных из игры я выбрал крайне возмутительный: перехватываю вызов CreateFileA.
Почему:
  1. Так проще всего. Достаточно открыть файл с
    «нужным названием», внутри которого будут данные, которые мы передаем.
  2. Я так и не смог научить Frida искать нужный
    текст в памяти приложения, для дальнейшего использования определенной
    области памяти.
  3. Это работает.

Важный момент: в этом скрипте надо тщательно следить за созданием переменных и выделением памяти. Если будет создано слишком много переменных, то в один прекрасный момент запустится сборщик мусора, и процесс подвиснет на время намертво. Также если постоянно выделять память, получится утечка, с которой даже GC не справится.
 
//Сохраняем в память строку (путь в никуда)
var dummy = Memory.allocAnsiString("\\\\nothing\\dev\\null");
 
var message="";
 
//Подключаем перехватчик
для функции CreateFileA. Аналог Hook 
Interceptor.attach(Module.findExportByName('kernel32.dll','CreateFileA'), {
            onEnter: function onEnter(args) {
                        //Считываем первый параметр функции
                        message = Memory.readUtf8String(args[0]);

                        //Проверяем на наличие константы DTM^ (ничеголучше не придумал)
                        //Данные из игры передаются в формате DTM^payloadname^data
                        if (message.indexOf('DTM^') != -1) {
                                    //Разделяем, убираем лишнее, отправляем «наверх»
                                  message = message.split('^'); 
                                   send({name: message[1], data:message[2]});                    
                        }
 
            },
            onLeave: function onLeave(retval) {
            }
});

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

Немного Java


Поскольку игра использует внутри себя урезанную версию Java, придется немного написать на этом великолепном языке.

Интересный момент: прямо внутрь игры встроен компилятор java в байт код для JVM,  достаточно положить java файл в соответствующую директорию внутрь папки src, и при запуске игры будет создан class файл.
 
Для теста я использовал класс City (реализует базовые функции для управления городом в игре). В дальнейшем планирую вынести реализацию своего псевдо-сокета в отдельный глобальный класс.

Пока реализует только передачу данных наружу.
 
public class MultiplayerSocket {
            int connected = 0; //на потом
            File dtm;

            public void MultiplayerSocket(){
                        dtm = new File();
            }

            public int send(String type, String msg) {
                        //Открываем «никакой» файл и формируем датаграмму, которую распарсим потом
                        dtm = new File("&nofolder\\DTM^" + type + "^" + msg);
                        dtm.open(File.MODE_READ); //открываем файл на чтение (открытие на запись вызовет ошибку с соответствующим сообщением в error.log)
                        dtm.close(); //закрываем. Из-за бага игры это обязательно
                        return 0; //По идее тут должен быть статус отправки
            }
}
 
//создаем экземпляр объекта
MultiplayerSocket MP;
          ….
           public void enter(GameState prev_state ){
                                   //Инициализируем
                                   MP = new MultiplayerSocket();
                                   ….
           }
          ….
 
            //функция для отправки текущего положения машины игрока 
            public void sendPositionDatagram() {
                        if (MP) {
                                   if (player.car && player.car.chassis) {
                                               Vector3 pos = player.car.getPos(); //Получаем позицию игрока
                                               MP.send("POS", pos.x + ";" + pos.y + ";" + pos.z); //отправляем наружу
                                   }
                        }
            }
 
            //Вызывается перед рендером каждого кадра (до 30 fps)
            public void frame(){
                        sendPositionDatagram();
            }

 

Что дальше?


Надо научится передавать данные внутрь игры. Находить нужную строку в памяти с Frida пока так и не получилось (а-ля ArtMoney, руки золотые, но растут не оттуда). Будет супер, если найдутся люди, которые подскажут как. Была реализация с обменом данными через файл (а у игры свой формат), было медленно, нестабильно, лагало. В мыслях попробовать отправлять данные через подмену return какой-либо функции (но в идеале нужно через память). Также в плане интеграции в саму игру предстоит много работы. Содержание следующих статей зависит от прогресса по проекту.
Андрей Недобыльский @jhonyxakep
карма
6,7
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (6)

  • 0
    Что вы имеете в виду под отправкой внутрь игры? В документации описано как передавать данные в скрипт, который вы внедрили. Здесь описано как искать информацию в памяти (функция Memory.scan).
    • 0
      Я имел в виду передачу данных внутрь JVM игры, прокинуть строку во внедренный скрипт проблемы нет.

      Как раз про использование Memory.scan я и говорил. Документация есть, но чего-то мне не хватает для полного осознания как пользоваться этой функцией. Например я пробовал так:

      //Создаем строку в памяти и делаем паттерн для поиска
      var p = Memory.allocAnsiString("DEMO");
      var pattern = p.toMatchPattern();
      
      	var modules = Process.enumerateModulesSync();
      
              //Проходимся по блокам памяти каждого модуля
      	for(var a in modules){
      
                              //сканируем память модуля на соответствие паттерну
      			Memory.scan(modules[a].base, modules[a].size, pattern, {
      				onMatch: function (address, size) {
      					console.log("something found"); //Ура, нашли, выводим
      					console.log(address);
      				},
      				onComplete: function () {
      					console.log("Memory scan complete"); 
      				},
      				onError: function (reason) {
      					console.log(reason); //что-то пошло не так
      				}
      			});
      	}
      


      Среди модулей естественно есть и сам exe игры. Если заставить её сканировать всю область памяти процесса поиск останавливается и выводит ошибку доступа к защищенному сегменту памяти.

      Поиск такой-же строки через ArtMoney вполне её находит и позволяет менять.

      З.Ы. Вчера также подсказали, что использовать String не самая лучшая идея — сборщик мусора может перенести её в другую область памяти, хотя у меня такого не случалось.
      • 0
        Из первого, что приходит в голову — а там точно ANSI, а не UTF?
        У фриды на выбор есть Memory.allocUtf8String(str), Memory.allocUtf16String(str), Memory.allocAnsiString(str), может, стоит попробовать 1 из них?
        Еще можно попробовать для проверки найти набор байт(взять из того же artmoney/cheatengine).
        Еще тупой вопрос — а точно этот адрес принадлежит главному модулю?
        Ну и если есть какой-никакой скилл в реверсе, можно попробовать найти адреса нужных функций и перехватывать их, вместо поиска строк.
        • 0
          Тестировал и на Utf вариантах. ArtMoney все-таки находит при выборе 1байтной кодировки, возможно из-за того, что латиница.

          Среди перечисляемых адресов есть главный модуль, и туда попадает диапазон памяти, внутри которого ArtMoney нашел строку.

          Насчет функций была мысль, скилла в реверсе мало, но HexRays в IDA что-то все-таки смог выдать. Возникала мысль даже прокинуть из Frida свою функцию прямо в JVM, т.к. аргументы функции RegisterNative известны, но мне не хватает опыта.
          • 0
            var func=new NativeFunction(ptr(«0xFFFFFF»), 'void', ['int']); //адрес функции, тип возвращаемого значения, типы аргументов
            func(123);

            Так можно вызывать нативные функции. Что до явовских — не в курсе, не пробовал.

            Что до поиска адресов, что получится, если искать паттерн в байтах вместо строки?
            • 0
              Как я понял, поиск строк нужен чтобы передать данные в игру. И если решится как передавать данные, то необходимость в поиске строк отпадет сама собой.
              Насчет передачи данных в игру, я бы попробовал поискать метод в jvm, которая позовляет выполнить метод (invoke), а затем с помощью него вызывать ваш java код и передавать в него необходимые данные.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.