Исправление ошибки в поддержке асинхронного COM при запуске приложения

Исправление ошибки в поддержке асинхронного COMВ статье Асинхронный COM под Windows Vista и Windows 7 была описана ошибка, которая появилась при выходе указанных операционных систем. Для разрабатываемого (а на тот момент уже и продававшегося) программного обеспечения надежная работа асинхронного COM была очень важна. Обращения в Microsoft с просьбой внести исправления ничем не закончились. Оставался только один вариант — самостоятельное исправление ошибки в поддержке асинхронного COM. При этом наиболее эффективным оказался способ внесения изменений в код ole32.dll «на лету» при старте наших приложений.

Технологии, необходимые для выполнения этого, я описал в статьях «Динамическая модификация кода» и «Перехват вызова функций заменой байтов заголовка». Проблема с внесением исправлений в функцию CAsyncUnknownMgr::IBegin_QueryMultipleInterfaces заключается в том, что она не экспортируема. Для поиска ее адреса внутри образа ole32.dll применили метод «поиск по шаблону«.

Поиск кода по шаблону

Идея метода проста. Необходимо сформировать массив байт кода (сигнатуру), длина которого достаточна для однозначного нахождения требуемого участка. Те байты сигнатуры, которые соответствуют смещениям и могут меняться при изменении базового адреса образа dll в памяти, помечаются как не используемые при сравнении.

Для поиска CAsyncUnknownMgr::IBegin_QueryMultipleInterfaces была сформирована следующая сигнатура:

WORD tmpltVistaIBeginMQI[] =
{
    0x008D, 0x004D, 0x00F8,                         // lea         ecx,[ebp-8]
    0x0051,                                         // push        ecx
    0x0057,                                         // push        edi
    0x006A, 0x000C,                                 // push        0Ch
    0x0033, 0x00C0,                                 // xor         eax,eax
    0x0050,                                         // push        eax
    0x0053,                                         // push        ebx
    0x00E8, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,         // call        __allmul (766143D3h)
    0x0052,                                         // push        edx
    0x0050,                                         // push        eax
    0x00E8, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,         // call        ULongLongToUInt (7661537Ah)
    0x003B, 0x00C7,                                 // cmp         eax,edi
    0x0050,                                         // push        eax
    0x007D, 0x0006,                                 // jge         CAsyncUnknownMgr::IBegin_QueryMultipleInterfaces+0A5h (7668340Ch)
    0x008B, 0x004E, 0x0028,                         // mov         ecx,dword ptr [esi+28h]
    0x0051,                                         // push        ecx
    0x00EB, 0x00D6,                                 // jmp         CAsyncUnknownMgr::IBegin_QueryMultipleInterfaces+7Bh (766833E2h)
    0x0057,                                         // push        edi
    0x00FF, 0x0035, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // push        dword ptr [g_hHeap (766EE304h)]
    0x00FF, 0x0015, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // call        dword ptr [pfnHeapAlloc (766EE8B0h)]
};

#define SIZE_OF_BEGINMQI_TEMPLATE \
 (sizeof(tmpltVistaIBeginMQI) / sizeof(tmpltVistaIBeginMQI[0]))

Для каждого байта кода, который мы хотим найти в образе, используется два байта. Первый содержит код инструкции. Если второй содержит 0xFF, сравнение в этой позиции не используется при поиске.

Поиск кода для внесения исправлений осуществляется строкой:

LPBYTE pFragment = 
  FindCodeByTemplate(
    PBYTE(mi.lpBaseOfDll) // базовый адрес ole32.dll
    , mi.SizeOfImage // размер образа ole32.dll в памяти
    , tmpltVistaIBeginMQI
    , SIZE_OF_BEGINMQI_TEMPLATE);

Реализацию FindCodeByTemplate можно посмотреть, скачав этот файл источников.

Внесение исправления

Для внесения исправления код функции оказался удачным. Так как фрагмент заканчивается инструкцией длинного вызова (far call), есть возможность перенаправить вызов на наш код с исправлениями (stub), перезаписав байты смещения. После выполнения выделения памяти код исправления может выполнить инструкцию ret и CAsyncUnknownMgr::IBegin_QueryMultipleInterfaces, ничего не заметив, будет продолжать работу. Но при этом размер выделенного блока будет корректный.

Код заглушки не сложен (полный код можно скачать по этой ссылке):

IBeginMQIStub PROC C
        push    dword ptr [ebp - 8]   // размер блока для выделения
        push    dword ptr [esp + 12]  // flags
        push    dword ptr [esp + 12]  // hHeap
        call    [pfnHeapAlloc]        // вызов функции выделения памяти
        ret     12                    // возврат в IBegin_QueryMultipleInterfaces
IBeginMQIStub ENDP

Ну и, собственно, код модификации байтов кода функции IBegin_QueryMultipleInterfaces, для передачи управления IBeginMQIStub:

PVOID *ppfnHeapAlloc = (PVOID *)(pFragment + SIZE_OF_BEGINMQI_TEMPLATE - 4);
ppfnHeapAlloc = (PVOID *)*ppfnHeapAlloc;
pfnHeapAlloc = *ppfnHeapAlloc;

BYTE  Code[6];
DWORD offs = 
  DWORD(LPBYTE(&IBeginMQIStub) - 
  (pFragment + SIZE_OF_BEGINMQI_TEMPLATE - 1));

Code[0] = 0xE8;            // Call near
*(DWORD *)&Code[1] = offs; // offs
Code[5] = 0x90;            // Nop

WriteProcessMemory(
  hProcess
  , pFragment + SIZE_OF_BEGINMQI_TEMPLATE - 6
  , Code, sizeof(Code)
  , NULL
);

Заключение

Проблему удалось решить достаточно просто, применив несколько из описанных мной в этой и предыдущих статьях методов. В некоторых случаях применение их оправдано. Более того, иногда и нет другого выхода. Но вот злоупотреблять модификацией кода не стоит. Ошибки в реализации stub могут оказаться сложными для отладки и опасными для устойчивости работы ваших приложений.

Tags:, ,

Добавить комментарий