GZone-Goals

From GZProject Wiki

Revision as of 04:39, 3 November 2007; view current revision
←Older revision | Newer revision→
Jump to: navigation, search

Contents

Цель

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

Чтобы правильно понимать процесс управления необходимо сначала поставить саму задачу ради которой мы будем вносить изменения в игровой клиент. И второе - исходя из задачи - обрисовать список функций, которые необходимо получить для управления клиентом.

Возможное множество задач, которые возникают вовремя игры из основной нашей цели, сводятся к автоматизации игрового процесса. То есть мы хотим, чтобы наш игровой персонаж сам рубил лес, продавал предметы, разговаривал, ходил, плавал. Другими словами мы хотим чтобы он принял от нас основные навыки - такие как кликанье мышкой по определенным виртуальным объектам и производил с ними какие то действия, а так же научился этими навыками пользоваться на свое усмотрение в зависимости от ситуации. Получается нам нужно - первое - научить персонажа произносить заклинание переноса в безопасное место Kal Ort Por, а второе - чтобы персонаж сам принимал решение использовать это заклинание в случае опасности!

Простая задача

Но на первое время упростим задачу и предположим такую ситуацию: при появлении персонажа в зоне видимости клиента с именем Tiger нам необходимо выкрикнуть слово "Guards". Для решения такой задачи необходимо представить структуру объекта, с которым нам необходимо работать.

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

Давайте представим себе такую ситуацию более подробно. Как происходит отображения имени и показ его на экране клиента?

Описание алгоритма

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

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

  • создание пакета для запроса информации с сервера.
  • шифрование.
  • передача на сторону сервера.
  • получение пакета.
  • расшифровывание.
  • отображение информации.

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

Создание события

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

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

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

Так как у нас на руках есть функция отвечающая за обработку новых объектов, то есть мы получаем такую цепочку: сервер генерирует событие, передает его клиенту, клиент вызывает нужную нам функцию, отрисовывает событие и отвечает серверу. В этой схеме нету самого главного - нас :) То есть нашего приложения, которое будет синхронно с игровым клиентом, не вмешиваясь в его алгоритмы, реагировать на обрабатываемое нами событие. После того как мы получим событие, нам необходимо создать реакцию на него - произнести заветные слова "Guards". Это будет более сложный этап поскольку мы из наблюдателей превращаемся в управляющий процесс, и нам продеться как опытному хирургу вмешиваться в привычный для процесса жизненный цикл внося новые данные.

Добавление реакции

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

Практика

Так! Это была теория, не стоит обольщаться полученным результатом - на практике все будет по-другому :) Но хочу похвалить читателя, мы сделали очень важные выводы, они позволили получить общее представление о решаемой проблеме и эти выводы очень близко к тому как разрабатывался игровой клиент, наверно все выше описанные алгоритмы были в отчете системного архитектора, который разрабатывал этот продукт и поэтому мы очень легко найдем все точки в проекте. Так же мы очень сильно приблизились к тому, что нам необходимо запрограммировать. Теперь мы точно описали поставленную задачу - нам необходимо перехватить функцию которая должна быть вызвана с помощью оператора call, и наверно где-то в другой фиксированной области памяти будет находиться данные, которые содержат нужную нам информацию. И так переходим к самому интересному "Фичи".

Фичи

К фичам относиться нестандартное программирование. Не стандартным оно называется потому, что такие задачи трудно встретить на практике в следствии отсутствия людей, которые могли бы этим заниматься. Это означает что получив такие знания разработчик становится перспективным сотрудником любой компании в отдел исследования и разработок.

Хочется подчеркнуть способ работы с клиентским приложением - игровым клиентом. Наш подопытный умеет устанавливать соединение, включать шифрование, обрабатывать все возможные события со стороны сервера, писать логи, отрисовывать анимацию персонажей и разговоры игроков, и тд и тп. Возникает вопрос как воспользоваться этими возможностями прикладывая минимальные усилия со своей стороны?

Фича первая - перехват вызова функций

Память компьютера представляет из себя один большой массив нумеруемый от 0 до 4гб. Загруженный ехе файл в системе Windows расположиться по адресу 0x400000- 0xd61000, за ним библиотека. Предположим что загруженный в памяти клиент получает команду подгрузить нашу библиотеку в которой имеется отладочная информация. На памяти такое произведение будет выглядеть следующим образом:

0x400000-0xd61000 client.exe - игровой клиент

0x1fc10000-0x1fe5e000 clientaddin.dll - наша библиотека

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

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

Давайте вернемся к поставленной задаче. Нам необходимо создать событие о появлении нового объекта на экране. Это значит, что нам необходимо после получения информации с сервера, ее расшифровывания и анализа клиентом найти место в коде в котором уже однозначно понятно, что на в зоне видимости уже появился новый игрок, противник. И в этом месте кода уже ясно его имя, уникальный номер и координаты. Скорей всего это место будет новой функцией кода, в которую передадутся наши параметры с именем. Для вызова функции будет использована команда процессора CALL, для передачи информации, то есть имени и координат нового игрока, программный стек. Информация скорей всего будет состоять из ссылок на временные и статические объекты, структуры где одно из полей будет текстовым - наше искомое имя. Поскольку вызов функции происходит как внутренний вызов и не похож с внешними вызовами библиотечных функций нам придется создать такой переходник собственными руками.

Для контроля функции нужно понимать что она находиться в фиксированной области памяти и никуда не перемещается вовремя работы процесса. Это позволяет нам заменить тело функции, которое отвечает за обработку нового объекта, своим телом. Для этого нам необходимо: во-первых уложиться в объем инфицированной функции, во-вторых выполнить работу за замененную функцию, и в третьих - ничего не испортить и не внести лишней информации в данные и регистры процессора процесса, в котором мы делаем замену. Чтобы удовлетворить всем этим условиям и не писать каждый раз индивидуальный вызов и обработку мы напишем одну прослойку, которая позволит передавать вызовы в любую функцию нашего приложения и сделает всю грязную работу по очистке регистров и возвращению управления обратно. Основной принцип работы этой функции в том что мы знаем адрес функции для перехвата, так же нам необходимо знать где находятся ключевые параметры переданные в функцию. Для управления прослойкой мы вызываем функцию инфицирования в которую передаем всю эту информацию: адрес инфицирования, тип параметров - на стеке, адрес нашего обработчика. Наш обработчик будет написан на языке высокого уровня, С++. Что позволит нам работать с привычными и удобными структурами и библиотеками. Дальнейшая работа сводиться к обработки данных переданных в наш обработчик и создание любых вызовов любой сложности. Для того чтобы показать революционность данного метода - продемонстрируем создание события на языке Ява, который будет скомпилирован на лету в игровом клиенте из исходного текста.

Фича вторая

Пишем Ява приложение которое работает внутри игрового клиента.