Методы перехвата 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;
}
}
}
}
//Здесь этой функции не нашлось
}
Похожие работы на - Методы перехвата API-вызовов в Win32
|