Смекни!
smekni.com

Методы перехвата API-вызовов в Win32 (стр. 2 из 4)

ПРИМЕЧАНИЕВ Windows NT функции Module32First и Module32Next не реализованы, и для перечисления модулей процесса вместо них придётся воспользоваться функциями из PSAPI.dll.

Локальный перехват посредством изменения перехватываемой функции (только WinNT)

Данный метод перехвата основан на следующем: первые несколько байт перехватываемой функции заменяются на команду безусловного перехода к функции перехвата. Этот трюк достаточно просто реализуется в WinNT (как я уже упоминал, в WinNT для каждого процесса создается своя копия образов системных библиотек), но практически нереализуем в Win9X (так как в Win9X если и можно внести изменения в образ системной библиотеки, то только в адресных пространствах всех процессов сразу).

Существует множество примеров реализации этого метода. Я рассмотрю метод, предлагаемый Microsoft – Detours library.

Detours – это первая официальная библиотека, предназначенная для перехвата функций (не только системных, но и любых других). К основным понятиям Detours относятся:

целевая функция (target function) – функция, перехват которой осуществляется;

функция-перехватчик (detour function) – функция, замещающая перехватываемую;

функция-трамплин (trampoline function) – функция, состоящая из заголовка целевой функции и команды перехода к остальному коду целевой функции.

ПРИМЕЧАНИЕTrampoline в переводе с английского – «батут», однако словосочетание «функция-трамплин» более точно передаёт логику её работы.

Таким образом, если целевая функция имеет следующий заголовок:

TargetFunction: push ebp mov ebp, esp push ebx push esi push edi...

то в результате перехвата получится следующее:

TargetFunction: jmp DetourFunction:TargetFunction+5: push edi ... TrampolineFunction: push ebp mov ebp, esp push ebx push esi jmp TargetFunction+5...

Причём функция-перехватчик может вызывать функцию-трамплин в качестве оригинальной целевой функции.

Библиотека Detours предлагает два метода внедрения «трамплинов» – статический и динамический. Статический метод используется, когда адрес целевой функции известен на этапе сборки модуля. Реализуется он так:

#include <windows.h>#include <detours.h> //Подключимбиблиотеку Detours//Этот макрос создаёт функцию-трамплин для функции SleepDETOUR_TRAMPOLINE(VOID WINAPI SleepTrampoline(DWORD), Sleep); VOID WINAPI SleepDetour(DWORD dw) //Это – функция-перехватчик{ //В этом примере она ничего не делает, просто вызывает оригинальную функциюreturn SleepTrampoline(dw);}void main(void){ //Здесьосуществляетсяперехват DetourFunctionWithTrampoline((PBYTE)SleepTrampoline, (PBYTE)SleepDetour); //... //Аздесьснимается DetourRemoveTrampoline(SleepTrampoline); }

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

#include <windows.h>#include <detours.h> //Подключимбиблиотеку DetoursVOID (*DynamicTrampoline)(VOID) = NULL; //Этобудетфункция-трамплинVOID DynamicDetour(VOID) //Это – функция-перехватчик{ //В этом примере она ничего не делает, просто вызывает оригинальную функциюreturn DynamicTrampoline();}void main(void){ //Получимадресцелевойфункции VOID (*DynamicTarget)(VOID) = SomeFunction; //Здесьосуществляетсяперехват DynamicTrampoline=(FUNCPTR)DetourFunction((PBYTE)DynamicTarget, (PBYTE)DynamicDetour); //... DetourRemoveTrampoline(DynamicTrampoline); //Аздесьснимается}

При перехвате функция DetourFunction динамически создаёт трамплин и возвращает его адрес. В качестве функции SomeFunction, которая в данном примере возвращает адрес целевой функции, можно использовать DetourFindFunction, которая пытается найти нужную функцию в нужном модуле. Сначала она пытается сделать это через LoadLibrary и GetProcAddress, а в случае неудачи – использует библиотеку ImageHlp для поиска отладочных символов.

Макрос DETOUR_TRAMPOLINE и функция DetourFunction включают в себя встроенный табличный дизассемблер, который определяет, какое количество байт из заголовка целевой функции должно быть скопировано в функцию-трамплин (не менее 5 байт (размер команды jmp), составляющих целое число команд процессора). Если целевая функция занимает менее 5 байт, то перехват оканчивается неудачей.

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

push ecx ;в функцию передаётся количество итераций циклаbegin_loop: ;... ;здесь какой-то код ;... loop begin_loop ret

Вышеприведённый пример Galen Hunt, один из авторов Detours, прокомментировал следующим образом: «Существует множество теоретических примеров кода, где пролог функции меньше 5 байт, требуемых для команды jmp. Однако никто не сообщал о реальных примерах функции с такими проблемами».

ПРЕДУПРЕЖДЕНИЕНа момент установки/снятия перехвата нужно останавливать все остальные потоки процесса, в котором происходит перехват (или удостовериться, что они не могут вызывать перехватываемую функцию).

Существует другой способ реализации данного метода. Вместо команды jmp в начало функции помещается команда INT 3, а управление функции-перехватчику передаётся косвенно в обработчике необработанных исключений (её адрес заносится в pExceptionInfo->ContextRecord->Eip и обработчик возвращает EXCEPTION_CONTINUE_EXECUTION). Так как команда INT 3 занимает 1 байт, то вышеописанная ситуация в этом случае даже теоретически невозможна.

ПРЕДУПРЕЖДЕНИЕОсновным недостатком данного способа является его крайне малое быстродействие (обработка исключения в Windows занимает довольно продолжительное время). Кроме того, наличие обработчика исключений в перехватываемом процессе приведёт к тому, что данный метод работать не будет. Также данный способ не будет работать под отладчиком.

Глобальный перехват

Глобальный перехват может быть реализован различными способами. Первый способ – применение локального перехвата ко всем приложениям в системе (запущенным в момент перехвата или позже). Второй способ – «взлом системы» – подразумевает подмену кода перехватываемой функции непосредственно в DLL-файле или его образе в памяти.

Глобальный перехват методом тотального локального перехвата

Данный метод основан на следующем: если можно перехватить функцию из текущего процесса, то нужно выполнить код перехвата во всех процессах в системе. Существует несколько методов заставить чужой процесс выполнить код перехвата. Самый простой – внести этот код в DllMain некоторой библиотеки, а затем внедрить её в чужой процесс. Методов внедрения DLL также существует несколько (см. Джеффри Рихтер). Самый простой, работающий и в Win9X, и в WinNT – внедрение DLL при помощи ловушек. Реализуется он так: в системе устанавливается ловушка (при помощи функции SetWindowsHookEx) типа WH_GETMESSAGE (эта ловушка служит для перехвата Windows-сообщений). В этом случае модуль, в котором находится ловушка, автоматически подключается к потоку, указанному в последнем аргументе SetWindowsHookEx (если указан 0, то производится подключение ко всем потокам в системе). Однако подключение происходит не сразу, а перед тем, как в очередь сообщений потока будет послано какое-нибудь сообщение. Поэтому перехват осуществляется не сразу после запуска приложения, а перед обработкой процессом первого сообщения. Так что все вызовы перехватываемой функции до обработки процессом первого сообщения перехватываться не будут. А в приложениях без очереди сообщений (например, консольных) этот способ внедрения вообще не работает.

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

Функция GetDriveTypeA из библиотеки kernel32.dll используется программами Windows для определения типа диска (локальный, CD-ROM, сетевой, виртуальный и т. д.). Она имеет следующий прототип:

UINT GetDriveType(LPCTSTR lpRootPathName);

lpRootPathName – путь до диска (А:&bsol;, В:&bsol; и т.д.)

GetDriveTypeA возвращает одно из следующих значений:

#define DRIVE_UNKNOWN 0#define DRIVE_NO_ROOT_DIR 1#define DRIVE_REMOVABLE 2#define DRIVE_FIXED 3#define DRIVE_REMOTE 4#define DRIVE_CDROM 5#define DRIVE_RAMDISK 6

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

Программа DriveType2 состоит из двух модулей: DriveType2.exe и DT2lib.dll.

DriveType2.exe реализует интерфейс, а вся работа выполняется в DT2lib.dll.

Проект DT2lib состоит из трёх основных файлов:

APIHook.cpp – этот файл написан Джеффри Рихтером (за исключением некоторых исправлений, сделанных мной. О них я расскажу ниже). В этом файле описан класс CAPIHook, реализующий перехват заданной API-функции во всех модулях текущего процесса. Здесь же автоматически перехватываются функции LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW и GetProcAddress.

Toolhelp.h – этот файл также написан Джеффри Рихтером. В нём описан класс CToolhelp, реализующий обращение к системным toolhelp-функциям. В данном случае он используется классом CAPIHook для перечисления всех модулей, подключенных к процессу.

DT2Lib.cpp – в этом файле я реализовал перехват функции GetDriveTypeA с использованием класса CAPIHook, а также установку ловушки типа WH_GETMESSAGE, обеспечивающей подключение данного модуля (DT2lib.dll) ко всем потокам в системе.

Как же происходит перехват?

Сразу же после запуска DriveType2.exe вызывается функция DT2_HookAllApps из DT2lib.dll, которая устанавливает ловушку.

BOOL WINAPI DT2_HookAllApps(BOOL fInstall, DWORD dwThreadId) { BOOL fOk; if (fInstall) { chASSERT(g_hhook == NULL); // 2 разаперехватыватьникчему g_hhook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, ModuleFromAddress(DT2_HookAllApps), dwThreadId); // Установимловушку fOk = (g_hhook != NULL); } else {chASSERT(g_hhook != NULL); // Снимать-то нечего fOk = UnhookWindowsHookEx(g_hhook); // Снимем ловушкуg_hhook = NULL; } return(fOk);}

Функция ловушки GetMsgProc ничего не делает, а просто вызывает следующую функцию ловушки (возможно, не только наша программа установила ловушку, и это, как минимум нужно проверить). Перед тем, как поместить в очередь, ассоциированную с некоторым потоком, какое-то сообщение, система должна вызвать все установленные ловушки типа WH_GETMESSAGE (обычно такие ловушки используются для мониторинга или изменения некоторых сообщений, однако мы ничего подобного не делаем – нам нужно просто подключиться ко всем потокам в системе). Система не может просто вызвать нашу функцию ловушки – она и получатель сообщения находятся в разных процессах, а значит, и в разных адресных пространствах. И выход из этой ситуации один – система просто подключает модуль (а это обязательно должен быть DLL-модуль), в котором находится ловушка, к тому процессу, которому посылается сообщение (что нам собственно и нужно).