Пишем бота для MMORPG с ассемблером и дренейками. Часть 3

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

Disclaimer: Автор не несет ответственности за применение вами знаний полученных в данной статье или ущерб в результате их использования. Вся информация здесь изложена только в познавательных целях. Особенно для компаний разрабатывающих MMORPG, что бы помочь им бороться с ботоводами. И, естественно, автор статьи не ботовод, не читер и никогда ими не был.




Для тех кто пропустил прошлые статьи вот содержание, а кто все прочел идем дальше:
Содержание

  1. Часть 0 — Поиск точки внедрения кода
  2. Часть 1 — Внедрение и исполнение стороннего кода
  3. Часть 2 — Прячем код от посторонних глаз
  4. Часть 3 — Под прицелом World of Warcraft 5.4.x (Структуры)
  5. Часть 4 — Под прицелом World of Warcraft 5.4.x (Перемещение)
  6. Часть 5 — Под прицелом World of Warcraft 5.4.x (Кастуем фаерболл)


Перед тем как начать, можете поставить на закачку 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 — уникальное значение объекта в игре, OjectType — тип объекта в игре и может принимать следующие значения:
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). На сегодня все, статья и так вышла огромная для усвоения. Если что-то будет не получаться, пишите в личку с ссылочкой на сорсы на GitHub. Желаю удачи!
  • +15
  • 19,7k
  • 2
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

Комментарии 2

    0
    Добавлю, что для быстрого, удобного и лёгкого поиска оффсетов и потрошения структур традиционно используется такая программа, как CheatEngine, которая содержит в себе весь необходимый функционал, включая отладчик и дизассемблер (в т.ч. 64-битный).
    Поэтому в теории все анти-чит защиты должны бы её выявлять и пытаться блокировать. На деле же просто удивительно, как мало защитных программ реагирует на CE. Тот же ArcheAge например на момент запуска в России спокойно потрошился с помощью данного инструмента — не знаю как сейчас, может уже и пофиксили.
    Кроме того, существуют популярные читерские ресурсы, где можно, не тратя времени на самостоятельный поиск, получить уже готовые структуры с описаниями и значениями оффсетов, а заодно прочитать о последних достижениях на ниве борьбы с античитами и ботхантерами. Почему-то наиболее полезная информация попадается на немецкоязычных порталах, т.ч. придётся использовать Google Translate или иной переводчик.
    Также в порядке борьбы с читерами многие разработчики периодически перекомпилируют клиент, изменяя оффсеты. Тогда захардкоженные боты будут после каждого обновления отваливаться. Для борьбы с плавающими оффсетами ботоводы и читеры используют самописные эвристические анализаторы, которые после каждого обновления игры находят новые оффсеты. В простейшем случае такая эвристика представляет собой просто набор регулярок, которыми и парсится память процесса.
      0
      А. что удивительного? Это для нас может быть вопрос заканчивается на технической стадии, а у разработчика еще экономическая и юридическая хренотень. Ботовод исправно платит за все свои аккаунты и нежужжит, сидит тихо. С одной стороны. С другой, сканировать процессы на компьютере гражданина евросоюза или сша может быть опасно с точки зрения юридеческих последствий. Вот и сидит себе разработчик на попе ровно и прибль считает. Чего лезть туда где неждут.

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

      Самое читаемое