HOME FORUMS MEMBERS RECENT POSTS LOG IN  
× Авторизация
Имя пользователя:
Пароль:
Нет аккаунта? Регистрация
Баннер 1   Баннер 2
НОВЫЕ ТОРГОВАЯ НОВОСТИ ЧАТ
loading...
Скрыть
Вернуться   ANTICHAT > ПРОГРАММИРОВАНИЕ > С/С++, C#, Rust, Swift, Go, Java, Perl, Ruby
   
 
 
Опции темы Поиск в этой теме Опции просмотра

  #1  
Старый 14.06.2021, 01:43
kin4stat
Флудер
Регистрация: 06.11.2017
Сообщений: 2,759
С нами: 4483143

Репутация: 183


По умолчанию

Хотел продолжить первый гайд, но понял что нужно объяснить что такое хуки
  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">


Перейдем к 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, а не одно из ее использований

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




 
Ответить с цитированием
 





Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
 


Быстрый переход




ANTICHAT ™ © 2001- Antichat Kft.