Перехват функций не во сне, а наяву
Самое сложное в перехвате — это определить границы машинных инструкций, поверх которых записывается команда перехода на перехватчик (он же thunk, расположенный в нашем случае в теле функции gets). По-хорошему, для решения этой задачи требуется написать мини-дизассемблер, но… это же сколько всего писать придется! А можно ли без него обойтись? Можно!
В начале большинства библиотечных функций расположен стандартный пролог вида PUSH EBP/MOV EBP,ESP/SUB ESP,XXXh (55h/89h E5h/ 83h ECh XXh), дающий нам пять байт — необходимый минимум для внедрения! Встречаются и другие, слегка видоизмененные прологи, например: PUSH EBP/MOV EBP,ESP/PUSH EDI/PUSH ESI (55h/89h E5h/ 57h/ 56h); PUSH EBP/MOV EAX, 0FFFFFFFFh/MOV EBP, ESP (55h/B8h FFh FFh FFh FFh/89h E5h); PUSH EBP/XOR EAX, EAX/MOV EBP,ESP (55h/31h C0h/89h E5h). Хороший перехватчик должен их учитывать.
Таким образом, наш перехватчик должен проверить первые 5 байтов перехватываемой функции и, если они совпадают со стандартным (или слегка оптимизированным) прологом, скопировать этот пролог в свое тело и выполнить его перед передачей управления оригинальной функции. А куда его можно скопировать? Сегмент данных, как уже говорилось, нам недоступен, стек трогать нельзя (перед передачей управления на функции он должен быть восстановлен), а сегмент кода запрещен от модификации.
Существует по меньше мере три решения: во-первых, мы можем вызывать функцию mprotect, присвоив кодовой странице атрибут writable, (но это некрасиво), во-вторых, трогать стек все-таки можно: забрасываем пролог на верхушку, забрасываем туда же копию всех аргументов (а сколько у функции аргументов? да хрен его знает, вот и приходится копировать с запасом) и передаем ей управление как ни в чем не бывало (но это уже не просто "некрасиво", это вообще уродство). В-третьих, мы можем поступить так:
// "коллекция" разнообразных прологов для сравнения
unsigned char prolog_1[]={0x55h,0x89,0xE5,0x83,0xEC};
unsigned char prolog_2[]={0x55,0x89,0xE5,0x57,0x56};
// буфер в который будет записан сгенерированный код
unsigned char buf_code[1024];
// определяем адрес перехватываемой функции
p = msym(base, fnc_name);
// если в начале перехватываемой функции расположен prolog_1
// внедряем в ее начало call на prepare_prolog_1
if (!memcmp(p,prolog_1,sizeof(prolog_1))
call_r(base, fnc_name, "gets", 0);
// если в начале перехватываемой функции расположен prolog_2
// внедряем в ее начало call на prepare_prolog_2
if (!memcmp(p,prolog_1,sizeof(prolog_2))
call_r(base,fnc_name,"gets", offset prapare_prolog_2-offset prepare_prolog_1);
Листинг 10 фрагмент программы-инсталлятора, анализирующей пролог перехватываемой функции и устанавливающей обработчик с соответствующим прологом
; // заносим номер "нашего" пролога в регистр EAX,
; // чтобы перехватчик знал какой ему пролог эмулировать
; // ВНИМАНИЕ! этот код засирает EAX
и не работает на fastcall-функциях,
; // для поддержки которых регистры трогать нельзя, а номер пролога класть на стек,
; // восстанавливая его перед передачей управления оригинальной функции
prepare_prolog_1:
MOV EAX, 0x1
JMP short do_begin
prepare_prolog_2:
MOV EAX, 0x2
JMP short do_begin
prepare_prolog_n:
MOV EAX, 0x2
JMP do_begin
do_begin:
// ОСНОВНОЙ КОД ПЕРЕХВАТЧИКА
// ДЕЛАЕМ ЧТО ЗАДУМАНО
// [ESP+4]+5 содержит адрес вызванной функции
// это поможет нам отличить перехваченные функции друг от друга
…
…
…
// ПЕРЕДАЧА УПРАВЛЕНИЯ ПЕРЕХВАЧЕННОЙ ФУНКЦИИ
// С ЭМУЛЯЦИЕЙ ЕЕ "РОДНОГО" ПРОЛОГА
DEC EAX
JZ prolog_1
DEC EAX
JZ prolog_2
…
prolog_1: ; // эмулируем выполнение пролога типа PUSH EBP/MOV EBP,ESP/SUB ESP,XXX
PUSH EBP
MOV EBP,ESP
SUB ESP, byte ptr [EAX] ; берем XXh из памяти
INC EAX ; на след. машинную команду
JMP EAX
prolog_2: ;// эмулируем
выполнение пролога
типа PUSB EBP/MOV EBP,ESP/PUSH EDI/PUSH ESI
PUSH EBP
MOV EBP, ESP
PUSH EDI
PUSH ESI
JMP EAX
Листинг 11 базовый код перехватчика (расположенный в gets), поддерживающий несколько различных прологом
Программа- инсталлятор анализирует пролог перехватываемой функции и, в зависимости от результата, внедряет в ее начало либо call prepare_prolog_1 либо call prepare_prolog_2, где prepare_prolog_x – метка, расположенная внутри thunk-кода, помещенного нами в функцию gets. Команда call занимает 5 байт и потому в аккурат накладывается на команду SUB ESP,XXh так, что XXh оказывается прямо за ее концом. Поэтому, сохранять XXh в теле самого перехватчика не нужно!!! Команда SUB ESP, byte ptr [EAX], вызываемая из thunk-кода эмулирует выполнение SUB ESP,XXh на ура!
Приведенный пример портит регистр EAX и работает только с cdecl и stdcall функциями. Перехват fastcall-функции, передающих аргументы через EAX, по этой схеме невозможен. Однако, оригинальный EAX можно сохранять в стеке и восстанавливать непосредственно перед передачей управления перехваченной функции, но в этом случае JMP EAX придется заменить на RETN, а на верхушку стека предварительно положить адрес для перехода.
Вот, собственно говоря, и все. Скелет перехватчика успешно собран и готов к работе. Остается дописать "боевую начинку". Это может быть и логгер, протоколирующий вызовы, и анти-протектор, блокирующий вызовы некоторых функций (например, удаление файла), и макро-машина, "подсовывающая" функциям клавиатурного ввода готовые данные и… да все что угодно!