Приложение HookAPI
Демонстрационное приложение (WTL Dialog) HookAPI (100kb) Требует наличия звуковой карты. Методы 3, 4 и 5 не будут работать под windows9x/ME.
Приложение HookAPI2
Демонстрационное приложение (WTL Dialog) HookAPI2 (20kb) Требует наличия WinSockets 1.0.
#include <windows.h> WINUSERAPI BOOL WINAPI MyMessageBeep(IN UINT uType) { //Your code here } #define MessageBeep MyMessageBeep |
Теперь если в коде программы встретится MessageBeep препроцессор заменит ее на нашу MyMessageBeep. Очень просто.
Но что если хочется добавить немного своей логики в уже откомпилированный код, изменить работу чужой библиотеки, пересобрать которую нет никакой возможности? Иными словами, заставить уже откомпилированный код вызвать нашу функцию вместо стандартной. Это вполне реально. Давайте поближе рассмотрим, как под windows процедуры одного модуля используют процедуры другого.
Весь API, доступный из какого-либо модуля, описан в так называемой таблице экспорта этого модуля. С другой стороны, список API, необходимый для нормальной работы опять-таки, любого модуля, находится в его таблице импорта.
Код вызова процедуры из другого модуля выглядит примерно так:
call dword ptr [__imp__MessageBeep@4 (004404cc)] |
И, если изменить значение по этому адресу, можно подменить оригинальную функцию своей. Для этого нам понадобится:
HRESULT ApiHijackImports( HMODULE hModule, LPSTR szVictim, LPSTR szEntry, LPVOID pHijacker, LPVOID *ppOrig ) { // Check args if (::IsBadStringPtrA(szVictim, -1) || (!IS_INTRESOURCE(szEntry) && ::IsBadStringPtrA(szEntry, -1)) || ::IsBadCodePtr(FARPROC(pHijacker))) { return E_INVALIDARG; } PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule); if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) || IMAGE_DOS_SIGNATURE != pDosHeader->e_magic) { return E_INVALIDARG; } PIMAGE_NT_HEADERS pNTHeaders = MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew); if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) || IMAGE_NT_SIGNATURE != pNTHeaders->Signature) { return E_INVALIDARG; } HRESULT hr = E_UNEXPECTED; // Locate the victim IMAGE_DATA_DIRECTORY& impDir = pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; PIMAGE_IMPORT_DESCRIPTOR pImpDesc = MakePtr(PIMAGE_IMPORT_DESCRIPTOR, hModule, impDir.VirtualAddress), pEnd = pImpDesc + impDir.Size / sizeof(IMAGE_IMPORT_DESCRIPTOR) - 1; while(pImpDesc < pEnd) { if (0 == ::lstrcmpiA(MakePtr(LPSTR, hModule, pImpDesc->Name), szVictim)) { if (0 == pImpDesc->OriginalFirstThunk) { // no import names table return E_UNEXPECTED; } // Locate the entry PIMAGE_THUNK_DATA pNamesTable = MakePtr(PIMAGE_THUNK_DATA, hModule, pImpDesc->OriginalFirstThunk); if (IS_INTRESOURCE(szEntry)) { // By ordinal while(pNamesTable->u1.AddressOfData) { if (IMAGE_SNAP_BY_ORDINAL(pNamesTable->u1.Ordinal) && WORD(szEntry) == IMAGE_ORDINAL(pNamesTable->u1.Ordinal)) { hr = S_OK; break; } pNamesTable++; } } else { // By name while(pNamesTable->u1.AddressOfData) { if (!IMAGE_SNAP_BY_ORDINAL(pNamesTable->u1.Ordinal)) { PIMAGE_IMPORT_BY_NAME pName = MakePtr(PIMAGE_IMPORT_BY_NAME, hModule, pNamesTable->u1.AddressOfData); if (0 == ::lstrcmpiA(LPSTR(pName->Name), szEntry)) { hr = S_OK; break; } } pNamesTable++; } } if (SUCCEEDED(hr)) { // Get address LPVOID *pProc = MakePtr(LPVOID *, pNamesTable, pImpDesc->FirstThunk - pImpDesc->OriginalFirstThunk); // Save original handler if (ppOrig) *ppOrig = *pProc; // write to write-protected memory return WriteProtectedMemory(pProc, &pHijacker, sizeof(LPVOID)); } break; } pImpDesc++; } return hr; } HRESULT WriteProtectedMemory(LPVOID pDest, LPCVOID pSrc, DWORD dwSize) { // Make it writable DWORD dwOldProtect = 0; if (::VirtualProtect(pDest, dwSize, PAGE_READWRITE, &dwOldProtect)) { ::MoveMemory(pDest, pSrc, dwSize); // Restore protection ::VirtualProtect(pDest, dwSize, dwOldProtect, &dwOldProtect); return S_OK; } return HRESULT_FROM_WIN32(GetLastError()); } |
Впрочем, такой способ не будет работать если используется позднее связывание (delay load) или связывание во время исполнения (run-time load) с помощью ::GetProcAddress(). Это можно побороть если перехватить саму ::GetProcAddress(), и подменять возвращаемое значение при необходимости. А можно и подправить таблицу экспорта аналогичным способом:
HRESULT ApiHijackExports( HMODULE hModule, LPSTR szEntry, LPVOID pHijacker, LPVOID *ppOrig ) { // Check args if ((!IS_INTRESOURCE(szEntry) && ::IsBadStringPtrA(szEntry, -1)) || ::IsBadCodePtr(FARPROC(pHijacker))) { return E_INVALIDARG; } PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule); if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) || IMAGE_DOS_SIGNATURE != pDosHeader->e_magic) { return E_INVALIDARG; } PIMAGE_NT_HEADERS pNTHeaders = MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew); if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) || IMAGE_NT_SIGNATURE != pNTHeaders->Signature) { return E_INVALIDARG; } HRESULT hr = E_UNEXPECTED; IMAGE_DATA_DIRECTORY& expDir = pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; PIMAGE_EXPORT_DIRECTORY pExpDir = MakePtr(PIMAGE_EXPORT_DIRECTORY, hModule, expDir.VirtualAddress); LPDWORD pdwAddrs = MakePtr(LPDWORD, hModule, pExpDir->AddressOfFunctions); LPWORD pdwOrd = MakePtr(LPWORD, hModule, pExpDir->AddressOfNameOrdinals); DWORD dwAddrIndex = -1; if (IS_INTRESOURCE(szEntry)) { // By ordinal dwAddrIndex = WORD(szEntry) - pExpDir->Base; hr = S_OK; } else { // By name LPDWORD pdwNames = MakePtr(LPDWORD, hModule, pExpDir->AddressOfNames); for (DWORD iName = 0; iName < pExpDir->NumberOfNames; iName++) { if (0 == ::lstrcmpiA(MakePtr(LPSTR, hModule, pdwNames[iName]), szEntry)) { dwAddrIndex = pdwOrd[iName]; hr = S_OK; break; } } } if (SUCCEEDED(hr)) { if (pdwAddrs[dwAddrIndex] >= expDir.VirtualAddress && pdwAddrs[dwAddrIndex] < expDir.VirtualAddress + expDir.Size) { // We have a redirection LPSTR azRedir = MakePtr(LPSTR, hModule, pdwAddrs[dwAddrIndex]); ATLASSERT(!IsBadStringPtrA(azRedir, -1)); LPSTR azDot = strchr(azRedir, '.'); int nLen = azDot - azRedir; LPSTR azModule = (LPSTR)alloca(nLen); memcpy(azModule, azRedir, nLen); azModule[nLen] = '\x0'; // Try to patch redirected function return ApiHijackExports( ::GetModuleHandle(azModule), azDot + 1, pHijacker, ppOrig); } if (ppOrig) *ppOrig = MakePtr(LPVOID, hModule, pdwAddrs[dwAddrIndex]); DWORD dwOffset = DWORD_PTR(pHijacker) - DWORD_PTR(hModule); // write to write-protected memory hr = WriteProtectedMemory(pdwAddrs + dwAddrIndex, &dwOffset, sizeof(LPVOID)); } return hr; } |
Имейте в виду, под Windows9x нельзя честно подменить экспорты для разделяемых библиотек, таких как user32.dll, kernel32.dll и gdi32.dll. Это связано с тем, что область памяти начиная с адреса 7FC00000h и выше совместно используется всеми процессами в системе, и модификация сказалась бы на каждом из них. А это нежелательно, поскольку память, занимаемая нашей функцией-перехватчиком, наоборот, принадлежит только нашему процессу. Во всех остальных процессах в системе ::GetProcAddress(), после подмены таблицы экспорта, вернула бы неправильный указатель. Тем не менее, если нельзя, но очень хочется, то можно. Для этого нам придется вручную создать новый дескриптор в GDT (вот тут-то у Windows9x проблем не возникает) и используя этот дескриптор произвести необходимые изменения. Но будьте готовы к тому, что понадобится написать свою разделяемую библиотеку, установить ее в системе и проверять ID процесса при каждом обращении. Рабочий пример есть на internals.com.
Эти два способа работают в 99% случаев. Последний процент - это подмена функции, вызываемой внутри чужого модуля, т.е. когда и вызываемая и вызывающая процедура находятся в одном и том же, да к тому же чужом, модуле. В этом случае, вызов будет сделан напрямик, а не через таблицы импорта/экспорта. Тут уже ничего сделать нельзя. Почти. Можно изменить саму функцию-обработчик, с тем чтобы перенаправить вызовы в нашу собственную. Делается это довольно просто: в начало исходного обработчика прописывается команда безусловного перехода на нашу процедуру, а если нужно вызвать оригинал, то нужно просто сохранить первые 5 байт затертых командой перехода, добавить после них опять-таки команду безусловного перехода на изначальный код +5 байт. Разумеется, эти пять байт кода не должны содержать команд перехода или вызова. Кроме того, может понадобиться больше чем 5 байт, ведь команда перехода посреди длинной инструкции работать не будет. Это случается крайне редко. Обычно код функции, как его генерирует компилятор для I86 выглядит примерно так: инициализация стека, загрузка в регистры параметров функции, их проверка и переход в случае неудовлетворительных результатов. Этого вполне хватает чтобы вставить наш маленький перехватчик. Но бывает и так:
CSomeClass::Release: FF152410E475 call dword ptr [InterlockedDecrement] 85C0 test eax,eax |
Или даже
CSomeClass::NonImplemented: C20400 ret 4 |
Что, впрочем, можно распознать и вернуть код ошибки если инструкции ret, jmp или call встретится слишком рано. Но вот такой случай распознать не получится:
SomeFunction: 33C0 xor eax,eax SomeFunction2: 55 push ebp 8BEC mov ebp,esp |
Иными словами, модификация SomeFunction приведет к неизвестным изменениям в SomeFunction2, и, возможно, краху всей системы.
Все это сильно усложняет нам задачу. Нужно дизассемблировать эти байты и проверить каждую инструкцию. Чтобы немного облегчить нам жизнь, фирма Майкрософт разработала специальный SDK для такого рода трюков: Microsoft Detours. С этим SDK задача подмены чужой функции реализуется удивительно просто:
#include <detours.h>
DetourFunction(PBYTE(::MessageBeep), PBYTE(MyMessageBeep));
|
После чего все вызовы ::MessageBeep(), откуда бы они не были произведены, окажутся вызовами нашей MyMessageBeep(). Что и требовалось.
Довольно оригинальный вариант предыдущего способа был предложен Дмитрием Крупорницким: первая инструкция перехватываемой функции заменяется инструкцией прерывания INT 3. Далее процедура обработки необработанных исключений (unhandled exception handler) подменяет регистр EIP на адрес нашей функции-перехватчика.
static DWORD_PTR m_dwFunction; static LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo) { if (pExceptionInfo->ContextRecord->Eip != m_dwFunction) return EXCEPTION_CONTINUE_SEARCH; // Continue execution from MyMessageBeep pExceptionInfo->ContextRecord->Eip = (DWORD_PTR)MyMessageBeep; return EXCEPTION_CONTINUE_EXECUTION; } LRESULT CMainDlg::OnMethod5(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { m_dwFunction = (DWORD_PTR)::GetProcAddress(::GetModuleHandle("USER32.dll"), "MessageBeep"); BYTE nSavedByte = *(LPBYTE)m_dwFunction; LPTOP_LEVEL_EXCEPTION_FILTER pOldFilter = ::SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); const BYTE nInt3 = 0xCC; // Inject int 3 HRESULT hr = WriteProtectedMemory(LPVOID(m_dwFunction), &nInt3, sizeof(const BYTE)); if (SUCCEEDED(hr)) { ::MessageBeep(m_uType); // Restore function hr = WriteProtectedMemory(LPVOID(m_dwFunction), &nSavedByte, sizeof(BYTE)); } ::SetUnhandledExceptionFilter(pOldFilter); return 0; } |
Недостатком такого способа является его непредсказуемость. Кто угодно может зарегистрировать свой обработчик исключений и поломать нам логику. Более того, инструкции try catch(...)/__except(1), часто встречающиеся в программах, могут перехватить управление и не дать нашему обработчику шанса.
Еще один способ, о котором я хотел бы упомянуть. Он заключается в написании собственной динамической библиотеки с тем же именем и набором экспортируемых функций. Такая библиотека кладется на место оригинальной, запускается использующий эту библиотеку процесс, тот находит нашу DLL и ничего не подозревая ее использует. А если переименовать оригинальную DLL и положить рядом, то можно даже переадресовывать часть вызовов в оригинальную библиотеку, а часть оставлять себе. К сожалению, это будет работать с экспортируемыми функциями, но не с экспортируемыми переменными. Пример такого рода обертки можно найти у Алексея Остапенко.
А вот с COM-объектами и интерфейсами обертки работают как нельзя лучше. Для этого создается другой COM-объект, реализующий нужный нам интерфейс, к нему создатся аггрегированный оригинальный COM-объект, а перехватчик отдается тем кто с ним будет в дальнейшем работать. Если оригинальный COM-объект не поддерживает агрегацию, то придется реализовать все его интерфейсы, и если у него нету никаких внутренних (недокументированных) интерфейсов, то, возможно, все и заработает.
Все это касается подмены API текущего процесса. Если понадобится перехватить вызов в чужом процессе, то следует выбрать любой из приведенных здесь методов, поместить их в DLL и закинуть ее в чужой процесс. Это тема для отдельного разговора и здесь рассматриваться не будет.