Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Всем привет. В последнее время я стал замечать, что абсолютное большинство использует всякие готовые библиотеки для хуков, и даже не заморачиваются о строении функций, типах хуков и прочим. Как таковых гайдов очень мало, а если и есть - на английском языке. В этой теме я расскажу обо всем просто и понятно.
Начнем с банального. Да кто такой все таки, этот ваш, хук?! Хук, с английского обозначает: "ловить". Назвали его так, потому что он ловит вызовы каких либо команд. Не забегая вперед, расскажу о основных командах, которые вы должны знать.
Абсолютно все процессы состоят из машинного кода. Это последовательность битов, но не бойтесь, с битами работать нам не потребуется (на начальных этапах). При помощи различных дизассемблеров (я использую IDA Pro), можно этот машинный код превратить в ассемблерный. Если вы собираетесь работать с хуками, вы не обойдетесь без IDA Pro и Cheat Engine, так что устанавливайте эти программы. Рассказывать, как с ними работать, я не буду, в интернете полно гайдов.
Нам предоставлен ассемблерный код программы, нам нужно знать две основные команды:
JMP - команда прыжка. Ее опкод - 0xE9. Выполняет прыжок с одного место, на другое. Если есть JMP на одно место, обязательно есть прыжок обратно, чтобы не прервать цикл игры.
CALL - команда вызова. Имет одинаковую инструкцию размером в 5 байт, как и JMP. Единственное, чем отличается, так это не обязательно прыгать обратно.
У этих команд одинаковые инструкции. Допустим, у нас есть адрес 0xFF00FF. Это адрес на инструкцию одной из этих команд. Если не прибавлять ничего, считывая 1 байт (uint8_t) с этого адреса мы получим как раз таки опкод одной из этих команд. По смещению +1 от адреса инструкции, мы получим релативный адрес, размером в 4 байта. Он отличается от обычного адреса, куда хочет прыгнуть или откуда хочет вызвать опкод. Он высчитывается по такой формуле: (куда_прыгаем или откуда_вызываем) - (откуда_прыгаем или где_вызываем) - 5.
Допустим, у нас есть такая штучка:
Это вызов функции _Idle. Адрес функции _Idle - 0x0053E920. В данном случае, релативный адрес будет таким: 0x0053E920 - 0x0053ECBD - 5. Этот релативный адрес будет располагаться по адресу инструкции + 1. В данном случае, 0x0053ECBD + 1, и будет иметь размер 4 байта (uint32_t).
Получается, чтобы получить опкод, нам нужно выполнить такое чтение:
Чтобы получить релативный адрес, такое:
Я думаю, основную логику инструкций JMP и CALL вы поняли, расскажу про первый метод хука, как я его назвал, Redirect.
Суть хука заключается в том, чтобы подменить релативный адрес команды JMP/CALL на свой. Чтобы данное провернуть, нужно снять протекцию с региона функцией VirtualProtect, занопить всю инструкцию размером в 5 байт функцией memset (на всякий), подменить опкод на CALL/JMP (0xE8/0xE9), записав 1 байт, и подменить релативный адрес, который высчитать по формуле, которую я представил выше, и восстановить протекцию.
pointer_on_source - указатель на адрес инструкции JMP/CALL. pointer_on_destination - указатель на вашу функцию, на которую вы подменили вызов.
В вашей функции, вы должны вызвать оригинальную функцию, на которую был вызов.
В данном случае:
Прошу заметить, в idle_hook я не указал __cdecl, только потому что в С++ функции по умолчанию имеют такое соглашение о вызовах. Если бы функция была __stdcall - вы бы записали __stdcall, если бы __thiscall, то пришлось бы поступить немного по-другому.
В idle_hook вы бы приписали соглашение о вызовах __fastcall, в idle_t - __thiscall, а в idle_hook первым параметром вы бы записали void *, и вторым тоже. В итоге у вас бы получилось:
Почему не __thiscall, спросите вы. А все потому, что компиляторы не дают статическим функциям иметь данное соглашение. __fastcall очень похож по строению пролога (поговорим чуть позже об этом) на __thiscall, только одна разница - во второй параметр добавляется еще один указатель, он не используется. Просто не трогайте его.
С Redirect хуками мы разобрались, теперь расскажу о Trampoline.
Trampoline от Redirect кардинально отличается. Если Redirect просто подменяет релативный адрес команды вызова или прыжка, то Trampoline взаимодействует с прологом функции.
Что такое пролог функции? Пролог функции - первые несколько байт функции, которые подготавливают стек, пушат регистры.
У пролога есть свой эпилог. Эпилог отличается тем, что он располагается в конце функции, и восстанавливает стек и регистры до того состояния, которое было до вызова.
Расскажу на примере функции void __cdecl CTimer__Update(void):
Логика трамплин хука заключается в том, чтобы этот самый пролог сохранить в отдельную функцию, занопить весь пролог, поставить там прыжок на нашу функцию, в нашей функции вызвать ту, в которой мы сохранили пролог, и вдобавок приписать туда прыжок обратно.
Получается так: вместо пролога, jmp -> наша_функция -> jmp трамплин -> jmp обратно (+1, чтобы не было рекурсии).
В использовании:
Если останутся вопросы, напишите в комментарии.
Исходный код на GitHub - https://github.com/dev-Const/hook/blob/master/hook.h
Пара примеров:
Начнем с банального. Да кто такой все таки, этот ваш, хук?! Хук, с английского обозначает: "ловить". Назвали его так, потому что он ловит вызовы каких либо команд. Не забегая вперед, расскажу о основных командах, которые вы должны знать.
Абсолютно все процессы состоят из машинного кода. Это последовательность битов, но не бойтесь, с битами работать нам не потребуется (на начальных этапах). При помощи различных дизассемблеров (я использую IDA Pro), можно этот машинный код превратить в ассемблерный. Если вы собираетесь работать с хуками, вы не обойдетесь без IDA Pro и Cheat Engine, так что устанавливайте эти программы. Рассказывать, как с ними работать, я не буду, в интернете полно гайдов.
Нам предоставлен ассемблерный код программы, нам нужно знать две основные команды:
JMP - команда прыжка. Ее опкод - 0xE9. Выполняет прыжок с одного место, на другое. Если есть JMP на одно место, обязательно есть прыжок обратно, чтобы не прервать цикл игры.
CALL - команда вызова. Имет одинаковую инструкцию размером в 5 байт, как и JMP. Единственное, чем отличается, так это не обязательно прыгать обратно.
У этих команд одинаковые инструкции. Допустим, у нас есть адрес 0xFF00FF. Это адрес на инструкцию одной из этих команд. Если не прибавлять ничего, считывая 1 байт (uint8_t) с этого адреса мы получим как раз таки опкод одной из этих команд. По смещению +1 от адреса инструкции, мы получим релативный адрес, размером в 4 байта. Он отличается от обычного адреса, куда хочет прыгнуть или откуда хочет вызвать опкод. Он высчитывается по такой формуле: (куда_прыгаем или откуда_вызываем) - (откуда_прыгаем или где_вызываем) - 5.
Допустим, у нас есть такая штучка:
Код:
.text:0053ECBD 004 call _Idle
Получается, чтобы получить опкод, нам нужно выполнить такое чтение:
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(); // Вызываем деструктор класса, восстанавливаем оригинальный уровень протекции региона.
В вашей функции, вы должны вызвать оригинальную функцию, на которую был вызов.
В данном случае:
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 вы бы приписали соглашение о вызовах __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); // Вызываем оригинальную функцию.
}
С Redirect хуками мы разобрались, теперь расскажу о Trampoline.
Trampoline от Redirect кардинально отличается. Если Redirect просто подменяет релативный адрес команды вызова или прыжка, то Trampoline взаимодействует с прологом функции.
Что такое пролог функции? Пролог функции - первые несколько байт функции, которые подготавливают стек, пушат регистры.
У пролога есть свой эпилог. Эпилог отличается тем, что он располагается в конце функции, и восстанавливает стек и регистры до того состояния, которое было до вызова.
Расскажу на примере функции void __cdecl CTimer__Update(void):
Логика трамплин хука заключается в том, чтобы этот самый пролог сохранить в отдельную функцию, занопить весь пролог, поставить там прыжок на нашу функцию, в нашей функции вызвать ту, в которой мы сохранили пролог, и вдобавок приписать туда прыжок обратно.
Получается так: вместо пролога, 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;