Контроль вызова API функций в среде систем Windows '95, Windows '98 и Windows NT
Данная статья является очень вольным переводом статьи "API Spying Techniques For Windows 95, 98, NT" написанной Yariv Kaplan. Я постарался максимально полно описать способы анализа вызовов функций в среде Windows, применив при этом, как основу, вышеупомянутую статью и, частично, личный опыт.
Итак, утилиты анализа вызовов API функций являются самыми мощными при написании целого ряда программ, таких как Firewalls, отладчики, и т.д. Однако ни DDK ни SDK не обеспечивает тот минимум информации, который необходим для написания такого рода программ.
Существует несколько подходов для написания программ с "перехватом" API функций - локальный (он используется для контроля за исполнением только одной программы) и глобальный (для контроля над всей системой и вызовами функций из всех запущенных/запускаемых программ). Выбор способа стоит за вами. Первый подход более характерен для отладчиков, анализаторов деятельности программ, и всех остальных "отладочных" средств. Второй способ имеет более широкое применение. В этом случае вы получаете контроль над всей системой и полный контроль над теми функциями, которые вам необходимы.
Предположим, что вы хотите написать программу, блокирующую запуск некоторой программы. Очевидно, что блокировать запуск вам необходимо до того момента, как система завершит свою работу или пока программа не будет завершена через предусмотренный вами (или может нет) способ завершения программы. В этом случае, вам необходимо перехватить функции CreateProcess (причём обе - и ANSI и Unicode). Когда бы ни пыталась система запустить новый процесс (== программу), то вызов будет произведён и ваша процедура обработки получит управление.
Давайте же рассмотрим способы перехвата вызовов.
Замена оригинальной DLL библиотеки.
Самый простой способ перехвата вызовов, однако не всё так просто. Суть состоит в том, что вы создаёте DLL файл, который содержит процедуру обработки каждой функции из заменяемой вами DLL. В этом и состоит вся сложность. К примеру, заменяя DLL которая экспортирует, к примеру, 300 функций, то вам придётся писать код для каждой функции, если даже вам она и не требуется для задачи. А ведь не все функции документированы в полной мере.
Принцип работы: вы при помощи программ отладки получаете список экспортируемых функций искомой DLL; переименовываете исходную DLL; создаёте DLL файл, в котором после обработки вызова вашей процедурой, вызов переадресовывается оригинальной DLL. После чего кладёте вашу DLL в каталог с таким же именем, что было у исходной. При следующем же вызове функции ваша процедура обработки получит контроль, обработает его, и переадресует исходной. Перехват возникает сам, вследствие того, что имя вашей DLL будет совпадать с именем исходной.
Замена вызовов функций в теле программы
Очень непрактичный способ, однако я его опишу. При вызове функции, обычно существует 2 этапа: вызов в программе и исполнение. А что если вызов в программе будет заменён на вызов другой процедуры (например вашей)? Всё верно, ваша процедура получит управление, обработает его, и вернет управление (если надо) оригинальной функции. Сложность состоит в том, что придётся очень сильно постараться и писать ещё и дизассемблер для получения точки вызова функции. И при том надо знать заранее, какие программы необходимо изменять, что делает чести способу.
Изменение таблицы IAT (Import Address Table)
Данный способ является самым практичным и наиболее простым (с моей точки зрения) для реализации. Однако он тоже требует некоторых знаний о структуре исполняемых данных в системе Windows.
Все DLL/EXE файлы, используемые в Windows, имеют формат PE (Portable Executable). Файлы эти примечательны тем, что их тело состоит из блоков (например блок ресурсов - .rsrc). Так вот, в DLL файлах есть особая секция -
.idata, задача которой состоит в том, чтобы хранить данные о смещениях функций внутри файла. Когда бы ни загружала система исполняемые файлы в память, она просматривает эту секцию и изменяет значения этой таблицы в соответствии с реальным положением кода в памяти. Это делается для того, чтобы каждый вызов процедуры дошел до её обработчика, так как в связи с перемещениями сегментов памяти, эти значения изменяются.
Вызовы, поступившие на определённую DLL, сначала просматриваются в данной секции, затем находится смещение начала вызываемой функции и ей передаётся управление.
При изменении значения указателя на функцию на смещение своей собственной процедуры, то управление получает сначала именно она, а затем передает его оригинальной функции.
Изменение функции непосредственно
Тоже неплохой способ, однако тут существует много своих "но".
Вся идея состоит в замене первого байта необходимой функции инструкцией прерывания INT 3. Все вызовы к этой процедуре будут вызывать исключение, которое затем перехватывается вашей программой (тут она должна выступать в роли отладчика). НО! Механизм обработки исключений в Windows очень жесток и это всё работает пока всего этого мало. Но при большом количестве перехваченных функций, всё в конце концов зависает в связи с загруженностью системы.
Определение запущенного процесса
Чаще всего для программу требуется определение "запущенности" какого-то определённого процесса. Один из методов здесь уже рассматривался (перехват функции CreateProcess). Но в системах Windows есть способ, который может помочь избавиться от дополнительного написания кода перехвата функции.
Для драйвера виртуального устройства есть возможность обработки сообщения CREATE_PROCESS, которое рассылается VWIN32 когда бы ни запустился новый процесс. В системе Windows NT/2000 существует дополнительная возможность установки такой функции через вызов процедуры PsSetCreateProcessNotifyRoutine из NTOSKRNL. Такая функция (описание смотрите в MSDN) позволяет драйверу устройства получать извещения системы о запуске новой программы.
Ну вот в принципе и всё. Всё остальные способы анализа (COM Hooking, Winsock Hooking, DDE and Browser Helper Object, :.) уже описаны и довольно хорошо, поэтому я снова изобретать велосипед не буду.