Установка хука на вызов функции через таблицу импорта
При написании крупных проектов случаются ситуации, когда необходимо подправить работу одного или более компонентов сторонних производителей (например, библиотек в составе приложения). В этих случаях исходный код редко бывает доступен и приходится использовать подходы из арсенала хакера. В данной статье я собираюсь рассмотреть один из наиболее простых подходов в реализации перехвата вызова функций — перехват вызовов импортируемых функций.
Метод основан на том, что вызов из одного PE (Portable Executable) модуля другого является вызовом по адресу в таблице связи (IAT). Таблица заполняется фактическими адресами функций системным загрузчиком на стадии инициализации модуля. Таким образом, подмена адреса в этой таблице приводит к вызову нашего обработчика вместо оригинального до момента завершения процесса или выгрузки и повторной загрузки модуля. Вдобавок ко всему, немаловажен тот факт, что каждый PE модуль, вызывающий функции библиотеки A, имеет свою таблицу IAT, и, если мы модифицируем ее только для одного модуля — B — то все остальные (C, D, …) будут продолжать вызывать оригинальную функцию. Это делает метод установки хука селективным.
Итак, наша задача — реализовать набор функций, которые будут принимать в качестве параметров:
Имя модуля (B), вызовы из которого в модуль (A) мы собираемся перехватывать
Имя модуля (A), вызов функций которого мы собираемся перехватывать
Имя функции модуля (A) импортируемой модулем (B) для перехвата
Адрес нашего обработчика
Функция верхнего уровня по установке хука будет возвращать статус (удачно / не удачно) и адрес оригинального обработчика, поскольку в большинстве случаев хук процедура нуждается в его вызове.
bool PatchImportTable(LPCSTR lpszModule, // B LPCSTR lpszDLL, // A LPCSTR lpszEntry, // Имя импортируемой функции PVOID pHook, // Наш обработчик PVOID *ppOriFunc); // Сюда возвращаем оригинальный адрес
Первый этап — это определение адреса загрузки модуля B и поиск элемента в таблице IAT, соответствующего функции с именем lpszEntry модуля A. Второй этап — собственно модификация данных таблицы. При этом используется метод, описанный мной в одной из предыдущих статей (Самомодифицирующиеся программы).
Поиск модуля
Для определения адреса загрузки модуля проще всего использовать функции PSAPI
EnumProcessModules
GetModuleInformation
Первая из них строит список всех загруженных модулей для приложения. При помощи второй мы, собственно, получаем адрес загрузки интересующего нас модуля:
//----------------------------- Open process ------------------------------ HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId()); if (!hProcess) return false; //------------------------ Retrieve module (B) handle --------------------- DWORD cbNeeded; HMODULE hMods[1024]; if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { HMODULE hModule = NULL; for (ULONG uModule = 0; uModule < (cbNeeded / sizeof(HMODULE)); uModule++) { char szModName[MAX_PATH]; // Get the full path to the module's file. if (GetModuleFileNameExA(hProcess, hMods[uModule], szModName, sizeof(szModName))) { _strlwr(szModName); if (strstr(szModName, lpszModule)) { hModule = hMods[uModule]; break; } } } // -------------------- Get base address of module (B) ----------------------- if (hModule) { MODULEINFO mi; memset(&mi, 0, sizeof(mi)); BOOL f = GetModuleInformation(hProcess, hModule, &mi, sizeof(mi)); ... } }
IAT
Переходим к этапу поиска элемента таблицы IAT. Получаем адрес заголовка PE
PIMAGE_NT_HEADERS pPEHdr; // PE header address pPEHdr = GetNtHeader(pImage);
и смещений/размеров таблиц импорта и IAT (import address table):
ULONG uDirBaseRVA = GetDirBaseRVA(pPEHdr, IMAGE_DIRECTORY_ENTRY_IMPORT); ULONG uDirSize = GetDirSize (pPEHdr, IMAGE_DIRECTORY_ENTRY_IMPORT); ULONG uIATDirBase = GetDirBaseRVA(pPEHdr, IMAGE_DIRECTORY_ENTRY_IAT); ULONG uIATDirSize = GetDirSize (pPEHdr, IMAGE_DIRECTORY_ENTRY_IAT);
Далее находим IMAGE_IMPORT_DESCRIPTOR, соответствующий библиотеке A. Далее находим перебором импорт дескриптор (IMAGE_IMPORT_BY_NAME) для функции lpszEntry и соответствующий ему вход в IAT:
//------------ Look for descriptor which match to module name ---------------- PIMAGE_IMPORT_DESCRIPTOR pid; // Import directory pid = PIMAGE_IMPORT_DESCRIPTOR(uDirBaseRVA + uBase); for (; !pdwRet; pid++) { if (!pid->OriginalFirstThunk) break; // No more descriptors if (!_stricmp(LPCSTR(uBase + pid->Name), pszModuleName)) { //---------------------- Look for specified ordinal -------------------------- DWORD * pdwThunk = (DWORD *)(pid->OriginalFirstThunk + uBase); DWORD dwIdx = 0; for (; *pdwThunk; pdwThunk++, dwIdx++) { if (*pdwThunk & IMAGE_ORDINAL_FLAG) continue; PIMAGE_IMPORT_BY_NAME p = PIMAGE_IMPORT_BY_NAME(*pdwThunk + uBase); if (IsBadReadPtr(p, sizeof(IMAGE_IMPORT_BY_NAME))) break; if (!_stricmp((LPCSTR)p->Name, pszFunction)) { pdwThunk = (DWORD *)(pid->FirstThunk + uBase) + dwIdx; //------------- Check if Thunk is lying inside of IAT bounds ----------------- if ((DWORD)pdwThunk >= uIATDirBase + uBase && (DWORD)pdwThunk < uIATDirBase + uBase + uIATDirSize) { pdwRet = pdwThunk; } break; // Assumes, only one match } } } }
Если вход в таблицу IAT был найден, сохраняем оригинальный адрес и прописываем туда адрес хука.
Обращаю внимание, что число принимаемых параметров хук процедуры и ее тип вызова (calling convention) должны быть точно такими же, как у оригинальной процедуры. Иначе это приведет к неминуемому краху приложения.
Все перечисленное выше реализовано в тестовом приложении, которое вы можете скачать по ссылке внизу страницы. Приложение перехватывает вызовы функции MessageBoxA из основного модуля приложения (из EXE) и модифицирует флаги вызова. В результате этого окно сообщения появляется всегда c двумя кнопками (OK и Отмена), независимо от того, с какими флагами вызывается MessageBoxA.