Перехват вызова функций заменой байтов заголовка инструкциями JMP или CALL
В одной из предыдущих статей я описывал метод перехвата функций, используя таблицу импорта. Данный метод более универсален, так как дает возможность осуществлять перехват вызова функций, если известен только их адрес в памяти (ограничения приведены ниже). Однако он и более сложен, поскольку, как дальше будет показано, требует от кода модифицирования заголовка функции навыков дизассемблирования.
Суть метода в прописывании инструкции JMP или CALL в первые несколько (5 байт, если прописывается инструкция перехода или вызова с 32-битным смещением относительно EIP) байт заголовка функции с адресом функции перехвата.
Сразу видны ограничения метода. Модифицируемая функция (или функция плюс байты выравнивания после нее, если таковые имеются) должна быть длиннее прописываемого кода. Иначе будет испорчен код следующей функции, что может привести к краху приложения.
Оригинальная функция: 004013E0 55 push ebp 004013E1 8B EC mov ebp,esp 004013E3 81 EC F4 00 00 00 sub esp,0F4h 004013E9 53 push ebx 004013EA 56 push esi После модификации: 004013E0 E9 xx xx xx xx jmp HookFunction 004013E5 F4 00 00 00 db 0F4h, 0, 0, 0 ; Байты остались от ; частично перезаписанной ; инструкции "sub esp,0F4h" 004013E9 53 push ebx 004013EA 56 push esi
Если оригинальную функцию вызывать не нужно, то проблем нет. Просто нужно подготовить массив байт инструкций перехода, а далее, используя метод, который я описал в статье Самомодифицирующиеся программы, прописать их в заголовок функции. Если же вызывать оригинальную функцию необходимо, то все гораздо сложнее…
Почему? Да потому, что, поскольку мы перезаписываем первые байты заголовка функции, то для ее корректного вызова, не приводящего к аварийному завершению приложения, требуется создание заглушки для вызова (другое название — вентиль вызова: call valve). Вентиль вызова представляет собой копию первых инструкций заголовка оригинальной функции, которых коснется перезаписывание, а далее команда перехода на первую не испорченную инструкцию заголовка.
Вентиль вызова: XXXXXXX0 55 push ebp XXXXXXX1 8B EC mov ebp,esp XXXXXXX3 81 EC F4 00 00 00 sub esp,0F4h XXXXXXX9 E9 xx xx xx xx jmp 4013E9h Вызов оригинального обработчика: HookFunction(...) { ... PFNORIFN pfn = (PFNORIFN)0xXXXXXXX0; // Адрес вентиля вызова pfn(...); ... }
Если первые байты кода заголовка содержат инструкцию перехода относительно EIP, то смещения нужно пересчитать. Ну, а в случае, если использовалась команда близкого перехода, преобразовать инструкцию в команду дальнего перехода.
Оригинальная функция с командами перехода в заголовке: 77f6054e 39 c8 cmp eax,ecx 77f60550 74 02 je 77f60554h 77f60552 50 push eax 77f60553 52 push edx 77f60554 33 c0 xor eax,eax Вентиль вызова в этом случае: XXXXXXX0 39 c8 cmp eax,ecx XXXXXXX2 8B EC je XXXXXXXA XXXXXXX4 50 push eax XXXXXXX5 E9 xx xx xx xx jmp 77f60553h XXXXXXXA E9 xx xx xx xx jmp 77f60554h
Как видно из приведенных примеров, для создания вентиля вызова необходимо дизассемблирование «на лету» заголовка функции, а также определенные умения ассемблера для пересчета смещений и создания команд длинных переходов. Ну, естественно, это нужно, если не закладываться на некий неизменный вид заголовка. Как показывает опыт, закладываться на фиксированный вид заголовка весьма опасно. Рано или поздно код перестанет работать.
Сохранение значений регистров
Автор данной статьи писал код по перехвату этим методом вызовов функций драйверов ядра Windows 98 и Windows ME. Некоторые, думаю, еще помнят такие операционные системы, а, возможно, и знают, что драйвера для них писались на ассемблере и параметры передавались в регистрах. Напомнил я об этом, чтобы обратить внимание на то, что, если функция перехвата пишется на языке высокого уровня, оригинальную функцию нужно вызывать, и оригинальная функция чувствительна на входе к флагам процессора и содержимому регистров, которые выбранный язык высокого уровня считает «мусорными», то всего перечисленного не достаточно для корректной работы. Необходимо еще сохранять регистры и флаги на входе в новый обработчик, уметь их устанавливать в нужные значения перед вызовом оригинальной функции, а также перед возвратом в вызывающий код.
mov eax, 1 call IntercepringFunc // Вызов функции, которую мы перехватываем ... IntercepringFunc: jmp HookFunction ... HookFunction: сохранение регистров и флагов в некую структуру ... изменение значений в этой структуре, если необходимо .. вызов оригинальной функции через расширенный вентиль вызова (CallValveEx) ... восстановление регистров и флагов возврат в вызывающую функцию CallValveEx: восстановить регистры и флаги из данных структуры вызов оригинальной функции обновление значений регистров и флагов в данных возврат в HookFunction
Заключение
Полнофункциональная версия библиотеки для перехвата вызовов со встроенным дизассемблером для поддержки произвольных заголовков функций доступна для скачивания по запросу. Библиотека поддерживает как приложения пользовательского уровня, так и драйвера ядра системы.
Пример приложения, прилагаемый к статье, использует упрощенную схему метода модификации заголовка, который умеет работать только с ограниченным числом инструкций.
Добавить комментарий
Для отправки комментария вам необходимо авторизоваться.
Нет комментариев