Волшебные хуки как перехватывать управление любой программой через winapi

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

Публикация данной статьи на портале www.spy-soft.net несет исключительно образовательный характер. Мы не несем никакую ответственность за незаконное использование полученных вами знаний!

Какие бывают хуки

Ловушки (hook) могут быть режима пользователя (usermode) и режима ядра (kernelmode). Установка хуков режима пользователя сводится к методу сплайсинга и методу правки таблиц IAT. Ограничения этих методов очевидны: перехватить можно только userspace API, а вот до функций с префиксом Zw*, Ki* и прочих «ядерных» из режима пользователя дотянуться нельзя.

Установка хуков режима ядра позволяет менять любую информацию, которой оперирует Windows на самом низком уровне. Для перехватов подобного типа необходимо модифицировать таблицы SSDT/IDT либо менять само тело функции (kernel patch).

Windows API Hooking — Hide Process from Task Manager tutorial

Надо сказать, что в Windows на архитектуре x64 ядро контролирует свою целостность при помощи механизма KPP (Kernel Patch Protection), который является частью PatchGuard и просто так подобные манипуляции с системными таблицами сделать не позволит.

Почему хуки работают?

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

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

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

Сплайсинг функций WinAPI

Пролог функций, трамплин и т.д

Функции WinAPI начинаются с пролога — это стандартный код, отвечающий за балансировку стека для корректного доступа к локальным переменным, которые использует функция. Обычно пролог выглядит таким образом:

Источник: spy-soft.net

Windows hook: просто о сложном

image

Что такое хук?
Что такое хук функций и для чего он нужен? В переводе с английского «hook» — ловушка. Поэтому о назначении хуков функции в Windows можно догадаться — это ловушка для функции. Иными словами, мы ловим функцию и берем управление на себя. После этого определения нам открываются заманчивые перспективы: мы можем перехватить вызов любой функции, подменить код на свой, тем самым изменив поведение любой программы на то, которое нам нужно (конечно, в рамках определенных ограничений).

[№1].C++ глазами школьника хакера(нет) — winapi hooking[MessageBoxA].

Целью данной статьи является демонстрация установки хука и его непосредственная реализация.

— Нельзя поверить в невозможное!
— Просто у тебя мало опыта, – заметила Королева. – В твоем возрасте я уделяла этому полчаса каждый день! В иные дни я успевала поверить в десяток невозможностей до завтрака!

Где мне реально пригодились эти знания

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

• Контроль входящего http-траффика и подмена «взрослого» контента на более безобидный.
• Логирование информации в случае копирования каких-либо файлов с подконтрольной сетевой папки.
• Незначительная модификация кода в проекте, от которого были утеряны исходники (да, и такое тоже случается)

Читайте также:
Как загрузить в компьютер программу word

Методы установки хуков

Давайте перейдем от общих фраз к более детальному рассмотрению хуков. Мне известно несколько разновидностей реализации хука:

● Использование функции SetWindowsHookEx. Это весьма простой, оттого и ограниченный, метод. Он позволяет перехватывать только определенные функции, в основном связанные с окном (например, перехват событий, которые получает окно, щелчков мышкой, клавиатурного ввода). Достоинством этого метода является возможность установки глобальных хуков (например, сразу на все приложениях перехватывать клавиатурный ввод).
● Использование подмены адресов в разделе импорта DLL. Суть метода заключается в том, что любой модуль имеет раздел импорта, в котором перечислены все используемые в нем другие модули, а также адреса в памяти для экспортируемых этим модулем функций. Нужно подменить адрес в этом модуле на свой и управление будет передано по указанному адресу.
● Использование ключа реестра HKEY_LOCAL_MACHINESoftwareMicrosoftWindows NTCurrentVersionWindowsAppInit_Dlls. В нем необходимо прописать путь к DLL, но сделать это могут только пользователи с правами администратора. Этот метод хорош, если приложение не использует kernel32.dll (нельзя вызвать функцию LoadLibrary).
● Использование инъектирования DLL в процесс. На мой взгляд, это самый гибкий и самый показательный способ. Его-то мы и рассмотрим более подробно.

