Сообщений 2    Оценка 109        Оценить  
Система Orphus

Как подменить функцию API?

Автор: Павел Блудов

Версия текста: 1.2.1

Переопределение с помощью препроцессора
Модификация таблиц импорта/экспорта
Модификация самого обработчика
Модификация самого обработчика 2
Подмена с использованием оберток(wrappers)
Использованные статьи и литература


Приложение 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(). Что и требовалось.

Модификация самого обработчика 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;
}

Недостатком такого способа является его непредсказуемость. Кто угодно может зарегистрировать свой обработчик исключений и поломать нам логику. Более того, инструкции try catch(...)/__except(1), часто встречающиеся в программах, могут перехватить управление и не дать нашему обработчику шанса.

Подмена с использованием оберток(wrappers)

Еще один способ, о котором я хотел бы упомянуть. Он заключается в написании собственной динамической библиотеки с тем же именем и набором экспортируемых функций. Такая библиотека кладется на место оригинальной, запускается использующий эту библиотеку процесс, тот находит нашу DLL и ничего не подозревая ее использует. А если переименовать оригинальную DLL и положить рядом, то можно даже переадресовывать часть вызовов в оригинальную библиотеку, а часть оставлять себе. К сожалению, это будет работать с экспортируемыми функциями, но не с экспортируемыми переменными. Пример такого рода обертки можно найти у Алексея Остапенко.

А вот с COM-объектами и интерфейсами обертки работают как нельзя лучше. Для этого создается другой COM-объект, реализующий нужный нам интерфейс, к нему создатся аггрегированный оригинальный COM-объект, а перехватчик отдается тем кто с ним будет в дальнейшем работать. Если оригинальный COM-объект не поддерживает агрегацию, то придется реализовать все его интерфейсы, и если у него нету никаких внутренних (недокументированных) интерфейсов, то, возможно, все и заработает.


Все это касается подмены API текущего процесса. Если понадобится перехватить вызов в чужом процессе, то следует выбрать любой из приведенных здесь методов, поместить их в DLL и закинуть ее в чужой процесс. Это тема для отдельного разговора и здесь рассматриваться не будет.


Использованные статьи и литература


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 2    Оценка 109        Оценить