Перехват WinAPI и других библиотечных функций

14 декабря 2009 г.

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

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

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

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

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

Что же будет делать эта библиотека? При загрузке она найдет адрес перехватываемой функции, считает из ее начала N байт, и на их место запишет переход на нашу функцию. Позже, при выгрузке библиотеки или когда нам понадобится вызывать оригинал функции, необходимо будет вернуть на место эти N байт, тем самым сняв перехват. Ниже приведет код библиотеки, которая при загрузке подменяет функцию GetSystemTime на функцию dummy, реализованную в ней же.

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

Кроме того, прототип нашей функции dummy должен в точности соответствовать прототипу перехватываемой функции (GetSystemTime), в противном случае необходима корректировка стека.

Итак:

  1. Инициализируемся
    Сохраняем первые байты перехватываемой функции и формируем код безусловного перехода

    (mov rax, адрес заглушки; jmp rax)
  2. Останавливаем потоки
  3. Пишем команды безусловного перехода в начало перехватываемой функции
  4. Запускаем все потоки

В функции, которая выполняется вместо оригинальной, делаем следующее

  1. Останавливаем потоки
  2. Снимаем перехват
  3. Вызываем оригинальную функцию
  4. Устанавливаем перехват
  5. Запускаем потоки
  6. Корректируем результат работы оригинальной функции
  7. Возвращаем управление
#include "windows.h"
#include "tlhelp32.h"

PVOID proc(0);
#define PROLOG_SIZE 16
byte old_prolog[PROLOG_SIZE], new_prolog[PROLOG_SIZE];

void StopThreads(BOOL bStop)
{
DWORD cp = GetCurrentProcessId();
DWORD ct = GetCurrentThreadId();
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE) return;
THREADENTRY32 te;
ZeroMemory(&te, sizeof(te));
te.dwSize = sizeof(te);
if (!Thread32First(hSnap, &te))
{
CloseHandle(hSnap);
return;
}
do
if ((te.th32ThreadID != ct)&(te.th32OwnerProcessID == cp))
{
int err = GetLastError();
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, false, te.th32ThreadID);
if (hThread)
{
if (bStop)
SuspendThread(hThread);
else
ResumeThread(hThread);
CloseHandle(hThread);
}
}
while (Thread32Next(hSnap, &te));
CloseHandle(hSnap);
return;
}

void Splice()
{
WriteProcessMemory(GetCurrentProcess(), proc, &new_prolog, PROLOG_SIZE, NULL);
return;
}

void Unsplice()
{
WriteProcessMemory(GetCurrentProcess(), proc, &old_prolog, PROLOG_SIZE, NULL);
return;
}

void __stdcall dummy(LPSYSTEMTIME lpst)
{
StopThreads(true);
Unsplice();
GetSystemTime(lpst);
Splice();
StopThreads(false);
lpst->wDay -= 10;
return;
}

void Init()
{
HMODULE hKer = GetModuleHandle(L"kernel32.dll");
proc = GetProcAddress(hKer, "GetSystemTime");
#ifndef _WIN64
new_prolog[0] = 0x90;//nop
new_prolog[1] = 0xb8;//mov eax, dummy
new_prolog[6] = 0xff;//jmp eax
new_prolog[7] = 0xe0;
#else
new_prolog[0] = 0x48;//mov rax, dummy
new_prolog[1] = 0xb8;
new_prolog[10] = 0xff;//jmp rax
new_prolog[11] = 0xe0;
#endif

PVOID * newfunc = (PVOID *)((char *)&new_prolog[2]);
*newfunc = (PVOID *)&dummy;
ReadProcessMemory(GetCurrentProcess(), proc, &old_prolog, PROLOG_SIZE, NULL);

return;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
/* Init Code here */
Init();
StopThreads(true);
Splice();
StopThreads(false);
break;

case DLL_THREAD_ATTACH:
/* Thread-specific init code here */
break;

case DLL_THREAD_DETACH:
/* Thread-specific cleanup code here.
*/
break;

case DLL_PROCESS_DETACH:
/* Cleanup code here */
Unsplice();
break;
}
/* The return value is used for successful DLL_PROCESS_ATTACH */
return TRUE;

}

Как же теперь заставить жертву загрузить эту библиотеку. Можно подправить реестр так, чтобы она загружала во все создаваемые процессы. Но я предпочитаю действовать более адресно. В Windows есть такая замечательная функция CreateRemoteThread, которая позволяет запустить поток в чужом процессе. Так что нам осталось только запустить код этого процесса в адресное пространство жертвы.

Вот, что для этого надо:

  1. Узнаем, по какому адресу расположены в адресном пространстве жертвы функции LoadLibrary и ExitThread.
    Благодаря стараниям Microsoft по защите от вирусов и червей, мы не можем узнать адреса этих функций в своем процессе и вызвать по этим же адресам в соседнем (можем, конечно, но результат — непредсказуем). Поэтому узнаем базу kernel32 в соседнем процессе, смещение нужных функций от начала модуля, складываем и… вуаля, можем вызывать.
  2. Формируем набор команд, которые загрузят библиотеку
    А здесь мы как раз и вызываем. То есть делаем

    push адрес имени библиотеки
    mov eax, адрес LoabLibrary
    call eax
    push 0
    mov eax, адрес ExitThread
    call eax

    Это для win32. Для 64 бит аналогично, только вместо eax — rax и вместо push — mov rcx.

  3. Пишем эти команды в жертву
    Выделяем память VirtualAllocEx’ом и пишем WriteProcessMemory
  4. Создаем в жертве поток.Вот код, который это делает:
    #include "windows.h"
    #include "tlhelp32.h"
    
    int IsProcessPresent(wchar_t * szExe) //get pid by exe filename
    {
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe;
    pe.dwSize = sizeof(PROCESSENTRY32);
    Process32First(hSnapshot, &pe);
    do
    if (!_wcsicmp((wchar_t *)&pe.szExeFile, szExe))
    {
    return pe.th32ProcessID;
    }
    while (Process32Next(hSnapshot, &pe));
    return 0;
    }
    
    PVOID ModuleBaseEx(DWORD pid, wchar_t * szModule)
    {
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE|TH32CS_SNAPMODULE32, pid);
    if (hSnapshot == INVALID_HANDLE_VALUE) return 0;
    
    MODULEENTRY32 me;
    me.dwSize = sizeof(me);
    Module32First(hSnapshot, &me);
    do
    if (!_wcsicmp((wchar_t *)&me.szModule, szModule))
    {
    return me.modBaseAddr;
    }
    while (Module32Next(hSnapshot, &me));
    CloseHandle(hSnapshot);
    
    return 0;
    }
    
    int main()
    {
    int pid = IsProcessPresent(L"splicer_victim.exe");
    HANDLE hProc = OpenProcess(PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ,\
    false, pid);
    if (!hProc) return -1;
    
    PVOID kernelbase = ModuleBaseEx(pid, L"kernel32.dll");
    HMODULE hKern = GetModuleHandle(L"kernel32.dll");
    PVOID llrva = GetProcAddress(hKern, "LoadLibraryW");
    llrva = (PVOID)((byte *)llrva - (byte *)hKern + (byte *)kernelbase);
    PVOID etrva = GetProcAddress(hKern, "ExitThread");
    etrva = (PVOID)((byte *)etrva - (byte *)hKern + (byte *)kernelbase);
    
    byte Buff[1024];
    
    #ifndef _WIN64
    Buff[0] = 0x90;//nop
    Buff[1] = 0x68;//push libname
    Buff[6] = 0xb8;//mov eax, loadlib
    PVOID *addr = ((PVOID *)&Buff[7]);//addr of loadlib
    *addr = (PVOID)llrva;
    Buff[11] = 0xff;//call eax
    Buff[12] = 0xd0;
    Buff[13] = 0x6a;//push 0
    Buff[14] = 0x0;
    Buff[15] = 0xb8;//mov eax, exitthread
    addr = ((PVOID *)&Buff[16]);//addr of exitthread
    *addr = (PVOID)etrva;
    Buff[20] = 0xff;//call eax
    Buff[21] = 0xd0;
    #else
    Buff[0] = 0x48;//mov rcx, imm64
    Buff[1] = 0xb9;
    Buff[10] = 0x48;//mov rax, imm64
    Buff[11] = 0xb8;
    PVOID *addr = ((PVOID *)&Buff[12]);//addr of loadlib
    *addr = (PVOID)llrva;
    Buff[20] = 0xff;//call rax
    Buff[21] = 0xd0;
    Buff[22] = 0x48;//xor rcx, rcx
    Buff[23] = 0x33;
    Buff[24] = 0xc9;
    Buff[25] = 0x48;//mov rax, imm64
    Buff[26] = 0xb8;
    addr = ((PVOID *)&Buff[27]);//addr of exitthread
    *addr = (PVOID)etrva;
    Buff[35] = 0xff;//call rax
    Buff[36] = 0xd0;
    #endif
    wchar_t injdll[] = L"injection.dll";
    PVOID libname = VirtualAllocEx(hProc, NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
    if (!libname) return -1;
    WriteProcessMemory(hProc, libname, &injdll, sizeof(injdll), NULL);//libname
    addr = ((PVOID *)&Buff[2]);
    *addr = (PVOID)libname;
    
    PVOID mem = VirtualAllocEx(hProc, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!mem) return -1;
    
    WriteProcessMemory(hProc, mem, &Buff, 1024, NULL);
    
    DWORD tid;
    HANDLE ht = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)mem, NULL, 0, &tid);
    
    WaitForSingleObject(ht, 1000);
    DWORD ec (-1);
    GetExitCodeThread(ht, &ec);
    
    if (ec == 0)
    {
    VirtualFreeEx(hProc, mem, 0, MEM_RELEASE);
    VirtualFreeEx(hProc, libname, 0, MEM_RELEASE);
    }
    
    CloseHandle(ht);
    CloseHandle(hProc);
    return 0;
    }

В конце не забываем подчистить за собой.

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

Теги:
рубрика Windows, Программирование
  • Похожие статьи
  • Предыдущие из рубрики