Сrackme, прячущий код на API-функциях

       

Создаем jump на thunk


Написать перехватчик можно и на чистом Си, но настоящие хакеры так не поступают, да и не интересно это. Мы будем писать на ассемблере! Для упрощения интеграции перехватчика с кодом защищаемой программы используем ассемблерные вставки. Microsoft Visual C++ поддерживает спецификатор naked ("голый"), запрещающий компилятору самовольничать. "Обнаженные" функции не содержат ни пролога, ни эпилога, компилятор даже не вставляет ret— все это мы должны сделать самостоятельно. Если, конечно, захотим.

Лучшего средства для создания защитных механизмов, пожалуй, и не придумаешь! Подробное описание naked-функций можно найти в SDK (см. раздел "Naked Function Calls"), нам же достаточно знать, что naked-функции объявляются как __declspec(naked) function_name(), придерживаются cdecl-соглашения (аргументы передаются справа налево и удаляются из стека материнской функцией) и не формируют стековый фрейм, то есть смещения локальных переменных и аргументов нам придется рассчитывать самостоятельно.

Рисунок 2 голые функции на MSDN

Попробуем в качестве разминки создать процедуру, внедряющуюся в начало некоторой API-функции и передающую управление на thunk. Проще всего это сделать с помощью команды jmp thunk, однако, при этом значение thunk придется рассчитывать вручную, поскольку в x86 процессорах команда jmp ожидает не само смещение, а разницу между смещением целевого адреса и концом команды jmp. Вычислить-то его несложно, просто отнял/прибавил — и все, но… это же нужно высаживаться на самомодифицрующийся код, что не входит в наши планы. Лучше пойти другим путем, обратившись к конструкции mov reg32, offset thunk/jmp reg32. Это на один байт длиннее и к тому же портит регистр reg32, однако, это не страшно. Все API-функции придерживаются соглашения stdcall, то есть принимают аргументы через стек, а возвращают значение через регистровую пару [edx]:eax, то есть значение eax при входе в функцию не играет никакой роли и может быть безболезненно искажено.


А вот с "лишним" байтом все значительно сложнее. Некоторые (впрочем, очень немногочисленные функции) состоят из одного jmp xxx, следом за которым расположен другой jmp. Естественно, это не сами функции, это просто линкер сформировал таблицу переходов, но нам-то от этого не легче! К тому же, иногда встречаются функции короче пяти байт (например, GetCurrentProcess) и внедрить в них jmp (даже без mov) уже невозможно!

.text:77E956D7 ; HANDLE GetCurrentProcess(void)

.text:77E956D7 public GetCurrentProcess

.text:77E956D7 GetCurrentProcess  proc   near   ; CODE XREF: UnhandledExceptionF

.text:77E956D7 83 C8 FF           or     eax, 0FFFFFFFFh

.text:77E956DA C3                 retn

.text:77E956DA GetCurrentProcess  endp



.text:77E956DA

.text:77E956DB ; Exported entry 315. GetModuleHandleA

.text:77E956DB

.text:77E956DB ; HMODULE __stdcall GetModuleHandleA(LPCSTR lpModuleName)

.text:77E956DB public GetModuleHandleA

.text:77E956DB GetModuleHandleA   proc near     ; CODE XREF: .text:77E815D6^p

.text:77E956DB

.text:77E956DB lpModuleName       = dword ptr  8

.text:77E956DB

.text:77E956DB 55                 push ebp

.text:77E956DC 8B EC              mov ebp, esp

Листинг 1 API-функция GetCurrentProcess занимает всего 4 байта и внедрить в нее jump, не испортив начала следующей функции уже невозможно (на самом деле — можно, но сложно! в GetCurrrentProcess мы пишем push esp/push esp/push esp/push esp/push esp, а в GetModeleHandleA внедряем jump на sub_thunk, анализирующий что находится на вершине стека — если там четыре esp, то был вызван GetCurrentProcress, в противном случае это GetModuleHandleA)

Ладно, не будем высаживаться на измену. Все это заморочки. В общем случае, перехват работает вполне корректно и простейший перехватчик выглядит так:

#define JUMP_SZ      0x6           // размер jump

__declspec( naked

) jump()        // "голая" функция без пролога и эпилога

{

       __asm



       {

              mov eax, offset thunk      ; заносим в eax

смещение нашего thunk'а

              jmp eax                    ; передаем на него управление

       }

}

Листинг 2 код, внедряющийся в начало API-функции и передающий управление на thunk

Единственная проблема — как определить его длину? Должны же мы знать сколько байт копировать? Оператор sizeof возвращает размер указателя на функцию, но отнюдь не размер самой функции. Какие еще есть пути? Можно, например, определить размер jump'а вручную (в данном случае он равен 6 байтам) или расположить за его концом фиктивную процедуру fictonic(). Разница смещений fictonic() и jump() в общем случае и будет размером jump. Почему в общем случае? Да потому, что компилятор не подписывался всегда размещать функции в порядке их объявления (хотя чаще всего все происходит именно так). Но это еще что! Если откомпилировать программу с ключом /Zi (отладочная информация), компилятор будет возвращать совсем не адрес функции jump(), а указатель на "переходник", расположенный совсем в другом месте!

.text:00401019 loc_401019:                      ; DATA XREF: .text:loc_40106Dvo

.text:00401019                                         ; _main+39vo

.text:00401019                    jmp    _jump

.text:00401019 ; --------------------------------------------------------------------

.text:0040101E                    dd 8 dup(0CCCCCCCCh)

.text:0040103E                    align 10h

.text:00401040

.text:00401040 _thunk             proc near            ; CODE XREF: .text:loc_401005^j

.text:00401040                    ;

.text:0040106D                    ; [мыщъх поскипал]

.text:0040106D                    ;

.text:0040106D _thunk             endp;

.text:00401081; ---------------------------------------------------------------------

.text:00401081

.text:00401081 _jump       proc near            ; CODE XREF: .text:loc_401019^j

.text:00401081                    mov    eax, offset loc_401005

.text:00401086                    jmp    eax

.text:00401086 _jump       endp

.text:00401086

Листинг 3 при компиляции с ключом /Zi компилятор MS VC вместо указателя на саму функцию возвращает указатель на переходник, что есть бэд (правда, можно написать простейший анализатор, распознающий jmp и вычисляющий эффективный адрес функции, но это же лишний код!)

Переходник и указатель разделяют целых 68h байт, а в некоторых случаях и побольше. Кошмар! Даже если копировать с "запасом" мы все равно уйдем лесом и рухнем в прорубь. Тем не менее, без ключа /Zi все работает вполне нормально, а отлаживать программу можно и в машинных кодах.


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