- Статус
- Оффлайн
- Регистрация
- 2 Июл 2020
- Сообщения
- 139
- Реакции
- 283
СОДЕРЖАНИЕ:
Введение
1. Import/Export hook
2. BP hook
3. HWBP hook
4. PG hook
5. Trampoline hook
6. Excepthion callback
Заключение
ВВЕДЕНЕИЕ
⠀⠀
⠀⠀⠀⠀⠀Хотя на первый взгляд многое кажется относительно простым и понятным, предлагаю вернуться к основам и проверить, так ли это на самом деле. В той же обработке исключений можно довольно неплохо "пошаманить", но об этом поговорим в другой раз ;).
⠀⠀⠀⠀⠀Целью статьи является написание небольшой библиотеки для хуков, рассмотрение недостатков и ограничений, а также способы обнаружений. Предлагаю не тянуть кота за фантастическое место и сразу начать. Для копирования и фикса некоторых инструкций вам потребуется дизассемблер и ассемблер(в статье используется
Пожалуйста, авторизуйтесь для просмотра ссылки.
&
Пожалуйста, авторизуйтесь для просмотра ссылки.
). Рассматривается больше реализация для архитектуры x64 Windows 7-11.⠀⠀⠀⠀⠀Автор не претендует на замену существующих библиотек т.к этот код является больше учебным(хотя его можно спокойно применять) и попыткой передать небольшой опыт :).
Подмена указателя/данных в импорте/экспорте
⠀⠀⠀⠀⠀Я предлагаю начать с самого простого, поскольку простая обработка исключений представляется довольно легко (тот же VEH). Однако, проблема связанные с копированием инструкции(rip/jcc) и правильное ассемблирования действительно ставят другую планку (мы поговорим об этом в следующей статье) при копирование инструкций в другую исполняемую память.
⠀⠀⠀⠀⠀Самый простое в реализации - подмена указателя в импорте(IMAGE_DIRECTORY_ENTRY_IMPORT),поскольку это просто изменение данных, которые инициализируются при загрузке PE. Однако, это бесполезно против динамического получения импорта. Нам потребуется сохранить оригинальный интересующийся для нас адрес и сама реализация довольно тривиальна:
C++:
auto imp_swap(PVOID mod_addr, CHAR* name_dll, CHAR* name_api, PVOID point) -> BOOLEAN
{
BOOLEAN is_imp_change = FALSE;
DWORD old_prot = NULL;
CHAR* name_imp_dll = NULL;
uint64_t* orig_first_thunk = NULL;
uint64_t* first_thunk = NULL;
PIMAGE_NT_HEADERS headers = NULL;
PIMAGE_SECTION_HEADER sections = NULL;
PIMAGE_IMPORT_BY_NAME import_name = NULL;
PIMAGE_IMPORT_DESCRIPTOR imp_descript = NULL;
headers = reinterpret_cast<PIMAGE_NT_HEADERS>(static_cast<CHAR*>(mod_addr) + static_cast<PIMAGE_DOS_HEADER>(mod_addr)->e_lfanew);
sections = IMAGE_FIRST_SECTION(headers);
if (headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size)
{
imp_descript = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(reinterpret_cast<CHAR*>(name_dll) + headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
for (; imp_descript->Name; ++imp_descript)
{
name_imp_dll = reinterpret_cast<CHAR*>(mod_addr) + imp_descript->Name;
if (!stricmp(name_imp_dll, name_dll))
{
orig_first_thunk = reinterpret_cast<uint64_t*>(reinterpret_cast<CHAR*>(mod_addr) + imp_descript->OriginalFirstThunk);
first_thunk = reinterpret_cast<uint64_t*>(reinterpret_cast<CHAR*>(mod_addr) + imp_descript->FirstThunk);
if (!orig_first_thunk) //load by index https://stackoverflow.com/questions/42413937/why-pe-need-original-first-thunkoft
{
return is_imp_change;
}
for (; *orig_first_thunk; orig_first_thunk++, first_thunk++)
{
import_name = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(reinterpret_cast<CHAR*>(mod_addr) + *orig_first_thunk);
if (import_name->Name && !stricmp(import_name->Name, name_api))
{
if (VirtualProtect(first_thunk,PAGE_SIZE, PAGE_EXECUTE_READWRITE,&old_prot))
{
*first_thunk = reinterpret_cast<uint64_t>(point);
VirtualProtect(first_thunk, PAGE_SIZE, old_prot, &old_prot);
is_imp_change = TRUE;
}
}
}
}
}
}
return is_imp_change;
}
1) исключение на основе изменения rva на секцию с правами только на чтение/запись
2) изменение rva на байт, который вызывает исключение(например, 0xCC)
3) изменение rva на рядом расположенный адрес с батутом или получении дельты, чтобы сделать прыжок на расположенный рядом модуль.
Нам потребуется сохранить первоначальный rva,чтобы сохранить первоначальный rip.
Пример реализации с изменением экспорта модуля(кроме некоторых),чтобы rva указывал на инструкцию, вызывающую исключение:
Код:
auto exp_break(PVOID mod_addr) -> BOOLEAN
{
BOOLEAN is_success = FALSE;
DWORD old_prot = NULL;
uint32_t code_cave = NULL;
CHAR* name_exp = NULL;
uint8_t* memory_sec = NULL;
PIMAGE_NT_HEADERS headers = NULL;
PIMAGE_SECTION_HEADER sections = NULL;
PIMAGE_EXPORT_DIRECTORY export_info = NULL;
POINTER_BAD cur_point_bad = { NULL };
if (static_cast<PIMAGE_DOS_HEADER>(mod_addr)->e_magic == IMAGE_DOS_SIGNATURE)
{
headers = reinterpret_cast<PIMAGE_NT_HEADERS>(static_cast<CHAR*>(mod_addr) + static_cast<PIMAGE_DOS_HEADER>(mod_addr)->e_lfanew);
sections = IMAGE_FIRST_SECTION(headers);
if (headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)
{
export_info = reinterpret_cast<PIMAGE_EXPORT_DIRECTORY>(reinterpret_cast<CHAR*>(mod_addr) + headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
if (sizeof(uint8_t) > export_info->NumberOfFunctions)
{
return is_success;
}
auto names = (PDWORD)(reinterpret_cast<CHAR*>(mod_addr) + export_info->AddressOfNames);
auto ordinals = (PWORD)(reinterpret_cast<CHAR*>(mod_addr) + export_info->AddressOfNameOrdinals);
auto functions = (PDWORD)(reinterpret_cast<CHAR*>(mod_addr) + export_info->AddressOfFunctions);
for (uint32_t i = NULL; i < export_info->NumberOfFunctions; ++i)
{
if (!code_cave)
code_cave = get_rva_code_cave_execute(mod_addr, headers, sections);
if (!code_cave)
{
printf("bad ->\t%s\n", reinterpret_cast<CHAR*>(mod_addr) + names[i]);
}
if (!is_bad_imp(reinterpret_cast<CHAR*>(mod_addr) + names[i]) &&& code_cave && VirtualProtect(&functions[ordinals[i]], PAGE_SIZE, PAGE_READWRITE, &old_prot))
{
//push addr excep(mod+i)
//copy correct address
//after change only set fake;
cur_point_bad.bad_pointer = reinterpret_cast<CHAR*>(mod_addr) + code_cave;
cur_point_bad.correct_pointer = reinterpret_cast<CHAR*>(mod_addr) + functions[ordinals[i]];
cur_point_bad.execute_only = TRUE;
cur_point_bad.exp_name = reinterpret_cast<CHAR*>(mod_addr) + names[i];
cur_point_bad.correct_rva = functions[ordinals[i]];
//for remove if need
cur_point_bad.swap_pointer = &functions[ordinals[i]];
breaked_pointer.push_back(cur_point_bad);
functions[ordinals[i]] = code_cave;
is_success = TRUE;
VirtualProtect(&functions[ordinals[i]], PAGE_SIZE, old_prot, &old_prot);
code_cave = NULL;
}
}
}
}
return is_success;
}
Возможность легко отслеживать импорт.
Крайне дешёвый хук т.к потребуется изменить для фикса только RIP/EIP потока.
Минусы
Требуется обработчик исключений для экспорта(1/2 вариант реализации)
Относительно легко обнаружить при сравнении с файлом на диске или специфические проверки(функция начинается с 0xCC, export rva находится не в модуле)
Сильно желательно хук на ранней инициализации.
Ничего более, кроме отслеживания импорта
BP hook
⠀⠀⠀⠀⠀Не совсем правильное название, но основная идея в использовании крайне дешёвой по размеру инструкции (обычно 1-2 байта) для вызова исключения. Обычно такой хук используют дебаггеры( тот же x64dbg предоставляет int3, long int3, ud2). Вот лишь пример из некоторых инструкций в 1 байт: hlt, int1(секси icebp), int3.При реализации есть 2 варианта обработки инструкции: сохранить украденные байты до хука и временно восстанавливать с использованием TRAP_FLAG(для восстановления хука и правильной обработки при следующей вызове) или копирование инструкции в новый адрес с прыжком на следующую инструкцию, чтобы не пришлось временно восстанавливать байты. Это, наверное, самая простая реализация, если не брать копирование байтов(об этом в конце):
Код:
NO_INLINE auto add_bp(PVOID addr, BP_TYPE_INFO bp_type) -> BOOLEAN
{
uint32_t copy_size = NULL;
BP_INFO bp = { NULL };
bp.addr_bp = addr;
if (bp_type == bp_icebp || bp_type == bp_hlt || bp_type == bp_int3)
{
copy_size = 1;
}
else
{
copy_size = 2;
}
//not entirely sure, but this is just an example
bp.addr_single_step = reinterpret_cast<uint8_t*>(addr) + sizeof(uint8_t);
bp.type_info = bp_type;
memcpy(bp.orig_byte, addr,copy_size );
bp.rip_fixer = cg_util::get_rip_fixer(addr, copy_size);
if (bp.rip_fixer)
{
return set_patch(addr, bp_type, &bp);
}
return FALSE;
}
Минусы:Крайне дешёвый хук (от 1 байта)
Требуется обработчик исключений
Патчинг инструкции в памяти, что позволяет её обнаружить(странное начало функции, сравнение с файлом на диске).
HWBP
⠀⠀⠀⠀⠀Довольно приятно изменить только 2 значения(Dr(0-3) & Dr7) в контексте потока, чтобы получить управление над потоком и не патчить ни 1 инструкцию, но нужно опять регистрировать фильтр для обработки исключений… Данный хук приятно использовать для игнорирования тех же проверок целостности, но мы ограничены только 4 аппаратными точками остановками.
Принцип работы хорошо документирован в Intel manual и AMD64 manual:
Intel:


AMD:


⠀⠀⠀⠀⠀Однако, такой тип хука наиболее удобно использовать на уже существующих потоках, иначе вам потребуется хукнуть,например,RtlUserThreadStart иди другую функция связанная с созданием потоков. Нам нужно установить нужный бит в Dr7 для установки правильных параметров: номер Dr(0-3), длина(длина должна быть выровнена, если размер устанавливается не на 1 байт) и тип обращения( выполнение, запись, чтение/запись) + сам Dr(0-3). Реализацию можно сделать менее грамоткой, но вот пример:
C++:
O_INLINE auto set_hwbp(PVOID addr, HWBP_TYPE_ACCESS hwbp_access, HWBP_LEN_ACCESS hwbp_len, uint32_t thread_id = NULL, BOOLEAN set_all_thread = FALSE) -> BOOLEAN
{
BOOLEAN is_success = FALSE;
ULONG ret_lenght = NULL;
NTSTATUS nt_status = STATUS_UNSUCCESSFUL;
PVOID nt_get_context = NULL;
PVOID nt_set_context = NULL;
PVOID nt_query_sys = NULL;
PVOID buffer = NULL;
HANDLE acces = NULL;
PSYSTEM_PROCESS_INFORMATION process_info = NULL;
HMODULE ntdll_base = NULL;
CONTEXT ctx = { NULL };
HWBP_INFO cur_hwbp;
ntdll_base = GetModuleHandleW(L"ntdll.dll");
if (ntdll_base)
{
nt_get_context = GetProcAddress(ntdll_base, "NtGetContextThread");
nt_set_context = GetProcAddress(ntdll_base, "NtSetContextThread");
nt_query_sys = GetProcAddress(ntdll_base, "NtQuerySystemInformation");
if (nt_get_context && nt_set_context && nt_query_sys)
{
if (set_all_thread)
{
nt_status = reinterpret_cast<decltype(&NtQuerySystemInformation)>(nt_query_sys)(SystemProcessInformation, &ret_lenght, ret_lenght, &ret_lenght);
while (nt_status == STATUS_INFO_LENGTH_MISMATCH)
{
if (buffer != NULL)
free(buffer);
buffer = malloc(ret_lenght);
nt_status = reinterpret_cast<decltype(&NtQuerySystemInformation)>(nt_query_sys)(SystemProcessInformation, buffer, ret_lenght, &ret_lenght);
}
if (!NT_SUCCESS(nt_status))
{
if (buffer != NULL)
free(buffer);
return FALSE;
}
process_info = reinterpret_cast<PSYSTEM_PROCESS_INFORMATION>(buffer);
while (process_info->NextEntryOffset) // Loop over the list until we reach the last entry.
{
if (reinterpret_cast<uint32_t>(process_info->UniqueProcessId) == GetCurrentProcessId())
{
is_success = TRUE;
if (hwbp_access == hwbp_execute)
{
cur_hwbp.rip_fixer = cg_util::get_rip_fixer(addr, MIN_LENGHT_INSTR);
}
else
{
cur_hwbp.rip_fixer = NULL;
}
cur_hwbp.access = hwbp_access;
cur_hwbp.addr_bp = addr;
cur_hwbp.addr_single_step = reinterpret_cast<uint8_t*>(addr) + sizeof(uint8_t);
hwbp_list.push_back(cur_hwbp);
for (size_t i = NULL; i < process_info->NumberOfThreads; i++)
{
acces = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, reinterpret_cast<uint32_t>(process_info->Threads[i].ClientId.UniqueThread));
if (acces)
{
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
nt_status = reinterpret_cast<decltype(&NtGetContextThread)>(nt_get_context)(acces, &ctx);
if (NT_SUCCESS(nt_status))
{
if (set_thread_dr(addr, hwbp_access, hwbp_len, &ctx))
{
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
nt_status = reinterpret_cast<decltype(&NtSetContextThread)>(nt_set_context)(acces, &ctx);
if (!NT_SUCCESS(nt_status))
{
is_success = FALSE;
}
}
else
{
is_success = FALSE;
}
}
else
{
is_success = FALSE;
}
CloseHandle(acces);
}
else
{
is_success = FALSE;
}
}
break;
}
process_info = (PSYSTEM_PROCESS_INFORMATION)((LPBYTE)process_info + process_info->NextEntryOffset); // Calculate the address of the next entry.
}
free(buffer);
}
else
{
acces = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, thread_id);
if (acces)
{
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
nt_status = reinterpret_cast<decltype(&NtGetContextThread)>(nt_get_context)(acces, &ctx);
if (NT_SUCCESS(nt_status))
{
if (set_thread_dr(addr, hwbp_access, hwbp_len, &ctx))
{
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
nt_status = reinterpret_cast<decltype(&NtSetContextThread)>(nt_set_context)(acces, &ctx);
if (NT_SUCCESS(nt_status))
{
is_success = TRUE;
}
}
}
}
}
}
}
return is_success;
}
Плюсы:
Нет изменения в памяти и меняется в контексте потока только 2 значения(при инициализации)
Довольно удобен из-за возможности нахождения обращения к памяти
Минусы:
Очень часто Dr(0-3) или Dr6/Dr7 будут проверять на инициализацию из-за чего вам понадобится скрыть их присутствие(те же протекторы почти всегда их проверяют). Проверка Dr через SEH/NtGetContextThread или перезапись через NtSetContextThread/ NtContinue
Требуется обработчик исключений
Page Guard
⠀⠀⠀⠀⠀Самые неудобный и медленный хук,но он позволяет находить обращение не к нескольким байтам(как HWBP),а к целой странице в памяти. При установке хука мы устанавливаем флаг PAGE_GUARD в Protect. При обработке исключений мы должны временно убрать PAGE_GUARD и установить TRAP_FLAG,чтобы выполнилась следующая инструкция, чтобы код не находился в spinlock. При выполнении TRAP_FLAG нужно снова вернуть PAGE_GUARD. Из-за такой обработки код будет выполняться очень медленно(выполнение 1 инструкции = обработка TRAP_FLAG и PAGE_GUARD),поэтому рекомендую использовать этот хук очень осторожно. Реализация довольно простая, но я уже упоминал из-за чего этот хук медленный и даже ситуативный:
C++:
NO_INLINE auto set_pg_hook(PVOID addr, PAGE_GUARD_TYPE_ACCESS access)
{
DWORD old_prot = NULL;
MEMORY_BASIC_INFORMATION mbi = { NULL };
PAGE_GUARD_INFO cur_pg;
if(VirtualQuery(addr, &mbi, sizeof(mbi)))
{
if (!(mbi.Protect & PAGE_GUARD) && !(mbi.Protect & PAGE_NOACCESS))
{
cur_pg.is_single_step = FALSE;
cur_pg.access = access;
cur_pg.addr = addr;
cur_pg.reg_addr = mbi.BaseAddress;
cur_pg.reg_size = mbi.RegionSize;
if (VirtualProtect(addr, sizeof(PVOID), mbi.Protect | PAGE_GUARD, &old_prot))
{
pg_list.push_back(cur_pg);
return TRUE;
}
}
}
return FALSE;
}
Плюсы:
Происходит изменение только защиты страницы.
Довольно удобен из-за возможности нахождения обращения к памяти.
Редко проверяют данных хук.
Минусы:
Очень медленное выполнение.
Реверсер может лёгкое обнаружить этот хук основываясь на Characteristics секции(если речь про MEM_IMAGE) или просто сделать чтение 1 байта у каждой страницы из-за чего ваше ожидание на определённую логику может быть нарушено.
Trampoline hook
⠀⠀⠀⠀⠀С первого взгляда реализация этого хука лёгкая, пока вы не заденете больную тему: перемещение памяти программы… Основная проблема – обработка любой jcc/rip инструкции.
Пожалуйста, авторизуйтесь для просмотра ссылки.
приведён базовый пример решения с использованием
Пожалуйста, авторизуйтесь для просмотра ссылки.
&
Пожалуйста, авторизуйтесь для просмотра ссылки.
,но не все моменты прописаны. Для установки хука мы должны скопировать размер байтов равные/больше нужного размера батута и установить батут т.е переход к нашему коду. Сама реализация не является сложной, если не учитывать обработку jcc & rip инструкций:
C++:
NO_INLINE auto set_hook(PVOID addr, PVOID callback, PVOID* trampline_fun) -> BOOLEAN
{
DWORD old_prot = NULL;
PVOID rip_fixer = NULL;
PVOID copy_pointer = NULL;
TRAMPLINE_HOOK cur_hook = { NULL };
#ifndef _WIN64
uint8_t shell_jmp[] =
{
0x50, //push eax
0xC7, 0x04, 0x24, 0x00, 0x00, 0x00, 0x00, // mov [esp+4],NULL
0xC3 //ret
};
#else
uint8_t shell_jmp[] =
{
0x50, //push rax
0x50, //push rax
0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //mov rax,NULL
0x48, 0x89, 0x44, 0x24, 0x08, //mpv [rsp+8],rax
0x58, //pop rax
0xC3 //ret
};
#endif // !_WIN64
cur_hook.addr = addr;
cur_hook.orig_size = sizeof(shell_jmp);
copy_pointer = callback;
memcpy(&cur_hook.orig_byte, addr, sizeof(shell_jmp));
rip_fixer = cg_util::get_rip_fixer(addr, sizeof(shell_jmp));
if (rip_fixer)
{
*trampline_fun = rip_fixer;
if (VirtualProtect(addr, sizeof(shell_jmp), PAGE_EXECUTE_READWRITE, &old_prot))
{
memcpy(reinterpret_cast<uint8_t*>(shell_jmp) + 4, ©_pointer, sizeof(PVOID));
memcpy(addr, shell_jmp, sizeof(shell_jmp));
VirtualProtect(addr, sizeof(shell_jmp), old_prot, &old_prot);
trampline_list.push_back(cur_hook);
return TRUE;
}
VirtualFree(rip_fixer, NULL, MEM_RELEASE);
}
return FALSE;
}
Быстрое выполнение кода(как и при подмене указателя).
Не требуется обработчик исключений.
Минусы:
Патчинг байтов т.е может быть легко обнаружен при сравнении с памятью на диске.
В теории, скопированные инструкции могут относительно легко быть найдены и таким образом ваш хук могут элегантно обойти(как и реализация со с копированием инструкции, а не использованием TRAP_FLAG).
Жизнь до и после исключений(callback)
KiUserExceptionDispatcher
KiUserExceptionDispatcher
⠀⠀⠀⠀⠀Основная часть популярных хуков в UM построена на исключениях(export,HWBP,PG hook,bad pointer и можно добавить PAGE hook). Нас интересует быть максимально сильно в начале обработки исключений, чтобы была возможность манипулировать контекстом потока. Это обусловлено из-за обнаружения или неправильного поведения(например, программа использует VEH или SEH,но наш хук выполняется на уровне unhandled excepthion(при регистрации с помощью SetUnhandledExceptionFilter,например) из-за чего с большим шансом выполнится тот же VEH или SEH(если RIP попадёт в адресное пространство самого обработчика). Мы должны передать выполнение на оригинальные инструкции или на свой код, который сохраняет логику выполнения кода программы.
⠀⠀⠀⠀⠀Важно помнить об ограничениях: мы не сможем установить, например, на наш callback KiUserExceptionDispatcher для обработки исключений и туда же впихнуть любой хук на основе исключения т.к мы получим spinlock(т.е никаких хуков на код, который будет выполняться до передачи выполнение на наш обработчик исключений).
Важным моментом всей обработки исключений в UM является KiUserExceptionDispatcher.
В Windows 7,8,10,10 x64 она одинаковы и выглядит следующим образом:

x32(WoW64):

Из-за хранения аргументов слегка не в стандартном виде в x64, потребуется написать небольшую оболочку для переходу к вашему. Для x32 мы должны сохранить параметры в стеке, чтобы при вызове оригинальной функции они не исчезли. Вот небольшая часть реализации кода:
KiUserExceptionDispatcher_hook:
else if (type == excep_ki_dispatcher_callback)
{
ntdll_base = GetModuleHandleW(L"ntdll.dll");
if (ntdll_base)
{
//just hook KiUserExceptionDispatcher
api_addr = reinterpret_cast<uint8_t*>(GetProcAddress(ntdll_base, "KiUserExceptionDispatcher"));
if (api_addr)
{
rip_fixer = get_rip_fixer(api_addr, callback_ki_excep, sizeof(shell_jmp));
if (rip_fixer)
{
copy_pointer = rip_fixer;
if (VirtualProtect(api_addr, sizeof(shell_jmp), PAGE_EXECUTE_READWRITE, &old_prot))
{
callback_info.type = excep_ki_dispatcher_callback;
callback_info.user_callback = addr_filter;
callback_info.orig_callback = NULL;
#ifndef _WIN64
memcpy(reinterpret_cast<uint8_t*>(shell_jmp) + 1, ©_pointer, sizeof(PVOID));
#else
memcpy(reinterpret_cast<uint8_t*>(shell_jmp) + 2, ©_pointer, sizeof(PVOID));
#endif // !_WIN64
memcpy(api_addr, shell_jmp, sizeof(shell_jmp));
VirtualProtect(api_addr, sizeof(shell_jmp), old_prot, &old_prot);
return TRUE;
}
VirtualFree(rip_fixer, NULL, MEM_RELEASE);
}
}
}
return callback_info.orig_callback != NULL;
}
Плюсы такого обработчика исключения:
Минусы:Обработка исключения начинается с самого передачи выполнения в UM.
Избежание любых хуков, которые расположены ниже по выполнению(тот же VEH),
Байт патчинг.
Wow64PrepareForException
Данный обработчик можно легко зарегистрировать т.к это подмена указателя:

Нам нужно только сохранить и заменить указатель(Wow64PrepareForException). Этот хук можно реализовтаь только в x64 архитектуре или в x32,но под выполнением WoW64.Однако, здесь рассматривается только x64 архитектура т.к иначе это будет специфический хук.
Сама реализация довольно максимально простая:
C++:
ntdll_base = GetModuleHandleW(L"ntdll.dll");
if (ntdll_base)
{
api_addr = reinterpret_cast<uint8_t*>(GetProcAddress(ntdll_base, "KiUserExceptionDispatcher"));
if (api_addr && !memcmp(api_addr, ki_sig_check, sizeof(ki_sig_check)))
{
//memcpy(&offset, api_addr + sizeof(ki_sig_check), sizeof(offset));
//addr_hook = api_addr + sizeof(uint8_t) + 7 + offset; //7 - size instructhion, offset - the difference in values between the target value, sizeof(uint8_t) - targer instr next
if (ZYAN_SUCCESS(dis::get_dis(&dis_instr, api_addr + sizeof(uint8_t))))
{
addr_hook = dis::get_absolute_address(&dis_instr, api_addr + sizeof(uint8_t));
/*
mov rcx, rsp
add rcx, 4F0h
mov rdx, rsp
call rax ; Wow64PrepareForException
;Some code
mov rcx, rsp
add rcx, 4F0h
mov rdx, rsp
call RtlDispatchException
*/
if (addr_hook && VirtualProtect(addr_hook, sizeof(PVOID), PAGE_READWRITE, &old_prot))
{
callback_info.type = excep_wow_prepare_callback;
callback_info.user_callback = addr_filter;
callback_info.orig_callback = *reinterpret_cast<PVOID*>(addr_hook);
*reinterpret_cast<PVOID*>(addr_hook) = callback_ki_excep;
VirtualProtect(addr_hook, sizeof(PVOID), old_prot, &old_prot);
return TRUE;
}
}
}
}
}
Плюсы такого обработчика исключения:
Довольно скрытый т.к его редко проверяют на инициализацию и указывает ли он на ожидаемые данные(в x64 он будет 0).
Выполняется в прямом смысле после KiUserExceptionDispatcher и довольно удобен в использовании т.к нам не нужнл думать над исправлением передачей параметров.
Минусы:
Лёгкое обнаружение и выполняется после KiUserExceptionDispatcher
Использование только в x64
Ntdll32KiUserExceptionDispatcher
⠀⠀⠀⠀⠀Нет ничего интереснее, чем изменить адрес KiUserExceptionDispatcher т.е заставить выполнять ваш код после исключения(речь про x32 code под WoW64) ,а не обработчика и тем более при установленном вражеского хука. Это можно реализовать только в x64 системе под управлением WoW64,подменив указатель в wow64.dll. В x64 не получится реализовать простым способом подмену в ядре указатель KeUserExceptionDispatcher т.к адрес загрузки ntdll.dll(x64) фиксирован, что портит все планы(в теории можно добиться этого, но встаёт вопрос о хуке и о PatchGuard.

Для реализация этого творения потребуется получить адрес ntdll.dll(x64,но можно заменить manual syscall), wow64.dll(x64) и заставить выполнять код под x64,а не x32 под WoW64(можно добиться копированием памяти в буфер и перезаписать исправленный код,но я решил идти по простому пути). В коде
Пожалуйста, авторизуйтесь для просмотра ссылки.
с использованием библиотеки
Пожалуйста, авторизуйтесь для просмотра ссылки.
.Нам так же нужно будет пройтись по секциям и составить паттерн для нахождения указателя Ntdll32KiUserExceptionDispatcher.
Код:
/*
NTSTATUS
NTAPI
NtProtectVirtualMemory
(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG NewProtect,
PULONG OldProtect
);
#pragma optimize( "", off )
NO_INLINE auto change_callback2() -> BOOLEAN
{
uint32_t offset = NULL;
DWORD old_prot = NULL;
SIZE_T size_prot = NULL;
uint64_t mod_addr = 0X111111111111111;
uint64_t nt_protect_mem = 0X2222222222222222;
uint64_t hook = 0X3333333333333333;
uint64_t orig_callback = 0X4444444444444;
uint8_t* sec_addr = NULL;
uint8_t* targer_addr = NULL;
PIMAGE_NT_HEADERS headers = NULL;
PIMAGE_SECTION_HEADER sections = NULL;
if (reinterpret_cast<PIMAGE_DOS_HEADER>(mod_addr)->e_magic == IMAGE_DOS_SIGNATURE)
{
headers = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<CHAR*>(mod_addr) + reinterpret_cast<PIMAGE_DOS_HEADER>(mod_addr)->e_lfanew);
sections = IMAGE_FIRST_SECTION(headers);
for (size_t i = 0; i < headers->FileHeader.NumberOfSections; i++)
{
sec_addr = reinterpret_cast<uint8_t*>(mod_addr) + sections[i].VirtualAddress;
if ((sections[i].Characteristics & IMAGE_SCN_MEM_READ) && (sections[i].Characteristics & IMAGE_SCN_MEM_EXECUTE))
{
if (sections[i].Misc.VirtualSize > 20)
{
for (size_t j = 0; j < sections[i].Misc.VirtualSize - 20; j++)
{
//Wow64pSetupExceptionDispatch 8b ?? 24 ?? e8 ?? ?? ?? ?? BA 01 00 00 00 8B win 10-11
//Wow64SetupExceptionDispatch C7 ?? 02 00 01 00 8B 05 ?? ?? ?? ?? 89 ?? AC 00 00 00 win7-8.1
if
(
(
*(sec_addr + j) == 0x8B &&
*(sec_addr + j + 2) == 0x24 &&
*(sec_addr + j + 4) == 0xE8 &&
*(sec_addr + j + 9) == 0xBA &&
*(sec_addr + j + 10) == 0x01 &&
*(sec_addr + j + 11) == NULL &&
*(sec_addr + j + 12) == NULL &&
*(sec_addr + j + 13) == NULL &&
*(sec_addr + j + 14) == 0x8B
) ||
(
*(sec_addr + j) == 0xC7 &&
*(sec_addr + j + 2) == 0x02 &&
*(sec_addr + j + 3) == NULL &&
*(sec_addr + j + 4) == 0x01 &&
*(sec_addr + j + 5) == NULL &&
*(sec_addr + j + 6) == 0x8B &&
*(sec_addr + j + 7) == 0x05 &&
*(sec_addr + j + 12) == 0x89 &&
*(sec_addr + j + 14) == 0xAC &&
*(sec_addr + j + 15) == NULL &&
*(sec_addr + j + 16) == NULL &&
*(sec_addr + j + 17) == NULL
)
)
{
if(*(sec_addr + j) == 0x8B)
{
memcpy(&offset, sec_addr + j + 16, sizeof(uint32_t));
targer_addr = sec_addr + j + 14 + 6 + offset;
}
else if(*(sec_addr + j) == 0xC7))
{
memcpy(&offset, sec_addr + j + 8, sizeof(uint32_t));
targer_addr = sec_addr + j + 6 + 6 + offset;
}
size_prot = sizeof(uint32_t);
//hear some problem
if (reinterpret_cast<uint64_t>(targer_addr) >= mod_addr && (reinterpret_cast<uint8_t*>(mod_addr) + headers->OptionalHeader.SizeOfImage - sizeof(PVOID)) > targer_addr)
{
if (NT_SUCCESS(reinterpret_cast<decltype(&NtProtectVirtualMemory)>(nt_protect_mem)(NtCurrentProcess, reinterpret_cast<PVOID*>(&targer_addr), &size_prot, PAGE_READWRITE, &old_prot)))
{
if(*(sec_addr + j) == 0x8B)
{
targer_addr = sec_addr + j + 14 + 6 + offset;
}
else
{
targer_addr = sec_addr + j + 6 + 6 + offset;
}
*reinterpret_cast<uint32_t*>(orig_callback) = *reinterpret_cast<uint32_t*>(targer_addr);
*reinterpret_cast<uint32_t*>(targer_addr) = hook;
size_prot = sizeof(uint32_t);
if(*(sec_addr + j) == 0x8B)
{
targer_addr = sec_addr + j + 14 + 6 + offset;
}
else
{
targer_addr = sec_addr + j + 6 + 6 + offset;
}
reinterpret_cast<decltype(&NtProtectVirtualMemory)>(nt_protect_mem)(NtCurrentProcess, reinterpret_cast<PVOID*>(&targer_addr), &size_prot, old_prot, &old_prot);
return TRUE;
}
}
}
}
}
}
}
}
return FALSE;
}
#pragma optimize( "", on )
*/
Очень скрытый способ перехват исключения. Я пока не видел публичного примера использования, но, возможно, плохо искал.
Просо подмена указателя.
Минусы:
Использование только в x32 под управлением WoW64.
Нужно быть уверенным в нахождении данного указателя.
VEH(Vectored Exception Handler)
⠀⠀⠀⠀⠀Очень распространённый callback т.к установку предоставляет функция WINAPI AddVectoredExceptionHandler и callback давно известен. Мы должны зарегистрировать обработчик первым, чтобы игнорировать проблемы при обработке исключений. Я не вижу смысл говорить т.к реализация довольно простая:
C++:
//some code
callback_info.handle_veh = AddVectoredExceptionHandler(TRUE, reinterpret_cast<PVECTORED_EXCEPTION_HANDLER>(callback_pointer));
//some code
Плюсы такого обработчика исключения:
Очень прост в использовании(поскольку это просто WINAPI).
Можно написать код на кражу чужого VEH обработчика (если не хотим регистрировать свой).
Минусы:
Легко можно обнаружить обработчик, если зарегистрировать свой обработчик первым или просто выключить(убрать нужный бит в CrossProcessFlags).
Выполняется обработка не сразу.
Заключительные слова
⠀⠀⠀⠀⠀С одной стороны реализация и идея является лёгкой, а с другой позволяет задуматься и поиграться со многими моментами с другой стороны. Здесь продемонстрирована лишь часть хуков т.к можно сделать хук на основе PAGE_HOOK(изменение защиты страницы на PAGE_READONL), Wow64Transition(syscall hook WoW64), TurboDispatchJumpAddressStart(syscall hook WoW64(SharpOD использует их для перехода кода в x64 ntdll,например)) и пару ещё других видов хуков.
⠀⠀⠀⠀⠀ Для уже существующих хуков можете просто реализовать свою логику со своим excepthion callback, некоторые хуки не совсем удобно использовать или нужно писать свою логику(например, для обфусцированного кода). Здесь приходит много мыслей, поэтому оглянитесь и подумайте над своими новыми идеями.
⠀⠀⠀⠀⠀Это лишь небольшой список и существует более агрессивное использование хуков, поэтому рекомендую посмотреть на промахи протекторов или атичитов(счастье любит тишину, поэтому приводить примеры, а тем более готовый PoC не буду). Спасибо за чтение статьи и всем удачи :).
Пожалуйста, авторизуйтесь для просмотра ссылки.
Пожалуйста, авторизуйтесь для просмотра ссылки.
⠀⠀⠀
Последнее редактирование: