45824 (665168), страница 2
Текст из файла (страница 2)
Существует множество примеров реализации этого метода. Я рассмотрю метод, предлагаемый 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 #include //Подключим библиотеку Detours //Этот макрос создаёт функцию-трамплин для функции Sleep DETOUR_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 #include //Подключим библиотеку Detours VOID (*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 – путь до диска (А:\, В:\ и т.д.)
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-модуль), в котором находится ловушка, к тому процессу, которому посылается сообщение (что нам собственно и нужно).















