ANTICHAT

ANTICHAT (https://forum.antichat.xyz/index.php)
-   С/С++, C#, Rust, Swift, Go, Java, Perl, Ruby (https://forum.antichat.xyz/forumdisplay.php?f=24)
-   -   Хуки – что это такое и как с ними работать [2] (https://forum.antichat.xyz/showthread.php?t=1391079)

kin4stat 14.06.2021 01:43

Хотел продолжить первый гайд, но понял что нужно объяснить что такое хуки
  1. Создание ASI-плагина с нуля
  2. Хуки – что это такое и как с ними работать
  3. Безопасная инициализация и работа с SAMP
  4. Работа с рендером и Directx9
  5. Обработка событий окна + ImGui

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

При использовании на других ресурсах необходимо указание авторства и ссылки на оригинальную темы!

Перед тем как начать:

Так как этот гайд сделан в целях обучения, здесь не будет показано использование готовых библиотек, а только сырой код.

В процессе написания гайда я понял что без знаний ассемблера и низкоуровневых вы поймете лишь малую часть от написанного здесь. Но если вы поймете что здесь написано - будет очень хорошо.

Все действия производились на Visual Studio 2019 с параметром
Код:

/std:c++17
, в других версиях интерфейс может отличаться.

Все адреса указаны для SAMP R3-1, на других версиях будете ловить краши

И так, начнем:

Хук(от англ. Hook) - перехват. В нашем случае это перехват внутриигровых функций; Когда игра захочет их вызвать - будут выполняться наши действия (наш код), а затем уже можно продолжить выполнение функции, либо сразу сделать возврат, чтобы функция ничего не сделала.

Перед тем как я расскажу про сами хуки, нужно немного углубится в устройство вызова функций.

У каждой функции есть свое соглашение о вызове. Соглашение о вызове - "Правило" которое регулирует каким образом аргументы функции будут переданы самой функции и как именно будет произведен возврат значения, а также кто будет очищать стек после вызова функции(это не полный список, но самое основное что стоит знать). Если например вызвать
Код:

cdecl
функцию указав соглашение о вызове
Код:

stdcall
, то вы получите UB(Undefined behaviour - неопределенное поведение)

В архитектуре x86 исторически сложилось, что разным людям не нравилось что-то в других соглашениях о вызове, и они создавали свои. На x64 такой бардак тоже есть, но уже между разными OC.

Существует много соглашений о вызовах, но описывать все я не буду, ибо они вам вряд ли пригодятся(
Код:

pascal
к примеру)

Мы же рассмотрим 4 соглашения о вызовах:
Код:

cdecl
,
Код:

stdcall
,
Код:

fastcall
,
Код:

thiscall
У всех соглашений аргументы передаются справа налево через стек.

cdecl является основным соглашением о вызове и используется почти везде. Возврат осуществляется через регистр eax, регистр st0 для x87, и пару регистров eax:edx для значений размером в 5-8 байт. Стек очищает тот кто вызывает функцию, поэтому cdecl поддерживает переменное число аргументов. Установлено по умолчанию в MSVC.

stdcall является основным соглашением о вызовах в Windows, а также во многих библиотеках(например basslib). Возврат осуществляется через регистр eax, очистка стека производится самой функцией. Переменное число аргументов не поддерживает.

thiscall используется для вызова методов класса. В регистр ecx кладется скрытый аргумент this, очистка стека производится самой функцией, возврат значения через регистр eax. Переменное число аргументов не поддерживает.

fastcall используется редко. В хуках зачастую используется для обхода thiscall в msvc(чуть позже расскажу что это). Первые два аргумента кладутся в регистры ecx и edx, остальные в том же порядке через стек. Очистка стека производится самой функцией. Переменное число аргументов не поддерживает. Из-за использования регистров для передачи аргументов его назвали fastcall, т.к. операции с регистрами на старых компьютерах были заметно быстрее операциями со стеком.

Теперь можно перейти к теории о хуках.

Для перехвата используются две техники - подмена вызова(call hook) и уже после вызова(в прологе) прыжок в хук.

Начнем с первого: в основном вызовы происходят по релативному адресу, но бывают и вызовы по абсолютному адресу который находится в регистре.

Релативный адрес(от англ. Relative address) - это адрес, относительно места откуда происходит вызов.

Абсолютный адрес - это адрес, указываемый относительно всего адресного пространства программы.

Наперед скажу что мы будем работать только с релативными адресами.

Покажу на примере вызова функции в GTA:SA:

C++:





Код:

.
text
:
0053E972
00
C E8
89
AE
1
D
00
call    _ZN5CFont12InitPerFrameEv
;
CFont
::
InitPerFrame
(
void
)



Вызов происходит по адресу 0x53E972, вызываемая функция находится по адресу 0x719800

Но asm код выше - дизассемблированный код. В самой программе он хранится вот так:

Код:

E8 89 AE 1D 00
Как вы уже могли догадаться, размер инструкции вызова - 5 байт

E8 - опкод вызова. В asm мнемонике записывается как call

89 AE 1D 00 - Релативный адрес вызова, записанный в порядке байт little endian. Что такое порядок байт - лучше почитать на википедии

Если перевести релативный адрес в нормальное число, то получим 0x1DAE89. Откуда же вышло это число?

Оно было посчитано как разница между адреса вызова и адреса вызываемой функции. Считается по формуле: (Адрес назначения вызова/прыжка) - (Адрес вызова/прыжка) - 5

Цитата:

Сообщение от Спойлер

В процессоре есть специальный регистр EIP(RIP на x64). Расшифровывается как Instruction Pointer. После считывания процессором инструкции по адресу 0x53E972, Instrustion pointer смещается на 5 байт вперед(инструкция вызова имеет размер 5). А конечный адрес вызова вычисляется относительно EIP, поэтому нужно добавлять 5 байт смещения.

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

Займемся этим.

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

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

Установка call хука:





Код:

void
*
SetCallHook
(
uintptr_t HookAddress
,
void
*
DetourFunction
)
{
uintptr_t OriginalFunction
=
*
reinterpret_cast

(
HookAddress
+
1
)
+
HookAddress
+
5
;
DWORD oldProt
;
VirtualProtect
(
reinterpret_cast

(
HookAddress
+
1
)
,
sizeof
(
uintptr_t
)
,
PAGE_READWRITE
,
&
oldProt
)
;
*
reinterpret_cast

(
HookAddress
+
1
)
=
reinterpret_cast

(
DetourFunction
)
-
HookAddress
-
5
;
VirtualProtect
(
reinterpret_cast

(
HookAddress
+
1
)
,
sizeof
(
uintptr_t
)
,
oldProt
,
&
oldProt
)
;
return
reinterpret_cast

(
OriginalFunction
)
;
}



Показывать буду на примере хука вывода сообщения в чат.

Хук будем ставить вот сюда:

Код:





Код:

.text:10067A2A 008 6A 00                                  push    0              ; a6
.text:10067A2C 00C C1 E8 08                                shr    eax, 8
.text:10067A2F 00C 0D 00 00 00 FF                          or      eax, 0FF000000h
.text:10067A34 00C 50                                      push    eax            ; a5
.text:10067A35 010 6A 00                                  push    0              ; a4
.text:10067A37 014 56                                      push    esi            ; a3
.text:10067A38 018 6A 04                                  push    4              ; a2
.text:10067A3A 01C 8B CF                                  mov    ecx, edi        ; this

.text:10067A3C 01C E8 1F FA FF FF                          call    CChat__AddEntry  ; this call will be hooked



Соглашение о вызове у нас
Код:

thiscall
(перед вызовом в ecx кладется pChat)

MSVC не дает использовать
Код:

thiscall
вне классов, поэтому мы будем эмулировать его через
Код:

fastcall
. Единственное отличие - дополнительный параметр EDX. В самой функции вы увидите его как void* EDX

Сначала напишем тело функции хука:

C++:





[CODE]
void
__fastcall
HOOK_AddChatMessage
(
void
*
pChat
,
void
*
EDX
,
int
nType
,
const
char
*
szText
,
const
char
*
szPrefix
,
unsigned
long
textColor
,
unsigned
long
prefixColor
)
{
std
::
cout
: "



Ну а теперь установим сам хук:

C++:





Код:

// Где нибудь за пределами функции
using
CChat__AddChatMessage
=
void
(
__fastcall
*
)
(
void
*
,
void
*
,
int
,
const
char
*
,
const
char
*
,
unsigned
long
,
unsigned
long
)
;
// прототип функции, взят из IDA PRO
CChat__AddChatMessage pOriginalFunction
=
nullptr
;
// Сама установка хука
if
(
uintptr_t dwSAMP
=
reinterpret_cast

(
GetModuleHandleA
(
"samp.dll"
)
)
;
dwSAMP
!=
0
)
{
pOriginalFunction
=
reinterpret_cast

(
SetCallHook
(
dwSAMP
+
0x67A3C
,
&
HOOK_AddChatMessage
)
)
;
}



Компилируем код, кидаем асишник в папку игры, устанавливаем DebugConsole, заходим в игру и видим в консоли сообщения из чата.

Цитата:

Сообщение от Спойлер

1623618152617.pngkin4stat · 14 Июн 2021 в 00:43' data-fancybox="lb-post-766481" data-lb-caption-extra-html="" data-lb-sidebar-href="" data-single-image="1" data-src="https://www.blast.hk/attachments/101028/" style="cursor: pointer;" title="1623618152617.png">
https://forum.antichat.xyz/attachments/27766481/


Перейдем к jmp хукам.

jmp хуки ставятся где угодно(на самом деле call хуки тоже, но более запарно)

Для установки jmp хука требуется немного больше действий. Так как ставить мы его будем в случайном месте, не факт что там где мы будем ставить его, будет ровно 5 байт. Поэтому перед установкой хука нужно смотреть сколько байт занимают инструкции на месте установки хука. Я буду смотреть опять же на функции добавления сообщения в чат. Переходим в пролог(начало) функции и смотрим на ассемблерный код:

Код:





Код:

.text:10067460 000 55                                      push    ebp
.text:10067461 004 56                                      push    esi
.text:10067462 008 8B E9                                  mov    ebp, ecx
.text:10067464 008 57                                      push    edi



И видим что у нас тут 4 опкода занимающих ровно 5 байт. Но если вам не повезет как тут, то нужно брать в большую сторону.

Например тут, нужно будет взять 6 байт.

Код:





Код:

.text:10067478 00C 8B 74 24 18                            mov    esi, [esp+0Ch+a4]
.text:1006747C 00C 85 F6                                  test    esi, esi



Идем дальше. Если мы просто затрем код по адресу, то у нас все сломается. Поэтому перед тем как ставить сам хук, нужно скопировать оригинальный код.

Пишем функцию для установки jmp хука(опкод у JMP - E9):

C++:





Код:

void
*
SetJmpHook
(
uintptr_t HookAddress
,
size_t HookSize
,
void
*
DetourFunction
)
{
void
*
Trampoline
=
VirtualAlloc
(
0
,
HookSize
+
5
,
MEM_COMMIT
|
MEM_RESERVE
,
PAGE_EXECUTE_READWRITE
)
;
// Аллоцируем память для трамплина
if
(
Trampoline
)
{
uintptr_t TrampolineJmpBack
=
reinterpret_cast

(
Trampoline
)
+
HookSize
;
memcpy
(
Trampoline
,
reinterpret_cast

(
HookAddress
)
,
HookSize
)
;
// Копируем оригинальные байты в трамплин
DWORD oldProt
;
VirtualProtect
(
reinterpret_cast

(
HookAddress
)
,
HookSize
,
PAGE_READWRITE
,
&
oldProt
)
;
memset
(
reinterpret_cast

(
HookAddress
)
,
0x90
,
HookSize
)
;
// Заполняем место хука нопами(чтобы не ломать листинг ассемблера)
*
reinterpret_cast

(
HookAddress
)
=
0xE9
;
// Ставим опкод прыжка
*
reinterpret_cast

(
HookAddress
+
1
)
=
reinterpret_cast

(
DetourFunction
)
-
HookAddress
-
5
;
// Ставим релативный адрес для прыжка в функцию обработчик хука
VirtualProtect
(
reinterpret_cast

(
HookAddress
)
,
HookSize
,
oldProt
,
&
oldProt
)
;
*
reinterpret_cast

(
TrampolineJmpBack
)
=
0xE9
;
// Ставим в конец трамплина прыжок обратно
// Ставим релативный адрес для прыжка обратно в функцию для продолжения выполнения
*
reinterpret_cast

(
TrampolineJmpBack
+
1
)
=
(
HookAddress
+
HookSize
)
-
TrampolineJmpBack
-
5
;
return
Trampoline
;
}
return
nullptr
;
}



В конец трамплина мы добавляем прыжок обратно, чтобы продолжить выполнение и ничего не сломать

Теперь пишем обработчик хука:

К сожалению в голых хуках не обойтись без функции-обработчика с ассемблерным кодом.

C++:





[CODE]
void
HOOK_AddChatMessage
(
void
*
pChat
,
int
nType
,
const
char
*
szText
,
const
char
*
szPrefix
,
unsigned
long
textColor
,
unsigned
long
prefixColor
)
{
std
::
cout
: "



Вероятно посмотрев на код вы ничего не поняли. Сейчас объясню.

Т.к. теперь телом хука выступает функция HOOK_Raw_AddChatMessage и из нее мы вызываем наш обработчик, т.к. вызов делаем мы сами, то
Код:

__fastcall
и прочие заморочки в обработчике не нужны

Код:

__declspec(naked)
указывает компилятору на то что не нужно генерировать код на входе и выходе из функции - мы сделаем это сами

pushad и popad нужны для сохранения и возврата в исходное состояние регистров. Т.к. мы указали компилятору не генерировать код на входе и выходе, то сохранять регистры некому, поэтому делаем это вручную.

Аргументы со стека теперь придется тащить самим, т.к. мы перехватываем уже после вызова. В этот момент на стеке появится еще одно значение, и еще один вызов сместит все аргументы в функции на 1, и произойдет не то, чего мы ожидали.

Теперь устанавливаем сам хук. Ставить будем тут:

Код:





Код:

.text:10067460 000 55                                      push    ebp
.text:10067461 004 56                                      push    esi
.text:10067462 008 8B E9                                  mov    ebp, ecx
.text:10067464 008 57                                      push    edi



C++:





Код:

// Где-то вне функций
void
*
pOriginalFunction
=
nullptr
;
// Сама установка хука:
if
(
uintptr_t dwSAMP
=
reinterpret_cast

(
GetModuleHandleA
(
"samp.dll"
)
)
;
dwSAMP
!=
0
)
{
SetJmpHook
(
dwSAMP
+
0x67460
,
5
,
&
HOOK_Raw_AddChatMessage
)
;
}



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

Цитата:

Сообщение от Спойлер


trefa 14.06.2021 02:16

Цитата:

Сообщение от KiN4StAt

Хук(от англ. Hook) - перехват

Ловушка/Крюк. Но больше подходит слово ловушка*

kin4stat 14.06.2021 02:39

Цитата:

Сообщение от stereoliza

наконец то ты запилил гайд по хукам

Хотел сначала сделать вторую часть гайда, но потом понял что про хуки надо рассказать перед этим

Цитата:

Сообщение от ARMOR

У меня мозг закипел...

У меня у самого закипел пока гайд писал

manukhov 14.06.2021 14:14

Цитата:

Сообщение от KiN4StAt

защита там стоит воизбежание случайной записи в код и дальшейнего UB

что такое UB

#Rin 14.06.2021 14:21

Цитата:

Сообщение от T4yz1e

что такое UB

Undefined Behaviour (Неопределённое поведение)

kin4stat 14.06.2021 15:28

Цитата:

Сообщение от T4yz1e

что такое UB

Невнимательно читаешь

Цитата:

Сообщение от KiN4StAt

UB(Undefined behaviour - неопределенное поведение)


SR_team 14.06.2021 23:27

Цитата:

Сообщение от trefa

Но больше подходит слово ловушка*

А мне кажется, что крюк подходит больше

Ловушка больше подходит для UEF-хуков, но они медленные

kin4stat 08.07.2021 14:09

Цитата:

Сообщение от T4yz1e

86?

x87 FPU - Recherche Google

Akat 08.07.2021 14:19

Дайте ссылку на 1 тему, я не могу найти

manukhov 08.07.2021 14:27

Цитата:

Сообщение от KiN4StAt

да емае, я нашел почти сразу)


Время: 21:03