Привет, дорогой хабрапользователь!

Надеюсь, все хотя бы раз играли в такую замечательную игрушку, как Terraria, ведь сегодня речь пойдет о ней и о том, как не стоит писать игры с точки зрения безопасности. Если интересно — добро пожаловать под хабракат!


Вступление


Начнем, пожалуй, с того, что такое Terraria и как она появилась.

Феноменальный успех неизменно пребывающей в разработке «песочницы» Minecraft, уже принесшей Маркусу Персону миллионы, не мог остаться незамеченным. Так и случилось, вскоре появляется на свет Terraria. Занимается разработкой один единственный человек, Эндрю Спинкс, главный дизайнер и по совместительству не менее главный программист.

При взгляде на здешние «восьмибитные» пейзажи услужливое подсознание сразу спешит навесить ярлык «Minecraft в 2D». А что? В рюкзаке — кирка и топор, вокруг — случайно сгенерированные просторы. Цель — копать, строить, убивать, добывать.

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

Как оно работает?


Игра написана на языке C# (.NET 4.0) с использованием фреймворка XNA, о котором я достаточно много писал на хабр, например тут, тут и тут.

Изучаем саму игрушку


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

Игра написана с помощью XNA и .NET, а значит — все бинарные файлы и файлы библиотек можно посмотреть насквозь с помощью рефлектора, например: .NET Reflector.

Открываем Terraria.exe, ищем точку входа Main (Program):


Видим забавные строки:
Steam.Init();
if (Steam.SteamInit)
{
       main.Run();
} else {
       MessageBox.Show("Please launch the game from your Steam client.", "Error");
}


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

Для того, чтобы обойти эту «безопасность», достаточно подменить steam_api.dll (откуда импортируются функции) или же можно перес��брать приложение, закоментировав соответствующие строчки. Ведь сам Steam никак не влияет на игру, кроме того, что добавляет туда свой Layout. Но мы пойдем более интересным способом и попробуем даже влиять на саму игру.

Вспомним, что игра написана с использованием — XNA, а значит, у нее должен быть главный класс игры, который наследуется от Microsoft.XNA.Framework.Game, далеко идти не пришлось, это класс Main.

Любая игра, написанная на XNA, имеет в себе, так называемые «компоненты», которые можно туда добавить. Компоненты могут быть как обычными (логика), так и графическими (Drawable).

А теперь давайте подумаем, что можно сделать?

Главный класс у нас Main и он имеет модификатор public (public class Main: Game {… })!
Чем это грозит? Мы можем создать новое приложение, которые будет импортировать наш Terraria.exe в качестве библиотеки и запустит её, а дальше — можно добавить свой компонент игры, и этот компонент будет иметь почти полный доступ к игре.

Пройдясь еще по всяким классам, увидим, что основная идея этих классов — это индийская версия синглтона статический доступ, который, кстати, тоже public.

Стоило бы придать главному классу модификатор доступа отличный от public, как все бы у нас провалилось.

Все, дальше — очень просто, создаем компонент и добавляем его в main.Components. Однако, мне захотелось так же порисовать на spriteBatch'e террарии. С DrawableCompontent возникли сложности, т.к. он рисуется до основной прорисовки класса Main, как бы я не играл с DrawOrder.

Потом, я еще раз взглянул на класс Main, у него отсутствовал модификатор sealed, что так же доставило и упростило мне жизнь. Идея стала куда проще: просто унаследоваться от нашего Main.

Практика, пишем код


Создаем новое консольное приложение, подключаем в качестве библиотек Microsoft.Xna.Framework.*, Terraria.exe.

Теперь создадим класс, который будет наследоваться от Main:
sealed class InjectedMain : Terraria.Main
{
        private SpriteFont font;
        private SpriteBatch spriteBatch;

        internal InjectedMain() : base() { }

        protected override void LoadContent()
        {
            base.LoadContent();

            font = Terraria.Main.fontMouseText; // получаем какой-нибудь шрифт
            spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        protected override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
        }
}


Теперь идем в точку входа и заставим запустится наш игровой класс:
static void Main(string[] args)
{
     try { Program.game = new InjectedMain(); }
     catch { Console.WriteLine("fail, sorry :("); Console.ReadKey(); return; }

     Program.game.Run();
}


Ну и нарисуем что-нибудь, добавим в наш переопределенный Draw:

spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied);
spriteBatch.DrawString(font, "Hello habrahabr!", new Vector2(5f, 5f), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 1f);
spriteBatch.End();


Результат:


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

Ну и напоследок сделаем что-нибудь эффектное, какой-нибудь хак.

У игрока террарии есть одно интересное свойств��: ghost, которое превращает игрока в каспера приведение и позволяет проходить сквозь стены и летать по миру (наверняка, фишка для девелопера). Так сделаем же так, чтобы при нажатии и удержании Left Shift — игрок становился злым и коварным.

Идем в метод Update:

KeyboardState state = Keyboard.GetState();
Player local = Main.player[Main.myPlayer]; // получаем нашего игрока

local.ghost = state.IsKeyDown(Keys.LeftShift);
if (local.ghost)
{
        local.Ghost();
}

// пишем в чат
if (state.IsKeyDown(Keys.LeftShift) && oldKeyboardState.IsKeyUp(Keys.LeftShift))
{ Terraria.Main.NewText("Ghost activated!", 200, 200, 255); }

 if (state.IsKeyUp(Keys.LeftShift) && oldKeyboardState.IsKeyDown(Keys.LeftShift))
{ Terraria.Main.NewText("Ghost deactivaed!", 200, 200, 255); }

            oldKeyboardState = state;



Запускаем игру и становимся приведением по клику на шифт:


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

Отдельно хочется сказать про класс Player, где есть функция Save/Load, которая позволяет сохранять и загружать игроков соответственно, принимает и отдает она сам класс игрока Player. Т.е. мы можем изменить игрока чуть менее, чем полностью, сохранить его и использовать в игре. Или же, например, сохранить всех игроков на сервере в файлы, а потом закинуть их в папку Players и играть ими.

Мораль


Всегда используйте модификаторы доступа как надо, а классы, которые конечны — sealed (запрещает наследование). Для таблетк�� от паранойи верности можно еще и обфусцировать код.

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

Эта статья писалась исключительно в ознакомительных целях: как на примере простых модификаторов — можно написать нехилый хак.

Исходники статьи, увы, не буду прикладывать, идея понятна.

До новых встреч!