Перехват WinAPI и других библиотечных функций
Добавить в программу поддержку нового сетевого протокола, заставить ее считать, что период пробной эксплуатации еще не прошел, скрыть файл или папку. Это и много другое можно реализовать с помощью перехвата функций Windows API. Что же для этого надо?
Две основные методики перехвата библиотечных функций заключаются в корректировке импорта и сплайсинге функций.
В первом случае приходится перечислять все модули, загруженные в адресное пространство процесса, анализировать их таблицы импорта, корректировать, следить за загрузкой новых модулей… И все это не спасет, если адрес функции получается через GetProcAddress, а не через таблицу.
Второй метод куда более надежен. Он заключается в записи в начало перехватываемой функции безусловного перехода на нашу функцию-перехватчик. Его куда проще реализовать и он лишен недостатков первого метода (не надо следить за загрузкой новых модулей, GetAddressProc от него тоже не спасет).
Как же его реализовать? Самый хардкорный способ — записать весь наш код в адресное пространство интересующего процесса (далее «жертва»), остановить все потоки, перенаправить выполнение одного из них на наш код и ждать результатов. Но это не слишком красиво: придется писать весь внедряемый код на ассемблере, а то и в машинных кодах, к тому же он должен быть базонезависимым. Куда проще оформить весь код перехвата в отдельную библиотеку, которую потом останется только загрузить.
Что же будет делать эта библиотека? При загрузке она найдет адрес перехватываемой функции, считает из ее начала N байт, и на их место запишет переход на нашу функцию. Позже, при выгрузке библиотеки или когда нам понадобится вызывать оригинал функции, необходимо будет вернуть на место эти N байт, тем самым сняв перехват. Ниже приведет код библиотеки, которая при загрузке подменяет функцию GetSystemTime на функцию dummy, реализованную в ней же.
Замечания
Перед тем, как устанавливать или снимать перехват, необходимо остановить все потоки процесса, кроме текущего. Иначе другой поток может перехватить выполнение и обратиться к перехватываемой функции в момент, когда перехват не установлен или процесс записи для его установки не полностью завершен. В таком случае поведение процесса — жертвы непредсказуемо, и скорее всего он упадет с ошибкой доступа к памяти.
Кроме того, прототип нашей функции dummy должен в точности соответствовать прототипу перехватываемой функции (GetSystemTime), в противном случае необходима корректировка стека.
Итак:
- Инициализируемся
Сохраняем первые байты перехватываемой функции и формируем код безусловного перехода(mov rax, адрес заглушки; jmp rax)
- Останавливаем потоки
- Пишем команды безусловного перехода в начало перехватываемой функции
- Запускаем все потоки
В функции, которая выполняется вместо оригинальной, делаем следующее
- Останавливаем потоки
- Снимаем перехват
- Вызываем оригинальную функцию
- Устанавливаем перехват
- Запускаем потоки
- Корректируем результат работы оригинальной функции
- Возвращаем управление
#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, которая позволяет запустить поток в чужом процессе. Так что нам осталось только запустить код этого процесса в адресное пространство жертвы.
Вот, что для этого надо:
- Узнаем, по какому адресу расположены в адресном пространстве жертвы функции LoadLibrary и ExitThread.
Благодаря стараниям Microsoft по защите от вирусов и червей, мы не можем узнать адреса этих функций в своем процессе и вызвать по этим же адресам в соседнем (можем, конечно, но результат — непредсказуем). Поэтому узнаем базу kernel32 в соседнем процессе, смещение нужных функций от начала модуля, складываем и… вуаля, можем вызывать. - Формируем набор команд, которые загрузят библиотеку
А здесь мы как раз и вызываем. То есть делаемpush адрес имени библиотеки mov eax, адрес LoabLibrary call eax push 0 mov eax, адрес ExitThread call eax
Это для win32. Для 64 бит аналогично, только вместо eax — rax и вместо push — mov rcx.
- Пишем эти команды в жертву
Выделяем память VirtualAllocEx’ом и пишем WriteProcessMemory - Создаем в жертве поток.Вот код, который это делает:
#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; }
В конце не забываем подчистить за собой.
Данный код полностью работоспособен и пригоден для использования: на его основе был написан соксификатор. Но в него стоит добавить проверки на корректность результатов, возвращаемых функциями, вынести такие параметры, как имя процесса и модуля, в котором осуществляется перехват и имя функции, которую перехватывают, в одно место для простого изменения.