Метод инъектирования

Инъектирование возможно, потому что функция ThreadStart, которая передается функции CreateThread, имеет схожую сигнатуру с функцией LoadLibrary (да и вообще структура dll и исполняемого файла очень схожи). Это позволяет указать метод LoadLibrary в качестве аргумента при создании потока.

Алгоритм инъектирования DLL выглядит так:

1. Находим адрес функции LoadLibrary из Kernel32.dll для потока, куда мы хотим инжектировать DLL.
2. Выделяем память для записи аргументов этой функции.
3. Создаем поток и в качестве ThreadStart функции указываем LoadLibrary и ее аргумент.
4. Поток идет на исполнение, загружает библиотеку и завершается.
5. Наша библиотека инъектирована в адресное пространство постороннего потока. При этом при загрузке DLL будет вызван метод DllMain с флагом PROCESS_ATTACH. Это как раз то место, где можно установить хуки на нужные функции. Далее рассмотрим саму установку хука.

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

Подход, используемый при установке хука, можно разбить на следующие составные части:

1. Находим адрес функции, вызов которой мы хотим перехватывать (например, MessageBox в user32.dll).
2. Сохраняем несколько первых байтов этой функции в другом участке памяти.
3. На их место вставим машинную команду JUMP для перехода по адресу подставной функции. Естественно, сигнатура функции должна быть такой же, как и исходной, т. е. все параметры, возвращаемое значение и правила вызова должны совпадать.
4. Теперь, когда поток вызовет перехватываемую функцию, команда JUMP перенаправит его к нашей функции. На этом этапе мы можем выполнить любой нужный код.

Далее можно снять ловушку, вернув первые байты из п.2 на место.

Итак, теперь нам понятно, как внедрить нужную нам DLL в адресное пространство потока и каким образом установить хук на функцию. Теперь попробуем совместить эти подходы на практике.

Тестовое приложение

Наше тестовое приложение будет довольно простым и написано на С#. Оно будет содержать в себе кнопку для показа MessageBox. Для примера, установим хук именно на эту функцию. Код тестового приложения:

public partial class MainForm : Form < [DllImport(«user32.dll», CharSet = CharSet.Unicode)] public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type); public MainForm() < InitializeComponent(); this.Text = «ProcessID: » + Process.GetCurrentProcess().Id; >private void btnShowMessage_Click(Object sender, EventArgs e) < MessageBox(new IntPtr(0), «Hello World!», «Hello Dialog», 0); >>

В качестве инъектора рассмотрим два варианта. Инъекторы, написанные на С++ и С#. Почему на двух языках? Дело в том, что многие считают, что С# — это язык, в котором нельзя использовать системные вещи, — это миф, можно :). Итак, код инъектора на С++:

#include «stdafx.h» #include #include #include int Wait(); int main() < // Пусть до библиотеки, которую хотим инъектировать. DWORD processId = 55; char* dllName = «C:\_projects\CustomHook\Hooking\Debug\HookDll.dll»; // Запрашиваем PID процесса куда хотим инъектировать. printf(«Enter PID to inject dll: «); std::cin >> processId; // Получаем доступ к процессу.

HANDLE openedProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId); if (openedProcess == NULL) < printf(«OpenProcess error code: %drn», GetLastError()); return Wait(); >// Ищем kernel32.dll HMODULE kernelModule = GetModuleHandleW(L»kernel32.dll»); if (kernelModule == NULL) < printf(«GetModuleHandleW error code: %drn», GetLastError()); return Wait(); >// Ищем LoadLibrary (Суффикс A означает что работаем в ANSI, один байт на символ) LPVOID loadLibraryAddr = GetProcAddress(kernelModule, «LoadLibraryA»); if (loadLibraryAddr == NULL) < printf(«GetProcAddress error code: %drn», GetLastError()); return Wait(); >// Выделяем память под аргумент LoadLibrary, а именно — строку с адресом инъектируемой DLL LPVOID argLoadLibrary = (LPVOID)VirtualAllocEx(openedProcess, NULL, strlen(dllName), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); if (argLoadLibrary == NULL) < printf(«VirtualAllocEx error code: %drn», GetLastError()); return Wait(); >// Пишем байты по указанному адресу. int countWrited = WriteProcessMemory(openedProcess, argLoadLibrary, dllName, strlen(dllName), NULL); if (countWrited == NULL) < printf(«WriteProcessMemory error code: %drn», GetLastError()); return Wait(); >// Создаем поток, передаем адрес LoadLibrary и адрес ее аргумента HANDLE threadID = CreateRemoteThread(openedProcess, NULL, 0, (LPTHREAD_START_ROUTINE)loadLibraryAddr, argLoadLibrary, NULL, NULL); if (threadID == NULL) < printf(«CreateRemoteThread error code: %drn», GetLastError()); return Wait(); >else < printf(«Dll injected!»); >// Закрываем поток. CloseHandle(openedProcess); return 0; > int Wait() < char a; printf(«Press any key to exit»); std::cin >> a; return 0; >

Читайте также:
Что такое программы стриминга

Теперь тоже самое, но только на С#. Оцените, насколько код более компактен, нет буйства типов (HANDLE, LPVOID, HMODULE, DWORD, которые, по сути, означают одно и тоже).

public class Exporter < [DllImport(«kernel32.dll», SetLastError = true)] public static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId); [DllImport(«kernel32.dll», SetLastError = true)] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport(«kernel32.dll», CharSet = CharSet.Ansi, SetLastError = true)] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(«kernel32.dll», SetLastError = true)] public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, AllocationType flAllocationType, MemoryProtection flProtect); [DllImport(«kernel32.dll», SetLastError = true)] public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, out IntPtr lpNumberOfBytesWritten); [DllImport(«kernel32.dll», SetLastError = true)] public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out IntPtr lpThreadId); [DllImport(«kernel32.dll», SetLastError = true)] public static extern Int32 CloseHandle(IntPtr hObject); >public class Injector < public static void Inject(Int32 pid, String dllPath) < IntPtr openedProcess = Exporter.OpenProcess(ProcessAccessFlags.All, false, pid); IntPtr kernelModule = Exporter.GetModuleHandle(«kernel32.dll»); IntPtr loadLibratyAddr = Exporter.GetProcAddress(kernelModule, «LoadLibraryA»); Int32 len = dllPath.Length; IntPtr lenPtr = new IntPtr(len); UIntPtr uLenPtr = new UIntPtr((uint)len); IntPtr argLoadLibrary = Exporter.VirtualAllocEx(openedProcess, IntPtr.Zero, lenPtr, AllocationType.Reserve | AllocationType.Commit, MemoryProtection.ReadWrite); IntPtr writedBytesCount; Boolean writed = Exporter.WriteProcessMemory(openedProcess, argLoadLibrary, System.Text.Encoding.ASCII.GetBytes(dllPath), uLenPtr, out writedBytesCount); IntPtr threadIdOut; IntPtr threadId = Exporter.CreateRemoteThread(openedProcess, IntPtr.Zero, 0, loadLibratyAddr, argLoadLibrary, 0, out threadIdOut); Exporter.CloseHandle(threadId); >>

Инъектируемая библиотека

Теперь самое интересное — код библиотеки, которая устанавливает хуки. Эта библиотека написана на С++, пока без аналога на C#.

