Перехват API-вызовов с использованием раздела импорта
Данный способ API-пeрeхвата рeшает обе упомянутые мной проблемы, Он прост и довольно надежен Но для его понимания нужно иметь представление о том, как осуществляется динамическое связывание. В частности, Вы должны разбираться в структуре раздела импорта модуля. В главе 19 я достаточно подробно объяснил, как создается этот раздел и что в нем находится. Читая последующий материал, Вы всегда можете вернуться к этой главе.
Как Вам уже известно, в разделе импорта содержится список DLL, необходимых модулю для нормальной работы Кроме того, в нем перечислены все идентификаторы, которые модуль импортирует из каждой DLL. Вызывая импортируемую функцию, поток получает ее адрес фактически из раздела импорта.
Поэтому, чтобы перехватить определенную функцию, надо лишь изменить ее адрес в разделе импорта. Все! И никакой зависимости от процессорной платформы. А поскольку Вы ничего не меняете в коде функции, то и о синхронизации потоков можно не беспокоиться.
Вот функция, которая делает эту сказку былью. Она ищет в разделе импорта модуля ссылку на идентификатор по определенному адресу и, найдя ее, подменяет адрес соответствующего идентификатора.
void ReplaceIATEntryInOneMod(PCSTR pszCalleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller)
{
ULONG ulSize;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData(hmodCallor, TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
if (pImportDesc == NULL)
return,; // в этом модуле нет раздела импорта
// находим дескриптор раздела импорм со ссылками
// на функции DLL (вызываемого модуля)
for (; pImportDesc->Name; pImportDesc++)
{
PSTR pszModName = (PSiR) ((PBYFE) hmodCaller + pImportDcsc->Name);
if (lstrcmpiA(pszModName, pszCalleeModName) == 0)
break;
}
if (pImportDesc->Name == 0)
// этот модуль не импортирует никаких функций из данной DLL return;
// получаем таблицу адресов импорта (IAT) для функций DLL PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA) ((PBYTE} hirodCaller + pImportDesc->FirstThunk);
// заменяем адреса исходных функций адресами своих функций
for (; pThunk->u1.Function; pThunk++)
{
// получаем адрес адреса функции
PROC* ppfn = (PROC*) &pThunk->u1.Function;
// та ли это функция, которая нас итересует?
BOOL fFound = (*ppfn == pfnCurrent);
// см. текст программы-примера, в котором
// содержится трюковый код для Windows 98
if (fFound)
{
// адреса сходятся, изменяем адрес в разделе импорта
WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL );
return; // получилось, выходим
}
}
// если мы попали сюда, значит, в разделе импорта
// нет ссылки на нужную функцию
}
Чтобы понять, как вызывать эту функцию, представьте, что у нас есть модуль с именем DataBase.exe. Он вызывает ExitProcess из Kernel32.dll, но мы хотим, чтобы он обращался к MyExitProcess в нашем модуле DBExtend.dll, Для этого надо вызвать ReplaceMTEntryInOneMod следующим образом.
PROC pfnOrig = GctProcAddress(GetModuleHandle("Kernel32"), "ExitProcess");
HMODULE hmodCaller = GetModuleHandle("DataBase.exe");
void RoplaceIATEntryInOrioMod(
"Kernel32.dll", // модуль, содержащий ANSI-функцию
pfnOrig, // адрес исходной функции в вызываемой DLL
MyExitProcess, // адрес заменяющей функции
hmodCaller); // описатель модули, из которого надо вызывать новую функцию
Первое, что делает ReplacelATEntryInOneMod, - находит в модуле hmodCalIer раздел импорта. Для этого она вызывает ImageDirectoryEntryToData и передает ей IMAGE_ DlRECTORY_ENTRY_IMPORT. Если последняя функция возвращает NULL, значит, в модуле DataBase.exe такого раздела нет, и на этом все заканчивается
Если же в DataBase.exe раздел импорта присутствует, то ImageDirectoryEntryToData возвращает его адрес как укачатель типа PIMAGE_IMPORT_DESCRIPTOR. Тогда мы должны искать в разделе импорта DLL, содержащую требуемую импортируемую функцию В данном примере мы ищем идентификаторы, импортируемые из Kernel32.dll (имя которой указывается в первом параметре ReplacelATEntryInOneMod), В цикле for сканируются имена DLL.
Заметьте, что в разделах импорта все строки имеют формат ANSI (Unicode не применяется). Вот почему я вызываю функцию lstrcmpiA, а не макрос lstrcmpi.
Если программа не найдет никаких ссылок на идентификаторы в Kernel32 dll, то и в этом случае функция просто вернет управление и ничего делать не станет. А если такие ссылки есть, мы получим адрес массива структур IMAGE_THUNK_DATA, в котором содержится информация об импортируемых идентификаторах. Далее в списке из KerneI32.dll ведется поиск идентификатора с адресом, совпадающим с искомым. В данном случае мы ищем адрес, соответствующий адресу функции ExitProcess.
Если такого адреса нет, значит, данный модуль не импортирует нужный идентификатор, и ReplaceLWEntryInOneMod просто возвращает управление. Но если адрес обнаруживается, мы вызываем WriteProcessMemory, чтобы заменить его на адрес подставной функции Я применяю WriteProcessMemory, а не InterlockedExchangePomter, потому что она изменяет байты, не обращая внимания на тип защиты страницы памяти, в которой эти байты находятся Так, если страница имеет атрибут защиты PAGE_READONLY, вызов InterlockedExchangePointer приведет к нарушению доступа, а WriteProcessMemory сама модифицирует атрибуты защиты и без проблем выполнит свою задачу.
С этого момента любой поток, выполняющий код в модуле DataBase.exe, при обращении к ExitProcess будет вызывать нашу функцию. А из нее мы сможем легко получить адрес исходной функции ExitProcess в Kernel32.dll и при необходимости вызвать ее,
Обратите внимание, что ReplaceIATEntryInOneMod подменяет вызовы функций только в одном модуле. Если в его адресном пространстве присутствует другая DLL, использующая ExitProcess, она будет вызывать именно ExitProcess из Kernel32.dll.
Если Вы хотите перехватывать обращения к ExitProcess из всех модулей, Вам придстся вызывать ReplacelATEntryInOneMod для каждого модуля в адресном пространстве процесса. Я, кстати, написал еще одну функцию, ReplacelATEntryInAllMods. С помощыо Toolhelp-функций она перечисляет все модули, загруженные в адресное пространство процесса, и для каждого из них вызывает ReplatelATEritryInOneMod, передавая в качестве последнего параметра описатель соответствующего модуля.
Но и в этом случае могут быть проблемы Например, что получится, если после вызова ReplacelATEntrylnAlMods какой-нибудь поток вызовет LoadLibrary для загрузки
новой DLL? Если в только что загруженной DLL имеются вызовы ExitProcess, она будет обращаться не к Вашей функции, а к исходной. Для решения этой проблемы Вы должны перехватывать функции LoadLtbraryA, LoadLibraiyW, LoаdLibraryExA и LoadLibraryExW и вызывать Rер1асеlAТЕпtrу1пОпеMod для каждого загружаемого модуля.
И, наконец, есть еще одна проблема, связанная с GetProcAddress. Допустим, поток выполняет такой код
typedef int (WINAPI *PFNEXITPROCESS)(UINT uExitCode);
PFNEXITPROCESS pfnExitProcess = (PFNEXITPROCESS) GetProcAddress( GetModuleHandle("Kernel32"), "ExitProcess");
pfnExitProcess(0);
Этот код сообщает системе, что надо получить истинный адрес ExitProcess в Kernel32.dll, а затем сделать вызов по этому адресу. Данный код будет выполнен в обход Вашей подставной функции. Проблема решается перехватом обращений к GetProcAddress При cc вызове Вы должны возвращать адрес своей функции
В следующем разделе я покажу, как на практике реализовать перехват API-вызовов и решить все проблемы, связанные с использованием LoadLibrary и GetProcAddress.