R0 CREW

Пишем бота для MMORPG

Источник: habrahabr.ru

Покопавшись в статьях хабра, я нашел несколько оных про написание ботов для MMORPG. Несомненно, это очень интересные и познавательные статьи, но возможности в них весьма скудны. Что если, например, нужно пофармить мобов или руду по заданному маршруту убивая агрессивных мобов, игроков и всех кто будет на Вас нападать по пути, выкрикивая им вслед непристойности, да что б еще и определить не смогли. В общем полная эмуляция среднестатистического MMORPG игрока. Написание макросов для AutoIt, симуляция кликов в окне, анализ пикселей под курсором — это совсем не наш вариант. Заинтриговал? Добро пожаловать под кат!

Часть 0 – Поиск точки внедрения кода

Итак, с места да в карьер.

1. Выбор способа реализации внедрения (немного теории)

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

Для этого нужно найти/придумать точку внедрения, которая будет не столь очевидна для анти-читов и полезна для нас. Таких точек может быть очень много, но по многим причинам, лучшим решением будет внедрение в отрисовку игры, т.е. создание хука для Direct3D. Опять же по многим причинам лучше всего перехватывать EndScene функцию, потому как до ее вызова, все изменения игрового мира и иные расчеты уже произойдут. Вот происходящий процесс внутри для наглядности:

  1. Отрисовка объектов текущей сцены игры
  2. Вызов подмененной D3D EndScene
  3. Наш код
  4. Вызов оригинальной D3D EndScene
  5. Следующая сцена

Сцена в данном ключе это так называемый frame. Другими словами — наш код будет работать с частотой вашего fps.

Замечание: fps может быть достаточно высоким значением, по-этому не стоит обрабатывать каждый вызов кода. Думаю достаточно будет 10-15 вызовов в секунду

2. Инструментарий

Итак, план мы наметили, теперь нужны инструменты. Я (как и большинство надеюсь) люблю использовать все готовое. Посему предлагаю обзавестись следующими вещами:

  1. Любая IDE где будем писать код на C#
  2. IDA — на мой взгляд лучший дебагер
  3. HackCalc — калькулятор для пересчета VA (виртуального адреса) в Offset и обратно
  4. SlimDX — DirectX фреймворк для .NET
  5. FlatAsm Managed — библиотека преобразует мнемокоды ассемблера в байткод

3. Поиск точки внедрения

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

При отрисовке с помощью D3D создается виртуальный объект Direct3D device, который по сути представляет собой VMT (виртуальная таблица методов, которая является указателем на указатель на таблицу методов D3D). Это таблица хранит, опять же, указатели на методы Direct3D, например, BeginScene, EndScene, DrawText и т.д. Нас в данном случае интересует только EndScene, т.к. Direct3D device создается в единственном экземпляре, то нам нужно получить указатель на него, а затем получить указатели на таблицу. Опять же нам необходимо определить какой D3D используется в клиенте игры и т.к. вариантов у нас 2 (DX9 и DX11), то решить эту проблему можно простым перебором. Для этого мы и будем использовать SlimDX.

В коде processMemory.Read и processMemory.ReadBytes обертки стандартной ReadProcessMemory из kernel32.dll

Проверка на DX9:

//Создаем устройство D3D9
var device = new SlimDX.Direct3D9.Device(
    new SlimDX.Direct3D9.Direct3D(), 
    0, 
    DeviceType.Hardware, 
    Process.GetCurrentProcess().MainWindowHandle, 
    CreateFlags.HardwareVertexProcessing, 
    new[] { new PresentParameters() });
    using (device)
    {
        //Открываем текущий процесс
        var processMemory = new ProcessMemory((uint)Process.GetCurrentProcess().Id);
        //Считываем необходимый нам адрес расположения в памяти D3D9 функции по смещению 0xA8 от указателя на Com объект
        _D3D9Adress = processMemory.Read<uint>(processMemory.Read<uint>((uint)(int)device.ComPointer) + 0xa8);
        //Считываем опкоды функции
        _D3D9OpCode = (int)processMemory.Read<byte>(_D3D9Adress) != 0x6a ? processMemory.ReadBytes(_D3D9Adress, 5) : processMemory.ReadBytes(_D3D9Adress, 7);
    }

Откуда 0xA8? Если открыть d3d9.h и найти EndScene метод, то вы найдете:

STDMETHOD(EndScene)(THIS) PURE;

42 ой по счету в интерфейсе IDirect3DDevice9, а одна функция в архитектуре х86 адресуется 4-мя байтами, получаем 42 * 4 = 168, а это и есть 0xA8.

Все это необходимо обернуть в try/catch и если вылетела ошибка, значит нам нужно пробовать D3D11 и тут все немного сложнее, нам нужен не EndScene, а SwapChain, он находится по индексу 8, т.е. 8 * 4 = 32 = 0х20:

//Создаем форму отрисовки для получения устройства D3D11
using (var renderForm = new RenderForm())
{
    var description = new SwapChainDescription()
    {
        BufferCount = 1,
        Flags = SwapChainFlags.None,
        IsWindowed = true,
        ModeDescription = new ModeDescription(100, 100, new Rational(60, 1), SlimDX.DXGI.Format.R8G8B8A8_UNorm),
        OutputHandle = renderForm.Handle,
        SampleDescription = new SampleDescription(1, 0),
        SwapEffect = SlimDX.DXGI.SwapEffect.Discard,
        Usage = SlimDX.DXGI.Usage.RenderTargetOutput
    };
    SlimDX.Direct3D11.Device device;
    SlimDX.DXGI.SwapChain swapChain;
    var result = SlimDX.Direct3D11.Device.CreateWithSwapChain(
        DriverType.Hardware, 
        DeviceCreationFlags.None, 
        description, 
        //Здесь мы получаем устройство
        out device, 
        out swapChain);
    if (result.IsSuccess) using (device) using (swapChain)
    {
        //И открыв текущий процесс - считаем адрес функции и опкоды
        var processMemory = new ProcessMemory((uint)Process.GetCurrentProcess().Id);
        //Считываем наш SwapChain
        _D3D11Adress = processMemory.Read<uint>(processMemory.Read<uint>((uint)(int)swapChain.ComPointer) + 0x20);
        _D3D11OpCode = processMemory.ReadBytes(_D3D11Adress, 5);
    }
}

Все это опять необходимо опять же обернуть в try/catch. В случае неудач обоих попыток получить адрес функции D3D возможно изменились смещения либо у вас не D3D9 или D3D11.

Итог: У нас имеется адрес функции EndScene D3D и опкоды этой функции. Что с ними делать и как внедрить свой код я расскажу дальше, а пока осмысливайте код выше, гуглите, яндексуйте, бингуйте, яху…, читайте документацию по ассемблеру, будет полный хардкор!

Часть 1 – Внедрение и исполнение стороннего кода

Итак, продолжим написание нашего бота. Сегодня мы внедрим наш код в игровой процесс (не без помощи ассемблера), а позже позаботимся и о том, чтобы его было не так просто найти, ведь наказывают не за то, что жульничаешь, а за то, что попался. И если быть до конца честным, то даже не совсем в сам процесс игры будем его внедрять, да и 1 раз только за весь жизненный цикл.

1. Намечаем план внедрения

В прошлой части, мы нашли адрес нашей DirectX функции EndScene и считали 5 первых ее байт.

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

  • HookAddress — это адрес на выделенную память в процессе игры с помощью WinApi функции VirtualAllocEx из kernel32.dll
  • Address — это адрес в памяти DirectX функции EndScene или ChainSwap
  • OpCodes — это оригинальные опкоды функции и их нам нужно сохранить, т.к. в оригинале они будут изменены.

2. Операция внедрения

Что бы открыть процесс мы вызовем WinApi OpenProcess и разрешим отладку, а затем нам необходимо открыть главный поток, нашего процесса

var ProcessHandle = OpenProcess(processId);
Process.EnterDebugMode();
var dwThreadId = Process.GetProcessById(dwProcessId).Threads[0].Id;
var ThreadHandle = OpenThread(0x1F03FF, false, (uint)dwThreadId);
var HookAddress = Memory.AllocateMemory(6000);
var argumentAddress1 = Memory.AllocateMemory(80);
Memory.WriteBytes(argumentAddress1, new byte[80]);
var argumentAddress2 = Memory.AllocateMemory(BufferSize);
Memory.WriteBytes(argumentAddress2, new byte[80]);
var resultAddress = Memory.AllocateMemory(4);
Memory.Write<int>(_resultAddress, 0);

где 0x1F03FF — права доступа к потоку. Далее мы выделяем память под наш код и получаем на нее указатель HookAddress, так же резервируем память для двух аргументов argumentAddress1 и argumentAddress2, для результата resultAddress и заполняем все нулями. Теперь как и обещал немножко хардкора:

var asmLine = new List<string> {
    "pushfd",
    "pushad",
    "mov edx, 0",
    "mov ecx, " + resultAddress,
    "mov [ecx], edx",
    "@loop:",
    "mov eax, [ecx]",
    "cmp eax, " + 80,
    "jae @end",
    "mov eax, " + argumentAddress1,
    "add eax, [ecx]",
    "mov eax, [eax]",
    "test eax, eax",
    "je @out",
    "call eax",
    "mov ecx, " + resultAddress,
    "mov edx, " + argumentAddress2,
    "add edx, [ecx]",
    "mov [edx], eax",
    "mov edx, " + argumentAddress1,
    "add edx, [ecx]",
    "mov eax, 0",
    "mov [edx], eax",
    "@out:",
    "mov eax, [ecx]",
    "add eax, 4",
    "mov [ecx], eax",
    "jmp @loop",
    "@end:",
    "popad",
    "popfd"
};
Memory.Asm = new ManagedFasm(ProcessHandle);
Memory.Asm.Clear();
foreach (var str in asmLine)
{
    Memory.Asm.AddLine(str);
}
Memory.Asm.Inject(HookAddress);
var length = (uint) Memory.Asm.Assemble().Length;
Memory.WriteBytes(HookAddress + length, OpCodes);
Memory.Asm.Clear();
Memory.Asm.AddLine("jmp " + (Address + OpCodes.Length));
Memory.Asm.Inject((uint)((HookAddress + length) + OpCodes.Length));
Memory.Asm.Clear();
Memory.Asm.AddLine("jmp " + HookAddress);
for (var k = 0; k <= ((OpCodes.Length - 5) - 1); k++)
{
    Memory.Asm.AddLine("nop");
}
Memory.Asm.Inject(Address);

Ассемблерный код выше, записывается в HookAddress и будет передавать управление нашему коду и согласно таблице, после его отработки мы возвращаем управление в главный поток. Теперь я покажу как этим воспользоваться, пусть имеем:

public byte[] InjectAndExecute(IEnumerable<string> asm, bool returnValue = false, int returnLength = 0)
{
    Memory.Asm.Clear();
    foreach (var str in asm)
    {
          Memory.Asm.AddLine(str);
    }
    dwAddress = Memory.AllocateMemory(Memory.Asm.Assemble().Length + 60);
    Memory.Asm.Inject(dwAddress);
    Memory.Write<uint>(argumentAddress1, dwAddress);
    while (Memory.Read<int>(argumentAddress1) > 0)
    {
        Thread.Sleep(1);
    }
    byte[] result = new byte[0];
    if (returnValue)
    {
         result = Memory.ReadBytes(Memory.Read<uint>(argumentAddress2), returnLength);
    }
    Memory.Write<int>(argumentAddress2, 0);
    Memory.FreeMemory(dwAddress);

    return result;
}

В итоге у нас значения по адресам argumentAddress1 и argumentAddress2 должны стать нулями когда наша инъекция отработает. Если у вас много потоков которые вызывают InjectAndExecute, то нужно предусмотреть очередь, для этого я и использовал 80 байт размер, как его реализовать, подумайте сами. Дальше я покажу свою реализацию и как прятать наш код.

Часть 2 – Прячем код от посторонних глаз

Итак, продолжим написание нашего бота. Из прошлых статей, мы научились находить адрес перехватываемой функции для DirectX 9 и 11, а так же исполнять произвольный ассемблерный код в главном потоке игры. Естественно, эти все операции, могут быть замечены защитой игры и вы будете наказаны. Но сегодня, я покажу как спрятать этот код от защиты, в том числе и от такого монстра, которого все боятся, как Warden. Как я и говорил, я не ботовод потому, что меня не поймали. Жду вас под катом!

1. Рассуждения на тему защиты игр

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

1. Проверка файлов игры на оригинальность
Здесь все просто, игра запрашивает с сервера контрольные суммы файлов игры и сверяет с теми, что получатся в результате проверки. В данном случае можно просто модифицировать алгоритм проверки в исполняемом файле. Но все намного сложнее, если алгоритм проверки, тоже подгружается с сервера, как в случае с Warden.

2. Анализ подключенных DLL
В данном случае вы рискуете, только используя нечто популярное, т.к. информация об этом, скорее всего уже есть у разработчиков и как в случае с Valve Anti Cheat, неизвестные библиотеки отправляются прямиком на сервер для анализа, опять таки, если вызовет подозрение. Так что ваш бан может быть всего лишь отсрочен на неопределенный срок.

3. Анализ VMT импортируемых библиотек на перехваты
Здесь вообще все сложно, вы можете получить бан даже за TeamViwer или Fraps, если разработчики игры так захотят и ничего вы не докажете.

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

4. Защита игровой памяти от чтения/записи
Данным функционалом обладает набирающая популярность в Steam защита Easy Anti Cheat. В Windows запускается сервис EasyAntiCheat, который защищает память игры от чтения и записи. Если же сервис не запущен, то игра отказывается соединяться с сервером.

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

Итог: Из всего выше описанного, становится понятно, что используя популярные программы и методы мы очень рискуем попасть в бан и что бы этого избежать, нам необходимо сделать наше внедрение тяжело определяемым. Чем мы сейчас и займемся. Для начала напишем метод GetFakeCommand и ObfuscateAsm:

internal static string GetFakeCommand()
{
    var list = new List<string> { "mov edx, edx", "mov edi, edi", "xchg ebp, ebp", "mov esp, esp", "xchg esp, esp", "xchg edx, edx", "mov edi, edi" };
    int num = Random.Int(0, list.Count - 1);
    return list[num];
}
internal static IEnumerable<string> ObfuscateAsm(IList<string> asmLines)
{
    for (var i = asmLines.Count - 1; i >= 0; i--)
    {
        for (var k = Random.Int(1, 4); k >= 1; k--)
        {
            asmLines.Insert(i, GetFakeCommand());
        }
    }
    for (var j = Random.Int(1, 4); j >= 1; j--)
    {
        asmLines.Add(GetFakeCommand());
    }
    return asmLines;
}

Как видно, GetFakeCommand возвращает ассемблерную команду, которая не меняет состояния регистров и флагов и этот список можно расширить при необходимости в несколько раз, а ObfuscateAsm — понатыкает эти команды в случайных местах нашей подпрограммы. И так модифицируем код подмены адреса из прошлой статьи в месте получения HookAddress, я не буду дублировать весь код, а только покажу измененную часть:

//Открыли процесс и главный поток
var RandomOffset = (uint)Random.Int(0, 60);
var HookAddress = Memory.AllocateMemory(6000 + Random.Int(1, 2000)) + RandomOffset;
//Зарезервировали память и сформировали подпрограмму
Memory.Asm = new ManagedFasm(Memory.ProcessHandle);
Memory.Asm.Clear();
foreach (var str in ObfuscateAsm(asmLine))
{
    Memory.Asm.AddLine(str);
}

Этот же финт можно проделать и с памятью для аргументов argumentAddress1 и argumentAddress2. Обязательно запоминаем значение RandomOffset для случая освобождения памяти HookAddress.

Memory.FreeMemory(HookAddress - RandomOffset);
Memory.FreeMemory(argumentAddress1 - RandomOffsetArgs1);
Memory.FreeMemory(argumentAddress2 - RandomOffsetArgs2);

И изменим InjectAndExecute метод и как обещал, реализуем очередь для многопоточного вызова метода:

public byte[] InjectAndExecute(IEnumerable<string> asm, bool returnValue = false, int returnLength = 0)
{
    lock (Locker)
    {
        var offset = 0;
        var randomValue = (uint)Random.Int(0, 60);
        //Наша очередь может хранить 80/4 = 20 значений
        while (Memory.Read<int>(argumentAddress1 + offset) != 0 || Memory.Read<int>(argumentAddress2 + offset) != 0)
        {
            offset += 4;
            if (offset >= 80)
            {
                offset = 0;
            }
        }
        Memory.Asm.Clear();
        foreach (var str in asm)
        {
            for (var i = Random.Int(0, 3); i >= 1; i--)
            {
                Memory.Asm.AddLine(ProtectHook());
            }
            Memory.Asm.AddLine(str);
        }
        dwAddress = Memory.AllocateMemory(Memory.Asm.Assemble().Length + Random.Int(60, 80)) + randomValue;
        Memory.Asm.Inject(dwAddress);
        Memory.Write<uint>(argumentAddress1, dwAddress + offset);
    }
    while (Memory.Read<int>(argumentAddress1 + offset) > 0)
    {
        Thread.Sleep(1);
    }
    byte[] result = new byte[0];
    if (returnValue)
    {
         result = Memory.ReadBytes(Memory.Read<uint>(argumentAddress2 + offset), returnLength);
    }
    Memory.Write<int>(argumentAddress2 + offset, 0);
    Memory.FreeMemory(dwAddress - randomValue);

    return result;
}

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

Часть 3 – Под прицелом WoW 5.4.x (Структуры)

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

Перед тем как начать, можете поставить на закачку World of Warcraft 5.4.x, для начала, нам понадобиться только Wow.exe, так что можете качать только его. А теперь я Вас погружу в святая святых всех читеров и ботоводов игры World of Warcraft.

Начну совсем из далека. У каждой программы есть алгоритм по которому она работает и память в которой она хранит данные, другими словами — есть код, а есть данные иногда данные бывают и в самом коде. Так во для того что бы иметь представление об окружающем мире в игре, нам необходимы именно данные. Что бы их получить, рассмотрим как они хранятся в памяти и какие они бывают. Итак, введем понятие игрового объекта (в дальнейшем WowObject) — это базовый объект, который хранится в памяти и имеет такие свойства Descriptors, ObjectType и Guid, вот его структура:

    [StructLayout(LayoutKind.Sequential)]
    struct WowObjStruct
    {
        IntPtr vtable;              // 0x00
        public IntPtr Descriptors;  // 0x4
        IntPtr unk1;                // 0x8
        public int ObjectType;      // 0xC
        int unk3;                   // 0x10
        IntPtr unk4;                // 0x14
        IntPtr unk5;                // 0x18
        IntPtr unk6;                // 0x1C
        IntPtr unk7;                // 0x20
        IntPtr unk8;                // 0x24
        public ulong Guid;          // 0x28
    }

где Guid — уникальное значение объекта в игре, ObjectType — тип объекта в игре и может принимать следующие значения:

public enum WoWObjectType : int
{
    Object = 0,
    Item = 1,
    Container = 2,
    Unit = 3,
    Player = 4,
    GameObject = 5,
    DynamicObject = 6,
    Corpse = 7,
    AreaTrigger = 8,
    SceneObject = 9,
    NumClientObjectTypes = 0xA,
    None = 0x270f,
}

[Flags]
public enum WoWObjectTypeFlags
{
    Object = 1 << WoWObjectType.Object,
    Item = 1 << WoWObjectType.Item,
    Container = 1 << WoWObjectType.Container,
    Unit = 1 << WoWObjectType.Unit,
    Player = 1 << WoWObjectType.Player,
    GameObject = 1 << WoWObjectType.GameObject,
    DynamicObject = 1 << WoWObjectType.DynamicObject,
    Corpse = 1 << WoWObjectType.Corpse,
    AreaTrigger = 1 << WoWObjectType.AreaTrigger,
    SceneObject = 1 << WoWObjectType.SceneObject

}

а Descriptors — это указатель на память с данными об объекте WowObject. Что бы было понятнее приведу небольшой пример:

public class WowObject
{
    private IntPtr BaseAddress;
    private WowObjStruct ObjectData;

    public WowObject(IntPtr address)
    {
        BaseAddress = address;
        ObjectData = Memory.Process.Read<WowObjStruct>(BaseAddress);
    }

    public bool IsValid { get { return BaseAddress != IntPtr.Zero; } }

    public T GetValue<T>(Enum index) where T : struct
    {
        return Memory.Process.Read<T>(ObjectData.Descriptors + (int)index * IntPtr.Size);
    }

    public void SetValue<T>(Enum index, T val) where T : struct
    {
        Memory.Process.Write<T>(ObjectData.Descriptors + (int)index * IntPtr.Size, val);
    }

    public bool IsA(WoWObjectTypeFlags flags)
    {
        return (GetValue<int>(Descriptors.ObjectFields.Type) & (int)flags) != 0;
    }
}

Например, мы хотим получить EntryId (EntryId — это что-то вроде класса, для объектов, т.е. 2 одинаковых предмета в игровом мире имеют одинаковый EntryId, но Guid у них разный), вот базовые дескрипторы для WowObject:

public enum ObjectFields
{
    Guid = 0,
    Data = 2,
    Type = 4,
    EntryId = 5,
    DynamicFlags = 6,
    Scale = 7,
    End = 8,
}

[Flags]
public enum ObjectDynamicFlags : uint
{
    Invisible = 1 << 0,
    Lootable = 1 << 1,
    TrackUnit = 1 << 2,
    TaggedByOther = 1 << 3,
    TaggedByMe = 1 << 4,
    Unknown = 1 << 5,
    Dead = 1 << 6,
    ReferAFriendLinked = 1 << 7,
    IsTappedByAllThreatList = 1 << 8,
}

Очевидно, что код будет следующим:

public class WowObject
{
    //Ранее объявленные члены класса
    public int Entry
    {
        get { return GetValue<int>(ObjectFields.EntryId); }
    }
}

Все игровые объекты игры хранятся последовательно, т.е. структура следующего объекта будет найдена по смещению 0x28 (см. WowObjStruct) от текущего указателя при условии, что он существует. Теперь разберемся как найти все структуры игры. Всеми WowObject заправляет менеджер объектов (далее ObjectManager).

Объявим его используя следующие структуры:

 [StructLayout(LayoutKind.Sequential)]
    struct TSExplicitList // 12
    {
        public TSList baseClass; // 12
    }

    [StructLayout(LayoutKind.Sequential)]
    struct TSList // 12
    {
        public int m_linkoffset; // 4
        public TSLink m_terminator; // 8
    }

    [StructLayout(LayoutKind.Sequential)]
    struct TSLink // 8
    {
        public IntPtr m_prevlink; //TSLink *m_prevlink // 4
        public IntPtr m_next; // C_OBJECTHASH *m_next // 4
    }

    [StructLayout(LayoutKind.Sequential)]
    struct TSHashTable // 44
    {
        public IntPtr vtable; // 4
        public TSExplicitList m_fulllist; // 12
        public int m_fullnessIndicator; // 4
        public TSGrowableArray m_slotlistarray; // 20
        public int m_slotmask; // 4
    }

    [StructLayout(LayoutKind.Sequential)]
    struct TSBaseArray // 16
    {
        public IntPtr vtable; // 4
        public uint m_alloc; // 4
        public uint m_count; // 4
        public IntPtr m_data;//TSExplicitList* m_data; // 4
    }

    [StructLayout(LayoutKind.Sequential)]
    struct TSFixedArray // 16
    {
        public TSBaseArray baseClass; // 16
    }

    [StructLayout(LayoutKind.Sequential)]
    struct TSGrowableArray // 20
    {
        public TSFixedArray baseclass; // 16
        public uint m_chunk; // 4
    }
    [StructLayout(LayoutKind.Sequential)]
    struct CurMgr // 248 bytes x86, 456 bytes x64
    {
        public TSHashTable VisibleObjects; // m_objects 44
        public TSHashTable LazyCleanupObjects; // m_lazyCleanupObjects 44
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)]
        // m_lazyCleanupFifo, m_freeObjects, m_visibleObjects, m_reenabledObjects, whateverObjects...
        public TSExplicitList[] Links; // Links[10] has all objects stored in VisibleObjects it seems 12 * 11 = 132
