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

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

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

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

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

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

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

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

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

● Использование функции SetWindowsHookEx. Это весьма простой, оттого и ограниченный, метод. Он позволяет перехватывать только определенные функции, в основном связанные с окном (например, перехват событий, которые получает окно, щелчков мышкой, клавиатурного ввода). Достоинством этого метода является возможность установки глобальных хуков (например, сразу на все приложениях перехватывать клавиатурный ввод).
● Использование подмены адресов в разделе импорта DLL. Суть метода заключается в том, что любой модуль имеет раздел импорта, в котором перечислены все используемые в нем другие модули, а также адреса в памяти для экспортируемых этим модулем функций. Нужно подменить адрес в этом модуле на свой и управление будет передано по указанному адресу.
● Использование ключа реестра HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_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: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Ищем kernel32.dll
                HMODULE kernelModule = GetModuleHandleW(L"kernel32.dll");
                if (kernelModule == NULL)
                {
                                printf("GetModuleHandleW error code: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Ищем LoadLibrary (Суффикс A означает что работаем в ANSI, один байт на символ)
                LPVOID loadLibraryAddr = GetProcAddress(kernelModule, "LoadLibraryA");
                if (loadLibraryAddr == NULL)
                {
                                printf("GetProcAddress error code: %d\r\n", 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: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Пишем байты по указанному адресу.
                int countWrited = WriteProcessMemory(openedProcess, argLoadLibrary, dllName, strlen(dllName), NULL);
                if (countWrited == NULL)
                {
                                printf("WriteProcessMemory error code: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Создаем поток, передаем адрес LoadLibrary и адрес ее аргумента
                HANDLE threadID = CreateRemoteThread(openedProcess, NULL, 0, (LPTHREAD_START_ROUTINE)loadLibraryAddr, argLoadLibrary, NULL, NULL);
 
                if (threadID == NULL)
                {
                                printf("CreateRemoteThread error code: %d\r\n", 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 56,37
Компания
Поделиться публикацией
Комментарии 19
  • 0
    Спасибо. Все очень понятно расписано. В вашем примере адрес оригинальной функции ищется через GetProcAddress, но он возвращает адрес только из списка экспортируемых функций, что делать, если нужно подменить функцию которой нет в списке?
    • 0
      К сожалению, указанным в статье способом доступ к такой функции получить нельзя. Можно дизассемблировать dll, понять где находится код нужной вам функции и заменить его своим.
      • 0
        А как вы вообще предполагаете ее искать? Можно по сигнатуре.
      • 0
        • Контроль входящего http-траффика и подмена «взрослого» контента на более безобидный.

        Ээээ… да? Это так теперь делается? Да и остальной список задач меня заинтересовал даже больше, чем статья. Не потому что статья плохая, просто я Рихтера читал :)

        • 0
          Это один из способов, посмотрите на Fiddler :)
          • 0

            Ну, Fiddler и WireShark это инструменты мониторинга и отладки трафика. Не, ясно-понятно, что молотком можно и саморезы закручивать. Но это бы можно было в список задач и писать: перехват вызовов WinSocks-функций или чего там ещё. А так это провокация, я считаю. Но тонкая. Я ж вот заинтересовался, хотя Рихтера читал. :)

            • +2

              А Fiddler разве не как прокси себя регистрирует? Не раз сталкивался с тем, что если Fiddler некорректно закрыть, то он не отписывает себя из прокси и потом нет сети в тех приложениях, которые используют общесистемные настройки прокси.

          • +3
            Здесь описан простой, но проблемный способ установки хука.

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

            Ну и, если сильно не повезёт, при одновременном обращении к функции из разных потоков, можно словить Access Violation, когда восстановлена только часть оригинального начала функции, а другой поток на неё передал управление.
            • +2

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


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


              Ну и неплохо было бы упомянуть про Microsoft Detours, раз уж диалог идет об inline hooking'е.

              • 0
                Microsoft Detours немного отдельная тема. Я хотел показать сам процесс, Detours по сути это все скрывает. Более того коммерческая версия (и работа с архитектурой x64) стоят 10K $, поэтому интерес к этой библиотеке у меня поугас :)
                • +1
                  Есть бесплатные альтернативы (даже опенсорсные), самые интересные из которых EasyHook и Deviare-InProc.
              • 0
                Да, именно цель статьи и была показать что установка хука не такое уж сложное дело, попытаться дать направление куда мыслить тем кто только начал интересоваться указанной темой.

                Про многопоточность вы абсолютно правы. Указанный пример не рассчитан на нее. Вообще умение установить описанный вами хук достаточно нетривиальное.
              • 0
                «5. Наша библиотека инъектирована в адресное пространство постороннего потока.»
                тут наверное хотели сказать
                «5. Наша библиотека инъектирована в адресное пространство постороннего процесса».
                • 0
                  Вы абсолютно правы!
                • 0
                  материал изложен просто и доступно, тем кто действительно будет заинтересован данной темой — с легкостью найдут более подробное изложение нужных им тематик.
                  • 0
                    Один маленький вопрос: DEP (Data Execution Prevention) включен или нет?
                    • 0
                      DEP тут ни на что не влияет.
                    • 0
                      Указанные методы не сработает с защищенными процессами (например edge). Есть решения данной проблемы?
                      • –2
                        А разве Edge защищённый?
                        Впрочем вы можете не использовать Edge, или не использовать ОС, имеющую защищённые процессы (например, XP).

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое