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

в 14:28, , рубрики: C#, c++, dll, Блог компании ICL Services, разработка под windows, С++, системное программирование, хук

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

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

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

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

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

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

Давайте перейдем от общих фраз к более детальному рассмотрению хуков. Мне известно несколько разновидностей реализации хука:
● Использование функции 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 <iostream>
#include <Windows.h>
#include <cstdio>
 
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 <Windows.h>
 #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, &oldProtect);
                // Запоминаем старые байты
                memcpy(oldBytes, pOrigMBAddress, SIZE);
                // Пишем 4байта смещения. Да, код рассчитан только на x86
                memcpy(&JMP[1], &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#, т. к. механизм инъектирования управляемого кода заслуживает отдельной статьи.

Авторы статьи: nikitam, ThoughtsAboutProgramming

Автор: ICL Services

Источник

Поделиться

* - обязательные к заполнению поля