Сергей Холодилов
Я открываю свойства растений и трав..
Борис Гребенщиков
Словосочетанием «API Spying» называется слежение за вызовами функций API некоторым приложением. То есть, каждый факт вызова этим приложением выбранных функций каким-то образом фиксируется, например, добавляется запись в лог.
ПРИМЕЧАНИЕДля ясности назовём «некоторое приложение» исследуемым приложением, а «выбранные функции» – отслеживаемыми функциями. |
API Spying может использоваться на одном из этапов исследования программы, логику работы которой вы пока не до конца понимаете. Хотя эта технология и не позволяет получить детальную информацию, она может значительно сузить область последующих этапов исследования, сконцентрировав ваше внимание на тех вызовах, которые происходят в ключевые моменты работы программы.
На первый взгляд может показаться, что задача лучше решается с помощью перехвата API, так как он даёт возможность не только отследить вызов, но и изучить/изменить параметры и возвращаемое значение, или даже полностью переписать функцию.
Действительно, перехват API – замечательная и часто упоминаемая техника (на данный момент на RSDN этой теме посвящены три статьи), позволяющая довольно глубоко изучить исследуемое приложение, но это и гораздо более трудоёмкое решение. Даже если реализации функций будут почти пустыми (только запись в лог и вызов оригинальной функции), ваш код будет примерно таким:
typedef int (__stdcall* Function1_type)(int i);Function_type _Function1;// Обёртка, логирующаявызовыint __stdcall MyFunction1(int i){ printf("MyFunction1\n"); return _Function(i); // Вызоворигинальнойфункции}...// Перехватвсехфункцийvoid HookThemAll(){ ... // Перехват функции _Function1, экспортируемой some.dllHookIt("some.dll", "_Function1@4", MyFunction1, &_Function1);...} | ||
ПРИМЕЧАНИЕЭто приблизительный код, используемый при перехвате через таблицу импорта; другие варианты перехвата в данном случае не имеют существенных преимуществ. |
То есть, для каждой функции придётся:
определить тип;
определить переменную;
написать обёртку;
добавить строчку в HookThemAll.
Это, конечно, довольно простые операции… Но представьте, что таким образом вам нужно перехватить несколько сотен функций. А если не у всех функций известны прототипы? А если некоторые dll загружается динамически, и вы пока даже не знаете, какие их функции используются приложением? А если после того, как вы всё успешно перехватите и просмотрите получившиеся логи, станет понятно, что для детального понимания работы приложения нужно было перехватить всего две функции и изучить их параметры :) ?
Когда все эти вопросы встали передо мной, я занялся API Spying-ом.
API Spying не исключает перехвата API, но эти методики используются находятся на разных стадиях анализа программы. Сначала при помощи API Spying-а определяется несколько наиболее интересных функций, потом, если необходимо, эти функции перехватываются и изучаются «в более тесном контакте».
В самом общем виде задача выглядит так:
Необходимо получать информацию о фактах вызова выбранных функций исследуемым приложением.
Для получения статистики не обязательно заранее знать имена функций, которые будут вызываться приложением. Тем более, не нужно знать их прототипы.
Сбор статистики для любого (в том числе заранее неизвестного) количества функций из любых (в том числе, из загружаемых динамически) модулей не должен представлять трудностей.
Работоспособность исследуемого приложения не должна нарушаться.
Логически разовьём требования, высказанные в постановке задачи:
Мы рассматриваем только программные реализации. Это значит, что статистика собирается программно, и этим занимается наш код.
Поскольку при каждом вызове отслеживаемых функций управление должно передаваться нашему коду, все отслеживаемые функции нужно перехватить (тем или иным способом; подробное рассмотрение способов перехвата API выходит за рамки статьи). Нашу функцию, которой в результате перехвата будет передаваться управление, назовём функцией-шпионом (терминология моя, сожалею, если не прав).
Чтобы вести статистику вызовов и не нарушать работу приложения, функция-шпион должна определить, какую именно отслеживаемую функцию собиралось вызвать исследуемое приложение. Единственный способ реализовать это – сопоставить каждой отслеживаемой функции свою функцию-шпиона, «знающую», как минимум, адрес оригинала.
По возможности, присутствие функции-шпиона не должно влиять на выполнение отслеживаемой функции. Это получается не всегда, разумные исключения описаны ниже, в разделе «Почему приложение может перестать работать».
Так как количество отслеживаемых функций может быть велико или заранее не известно, функции-шпионы должны генерироваться автоматически в процессе исполнения.
Несколько дополнительных пожеланий:
Автоматически генерировать сложные функции-шпионы непросто. :) Их даже писать на ассемблере замаешься... Хорошо бы подсчёт статистики взял на себя кто-то другой.
ПРИМЕЧАНИЕВы классно знаете ассемблер, и считаете, что это пара пустяков? Возможно, вы не учли, что код функций будет расположен в произвольном месте адресного пространства и что (забегая вперёд; но вы-то это всё должны понимать) функции не могут модифицировать стек и регистры. Если и это для вас не проблема, то, во-первых, примите моё искреннее восхищение (без шуток!), во-вторых, прочитайте следующий пункт. :) |
Автоматическая генерация подразумевает выделение памяти для кода функций, а, так как их может быть много, желательно чтобы функции были короткими. Поэтому, опять же, хорошо бы подсчёт статистики взял на себя кто-то другой.
И несколько ограничений:
Эта реализация поддерживает только Intel x86-совместимые процессоры.
Для работы функции-шпиона от ОС требуется только одно: она должна позволять исполнять динамически сгенерированный код. Это условие соблюдается во всех версиях Windows и, скорее всего, в подавляющем большинстве остальных ОС общего пользования. Но, поскольку нам необходим ещё и способ перехвата, ограничимся линейкой Windows NT/2000/XP. Используя другие способы перехвата, можно реализовать API Spying для других ОС.
Неизвестно, как на исполнение сгенерированного кода будут реагировать антивирусы. Возможно, они будут недостаточно толерантны. :)
СОВЕТО подобных ограничениях лучше не забывать и в реальных проектах, так как иначе выполнить ТЗ будет практически невозможно. |
Почему приложение может перестать работать
Проблема заключается в том, что (увы!) статистика собирается не магически, её собирает наш код, внедрённый в исследуемое приложение. У этого простого факта есть три неприятных следствия:
При добавлении сбора статистики изменится скорость работы функций. Обычно это ни на что не влияет, но если в середине критичного к скорости выполнения кода мы неожиданно (для приложения и его автора) начнём запись в файл, может получиться плохо. Например, FPS упадёт раз в десять :) Но FPS – это не страшно, страшно, если вы исследуете многопоточное приложение c некорректно написанной синхронизацией, и изменение времени выполнения потоков приведет к дедлокам, падениям или просто непонятному поведению.
Помимо процессорного времени, наш код использует и другие ресурсы: память, стек, (возможно) окна, объекты ядра (файлы, события, и т.п.) и другие. В некотором фантастическом случае это может стать последней каплей, приводящей к исчерпанию доступных ресурсов потоком, процессом, или даже системой.
Если автор приложения решил позаботиться о защите своего детища, он вполне в состоянии засечь наши манипуляции, обидеться (он же не знает, что мы ничего плохого не хотели) и сделать какую-нибудь бяку. Например, откажется работать, или станет работать неправильно, или отформатирует случайно выбранный диск…
Все эти следствия в той или иной степени свойственны любой программной реализации API Spying-а, и ни в одной из этих ситуаций я не могу посоветовать вам ничего хорошего. Можно только попытаться уменьшить степень влияния и избежать столь пагубных последствий.
Предпроектные исследования: функции в Intel x86
Как вы уже, наверное, поняли, нам предстоит динамическая генерация кода функций-шпионов. Хотя ничего особо сложного в этом не будет (они действительно очень простые), небольшое теоретическое введение поможет вам понять (а мне – объяснить), как должна быть написана функция-шпион, чтобы вызов отслеживаемой функции завершился без помех.
С точки зрения процессора вызов функции выполняет инструкция call, имеющая несколько разных форм:
call xxxxxxh call xxxxh:xxxxxxh call eaxcall [eax] ... |
Она сохраняет в стеке адрес, по которому нужно передать управление после окончания функции, и передаёт управление на начало функции.
Процессор Intel x86 ничего не знает о параметрах вызываемых функций, поэтому механизм передачи параметров может быть произвольным, главное чтобы вызывающий и вызываемый код договорились о нём заранее. Мест, где можно сохранить параметры, не так уж и много: либо в регистрах, либо в стеке, либо часть там, а часть там.
ПРИМЕЧАНИЕКонечно, можно передавать параметры по ссылке или по значению, в прямом порядке или в обратном, но это для нас не важно, важно только то, где передаваемая информация (параметры или их адреса) находится. |
Передача параметров через регистры используется в основном в двух случаях:
Компилятором для оптимизации.
Ассемблер-программистом из лени или в погоне за производительностью. Чтобы достать параметры из стека, надо написать несколько дополнительных команд, а в регистрах они сразу под рукой.