Xreferat.com » Рефераты по информатике и программированию » Методы перехвата API-вызовов в Win32

Методы перехвата API-вызовов в Win32

Игорь В. Филимонов

Введение

Данная статья написана в результате анализа известных методов перехвата API-вызовов в Windows. В некоторых широко известных примерах реализации перехвата системных функций есть небольшие ошибки, которые в некоторых случаях приводят к тому, что перехват не работает. Один из таких примеров был описан в RSDN Magazine #1, другой – в известной книге Джеффри Рихтера «Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows», 4-е издание.

Перехват системных функций операционной системы – приём, известный давно. Обычно перехватывается некоторая системная функция с целью мониторинга или изменения её поведения. Во времена DOS программисты перехватывали программные прерывания (int 21h, int 16h, int 10h). С приходом Win16 понадобились средства для перехвата API-функций. И, наконец, с появлением Win32 средства перехвата ещё раз эволюционировали, подстроившись под новую систему. Операционные системы семейства Windows никогда не содержали встроенных средств, специально предназначенных для перехвата системных функций. И понятно почему – всё-таки это немного хакерский приём. Поэтому перехват обычно осуществляется «подручными средствами», и для его реализации нужно чётко представлять многие глубинные аспекты устройства и функционирования операционной системы.

В данной статье рассматриваются методы реализации перехвата системных API-функций в 32-разрядных операционных системах Windows. Рассматриваются особенности реализации перехвата в Win9X (Windows 95/98/98SE/ME) и WinNT (Windows NT/2000/XP/2003).

Особенности организации памяти в Windows

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

Каждому процессу (начиная с Windows 95) выделяется собственное виртуальное адресное пространство. Для 32-разрядных процессов его размер составляет 4 Гб. Это адресное пространство разбивается на разделы, функциональное назначение и свойства которых довольно сильно отличаются у семейств ОС WinNT и Win9Х.

Адресное пространство любого процесса в Win9Х можно разделить на три раздела:

Младшие два гигабайта (00400000-7FFFFFFF) – код и данные пользовательского режима (в диапазоне 00000000-003FFFFF расположены разделы для выявления нулевых указателей и для совместимости с программами DOS и Win16);

Третий гигабайт – для общих файлов, проецируемых в память (MMF), и системных DLL.

Четвёртый гигабайт – для кода и данных режима ядра (здесь располагается ядро операционной системы и драйверы).

Старшие два гигабайта являются общими для всех процессов. Основные системные DLL – kernel32.dll, advAPI32.dll, user32.dll и GDI32.dll загружаются в третий гигабайт. По этой причине эти четыре библиотеки доступны всем процессам в системе. Поскольку этот гигабайт общий, они существуют во всех процессах по одним и тем же адресам. Из соображений безопасности Microsoft запретила запись в область, куда они загружаются. Если же запись туда всё же произвести (а это возможно из режима ядра или недокументированными методами), то изменения произойдут во всех процессах одновременно.

В WinNT общих разделов у процессов нет, хотя системные библиотеки по-прежнему во всех процессах загружаются по одинаковым адресам (но теперь уже в область кода и данных пользовательского режима). Запись в эту область разрешена, но у образов системных библиотек в памяти стоит атрибут «копирование при записи» (copy-on-write). По этой причине попытка записи, например, в образ kernel32.dll приведёт к появлению у процесса своей копии изменённой страницы kernel32.dll, а на остальных процессах это никак не отразится.

Все эти различия существенно влияют на способы реализации перехвата функций, расположенных в системных DLL.

Перехваты можно разделить на два типа: локальные (перехват в пределах одного процесса) и глобальные (в масштабах всей системы).

Локальный перехват

Локальный перехват с использованием раздела импорта

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

В разделе импорта каждого exe- или DLL-модуля содержится список всех используемых DLL. Кроме того, в нем перечислены все импортируемые функции. Вызывая импортируемую функцию, поток получает ее адрес фактически из раздела импорта. Поэтому, чтобы перехватить определенную функцию, надо лишь изменить её адрес в разделе импорта. Для того чтобы перехватить произвольную функцию в некотором процессе, необходимо поправить её адрес импорта во всех модулях процесса (так как процесс может вызывать эту функцию не только из exe-модуля, но и из DLL-модулей). Кроме того, процесс может воспользоваться для загрузки DLL функциями LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW или, если она уже загружена, определить её адрес при помощи функции GetProcAddress. Поэтому для перехвата любой API-функции необходимо перехватывать и все эти функции.

Существует несколько широко известных примеров реализации этого метода, в частности один из них описан в книге Джеффри Рихтера «Windows для профессионалов: создание эффективных Win32 приложений с учетом специфики 64-разрядной версии Windows» (Jeffrey Richter «Programming Applications for Microsoft Windows»), 4-е издание. Другой пример – библиотека APIHijack, написанная Wade Brainerd на основе DelayLoadProfileDLL.CPP (Matt Pietrek, MSJ, февраль 2000). Для описания этого метода я взял за основу пример Джеффри Рихтера (с небольшими изменениями).

Для реализации перехвата был создан класс CAPIHook, конструктор которого перехватывает заданную функцию в текущем процессе. Для этого он вызывает метод ReplaceIATEntryInAllMods, который, перечисляя все модули текущего процесса, вызывает для каждого метод ReplaceIATEntryInOneMod, в котором и реализуется поиск и замена адреса в таблице импорта для заданного модуля.

void CAPIHook::ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,

 PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller)

