Навіщо це може знадобитися?
Слово «перехоплення» говорить саме за себе. Ця можливість може знадобитися в будь-якій ситуації, коли виникає потреба відстежити факт виклику системної функції, змінити вхідні або вихідні аргументи. Типовим сценарієм для такого завдання є реверс-інжиніринг або зміна поведінки ПЗ, у випадку коли немає доступу до програмного коду. Слід зазначити, що в завданнях, пов'язаних з реверс-інжинірингом, перехоплення – швидше допоміжний інструмент, тоді як на перший план виходить дізасемблювання та аналіз даних.
Вперше з цим завданням я зіткнувся у 2001 році. Раптово виявилося, що програма, якою я користувався (Fractal Design Painter 5), відмовлялася запускатися, якщо в системі встановлено понад 4 ГБ оперативної пам'яті. Одна справа коли пам'яті мало, але досить курйозно виглядає ситуація у випадку з «зайвою» пам'яттю 😐
Винуватцем виявилась системна функція GlobalMemoryStatus, яка повертала некоректне значення (-1).
Перехоплення і коректування параметрів цієї функції дозволило елегантно вийти з ситуації без складного дизасемблювання чи патчингу виконуваного файлу. Я штучно зробив так, щоб функція GlobalMemoryStatus завжди повертала 2 ГБ пам'яті застосунку, який запитував цю інформацію.
Обговорюючи перехоплення системних функцій, слід розділити цей процес на дві частини:
-
Впровадження (далі «ін'єкція») призначеної для користувача функції в адресний простір процесу, для якого здійснюється перехоплення. Функція для ін'єкції має бути оформлена у вигляді модуля DLL.
-
Підміна точки входу у функції з оригінальної, на призначену користувачем.
Способи ін'єкції
Remote thread
Цей спосіб найбільш поширений. Windows API надає функцію «CreateRemoteThread», яка створює потік в контексті будь-якого іншого процесу.
Один з аргументів функції вказує на точку входу в функцію потоку (thread worker). Трюк полягає в тому, що в ролі точки входу вказується адреса системної функції LoadLibrary, а в ролі додаткового параметра передається посилання (pointer) на рядок зі шляхом до DLL.
Це спрацьовує, оскільки для всіх процесів базова адреса бібліотеки User32.dll однакова і, відповідно, адреса LoadLibrary також.
Базова адреса завантаження системних DLL однакова в контексті однієї сесії. Після перезавантаження адреса може змінитися, але також буде однакова для всіх процесів. Така поведінка реалізується механізмом ASLR (Address Space Layout Randomization), який, теоретично, повинен ускладнювати експлуатацію деяких вразливостей ОС.
Windows hook
Цей спосіб ґрунтується на можливості встановлювати призначені для користувача «пастки» на події певного роду (наприклад, ввід даних за допомогою клавіатури або миші) для Windows програм.
AppInit_DLLs
\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows
DLL модулі перераховані в цьому розділі реєстру будуть автоматично завантажуватися в усі процеси при їх старті (крім тих процесів, які не використовують функції з user32.dll, але таких вкрай мало).
Цей спосіб ще з часів Windows XP позначений Microsoft як не рекомендований до використання (deprecated), з огляду на потенційну небезпеку і уразливості.
Методи підміни точки входу
Модифікація таблиці імпорту
Таблиця імпорту модуля містить в собі адреси імпортованих з DLL функцій. Адреси вираховуються при завантаженні DLL, з урахуванням базової адреси. Кожен раз, коли викликається яка-небудь функція, реалізована в DLL, адреса відповідної функції витягується з таблиці і відбувається її виклик. Таким чином змінивши адресу будь-якої системної функції, на адресу власної, буде викликана остання. Як було сказано раніше, перехоплення, як правило, полягає в зміні вхідних або вихідних даних перехопленої функції. Тобто для перехоплення, як мінімум, треба зберегти адресу оригінальної функції для виклику пізніше. Оригінальна функція зробить основну роботу, а функція користувача може здійснювати обробку параметрів до чи після виклику оригінальної функції. Для успішної заміни адреси важливо, щоб виконуваний модуль був з прилінкованою (linked) бібліотекою імпорту відповідної DLL. Бібліотека імпорту user32.dll практично завжди лінкується до всіх модулів, які є Win32 GUI застосунками.
Установка «трампліну»
Цей метод більш складний в реалізації, але разом з тим він потужніший. Метод полягає в модифікації частини виконуваного коду функції. У самий початок тіла функції записується машинна інструкція для безумовного переходу в зону користувацької функції («трамплін»), де виконуються необхідні маніпуляції над параметрами. У C/C++ при виконанні функції формується так званий stack-frame, який використовується для передачі параметрів у функцію (а також містить адресу повернення). Використання «трампліну» ламає звичайну роботу з stack-frame, яку генерує компілятор, тому цей метод вимагає особливої процедури відновлення стека в кінці виконання. На відміну від методу модифікації таблиці імпорту, цей спосіб буде працювати навіть якщо до виконуваного модуля не була прилінкована відповідна бібліотека імпорту.
Вищесказане справедливо не тільки стосовно системних функцій, а й будь-яких інших, які:
- реалізовані в DLL.
- експортуються за іменем або індексом.
- відомий прототип функції, і порядок передачі аргументів до стека (calling conversion).
Проксі DLL
Існують ще декілька способів для перехоплення системних викликів, серед яких – написання користувацького DLL модуля, який буде завантажуватися замість оригінального. Всі виклики функцій потім будуть перенаправлені в оригінальний модуль (проксинг).
Резюме
Перехоплення системних функцій стане гідним інструментом в арсеналі будь-якого фахівця, який займається реверс-інжинірингом, або у випадку, коли вихідні коди загублено (legacy), а треба щось виправити. При всьому цьому, інструмент не вважається чимось хакерським або «кримінальним» з точки зору Windows. Цей механізм активно використовувався з найраніших версій Windows самою Microsoft. На базі цих засобів реалізовані деякі антивіруси та системи моніторингу потенційних загроз.
Демо проект
Посилання на GitHub репозиторій: https://github.com/szhukovks/injection.git
Демо проект складається з трьох підпроектів:
-
example-app – Windows GUI застосунок, який виводить текст «Hello, World» на канві посеред вікна, за допомогою функції DrawTextW.
-
injection – DLL модуль для ін'єкції, з функцією користувача. Модифікований DrawTextW, підміняє будь-який оригінальний текст на «Goodbye, World».
-
injector – виконує запуск тестового застосунку, а також здійснює ін'єкцію і перехоплення оригінальної функції DrawTextW. Існує можливість впроваджувати DLL в уже запущені процеси – для цього в завантажувачі треба не створювати новий процес через Win32 API CreateProcess (...), а отримувати доступ до вже наявного через OpenProcess (...).
Ключові моменти
injector/injector.cpp
SECURITY_DESCRIPTOR sd = {0};
SECURITY_ATTRIBUTES sa = {0};
// Створення процесу, для якого буде здійснено перехоплення системної функції
auto success = ::CreateProcessW(szAppPath, L"", &sa, nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi);
//збереження дескриптора створеного процесу, для подальшого використання з CreateRemoteThread
hApp = pi.hProcess;
hThread = pi.hThread;
// Підготовка до запуску «віддаленого» потоку, в контексті раніше створеного процесу.
auto pLoadLibraryW = ::GetProcAddress(GetModuleHandleW(L"Kernel32"), "LoadLibraryW");
auto path_cchar = wcslen(szInjectionDll) + 1;
auto block_size = path_cchar * sizeof(wchar_t);
//створення блоку пам'яті в адресному просторі нового процесу (в який повинна бути ін'єктована DLL)
auto szPathMemory = ::VirtualAllocEx(hApp, NULL, block_size, MEM_COMMIT, PAGE_READWRITE);
//копіювання шляху до ін'єктованого модулю
SIZE_T count = 0;
auto success = ::WriteProcessMemory(hApp, szPathMemory, szInjectionDll, block_size, &count);
DWORD dwThreadId = 0;
//запуск віддаленого потоку, в результаті якого буде викликана функція LoadLibrary в контексті створеного процесу
HANDLE hThread = ::CreateRemoteThread(hApp, nullptr, 0, (LPTHREAD_START_ROUTINE)pLoadLibraryW, szPathMemory, 0, &dwThreadId);
injection/dllmain.cpp
//Функція яка буде викликатися замість системної. Для тестового прикладу відбувається підміна оригінального рядка на «Goodbye, World».
int FAR WINAPI DrawTextW_Custom(HDC dc, LPCTSTR text, int length, LPRECT rc, UINT format)
{
auto szText = L"Goodbye, World";
int len = (int)wcslen(szText);
return g_pDrawTextW(dc, szText, len, rc, format);
}
bool intercept()
{
if (!g_pDrawTextW) { return false; }
//Виклик допоміжної функції для безпосередньої зміни адреси функції в таблиці імпорту.
//Функція ImageDirectoryEntryToData дозволяє отримати доступ до цієї таблиці, і реалізована в спеціалізованій бібліотеці налагодження dbghelp.dll (входить в дистрибутив Windows)
return apply_IAT_patch("User32.dll", g_pDrawTextW, &DrawTextW_Custom, ::GetModuleHandle(NULL));
}
//перехоплення відбувається в стандартній точці входу DLL модуля при першому завантаженні в адресний простір процесу.
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH: {
if (!intercept()) {
//...
Ще немає коментарів