#if !X64
        public int Unknown1; // wtf is that and why x86 only? // 4
        public int Unknown2; // not sure if this actually reflects the new object manager structure, but it does get the rest of the struct aligned correctly
        public int Unknown3; // not sure if this actually reflects the new object manager structure, but it does get the rest of the struct aligned correctly
#endif
        public ulong ActivePlayer; // 8
        public int PlayerType; // 4
        public int MapId; // 4
        public IntPtr ClientConnection; // 4
        public IntPtr MovementGlobals; // 4
    }

А это смещения для конкретной версии World of Warcraft

    public enum ObjectManager
    {
        connection = 0xEC4140,
        objectManager = 0x462c,
    }

Сейчас объясню как их найти. Для начала запускаем IDA и открываем в нем, уже надеюсь скачанный, Wow.exe. Как откроется, запоминаем BaseAddress, чаще всего он 0x400000, жмем Ctrl+L и ищем метку aObjectmgrclien. После перехода на нее жмем Ctrl+X и открываем последний референс. Вы должны увидеть, что-то такое:

    push    0
    push    0A9Fh
    push    offset aObjectmgrclien ; "ObjectMgrClient.cpp"
    push    100h
    call    sub_5DC588
    test    eax, eax
    jz      short loc_79EEEB
    mov     ecx, eax
    call    sub_79E1E1
    jmp     short loc_79EEED
loc_79EEEB:
    xor     eax, eax
loc_79EEED:
    mov     ecx, dword_12C4140
    fldz
    mov     [ecx+462Ch], eax

Нас интересует первый dword, это dword_12C4140 и следующий за ним mov [ecx+462Ch], eax. Отсюда получаем, connection = 0x12C4140 — 0x400000 = 0xEC4140, objectManager = 0x462C. Для пересчета, я использую HackCalc.

    public class ObjectManager : IEnumerable<WowObject>
    {
        private CurMgr _curMgr;
        private IntPtr _baseAddress;
        private WowGuid _activePlayer;
        private WowPlayer _activePlayerObj;

        public void UpdateBaseAddress()
        {
            var connection = Memory.Process.Read<IntPtr>((int)ObjectManager.connection, true);
            _baseAddress = Memory.Process.Read<IntPtr>(connection + (int)ObjectManager.objectManager);
        }

        private IntPtr BaseAddress
        {
            get { return _baseAddress; }
        }

        public WowGuid ActivePlayer
        {
            get { return _activePlayer; }
        }

        public WowPlayer ActivePlayerObj
        {
            get { return _activePlayerObj; }
        }

        public IntPtr ClientConnection
        {
            get { return _curMgr.ClientConnection; }
        }

        public IntPtr FirstObject()
        {
            return _curMgr.VisibleObjects.m_fulllist.baseClass.m_terminator.m_next;
        }

        public IntPtr NextObject(IntPtr current)
        {
            return Memory.Process.Read<IntPtr>(current + _curMgr.VisibleObjects.m_fulllist.baseClass.m_linkoffset + IntPtr.Size);
        }

        public IEnumerable<WowObject> GetObjects()
        {
            _curMgr = Memory.Process.Read<CurMgr>(BaseAddress);
            _activePlayer = new WowGuid(_curMgr.ActivePlayer);
            IntPtr first = FirstObject();
            while (((first.ToInt64() & 1) == 0) && first != IntPtr.Zero)
            {
                var wowObject = new WowObject(first);
                if (wowObject.Guid.Value == _curMgr.ActivePlayer)
                {
                     _activePlayerObj = new WowPlayer(first);
                }
                first = NextObject(first);
            }
        }

        public IEnumerator<WowObject> GetEnumerator()
        {
             return GetObjects().GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

Таким образом мы можем получить все WowObject, ну и давайте научимся находить всех игровых юнитов (это все игровые и не игровые персонажи). Введем класс WowUnit:

public enum UnitField
{
    CachedSubName = 0,
    UnitClassificationOffset2 = 32,
    CachedQuestItem1 = 48,
    CachedTypeFlag = 76,
    IsBossOffset2 = 76,
    CachedModelId1 = 92,
    CachedName = 108,
    UNIT_SPEED = 128,
    TaxiStatus = 0xc0,
    TransportGUID = 2096,
    UNIT_FIELD_X = 0x838,
    UNIT_FIELD_Y = 0x83C,
    UNIT_FIELD_Z = 0x840,
    UNIT_FIELD_R = 2120,
    DBCacheRow = 2484,
    IsBossOffset1 = 2484,
    UnitClassificationOffset1 = 2484,
    CanInterrupt = 3172,
    CastingSpellID = 3256,
    ChannelSpellID = 3280,
}

public enum UnitFields
{
    Charm = ObjectFields.End + 0,
    Summon = 10,
    Critter = 12,
    CharmedBy = 14,
    SummonedBy = 16,
    CreatedBy = 18,
    DemonCreator = 20,
    Target = 22,
    BattlePetCompanionGUID = 24,
    ChannelObject = 26,
    ChannelSpell = 28,
    SummonedByHomeRealm = 29,
    Sex = 30,
    DisplayPower = 31,
    OverrideDisplayPowerID = 32,
    Health = 33,
    Power = 34,
    MaxHealth = 39,
    MaxPower = 40,
    PowerRegenFlatModifier = 45,
    PowerRegenInterruptedFlatModifier = 50,
    Level = 55,
    EffectiveLevel = 56,
    FactionTemplate = 57,
    VirtualItemID = 58,
    Flags = 61,
    Flags2 = 62,
    AuraState = 63,
    AttackRoundBaseTime = 64,
    RangedAttackRoundBaseTime = 66,
    BoundingRadius = 67,
    CombatReach = 68,
    DisplayID = 69,
    NativeDisplayID = 70,
    MountDisplayID = 71,
    MinDamage = 72,
    MaxDamage = 73,
    MinOffHandDamage = 74,
    MaxOffHandDamage = 75,
    AnimTier = 76,
    PetNumber = 77,
    PetNameTimestamp = 78,
    PetExperience = 79,
    PetNextLevelExperience = 80,
    ModCastingSpeed = 81,
    ModSpellHaste = 82,
    ModHaste = 83,
    ModRangedHaste = 84,
    ModHasteRegen = 85,
    CreatedBySpell = 86,
    NpcFlag = 87,
    EmoteState = 89,
    Stats = 90,
    StatPosBuff = 95,
    StatNegBuff = 100,
    Resistances = 105,
    ResistanceBuffModsPositive = 112,
    ResistanceBuffModsNegative = 119,
    BaseMana = 126,
    BaseHealth = 127,
    ShapeshiftForm = 128,
    AttackPower = 129,
    AttackPowerModPos = 130,
    AttackPowerModNeg = 131,
    AttackPowerMultiplier = 132,
    RangedAttackPower = 133,
    RangedAttackPowerModPos = 134,
    RangedAttackPowerModNeg = 135,
    RangedAttackPowerMultiplier = 136,
    MinRangedDamage = 137,
    MaxRangedDamage = 138,
    PowerCostModifier = 139,
    PowerCostMultiplier = 146,
    MaxHealthModifier = 153,
    HoverHeight = 154,
    MinItemLevel = 155,
    MaxItemLevel = 156,
    WildBattlePetLevel = 157,
    BattlePetCompanionNameTimestamp = 158,
    InteractSpellID = 159,
    End = 160,
}

public class WowUnit : WowObject
{
    public WowUnit(IntPtr address)
        : base(address)
    {
    }
    public int Health
    {
        get { return GetValue<int>(UnitFields.Health); }
    }

    public int MaxHealth
    {
        get { return GetValue<int>(UnitFields.MaxHealth); }
    }

    public bool IsAlive
    {
        get { return !IsDead; }
    }

    public bool IsDead
    {
        get { return this.Health <= 0 || (DynamicFlags & ObjectDynamicFlags.Dead) != 0; }
    }


    public ulong TransportGuid
    {
        get { return GetValue<ulong>(UnitField.TransportGUID); }
    }


    public bool InTransport
    {
        get { return TransportGuid > 0; }
    }

    public Vector3 Position
    {
        get
        {
            if (Pointer == IntPtr.Zero) return Vector3.Zero;
            if (InTransport)
            {
                var wowObject = Memory.ObjectManager.GetObjectByGUID(TransportGuid);
                if (wowObject != null)
                {
                    var wowUnit = new WowUnit(wowObject.Pointer);
                    if (wowUnit.IsValid && wowUnit.IsAlive)
                        return wowUnit.Position;
                }
            }

            var position = new Vector3(
                Memory.Process.Read<float>(Pointer + (int)UnitField.UNIT_FIELD_X),
                Memory.Process.Read<float>(Pointer + (int)UnitField.UNIT_FIELD_Y),
                Memory.Process.Read<float>(Pointer + (int)UnitField.UNIT_FIELD_Z),
                "None");

            return position;
        }
    }
}

Остается проитерировать ObjectManager и проверить wowObject.IsA(WoWObjectTypeFlags.Unit). На этом всё, переходим к следующей части.

Часть 4 – Под прицелом WoW 5.4.x (Перемещение)

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

Так уж сложилось что и взаимодействие с игровым объектом и перемещение по щелчку в World of Warcraft возможно при клике мыши. Но мы не будем симулировать клики, через WinApi, мы сделаем круче. Мы перехватим то место, где уже клик обрабатывается игрой, как клик на экране, причем он уже будет переведен из координат экрана в координаты игрового мира. Для начала получим адреса некоторых функций, они нам очень понадобятся в процессе, это просто сделать с помощью нашего всеми любимого дебагера IDA:

    public enum FunctionWow
    {
        ClntObjMgrGetActivePlayer = 0x39B615,
        ClntObjMgrGetActivePlayerObj = 0x4FC6,
        FrameScript_ExecuteBuffer = 0x4fd12,
        Spell_C_HandleTerrainClick = 0x38f129,
        FrameScript__GetLocalizedText = 0x414267,
        IsOutdoors = 0x414b53,
        UnitCanAttack = 0x41ad3c,
        CGUnit_C__InitializeTrackingState = 0x41fb57,
        CGWorldFrame__Intersect = 0x5eef7b,
        CGUnit_C__Interact = 0x8D01D0,
    }

    public enum ClickToMove
    {
        CTM = 0x420543,
        CTM_PUSH = 0xD0EEBC,
        CTM_X = 0xD0EF2C,
        CTM_Y = CTM_X+4,
        CTM_Z = CTM_Y+4,
    }

Объявим класс WorldClick:

public enum ClickType
{
    FaceTarget = 0x1,
    Face = 0x2,
    StopThrowsException = 0x3,
    Move = 0x4,
    NpcInteract = 0x5,
    Loot = 0x6,
    ObjInteract = 0x7,
    FaceOther = 0x8,
    Skin = 0x9,
    AttackPosition = 0xa,
    AttackGuid = 0xb,
    ConstantFace = 0xc,
    None = 0xd,
    Attack = 0x10,
    Idle = 0x13,
}

public static class WorldClick
{
    public static void ClickTo(float x, float y, float z, ulong guid, ClickType action, float precision)
    {
        if (Mathf.Abs(x) < 0.1 && Mathf.Abs(y) < 0.1 && (Mathf.Abs(z) < 0.1 && (long)guid == 0L))
            return;
        //память для 3х координат
        var positionAddress = Memory.Process.AllocateMemory(3 * sizeof(float));
        //guid типа ulong в 8 байт
        var guidAddress = Memory.Process.AllocateMemory(sizeof(ulong));
        //значение точности, до которой продолжать движение, я беру 0.5f
        var precisionAddress = Memory.Process.AllocateMemory(sizeof(float));
        if (positionAddress <= 0U || guidAddress <= 0U || precisionAddress <= 0U)
            return;
        Memory.Process.Write<ulong>(guidAddress, guid);
        Memory.Process.Write<float>(precisionAddress, precision);
        Memory.Process.Write<float>(positionAddress, x);
        Memory.Process.Write<float>(positionAddress + IntPtr.Size, y);
        Memory.Process.Write<float>(positionAddress + IntPtr.Size * 2, z);
        var asm = new[]
        {
        "call " + Memory.Process.GetAbsolute(FunctionWow.ClntObjMgrGetActivePlayer ),
         //Проверка на наличие активного игрока
        "test eax, eax",
        "je @out",
         //Получаем указатель на объект - понадобится ниже
        "call " + Memory.Process.GetAbsolute(FunctionWow.ClntObjMgrGetActivePlayerObjAddress),
        "test eax, eax",
        "je @out",
        "mov edx, [" + precisionAddress + "]",
        "push edx",
        "push " + positionAddress,
        "push " + guidAddress,
        "push " + (int)action,
        "mov ecx, eax",
        //Вызываем ClickToMove()
        "call " + Memory.Process.GetAbsolute((int)ClickToMove.CTM),
        "@out:",
        "retn"
        };
        Memory.Hook.InjectAndExecute(asm);
        Memory.Process.FreeMemory(positionAddress);
        Memory.Process.FreeMemory(guidAddress);
        Memory.Process.FreeMemory(precisionAddress);
    }

    public static ClickType GetClickTypePush()
    {
        return (ClickToMoveType)Memory.Process.Read<int>((int)ClickToMove.CTM_PUSH, true);
    }

    public static Vector3 GetClickPosition()
    {
        return new Vector3(
                Memory.Process.Read<float>((int)ClickToMove.CTM_X, true),
                Memory.Process.Read<float>((int)ClickToMove.CTM_Y, true),
                Memory.Process.Read<float>((int)ClickToMove.CTM_Z, true));
    }
}

Ну все, теперь можно бегать по просторам Азерота с помощью:

WorldClick.ClickTo(x,y,z, 0, ClickType.Move, 0.5f);

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