{

 //Получим адрес секции импорта

 ULONG ulSize;

 PIMAGE_IMPORT_DESCRIPTOR pImportDesc =

 (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(hmodCaller, TRUE,

 IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);

 if (pImportDesc == NULL)

 return; //Здесь её нет

 //Найдём нужный модуль

 for (; pImportDesc->Name; pImportDesc++)

 {

 PSTR pszModName = (PSTR)((PBYTE) hmodCaller + pImportDesc->Name);

 if (lstrcmpiA(pszModName, pszCalleeModName) == 0)

 {

  //Нашли

  if (pImportDesc->Name == 0)

  return; //Ни одна функция не импортируется

  //Получим адрес таблицы импорта

  PIMAGE_THUNK_DATA pThunk =

  (PIMAGE_THUNK_DATA)((PBYTE) hmodCaller + pImportDesc->FirstThunk);

  //Переберём все импортируемые функции

  for (; pThunk->u1.Function; pThunk++)

  {

  PROC* ppfn = (PROC*) &pThunk->u1.Function; //Получим адрес функции

  BOOL fFound = (*ppfn == pfnCurrent);  //Его ищем?

  if (!fFound && (*ppfn > sm_pvMaxAppAddr))

  {

   // Если не нашли, то поищем поглубже.

   // Если мы в Win98 под отладчиком, то

   // здесь может быть push с адресом нашей функции

   PBYTE pbInFunc = (PBYTE) *ppfn;

   if (pbInFunc[0] == cPushOpCode)

   {

   //Да, здесь PUSH

   ppfn = (PROC*) &pbInFunc[1];

   //Наш адрес?

   fFound = (*ppfn == pfnCurrent);

   }

  }

  if (fFound)

  {

   //Нашли!!!

   DWORD dwDummy;

   //Разрешим запись в эту страницу

   VirtualProtect(ppfn, sizeof(ppfn), PAGE_EXECUTE_READWRITE, &dwDummy);

   //Сменим адрес на свой

   WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew,

   sizeof(pfnNew), NULL);

   //Восстановим атрибуты

   VirtualProtect(ppfn, sizeof(ppfn), dwDummy , &dwDummy);

   //Готово!!!

   return;

  }

  }

 }

 }

 //Здесь этой функции не нашлось

}

При помощи функции ImageDirectoryEntryToData определяется дескриптор таблицы импорта и, если он есть, перебираются все DLL, из которых импортируются функции. Если DLL находится, то среди функций, импортируемых из неё, ищется нужная, а затем при помощи WriteProcessMemory её адрес меняется на адрес своего обработчика. Теперь он будет вызываться каждый раз, когда из данного модуля будет происходить обращение к перехваченной функции.

ПРИМЕЧАНИЕ

Если вы читали уже упоминаемую выше книгу Джеффри Рихтера, то могли заметить, что в функции ReplaceIATEntryInOneMod я сделал одно изменение. У него она работала так: в таблице импорта находился список функций того модуля, функция из которого импортировалась, и если в этом списке эта функция не находилась, то ReplaceIATEntryInOneMod больше ничего не делала (т. е. перехват не происходил). Я столкнулся с таким поведением, когда написал тестовую программу на Delphi для примера DriveType2 (этот пример описан ниже, в разделе «Глобальный перехват методом тотального локального перехвата», он перехватывает функцию GetDriveTypeA во всех приложениях с использованием описываемого метода). Тест, написанный на Visual C++, работал прекрасно – функция GetDriveTypeA перехватывалась. А вот программа на Delphi всё равно для всех перехватываемых мной дисков возвращала реальные значения. Я посмотрел таблицу импорта тестовой программы при помощи утилиты DUMPBIN и обнаружил, что компилятор Delphi не поместил все импортируемые функции из kernel32.dll в один список, а разбил их на 3 части, причём GetDriveTypeA оказалась в третьей. Поэтому функция ReplaceIATEntryInOneMod Джеффри Рихтера, просмотрев все функции из первого списка Kernel32.dll, не нашла функции GetDriveTypeA, хотя она и импортировалась модулем DriveTypeTest.exe. Я исправил эту функцию таким образом, чтобы она проверяла всю таблицу импорта и перебирала все списки с функциями из kernel32.dll (как оказалось, их может быть несколько). В описании формата РЕ-файла нигде не оговаривается, что каждый модуль, из которого импортируются функции, должен встречаться в секции импорта только один раз, и, видимо, некоторые компиляторы этим пользуются.

При реализации данного метода следует учитывать, что вызовы из DllMain библиотеки, в которой находится перехватываемая функция, перехватить не удастся. Это связано с тем, что перехват может быть осуществлён только по окончании выполнения LoadLibrary, а к этому времени DllMain уже будет вызвана. Конечно, можно написать свой вариант LoadLibrary (примеры загрузки DLL «вручную» существуют) и осуществлять перехват между загрузкой DLL и вызовом DllMain, но это сильно усложняет задачу.

Основным достоинством данного метода является то, что он одинаково реализуется как в Win9X, так и в WinNT.

ПРИМЕЧАНИЕ

В 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

//Этот макрос создаёт функцию-трамплин для функции 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 <windows.h>

#include <detours.h> //Подключим библиотеку 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 включают в себя встроенный табличный дизассемблер, который определяет, какое количество

Если Вам нужна помощь с академической работой (курсовая, контрольная, диплом, реферат и т.д.), обратитесь к нашим специалистам. Более 90000 специалистов готовы Вам помочь.
Бесплатные корректировки и доработки. Бесплатная оценка стоимости работы.

Поможем написать работу на аналогичную тему

Получить выполненную работу или консультацию специалиста по вашему учебному проекту
Нужна помощь в написании работы?
Мы - биржа профессиональных авторов (преподавателей и доцентов вузов). Пишем статьи РИНЦ, ВАК, Scopus. Помогаем в публикации. Правки вносим бесплатно.

Похожие рефераты: