Программирование на Visual C++. Архив рассылки - Алекс Jenter
Шрифт:
Интервал:
Закладка:
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(). Что и требовалось.
Модификация самого обработчика 2Довольно оригинальный вариант предыдущего способа был предложен Дмитрием Крупорницким: первая инструкция перехватываемой функции заменяется инструкцией прерывания 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;
}
Недостатком такого способа является его непредсказуемость. Кто угодно может зарегистрировать свой обработчик исключений и поломать нам логику. Более того, инструкции (…)/(), часто встречающиеся в программах, могут перехватить управление и не дать нашему обработчику шанса.
Подмена с использованием оберток(wrappers)Еще один способ, о котором я хотел бы упомянуть. Он заключается в написании собственной динамической библиотеки с тем же именем и набором экспортимых функций. Такая библиотека кладется на место ориганальной, запускается использующий эту библиотеку процесс, тот находит нашу DLL и ничего не подозревая ее использует. А если переименовать оригинальную DLL и положить рядом, то можно даже переадресовывать часть вызовов в оригинальную библиотеку, а часть оставлять себе. К сожалению, это будет работать с экспортируемыми функциями, но не с экспортируемыми переменными. Пример такого рода обертки можно найти у Алексея Остапенко.
А вот с COM-объектами и интерфейсами обертки работают как нельзя лучше. Для этого создается другой COM-объект, реализующий нужный нам интерфейс, к нему создатся аггрегированный оригинальный com-объект, а перехватчик отдается тем кто с ним будет в дальнейшем работать. Если оригинальный COM-объект не поддерживает аггрегацию, то придется реализовать все его интерфейсы, и если у него нету никаких внутренних (недокументированных) интерфейсов, то, возможно, все и заработает.
Все это касается подмены API текущего процесса. Если понадобится перехватить вызов в чужом процессе, то следует выбрать любой из приведенных здесь методов, поместить их в DLL и закинуть ее в чужой процесс. Это тема для отдельного разговора и здесь рассматриваться не будет.
Использованные статьи и литератураФорматы PE и COFF файлов
Расширение MSGINA – это просто
API Spying Techniques for Windows 9x, NT and 2000
APIHijack
EliCZ ApiHooks
Контроль вызова API функций в среде систем Windows '95, Windows '98 и Windows NT
Это все на сегодня. Пока!
Алекс Jenter [email protected] Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.Программирование на Visual C++