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

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

https://xakep.ru/2018/01/26/winapi-hooks/



Содержание статьи

Технология перехвата вызовов функций 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

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

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

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

 


Библиотеки для перехвата

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

Мы рассмотрим два популярных коммерческих решения: Detours производства непосредственно Microsoft и библиотеку madCodeHook. Почему именно эти две библиотеки? На них можно реализовать перехват с минимумом кода, что как нельзя лучше подходит для обучения. Полные версии обеих библиотек платные, но для обучения можно либо использовать ограниченные бесплатные версии, либо покупать полные, либо… ну, ты знаешь. 


С готовой библиотекой мы будем уверены, что

  • в ней встроен качественный дизассемблер длин, который не испугается разнообразных функций WinAPI;
  • встроен специальный корректор кода, способный работать вместе с функцией, реализующей трамплины;
  • при сборке проекта будут использованы блоки условной компиляции кода и нам не придется менять синтаксис перехватов при смене архитектур x86 и x64.

Одним словом, мы будем уверены, что в нашей DLL окажутся все необходимые функции.

 


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

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

#include <Windows.h>
#include <iostream>

void slp();

int main()
{

int x;

std:cout << "Enter 1: \n";
std::cin >> x;

if (x == 1) slp();

return 0;
}

void slp()

{
Sleep(60000);
}

Здесь все понятно: при запуске приложение ожидает ввода цифры 1, потом запускает функциюSleep(). Нам это необходимо, чтобы программа не закрылась слишком быстро и повисела немного в памяти, ожидая ввода. Ну и заодно наших инъекций библиотеки перехвата в ее адресное пространство. 


Теперь переходим к реализации самой динамической библиотеки. Наша библиотека (назовем ееHookA.dll) перехватывает вызов Sleep() и заменяет его вызовом CreateFile, который создает в корне диска C: пустой файл по имени virus.exe. Сначала код с использованием библиотеки Detours.

#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include "detours.h"

VOID(WINAPI * TrueSleep)(DWORD dwMilliseconds) = Sleep;

__declspec(dllexport) VOID WINAPI MySleep(DWORD dwMilliseconds)
{

HANDLE hFile = CreateFile(L"c:\\virus.exe", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
CloseHandle(hFile);
}

BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        DetourRestoreAfterWith();
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourAttach(&(PVOID&)TrueSleep, MySleep);
        DetourTransactionCommit();
    }

    return TRUE;
}

При использовании Detours перехват реализуется строкой DetourAttach(&(PVOID&)TrueSleep, MySleep), которая вызывает внутреннюю функцию DetourAttach с параметрами прототипа настоящей функции Sleep() по имени TrueSleep, и ее подделкой, которую написали мы (MySleep). Важно понимать, что наша функция должна соответствовать оригиналу по параметрам и конвенциям вызова.

Теперь все то же самое, только с использованием библиотеки madCodeHook.

#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include "madCHook.h"

VOID(WINAPI * TrueSleep)(DWORD dwMilliseconds) = Sleep;

__declspec(dllexport) VOID WINAPI MySleep(DWORD dwMilliseconds)
{

    HANDLE hFile = CreateFile(L"c:\\virus.exe", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    CloseHandle(hFile);
}


BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        HookAPI("User32.dll", "Sleep", MySleep, (PVOID*) &TrueSleep);
    }

    return TRUE;
}

Код реализации практически не изменился, за исключением строки HookAPI("User32.dll", "Sleep", MySleep, (PVOID*) &TrueSleep); и подключения заголовочного файлаmadCHook.h. В этой строчке кода мы видим, что функция Sleep из системной библиотекиUser32.dll будет заменена нашей реализацией.

INFO

Перед тем как что-то перехватывать, нужно понимать, чего мы хотим этим добиться. Мы хотим менять пути сохранения рабочих данных программы? Путать функции? Саботировать вычисления? В любом случае для исследования приложения нам понадобится API Monitor, программа, которая показывает, какие функции WinAPI использует приложение.

Итак, подопытное приложение готово, наша «вирусная» библиотека тоже, теперь осталось разобраться, как можно заставить DLL прицепиться к нашему приложению. Для этого есть несколько способов, мы рассмотрим два из них.

