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

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

Но обо всем по порядку, так что жду Вас под катом!


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


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

Содержание

  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 (Кастуем фаерболл)

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

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

  • 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 байт размер, как его реализовать, подумайте сами. А в следующей статье, я покажу свою реализацию и как прятать наш код.
  • +21
  • 33,1k
  • 9
Поделиться публикацией

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

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

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

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

    0
    Спасибо за статью!
    Мне кажется, что использование FASM в C# коде — это как кондиционер на велосипеде. Ну или велосипед на кондиционере. Да и вообще, внедрение в нативный код на C#, без DLLки, которую можно инжектить и спокойно вызывать (чтобы можно было хотя бы на С писать, а не на асме), странно.
    Интересно было бы посмотреть на вызов C# кода из внедренного кода, хотя это, кажется, из области фантастики.

    И еще,
    нужно предусмотреть очередь, для этого я и использовал 80 байт размер, как его реализовать, подумайте сами
    не понял фразу.
      +1
      Про DLL читайте Часть 0 и комментарии к ней, я доступно объяснил почему я отказался от dll injection.
      Если у вас много потоков которые вызывают InjectAndExecute, то нужно предусмотреть очередь, для этого я и использовал 80 байт размер, как его реализовать, подумайте сами. А в следующей статье, я покажу свою реализацию и как прятать наш код.

      Представьте, что у вас одновременно 2 вызова InjectAndExecute, которые пишут в argumentAddress1 и argumentAddress2, что случится? Случится, то что вы получите ответ одного из них в обоих вызовах.
        0
        > вызов C# кода из внедренного кода
        в Deviare и EasyHook были такие примочки.
        +2
        вызов C# кода из внедренного кода, хотя это, кажется, из области фантастики.
        Нет, не фантастика :)
        Готовых кусочков кода я вам не приведу (можете попробовать поискать на OwnedCore — хороший источник информации о читах, хаках, внутреннем устройстве WoW и не только; в том числе proof of concept интересующего вас вопроса), но если вкратце, то просто внедряется нативная dll, которая создаёт хост CLR, подгружает сборку и передаёт ей управление, как-то так (CppHostCLR).
        +2
        А можно поподробнее что из чего вызывается? (я в ассемблере не очень)
        Как я понял внедрённый код вызывается из хукнутой функции(куда предварительно воткнули jmp на этот код), а вот внедрённый код что делает? он самостоятельный или передаёт управление куда то ещё(«call eax»)? куда?
        Так-же не понял момент про очередь и её связь с многопоточностью: ведь если внедрённый код не использует глобальных переменных то проблем быть не должно — стек то у каждого потока свой.

        PS: popad/pushad — зачем, ведь, как я понимаю хукаются функции DirectX, а они используя соглашение stdcall получают аргументы через стек и следовательно(если мы внедрились в самое начало функции, а не в середину) сохранять регистры незачем.

        PPS: хотелось бы по больше теории на тему что и как делается ибо по неполным кускам кода всю логику восстановить не получается.
          0
          Можно и нужно! Совершенно верно, ассемблерный код вызывается каждый фрейм из перехваченной функции. А вот что он делает, это весьма очевидно, нужно всего лишь проявить внимательность. Я специально его сделал таким, что бы оставить подсказку для реализации очереди в InjectAndExecute. Но увы — никто не догадался. call eax, это переход по указателю в (argumentAddress1 + offset), может это что-либо прояснит и кто-нибудь все же догадается и да, argumentAddress1 глобальна внутри класса, ведь ее используем в InjectAndExecute. Команды нужны popad/pushad — ведь мы не знаем, что будет по указателю в argumentAddress1 и лучше не рисковать. В следующей статье я максимально попытаюсь объяснить этот код с хорошим примером на C#, давайте подождем, вдруг кто-нибудь догадается.
            +1
            Вы с какого процессора ассемблер изучать начали?
            Я таких извращений даже на 8080 не встречал. Чем-то MCS-51 напоминает, разве что.
            Не заставляйте новичков разбирать такое, да еще без комментариев. Садизм-же.
            Эквивалентный код:
            	mov	ebx, 0
            loop_it:
            	mov	eax, [ebx*4+argumentAddress1]
            	
            	test	eax, eax
            	jz	skip_it
            
            	
            	push	ebx
            	call	eax
            	pop	ebx
            
            	mov	[ebx*4+argumentAddress2], eax
            
            skip_it:
            	inc	ebx
            	cmp	ebx, 80/4
            	jnz	loop_it
            
              +1
              Мне как новичку в этой сфере тема очень интересна. Да и актуальная будет в ближайшие 20+ лет. Но хотелось бы более развёрнутой информации от автора, без лишних тайн и разгадок. Всё таки устройство не велосипеда разбираем. У меня стаж программирования 7 лет, но эта тема даётся сложно. Комментарии типа «догадайтесь/решите/подумайте» вводят в ступор, потому что без опыта очень тяжело…
          0
          Рекомендую также посмотреть в сторону Frida(внедрение JavaScript) и mmBBQ(внедрение Lua). Все легче, чем ассемблер.

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

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