Перехват вызовов функций на 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)