// dllmain.cpp : Defines the entry point for the DLL application. #include «stdafx.h» #include #define SIZE 6 // Объявления функций и кастомных типов typedef int (WINAPI *pMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT); int WINAPI MyMessageBoxW(HWND, LPCWSTR, LPCWSTR, UINT); void BeginRedirect(LPVOID); pMessageBoxW pOrigMBAddress = NULL; BYTE oldBytes[SIZE] = < 0 >; BYTE JMP[SIZE] = < 0 >; DWORD oldProtect, myProtect = PAGE_EXECUTE_READWRITE; BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) < switch (ul_reason_for_call) < case DLL_PROCESS_ATTACH: // Уведомим пользователя что мы подключились к процессу. MessageBoxW(NULL, L»I hook MessageBox!», L»Hello», MB_OK); // Идем адрес MessageBox pOrigMBAddress = (pMessageBoxW)GetProcAddress(GetModuleHandleW(L»user32.dll»), «MessageBoxW»); if (pOrigMBAddress != NULL) < BeginRedirect(MyMessageBoxW); >break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; > return TRUE; > void BeginRedirect(LPVOID newFunction) < // Массив-маска для записи команды перехода BYTE tempJMP[SIZE] = < 0xE9, 0x90, 0x90, 0x90, 0x90, 0xC3 >; memcpy(JMP, tempJMP, SIZE); // Вычисляем смещение относительно оригинальной функции DWORD JMPSize = ((DWORD)newFunction — (DWORD)pOrigMBAddress — 5); // Получаем доступ к памяти VirtualProtect((LPVOID)pOrigMBAddress, SIZE, PAGE_EXECUTE_READWRITE, // Запоминаем старые байты memcpy(oldBytes, pOrigMBAddress, SIZE); // Пишем 4байта смещения. Да, код рассчитан только на x86 memcpy(JMPSize, 4); // Записываем вместо оригинальных memcpy(pOrigMBAddress, JMP, SIZE); // Восстанавливаем старые права доступа VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL); > int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uiType) < // Получаем доступ к памяти VirtualProtect((LPVOID)pOrigMBAddress, SIZE, myProtect, NULL); // Возвращаем старые байты (иначе будет переполнение стека) memcpy(pOrigMBAddress, oldBytes, SIZE); // Зовем оригинальную функцию, но подменяем заголовок int retValue = MessageBoxW(hWnd, lpText, L»Hooked», uiType); // Снова ставим хук memcpy(pOrigMBAddress, JMP, SIZE); // Восстанавливаем старые права доступа VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL); return retValue; >

Читайте также:
Скаченная программа как пишется

Ну и несколько картинок напоследок. До установки хука:

image

И после установки:

image

В следующем нашей материале мы постараемся написать код библиотеки, которая устанавливает хуки на C#, т. к. механизм инъектирования управляемого кода заслуживает отдельной статьи.

Источник: habr.com

Волшебные хуки. Как перехватывать управление любой программой через WinAPI

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

Какие бывают хуки

Ловушки (hook) могут быть режима пользователя (usermode) и режима ядра (kernelmode). Установка хуков режима пользователя сводится к методу сплайсинга и методу правки таблиц IAT. Ограничения этих методов очевидны: перехватить можно только userspace API, а вот до функций с префиксом Zw*, Ki* и прочих «ядерных» из режима пользователя дотянуться нельзя.

Установка хуков режима ядра позволяет менять любую информацию, которой оперирует Windows на самом низком уровне. Для перехватов подобного типа необходимо модифицировать таблицы SSDT/IDT либо менять само тело функции (kernel patch). Надо сказать, что в Windows на архитектуре x64 ядро контролирует свою целостность при помощи механизма KPP (Kernel Patch Protection), который является частью PatchGuard и просто так подобные манипуляции с системными таблицами сделать не позволит.

Почему хуки работают?

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

Сплайсинг функций WinAPI

Пролог функций, трамплин и дизассемблер длин инструкций

Функции WinAPI начинаются с пролога — это стандартный код, отвечающий за балансировку стека для корректного доступа к локальным переменным, которые использует функция. Обычно пролог выглядит таким образом:

mov edi,edi push ebp mov ebp,esp

В большинстве функций он одинаков, и поэтому на его место можно добавить инструкцию безусловного перехода jmp, которая передаст управление на наш код. Это называется «трамплин» — мы просто уводим поток выполнения функции в наш код, где делаем все, что хотим: можем подменить результат выполнения функции, можем вызвать какой-то другой код, запустить процесс — одним словом, массу всего. Но чтобы грамотно реализовать перехватчик функций методом сплайсинга, нам нужен дизассемблер длин инструкций.

INFO

Дизассемблер длин позволяет вычислять длины команд процессора. Часто используется для анализа прологов функций.

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

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

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Источник: xakep.ru

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru