Шприц для bsd или функции на игле

       

Перехват функций не во сне, а наяву


Самое сложное в перехвате— это определить границы машинных инструкций, поверх которых записывается команда перехода на перехватчик (он же 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). Хороший перехватчик должен их учитывать.

Рисунок 4 функция gettimer с прологом далеким от стандартного

Таким образом, наш перехватчик должен проверить первые 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);

Листинг 3 фрагмент программы-инсталлятора, анализирующей пролог перехватываемой функции и устанавливающей обработчик с соответствующим прологом

; // заносим номер "нашего" пролога в регистр 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

Листинг 4 базовый код перехватчика (расположенный в 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, а на верхушку стека предварительно положить адрес для перехода.

Вот, собственно говоря, и все. Скелет перехватчика успешно собран и готов к работе. Остается дописать "боевую начинку". Это может быть и логгер, протоколирующий вызовы, и анти-протектор, блокирующий вызовы некоторых функций (например, удаление файла), и макро-машина, "подсовывающая" функциям клавиатурного ввода готовые данные и… да все что угодно!


Содержание раздела