Реферат: API Spying
Название: API Spying Раздел: Рефераты по информатике, программированию Тип: реферат | |||||||||||||||||||||||||||||||||||
Сергей Холодилов Я открываю свойства растений и трав.. Борис Гребенщиков Словосочетанием «API Spying» называется слежение за вызовами функций API некоторым приложением. То есть, каждый факт вызова этим приложением выбранных функций каким-то образом фиксируется, например, добавляется запись в лог.
API Spying может использоваться на одном из этапов исследования программы, логику работы которой вы пока не до конца понимаете. Хотя эта технология и не позволяет получить детальную информацию, она может значительно сузить область последующих этапов исследования, сконцентрировав ваше внимание на тех вызовах, которые происходят в ключевые моменты работы программы. На первый взгляд может показаться, что задача лучше решается с помощью перехвата API, так как он даёт возможность не только отследить вызов, но и изучить/изменить параметры и возвращаемое значение, или даже полностью переписать функцию. Действительно, перехват API – замечательная и часто упоминаемая техника (на данный момент на RSDN этой теме посвящены три статьи), позволяющая довольно глубоко изучить исследуемое приложение, но это и гораздо более трудоёмкое решение. Даже если реализации функций будут почти пустыми (только запись в лог и вызов оригинальной функции), ваш код будет примерно таким:
То есть, для каждой функции придётся: определить тип; определить переменную; написать обёртку; добавить строчку в 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, имеющая несколько разных форм:
Она сохраняет в стеке адрес, по которому нужно передать управление после окончания функции, и передаёт управление на начало функции. Процессор Intel x86 ничего не знает о параметрах вызываемых функций, поэтому механизм передачи параметров может быть произвольным, главное чтобы вызывающий и вызываемый код договорились о нём заранее. Мест, где можно сохранить параметры, не так уж и много: либо в регистрах, либо в стеке, либо часть там, а часть там.
Передача параметров через регистры используется в основном в двух случаях: Компилятором для оптимизации. Ассемблер-программистом из лени или в погоне за производительностью. Чтобы достать параметры из стека, надо написать несколько дополнительных команд, а в регистрах они сразу под рукой. В большинстве остальных случаев параметры передаются через стек. При этом вызов функции выглядит примерно так:
А стек к моменту начала выполнения функции – так: Рисунок 1. Состояние стека в начале выполнения функции. Возврат управления производит инструкция ret, имеющая четыре различные формы:
Задача, выполняемая ret*: Удалить из стека адрес возврата. (опционально) Удалить из стека указанное количество байт. Передать управление по адресу возврата. При этом все версии ret* предполагают, что адрес возврата находится на вершине стека, а байты, которые надо удалить (если надо) – сразу за ним. Поскольку, как и при вызове, процессор ничего не знает о параметрах, удалять их из стека при возврате или нет – личное дело функции и вызывающего её кода. Распространены оба варианта: согласно формату вызова функций __cdecl за очистку стека отвечает вызывающий код, а согласно формату __stdcall этим занимается сама функция.
Как и в случае с параметрами, про возвращаемые значения процессор тоже ничего не знает, и то, как именно и что именно вы будете возвращать, его не касается. Обычно возвращаемое значение передаётся через регистр eax или через пару eax:edx. Состояние регистров до и после вызова И этот вопрос остаётся полностью на совести программиста (в случае языка высокого уровня – программиста, писавшего компилятор). Если верить статье «Arguments Passing and Naming Conventions» в MSDN, для всех стандартных форматов вызова функций компилятор гарантирует сохранность регистров ESI, EDI, EBX и EBP. Это значит, что вызывающий код: Может рассчитывать на то, что эти регистры не поменяются. Не должен рассчитывать на регистры EAX, ECX, EDX, EFLAGS (с ним немного сложнее, очевидно, часть флагов всё-таки должна остаться неизменной, просто MSDN об этом не упоминает), а также на регистры MMX, FPU, XMM.
Система в целом состоит из четырёх частей: Функция-шпион. Механизм установки шпионов. Функция сбора статистики. Механизм сбора и отображения статистики. Задачи Задачи работы функции-шпиона: Вызвать функцию сбора статистики, каким-то образом сообщив ей, какая отслеживаемая функция вызывается. Вызвать отслеживаемую функцию. Ограничения Ограничения связаны с тем, что отслеживаемая функция должна работать без изменений. Для этого перед её вызовом: Необходимо привести стек в то же состояние, которое было до начала работы функции-шпиона. Это значит, что, во-первых, нельзя сохранить в стеке какое-нибудь значение для использования после возврата из отслеживаемой функции, во-вторых, нельзя использовать для вызова инструкцию call, так как она добавит в стек адрес возврата (на эту тему см. ниже, в разделе «Получение управления после возврата из отслеживаемой функции»). Поскольку в принципе параметры могут передаваться и в регистрах, желательно привести регистры в то же состояние, которое было до начала работы функции-шпиона, или хотя бы в максимально близкое. Код, который надо сгенерировать Так как код функции-шпиона может располагаться в памяти по произвольному адресу, при вызове из неё функций необходимо либо использовать абсолютную адресацию, либо при генерации вычислять их адреса для каждой новой функции-шпиона. Оба подхода одинаково просто реализуются, но из-за особенности системы команд Intel x86 ближний вызов/передача управления по абсолютному адресу будет выглядеть примерно так:
То есть, как ни старайся, а значение одного регистра (в данном примере регистра eax, но на его месте мог быть каждый) сохранить не удаётся. Поэтому выбрана версия с относительной адресацией:
Поскольку эта функция-шпион заканчивается непосредственным вызовом отслеживаемой функции, она может совместно работать только с методами перехвата, не изменяющими код перехватываемой функции. Это: перехват через таблицу импорта; перехват через таблицу экспорта; перехват GetProcAddress и подмена адреса запрашиваемой функции. Если вы используете другой метод перехвата (например, замену нескольких начальных байтов на команду jmp), вам придётся немного изменить мой код. Получение управления после возврата из отслеживаемой функции Если по каким-то причинам вам очень нужно получить возвращаемое значение отслеживаемой функции, или вы хотите измерить время её выполнения, или что-то ещё, недоступное моему пониманию, вы всё-таки можете написать функцию-шпион так, чтобы она использовала call для вызова отслеживаемой функции и получала управления после её завершения. Для этого нужно: Удалить из стека старый адрес возврата.
Где-то сохранить его на время вызова отслеживаемой функции. Вызвать функцию. Получить/измерить/.. то, что вы хотели. Вернуть управление по старому адресу. Ключевым вопросом этого алгоритма является: «где же это где-то, в котором можно сохранить адрес возврата?» Стек менять нельзя, поэтому он отпадает. Хранить в регистрах тоже нельзя: те регистры, которые могут измениться после вызова функции, может изменить отслеживаемая функция, и данные пропадут, а те регистры, которые не должны меняться после вызова, нельзя менять нам, так как восстановить их мы не сумеем – негде сохранить их старые значения :) Остаётся только хранение в глобальной области памяти. Так как приложение может быть многопоточным, доступ к памяти нужно синхронизировать, и отдельно хранить данные для каждого потока. Так как возможна рекурсия, необходимо хранить не один адрес возврата, а стек адресов… И, несмотря на все эти предосторожности, что будет, если в отслеживаемой функции произойдёт исключение и начнётся развёртывание стека? Правильно, будет очень плохо… В общем, это путь для людей, крепких духом и готовых к испытаниям. Далее в статье он не рассматривается. Алгоритм установки одной функции-шпиона: Генерируется функция-шпион, при генерации устанавливается её номер, адрес отслеживаемой функции и адрес функции сбора статистики. Перехватывается отслеживаемая функция, теперь вместо неё приложением должна вызываться функция-шпион. Где-то сохраняется информация, о том, что перехвачена функция с таким-то именем и ей сопоставлен такой-то номер. Эта информация будет использована при вызове функции сбора статистики. Очевидно, что этот алгоритм никак не зависит от прототипа/формата вызова/.. отслеживаемой функции, и может быть без изменений применён для любого количества функций. Тем не менее, рассмотрим два случая. Отслеживание вызовов функций динамически загружаемых dll Это самое простое. Поскольку адреса таких функций приложение получает через GetProcAddress, достаточно просто перехватить GetProcAddress и производить описанную выше процедуру для всех запрашиваемых функций. Отслеживание всех вызовов Общая идея: пройтись по таблицам импорта загруженных модулей и, не особо задумываясь, перехватить все упомянутые там функции. Кроме того, нужно позаботиться о GetProcAddress (см. предыдущий пункт) и о ещё не загруженных модулях: их таблицы импорта тоже необходимо обработать. Чтобы не пропустить появление новых модулей, можно, например, перехватить все версии LoadLibrary[Ex]A/W. Просто, правда? Просто, но, к сожалению, в таком виде работать, скорее всего, не будет.
Проблема этого подхода заключается в почти гарантированном возникновении бесконечной рекурсии. Например, пусть collectStatistic записывает данные в файл при помощи функции WriteFile. Если эта функция оказалась перехвачена и в вашем модуле, то попытка записи приведёт к вызову вашей функции-шпиона, которая вызовет collectStatistic и т.д. пока не кончится место в стеке. Ладно, вы поняли свою ошибку и больше не меняете таблицу импорта своего модуля. Но дело в том, что для реализации WriteFile kernel32.dll вызывает функцию NtWriteFile из ntdll.dll. А, поскольку таблицу импорта kernel32.dll вы изменили, опять вызывается функция-шпион, которая вызывает colleclStatistic и всё начинается заново. Отсюда вывод: при проведении перехвата необходимо пропустить модули, которые вы сами прямо или косвенно используете. Идеально было бы менять таблицы импорта только в «нестандартных» модулях, так как, скорее всего, именно это вам и нужно: вряд ли вас интересует, какие функции ntdll.dll вызываются во время вызова WriteFile, обычно достаточно просто знать, что приложение вызвало WriteFile. Определять нестандартные модули можно разными способами, мне пришли в голову следующие: По каталогу, в котором лежит файл. По дате создания файла (системные файлы обычно имеют вполне определённые даты создания). По фиксированному списку имён. Кроме того, всегда есть радикальное решение: написать графический интерфейс и взвалить эту задачу на пользователя. :) В соответствии с тем, как она используется функциями-шпионами, функция сбора статистики должна иметь следующие характеристики: Принимает один четырёхбайтный параметр, передаваемый через стек. Не возвращает значение (во всяком случае, оно игнорируется). Сама очищает стек. Очевидно, как-то собирает какую-то статистику. Как именно и какую, пока не важно. На C++ это реализуется примерно так:
В этом примере статистическая информация состоит из имени функции и количества вызовов, всё это хранится в массиве functions, отображением статистики занимается само исследуемое приложение. Механизм сбора и отображения статистики Что собирать Потенциально, функция сборки статистики может для каждого вызова сохранять следующие параметры: Имя функции. Имя модуля. Имя модуля, из которого произошёл вызов. Идентификатор текущего потока.Время вызова. Дамп стека. Состояние регистров процессора и так далее. В общем, уровень детализации может быть очень разным и зависит от задачи. Политика отображения Два принципиально разных подхода: Данные доступны в реальном времени (посредством какого-нибудь GUI). Данные доступны после завершения исследуемого приложения (в файле на диске). Оба подхода имеют свои плюсы и минусы: с точки зрения получения данных, очевидно, что первый обладает всеми возможностями второго (если уж данные отображаются, параллельно сохранять их в лог не проблема), а, с точки зрения влияния на исследуемое приложение, второй может получиться гораздо мягче, и в какой-то ситуации это может оказаться критичным. Кроме того, второй подход может оказаться значительно проще в реализации.
Но, поскольку первый подход гораздо эффектнее (real-time, on-line, и даже мультимедиа, если постараться, – все эти слова можно обоснованно употребить в пресс-релизе :) ), далее рассматривается в основном он. Где хранить и как отображать статистику Есть три варианта реализации «сбора и отображения»: Данные хранятся и отображаются dll, внедрённой в исследуемое приложение. Данные хранятся dll, внедрённой в исследуемое приложение, для отображения она пересылает их внешнему приложению. И хранением, и отображением занимается внешнее приложение, dll просто пересылает ему данные по мере поступления. Наиболее интересен последний вариант (рассматриваем отображение в реальном времени), так как за счёт выноса части логики во внешнее приложение dll получается относительно простой, в результате чего снижается риск случайно испортить что-нибудь в исследуемом приложении, упрощается отладка и повышается надёжность системы в целом. Ограничимся простым случаем: Отслеживаем только вызовы функций, адреса которых исследуемое приложение получает через GetProcAddress. Сохраняем только имена функций и модулей. Отображаем данные в реальном времени. В качестве GUI выступает консоль. :) Данные хранятся и отображаются во внешнем приложении. Основную работу по генерации выполняют следующие несложные классы:
С их помощью можно определить классы для команд процессора, а из них уже собрать функцию. Например, так:
Как пользоваться получившимся в итоге классом spy_function, продемонстрировано ниже. Не содержит в себе ничего сложного. Работает по алгоритму установки одной функции-шпиона, в качестве сохранения информации о перехваченной функции сообщает внешнему приложению имя функции и получает в ответ соответствующий этой функции номер.
Поскольку данных мало и посылать их несложно, функция collectStatistic получилась просто замечательная:
И тем и другим занимается внешнее приложение. Реализовано всё крайне незамысловато:
Внедрение в приложение и перехват GetProcAddress Так как эта статья не посвящена ни перехвату, ни внедрению (на эти темы есть много других хороших статей), для реализации выбраны простые, но радикальные средства. Внедрение сделано через CreateRemoteThread, а перехват GetProcAddress – заменой её первых пяти байт на команду jmp. Для передачи внедрённой dll описателя окна, которому она должна посылать сообщения (g_hSecretWindow в примере), использована техника из статьи «HOWTO: Вызов функции в другом процессе». Будет завершение процесса. Как известно, во время завершения процесса все dll выгружаются, и вся выделенная память освобождается. При этом могут произойти следующие неприятности: Наша dll будет выгружена раньше времени. Раньше времени будет освобождена память, в которой расположены сгенерированные функции. В обоих случаях исследуемое приложение получит Access Violation, после чего говорить о том, что его работа не нарушена, будет достаточно сложно. Невыгружаемая dll Поскольку у нашей dll счётчик ссылок всегда больше 0 (LoadLibrary была вызвана, а FreeLibrary нет), она выгружается одной из последних, но в некоторых случаях этого может оказаться недостаточно. Радикальным решением проблемы является «ручная» загрузка dll, описанная в статье Максима М. Гумерова «Загрузчик PE-файлов». Это довольно трудоёмкий, но зато практически гарантированный вариант. Другим возможным решением (для NT/2000/…) может быть удаление dll из списка загруженных модулей в PEB, но как это сделать и будет ли это работать, я пока не знаю… Последняя идея, пришедшая мне в голову: честно загрузить dll в процесс, позволить загрузчику выполнить свою работу скопировать получившийся образ выгрузить dll записать в то же место адресного пространства образ dll. молиться. Это один из самых «грязных хаков», которые я когда-либо проворачивал :) Иногда оно работает, иногда – нет. И даже если всё на первый взгляд работает, я не берусь сказать, какие будут побочные эффекты. Подводя итог: если задача и имеет хорошее решение, его описание выходит далеко за рамки этой статьи. Поэтому наша dll будет выгружаться, хотя иногда это и может привести к проблемам. Неосвобождаемая память С памятью проще: чтобы её точно никто не освободил, достаточно отказаться от стандартного оператора new, и использовать вместо него placement new, выделяя память как-нибудь иначе.
Yes! Оно работает!! :)
Для успешного старта надо положить spyloader.exe и apispy.dll в один каталог, после чего запустить spyloader, передав ему в командной строке путь к exe-файлу исследуемого приложения. Только приготовьтесь к тому, что GetProcAddress – довольно популярная функция, и получить сотню функций-шпионов (то есть вызовов GetProcAddress) при исследовании notepad.exe – не вопрос, достаточно попытаться открыть какой-нибудь файл. А уж если вы запустите справку и немного по ней походите… У меня получилось 530 функций-шпионов за две минуты :) Поэтому, если вы действительно будете реализовывать нечто подобное, то лучше фиксировать не всё подряд, а фильтровать вызовы хотя бы по имени модуля. Список литературы Тихомиров В.А. «Перехват API-функций в Windows NT/2000/XP». Игорь Филимонов «Методы перехвата API-вызовов в Win32» Intel Corporation «IA-32 Intel Architecture Software Developer’s Manual», части 2A и 2B Максим М. Гумеров «Загрузчик PE-файлов» Сергей Холодилов «HOWTO: Вызов функции в другом процессе» |