Перехват вызовов функций на x86

From
Vladimir Ivanov ()
To
All
Date
2002-10-08T17:24:59Z
Area
SU.WINDOWS.NT.PROG
From: "Vladimir Ivanov" <vivanov@tmsoft-ltd.kiev.ua>

    Небольшое обобщение:

    В результате бесед о "самомодифицирующемся коде" была плотно затронута
тема перехвата вызовов функций.
    Рассматривался подход, основная суть которого заключается в выполнении
патча кода перехватываемой функции, с целью встраивания команды безусловного
перехода на замещаемую функцию. Было даже предложено готовое решение от
Microsoft: Detours (http://research.microsoft.com/sn/detours/).

    Отмечен общий недостаток - связанный со сложностью обеспечения
атомарноти операции патча (замены 5 байт) на x86 (Vasily Nikishaev).
Существует, хоть и крайне небольшая, вероятность прерывания потока в момент
патча, что может привести к катастрофическим последствиям. Так же проблемы
могут возникнуть в случае вызова перехватываемой функции из потока,
работающего параллельно на другом процессоре.  Вероятность очень маленькая,
однако с утверждением Василия: <при написании надежных программ нельзя
допускать даже ничтожную вероятность ошибки> я согласен безоговорочно.
    Справедливости ради, нужно отметить, что существуют другие подходы,
которые лишены этой проблемы, например способ с перестройкой таблиц импорта
и перехватом LoadLibrary/GetProcAddress, описанный у Джефри
Рихтера в 5-м издании "Windows для Профессионалов".

    Вернёмся к патчам:

    Рассматривалось, в общем-то, одна идея устранения проблемы: обеспечить
непрерываемость операции патча.
    Это можно сделать c помощью команды процессора CMPXCHG8B (Gennady
Mayko) -- наверное самое красивое решение -- но комманда поддерживается не
всеми процессорами.
    Можно также повысить приоритет потока который будет выполнять патч,
обеспечить большой квант времени (вариантов масса),: но делать, путь даже и
логичные, допущения о недокументированном поведении планировщика - тоже
рискованно.
    Можно также понизить в приоритете или <заморозить> на время другие
потоки  - но здесь еще больше проблем с атомарностью (Gennady Mayko).
    Можно исключить вообще побочное влияние других потоков, выполняя
перехват функций сразу после создания процесса (смотрите готовую реализацию
в Detours), или выполнять патч из другого процесса, присоединившегося к
данному процессу в качестве отладчика - но это хорошо лишь в частных
случаях.

    Теперь конкретное предложение:

    Что если не заботится о непрерываемости и пойти по пути остановки других
потоков, но не всех, а только тех, которые пытаются вызвать недопатченную
функцию ?
    Для этого, создадим глобальное (для процесса) событие:
hHookEvent = CreateEvent(NULL,FALSE,TRUE,NULL);
состояния:
   <non-signaled> - в данный момент производится патч какой-либо функции
   <signaled> - не производится.

Алгоритм функции перехвата примерно следующий:
void DoHook(:)
{
   EnterCriticalSection(...); //  сериализуем перехваты
   ResetEvent(hHookEvent);

   int oldPriority = GetThreadPriority(GetCurrentThread()); // запоминаем
приоритет
   SetThreadPriority(GetCurrentThread(),THREAD_PRIORITY_TIME_CRITICAL);
    // повышаем приоритет
   Sleep(1); // По выходу из <сна> будем в начале кванта времени
   // Далее сохраняем где-то в глобальной переменной адрес первого байта
функции
   // и заменяем _первый_ байт фукции на 0xCC (Breakpoint на x86)
   //: ****

   // Заменяем оставшиеся байты
   //: даже если поток в это время прервётся то ничего страшного не случится

   //  !!!!
   // Заменяем первый байт функции на необходимое значение (код инструкции
JUMP)
   // :
   FlushInstructionCache();
   SetThreadPriority(GetCurrentThread(),oldPriority);

   SetEvent(hHookEvent);
   LeaveCriticalSection(...);
}

    Если вдруг поток прервется между *** и !!! и другим потоком будет
вызвана перехватываемая функция - последний наткнётся на breakpoint. Тоже
случится и при вызове функции в этот момент из потока, работающего на
параллельном процессоре.

    Осталось обработать breakpoint, для чего устанавливаем свои функции
обработки first-chance exception (это делается путём перехвата
KiUserExceptionDispatcher из ntdll и еще чего-то, подробнее смотрите в
примерах к Detours).

Функция обработки breakpoint будет выглядеть примерно след. образом

LONG WINAPI BreakPointFilter(PEXCEPTION_POINTERS pException)
{
            PEXCEPTION_RECORD pExceptRec = pException->ExceptionRecord;
            if (pExceptRec->ExceptionCode == EXCEPTION_BREAKPOINT
                     && /*проверка соответствует ли бреакпойнт сохранённому
адресу*/
               )   {
            WaitForSingleObject(hHookEvent); // засыпаем до момента
окончания
            // патча вызванной функции

                                    return EXCEPTION_CONTINUE_EXECUTION;
                                    // возвращаемся к выполнению функции
                        }
            }

            return EXCEPTION_CONTINUE_SEARCH; // работаем "по-умолчанию"
}

    Реальные накладные расходы (попадание на breakpoint, ожидание события),
могут возникнуть лишь в единичных случаях, вероятность которых ничтожно
мала.

P.S. Всё равно остался узкий момент в моменте перехвата
KiUserExceptionDispatcher, но эта функция вызывается в случае исключений,
которые, в общем-то случаются в исключительных случаях :). IMHO, этим можно
пожертвовать.

Жду конструктивной критики.

С уважением,
Владимир Иванов



--- ifmail v.2.15dev5
 * Origin: A poorly-installed InterNetNews site (2:5020/400)