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

Универсальный перехватчик API-функций


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

q       сохраняем несколько байт от начала API функции, копируя их в buf (>= sizeof(jump));

q       в начало API-функции ставим jump на наш thunk;

q       в thunk'е: анализируем аргументы функции и вообще делаем все, что хотим;

q       в thunk'е: восстанавливаем начало API-функции, копируя buf в ее начало;

q       в thunk'е: вызываем восстановленную API-функцию call'ом с подменой адреса возврата;

q       в thunk'е: в начало API-функции вновь устанавливаем jump на thunk и выходим;

Все это умещается буквально в 5-10 строк Сишного кода и очень быстро программируется. Это ликвидирует проблему точек останова, поскольку они исполняются на своем "законном" месте, причем отладчик всплывает на оригинальной API-функции, а код перехватчика остается незамеченным. Причем, никакая часть кода не исполняется в области данных, что очень хорошо с точки зрения DEP. Необходимость определения границ машинных команд отпадает сама собой, поскольку перед выполнением API-функции мы возвращаем ее содержимое на место и скопированные байты автоматически "стыкуются" со своим хвостом. Короче, сплошные преимущества. Так не бывает. А недостатки где? А вот!

При перехвате интенсивно используемых функций наша программа будет слегка тормозить за счет постоянного вызова VirtualProtect(api_func, 0x1000, PAGE_READWRITE, &old)/VirtualProtect(api_func, 0x1000, old, &old_old), правда, можно присвоить началу функции атрибут PAGE_EXECUTE_READWRITE и при удалении thunk'а его не восстанавливать. Теоретически, это ослабит безопасность системы, поскольку наш процесс может пойти в разнос и что-то сюда записать, однако, эта угроза не настолько велика, чтобы принимать ее всерьез.


Многопоточность — вот главная проблема и самый страшный враг. Что произойдет, если в процессе модификации API-функции ее попытается исполнить другой поток? Переключение контекста может произойти в любой момент. Допустим, поток A выполнил инструкцию push ebp и только собирался приступить к mov ebp,esp как был прерван системным планировщиком Windows, переключившим контекст управления на поток B, устанавливающий thunk. Когда поток A возобновит свою работу, то команды mov ebp,esp там уже не окажется. Вместо нее будет торчать "хвост" от jump, попытка выполнения которого ничем хорошим не кончится. Крах будет происходить не только не многоЦПшных, но даже на одноЦПшных системах, пускай и с небольшой вероятностью. Аналогичная проблема имеется и у классических перехватчиков, но, поскольку они устанавливают thunk один-единственный раз, для них она не так актуальна. (внимание: имеется вполне осязаемая вероятность вызова API-функции посторонним потоком в момент, когда она будет восстановлена перехватчиком! В этом случае перехватчик упустит вызов, поэтому использовать данный алгоритм в "сторожевых" программах типа брандмауэра ни в коем случае нельзя).



Рисунок 1 последствия неблагоприятного переключения контекста на функции CreateProcessA — машинные инструкции POP ESP/SAR byte ptr DS:[EDX+75FF2C96],28 на самом деле представляют собой хвост команды JMP 10001000, передающей управление на thunk

Семафоры и interlock'и здесь не спасают и единственное, что в наших силах —проектировать программу так, чтобы в каждый момент времени одну и ту же функцию мог вызывать только один поток. Самое простое — создать однопоточное приложение или вызывать API-функции через "переходники" с блокировками, примеры которых можно найти в любой книге, посвященной программированию многоЦПшных систем.

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


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