Первый способ заключается в использовании приложения withdll.exe, которое идет вместе с библиотекой Detours. Если положить это приложение в одну папку с нашими файлами test1.exe иHookA.dll, присоединить библиотеку-перехватчик можно командой withdll.exe -d:HookA.dll test1.exe. Далее приложение withdll.exe запустит наш файл test1.exe с уже присоединенной библиотекой.

Это неудобно и не всегда подходит нам. Что, если нужно инжектировать библиотеку-перехватчик в уже работающий процесс? Второй способ заключается в написании приложения-инжектора, которое присоединит нашу библиотеку-перехватчик к работающему процессу.

 


Инжектор

Для правильной работы инжектора нам нужно получить привилегию SE_DEBUG_NAME. Напишем универсальную функцию, которая получит нужную нам привилегию. Ее-то мы и передадим в качестве аргумента.

BOOL setPrivileges(LPCTSTR szPrivName)
{

    TOKEN_PRIVILEGES tp = { 0 };
    HANDLE hToken = 0;

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
        std::cout << "OpenProcessToken failed\n";

    if (!LookupPrivilegeValue(NULL, szPrivName, &tp.Privileges[0].Luid))
        std::cout << "LookupPrivilegeValue failed\n";

    if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL))
    {
        std::cout << "AdjustTokenPrivileges failed\n";
        CloseHandle(hToken);
        return TRUE;
    }

    return FALSE;

Вызов функции для получения SE_DEBUG_NAME будет таким:

setPrivileges(SE_DEBUG_NAME);

Теперь для работы инжектора нужно написать функцию, которая будет получать PID процесса для инжекта по его имени.

DWORD getPIDproc(wchar_t * procname)
{
    DWORD pid;

    HANDLE pHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    PROCESSENTRY32 ProcEntry;
    ProcessEntry.dwSize = sizeof(ProcEntry);

    do
    {
        if (!_wcsicmp(ProcEntry.szExeFile, procname))
        {
            DWORD pid = ProcEntry.th32ProcessID;
            CloseHandle(pHandle);

            return pid;
        }
    } while (Process32Next(pHandle, &ProcEntry));

    CloseHandle(pHandle);
    return 0;
}

Все готово для написания основного кода инжектора. Приступим!

BOOL inject()
{

    HANDLE victProc = OpenProcess(PROCESS_CREATE_THREAD
        | PROCESS_QUERY_INFORMATION
        | PROCESS_VM_OPERATION
        | PROCESS_VM_WRITE
        | PROCESS_VM_READ,
        false,
        getPIDproc(proc));

    if (victProc) {

        LPVOID pPathBuffer = (PWSTR)VirtualAllocEx(victProc, NULL, dwSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
        if (pPathBuffer == NULL) std::cout << "VirtualAllocEx err\n";

        WriteProcessMemory(victProc, pPathBuffer, (PVOID)path, dwSize, NULL);
        if (pPathBuffer == NULL) std::cout << "WriteProcessMemory err\n";

        HANDLE hRemoteThread = CreateRemoteThread(victProc, NULL, 0,
            (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryW"),
            pPathBuffer, 0, NULL);
        if (hRemoteThread == NULL) std::cout << "CreateRemoteThread err\n";
        else {
        CloseHandle(hRemoteThread);
        return TRUE;
            }
    return FALSE;
}

И вызывающий все эти функции код:

int main()
{

    setPrivileges(SE_DEBUG_NAME);

    std::cout << "test1.exe: " << getPIDproc(proc) << "\n";

    inject();

}

После выполнения этой программы в Process Explorer мы сможем увидеть, что к процессу под именем test1.exe присоединилась библиотека HookA.dll, а при вводе символа 1 в наше приложение в корне диска C: появляется пустой файл virus.exe.

Результат работы инжектора

 


Итого

Мы познакомились с механизмом перехвата WinAPI-функций, попытались вникнуть в техническую сторону процесса перехвата и реализовали учебный перехват функции Sleep() в тестовом приложении. Теперь у тебя достаточно знаний и опыта, чтобы продолжить изучение темы перехватов самостоятельно.

Покажи эту статью друзьям:



Nik Zerof

Report Page