Исходник Гайд Мини-гайд по хукам. [Теория + Практика]

Const

Потрачен
Автор темы
28
54
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Всем привет. В последнее время я стал замечать, что абсолютное большинство использует всякие готовые библиотеки для хуков, и даже не заморачиваются о строении функций, типах хуков и прочим. Как таковых гайдов очень мало, а если и есть - на английском языке. В этой теме я расскажу обо всем просто и понятно.

Начнем с банального. Да кто такой все таки, этот ваш, хук?! Хук, с английского обозначает: "ловить". Назвали его так, потому что он ловит вызовы каких либо команд. Не забегая вперед, расскажу о основных командах, которые вы должны знать.
Абсолютно все процессы состоят из машинного кода. Это последовательность битов, но не бойтесь, с битами работать нам не потребуется (на начальных этапах). При помощи различных дизассемблеров (я использую IDA Pro), можно этот машинный код превратить в ассемблерный. Если вы собираетесь работать с хуками, вы не обойдетесь без IDA Pro и Cheat Engine, так что устанавливайте эти программы. Рассказывать, как с ними работать, я не буду, в интернете полно гайдов.
Нам предоставлен ассемблерный код программы, нам нужно знать две основные команды:
JMP - команда прыжка. Ее опкод - 0xE9. Выполняет прыжок с одного место, на другое. Если есть JMP на одно место, обязательно есть прыжок обратно, чтобы не прервать цикл игры.
CALL - команда вызова. Имет одинаковую инструкцию размером в 5 байт, как и JMP. Единственное, чем отличается, так это не обязательно прыгать обратно.
У этих команд одинаковые инструкции. Допустим, у нас есть адрес 0xFF00FF. Это адрес на инструкцию одной из этих команд. Если не прибавлять ничего, считывая 1 байт (uint8_t) с этого адреса мы получим как раз таки опкод одной из этих команд. По смещению +1 от адреса инструкции, мы получим релативный адрес, размером в 4 байта. Он отличается от обычного адреса, куда хочет прыгнуть или откуда хочет вызвать опкод. Он высчитывается по такой формуле: (куда_прыгаем или откуда_вызываем) - (откуда_прыгаем или где_вызываем) - 5.
Допустим, у нас есть такая штучка:
Код:
.text:0053ECBD 004                 call    _Idle
Это вызов функции _Idle. Адрес функции _Idle - 0x0053E920. В данном случае, релативный адрес будет таким: 0x0053E920 - 0x0053ECBD - 5. Этот релативный адрес будет располагаться по адресу инструкции + 1. В данном случае, 0x0053ECBD + 1, и будет иметь размер 4 байта (uint32_t).
Получается, чтобы получить опкод, нам нужно выполнить такое чтение:
C++:
uint8_t call_opcode = *reinterpret_cast<uint8_t *>(0x0053ECBD);
Чтобы получить релативный адрес, такое:
C++:
uint32_t relative_address = *reinterpret_cast<uint32_t *>(0x0053ECBD + 1);

Я думаю, основную логику инструкций JMP и CALL вы поняли, расскажу про первый метод хука, как я его назвал, Redirect.
Суть хука заключается в том, чтобы подменить релативный адрес команды JMP/CALL на свой. Чтобы данное провернуть, нужно снять протекцию с региона функцией VirtualProtect, занопить всю инструкцию размером в 5 байт функцией memset (на всякий), подменить опкод на CALL/JMP (0xE8/0xE9), записав 1 байт, и подменить релативный адрес, который высчитать по формуле, которую я представил выше, и восстановить протекцию.
C++:
unprotect_region protect_of_region(pointer_on_source, size_of_default_instruction); // Инициализируем класс unprotect_region. Снимаем защиту с региона памяти.
            
original_instructions.first = *reinterpret_cast<uint8_t *>(
    reinterpret_cast<uint32_t>(pointer_on_source)); // Записываем в пару оригинальный опкод команды.
original_instructions.second = *reinterpret_cast<uint32_t *>(
    reinterpret_cast<uint32_t>(pointer_on_source) + 0x01); // Записываем в пару оригинальный релативный адрес.

std::memset(pointer_on_source, no_operation_opcode, size_of_default_instruction); // Ноплю всю инструкцию JMP/CALL, её статичный размер для x86 - 5 байт.
*reinterpret_cast<uint8_t *>(pointer_on_source) = method_of_hook; // Меняем оригинальный опкод на опкод команды, которую мы указали в параметрах.

uint32_t relative_address =
    reinterpret_cast<uint32_t>(pointer_on_destination) -
    reinterpret_cast<uint32_t>(pointer_on_source) - 5; // Вычисляем релативный адрес прыжка от pointer_on_source до pointer_on_destination.
// Если говорить проще - прыгаем с оригинальной функции на нашу.

*reinterpret_cast<uint32_t *>(
    reinterpret_cast<uint32_t>(pointer_on_source) + 0x01) = relative_address; // Перезаписываем релативный адрес на собственный.

protect_of_region.~unprotect_region(); // Вызываем деструктор класса, восстанавливаем оригинальный уровень протекции региона.
pointer_on_source - указатель на адрес инструкции JMP/CALL. pointer_on_destination - указатель на вашу функцию, на которую вы подменили вызов.

В вашей функции, вы должны вызвать оригинальную функцию, на которую был вызов.
В данном случае:
C++:
//int __usercall Idle@<eax>(int a1@<ecx>, int a2@<edx>, int bp0@<ebp>, int a4@<edi>, int a5@<esi>, long double a6@<st0>, int a3)

using idle_t = int(__cdecl *)(int, int, int, int, int, long double, int);
idle_t idle = reinterpret_cast<idle_t>(0x0053E920);

int idle_hook(int a1, int a2, int bp0, int a4, int a5, long double a6, int a3) {
    return idle(a1, a2, bp0, a4, a5, a6, a3); // Вызываем оригинальную функцию.
}
Прошу заметить, в idle_hook я не указал __cdecl, только потому что в С++ функции по умолчанию имеют такое соглашение о вызовах. Если бы функция была __stdcall - вы бы записали __stdcall, если бы __thiscall, то пришлось бы поступить немного по-другому.
В idle_hook вы бы приписали соглашение о вызовах __fastcall, в idle_t - __thiscall, а в idle_hook первым параметром вы бы записали void *, и вторым тоже. В итоге у вас бы получилось:
C++:
using idle_t = int(__thiscall *)(void *, int, int, int, int, int, long double, int);
idle_t idle = reinterpret_cast<idle_t>(0x0053E920);

int __fastcall idle_hook(void *_this, void *unused, int a1, int a2, int bp0, int a4, int a5, long double a6, int a3) {
    return idle(_this, a1, a2, bp0, a4, a5, a6, a3); // Вызываем оригинальную функцию.
}
Почему не __thiscall, спросите вы. А все потому, что компиляторы не дают статическим функциям иметь данное соглашение. __fastcall очень похож по строению пролога (поговорим чуть позже об этом) на __thiscall, только одна разница - во второй параметр добавляется еще один указатель, он не используется. Просто не трогайте его.

С Redirect хуками мы разобрались, теперь расскажу о Trampoline.
Trampoline от Redirect кардинально отличается. Если Redirect просто подменяет релативный адрес команды вызова или прыжка, то Trampoline взаимодействует с прологом функции.
Что такое пролог функции? Пролог функции - первые несколько байт функции, которые подготавливают стек, пушат регистры.
У пролога есть свой эпилог. Эпилог отличается тем, что он располагается в конце функции, и восстанавливает стек и регистры до того состояния, которое было до вызова.
Расскажу на примере функции void __cdecl CTimer__Update(void):

1596368331558.png


Логика трамплин хука заключается в том, чтобы этот самый пролог сохранить в отдельную функцию, занопить весь пролог, поставить там прыжок на нашу функцию, в нашей функции вызвать ту, в которой мы сохранили пролог, и вдобавок приписать туда прыжок обратно.
Получается так: вместо пролога, jmp -> наша_функция -> jmp трамплин -> jmp обратно (+1, чтобы не было рекурсии).
C++:
if (length_of_prologue < 5) { // РАЗМЕР ПРОЛОГА НЕ МОЖЕТ БЫТЬ МЕНЬШЕ 5! Запомните это раз и навсегда.
    return;
}

prologue_length = length_of_prologue;

/*
    Выделяем виртуальную память размером с размер пролога + 5.
    Представим ситуацию, мы хукаем функцию, у которой размер пролога 10,
    но мы выделяем 10 + 5, а не 10. Почему?
    Потому-что в первые 10 байт запишется пролог, а в остальные 5 байт
    запишется прыжок на оригинальную функцию, чтобы не сохранять в трамплин весь ее код.
*/
pointer_on_gateway = VirtualAlloc(nullptr, length_of_prologue + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(pointer_on_gateway, pointer_on_source, length_of_prologue); // Копируем байты пролога в указатель.

uint32_t relative_address =
    reinterpret_cast<uint32_t>(pointer_on_source) -
    reinterpret_cast<uint32_t>(pointer_on_gateway) - 5; // Вычисляем релативный адрес прыжка от трамплина до оригинальной функции.

*reinterpret_cast<uint8_t *>(
    reinterpret_cast<uint32_t>(pointer_on_gateway) +
    length_of_prologue) = 0xE9; // Записываем опкод прыжка, +1 байт после пролога в трамплине.

*reinterpret_cast<uint32_t *>(
    reinterpret_cast<uint32_t>(pointer_on_gateway) +
    length_of_prologue + 0x01) = relative_address; // Записываем релативный адрес прыжка на ориг. функцию, +2 байта после пролога.

unprotect_region protect_of_region(pointer_on_source, length_of_prologue); // Инициализируем класс, снимаем защиту памяти.
            
std::memset(pointer_on_source, 0x90, length_of_prologue); // Обнуляем весь пролог оригинальной функции.
*reinterpret_cast<uint8_t *>(pointer_on_source) = 0xE9; // Подменяем первый байт пролога на опкод прыжка.

relative_address =
    reinterpret_cast<uint32_t>(pointer_on_destination) -
    reinterpret_cast<uint32_t>(pointer_on_source) - 5; // Вычисляем релативный адрес прыжка от оригинальной функции на нашу.

*reinterpret_cast<uint32_t *>(
    reinterpret_cast<uint32_t>(pointer_on_source) + 0x01) = relative_address; // Перезаписываем релативный адрес.

protect_of_region.~unprotect_region(); // Восстанавливаем протекцию региона.
В использовании:
C++:
#include "hook/hook.h"

struct vec3d {
    float x, y, z;
};

using process_aim_t = void(__thiscall *)(void *cam_pointer, vec3d *position_of_camera,
    float *, float *, float *);
process_aim_t process_aim;

void __fastcall process_aim_hook(void *cam_pointer, void *not_used, vec3d *position_of_camera,
        float *_1, float *_2, float *_3) {
    process_aim(cam_pointer, position_of_camera, _1, _2, _3);
}

class guide_of_hooking {
public:
    hook *hooked_call_process_aim;

    guide_of_hooking() {
        hooked_call_process_aim = new hook(0x00521500, process_aim_hook, 13); // 13 - размер пролога.
        process_aim = hooked_call_process_aim->get_trampoline<process_aim_t>();
    }
    ~guide_of_hooking() {
        delete hooked_call_process_aim;
    }
} guide_of_hooking;

Если останутся вопросы, напишите в комментарии.
Исходный код на GitHub - https://github.com/dev-Const/hook/blob/master/hook.h

Пара примеров:

C++:
#include "hook/hook.h"

using timer_update_t = void(__cdecl *)();
timer_update_t timer_update =
    reinterpret_cast<timer_update_t>(0x00561B10); // void __cdecl CTimer__Update()
    

void timer_update_hook() {
    timer_update();
}

class guide_of_hooking {
public:
    hook *hooked_call_timer_update;

    guide_of_hooking() {
        // .text:0053E968 | 00C | call _ZN6CTimer6UpdateEv
        hooked_call_timer_update = new hook(0x0053E968, timer_update_hook,
            0, redirect_hook, call_method); // Размер пролога 0, потому что он здесь не требуется, мы подменяем CALL.
    }
    ~guide_of_hooking() {
        delete hooked_call_timer_update;
    }
} guide_of_hooking;
C++:
#include "hook/hook.h"

using timer_update_t = void(__cdecl *)();
timer_update_t timer_update;
    
void timer_update_hook() {
    timer_update(); // Вызываем его.
}

class guide_of_hooking {
public:
    hook *hooked_call_timer_update;

    guide_of_hooking() {
        /*
            .text:00561B10 | 000 | mov ecx, _timerFunction
            .text:00561B16 | 000 | sub esp, 0Ch
        */
        hooked_call_timer_update = new hook(0x00561B10, timer_update_hook, 6);
        timer_update = hooked_call_timer_update->get_trampoline<timer_update_t>(); // Получаем трамплин.
    }
    ~guide_of_hooking() {
        delete hooked_call_timer_update;
    }
} guide_of_hooking;