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

За последний год русскоязычному читателю было предложено несколько прекрасных книг, посвященных внутреннему устройству Windows NT/2000/XP. Более того, в конце марта 2003 года Москву посетил один из наиболее видных экспертов в этом вопросе Дэвид Соломон, который провел блестящий мастер-класс. В ходе семинара Соломон для иллюстрации тех или иных идей использовал утилиты, разработанные в компании System Internals, основанной не менее известными специалистами, написавшими в соавторстве с Соломоном ставшую бестселлером книгу «Внутреннее устройство Windows 2000», — Марком Руссиновичем и Брюсом Когсвелом (дополнительная информация по теме представлена во врезке «Литература»).

Эти утилиты, нередко применяющие возможности недокументированных API, позволяют решать задачи, недоступные при использовании стандартных средств управления операционной системой.

Утилиты предоставляются бесплатно, их можно найти на сайте http://www.wininternals.com. Более того, они поставляются с исходными текстами. Изучив эти замечательные программы, я подумал, неужели для решения достаточно очевидных задач мониторинга операционной системы необходимо разрабатывать столь сложные приложения? В случае с Windows NT 4.0 и предыдущих версий это, к сожалению, так. Операционные системы не предоставляют API, решающих поставленные задачи. Однако с выходом Windows 2000 ситуация кардинально изменилась. В состав операционной системы входит подсистема WMI, которая позволяет реализовать почти все возможности, которые представляют утилиты System Internals. Про это «почти» я скажу несколько слов в конце статьи.

Сейчас же, несмотря на то что принципам работы подобных утилит посвящено достаточно много публикаций, я еще раз остановлюсь на этом вопросе, так как в их основе лежат механизмы, необходимые для понимания инфраструктуры WMI.

SystemInternals изнутри

Программы мониторинга событий операционной системы RegMon, FileMon, PMon позволяют отслеживать события, связанные, соответственно, с доступом к системному реестру и файловой системе, созданием и завершением процессов. Все они построены по одному принципу, и далее я буду рассматривать программу RegMon, которая оказалась настолько хороша, что разработчики компании Microsoft официально использовали ее при создании и отладке Word 2000.

Программа RegMon, интерфейс которой изображен на экране 1, позволяет выбрать интересующие пользователя события доступа к реестру и отслеживать их в режиме, близком к режиму реального времени. При этом ресурсы операционной системы практически не расходуются! Как это получается? Не то чтобы очень просто. Программа RegMon (как, впрочем, и остальные утилиты мониторинга wininternals) состоит из двух частей. Первая — это модуль, реализующий пользовательский интерфейс. А вот вторая часть представляет собой драйвер режима ядра, осуществляющий мониторинг функций, через которые происходит взаимодействие с реестром.

Напомню, что функции API-доступа к системному реестру, такие как RegOpenKey, RegQueryValue, которые мы используем в прикладных программах, на самом деле являются обертками (wrappers). Они транслируют через специальный механизм вызовы в модули, работающие в режиме ядра. А вот те уже в свою очередь осуществляют доступ к данным и возвращают результаты функциям пользовательского режима. Механизм взаимодействия между режимами ядра и пользователя очень похож на применяемый в DOS вызов INT 21h.

Итак, проблему мониторинга событий доступа к системному реестру можно сформулировать иначе. А именно необходимо научиться перехватывать соответствующие вызовы функций режима ядра. То есть задача сводится к перехвату системных вызовов.

Первыми решение этой задачи предложили Матт Пиетрек и Джеффри Рихтер. В дальнейшем Марк Руссинович и Брюс Когсвел предложили принципиально новую технологию перехвата системных вызовов, названную ими System Call Hooking (SCH). Позднее эту теорию развил и обобщил Свен Шрайбер. Посмотрев на рис. 1, иллюстрирующий механизм действия SCH, можно заметить, что, научившись перехватывать вызовы функций режима ядра, мы получим возможность контролировать и соответствующие действия. Драйвер, который представляет собой вторую часть программы RegMon, этим и занимается. Суть технологии следующая: все модули пользовательского режима взаимодействуют с модулями режима ядра через обработчик прерывания INT 2Eh (рис. 1). Иначе говоря, все вызовы транслируются в нечто вроде:

mov eax,38h

lea edx,[esp+4]

int 2Eh

ret 28h

В регистре eax передается ID вызываемой функции (на самом деле ID совпадает с индексом), в регистре edx находится указатель на стек передаваемых аргументов.

Обработчик прерывания функционирует уже в режиме ядра. Он находится в модуле ntoskrnl.exe и называется KiSystemService(). Заметим, что в терминах ядра системным сервисом называется любая экспортируемая модулями ядра функция. Соответственно, говоря о системных службах режима ядра Windows, мы подразумеваем функции, предоставляемые модулями ядра. При инициализации ntoskrnl.exe формируются таблицы, одна половина которых содержит адреса функций (системных служб), а вторая описывает передаваемые им и возвращаемые ими типы аргументов и их число. Действия обработчика прерывания INT 2Eh — функции KiSystemService() — незамысловаты. Он пытается найти в таблице System Services адрес вызываемой функции по ее ID, точнее по индексу, переданному в регистре eax, и в случае успеха вызывает функцию, расположенную по этому адресу, простым вызовом call. То есть в приведенном выше коде подразумевается, что функция NtDeviceIoControlFile, к вызову которой приводит вызов функции пользовательского режима DeviceIoControl, находится в строке с номером 0x38h таблицы системных служб.

Конечно, структура таблиц весьма нетривиальная, что делает реализацию обработчика INT 2Eh достаточно сложной, но суть выполняемых им действий именно такова. Специально отметим, что ВСЕ вызовы функций режима ядра — в том числе адресованные модулю win32k.sys — сначала обрабатываются функцией KiSystemService. После того как вызванная функция вернула управление в KiSystemService, оно передается функции KiServiceExit, возвращающей значения приложению пользовательского режима. Для функций, содержащихся в ntoskrnl.exe (так называемый Native API), зарезервированы позиции в таблицах 0x0000 — 0x0FFF. Диапазон значений 0x1000 — 0x1FFF отведен под функции win32k.sys. Очевидно, что для перехвата системного вызова достаточно подставить в таблицу адресов системных вызовов адрес функции-перехватчика, а старый адрес запомнить. Теперь вместо, например, NtRegOpenKey будет вызываться наша функция. Мы можем сохранить необходимую информацию, вызвать настоящую функцию, получить возвращаемые значения и вернуть управление. Перехваченные таким образом данные остается сохранить в буфере и возвратить их в ответ на запрос программы режима пользователя. Таким путем может осуществляться трассировка всех системных вызовов.

На самом деле утилита PMon (или похожая на нее программа VTrace Tool, написанная Jacob Lorch и Alan Jay Smith) устроена несколько иначе. В версиях Windows NT начиная с 3.51 реализован механизм получения событий о процессах — функция PsSetCreateProcessNotify Routine. А начиная с версии 4.0 — и о потоках PsSetCreateThread NotifyRoutine. Этот API считается слабо документированным, поскольку в DDK приведены описания функций, но не указана технология их использования, которая, впрочем, вполне очевидна. Хотя на самом деле детали не так важны. Существенно же то, что для получения информации об изменениях, происходящих с объектами ядра, используются специфические драйверы режима ядра, которые передают эту информацию через стандартный механизм — функцию DeviceIoControl — в приложения режима пользователя. Как мы увидим дальше, WMI использует этот механизм для осуществления мониторинга. По существу, инженеры компании Microsoft стандартизировали интерфейсы, предложенные ранее независимыми специалистами.

Теперь, когда мы рассмотрели механизм перехвата вызовов режима ядра, можно поговорить об одной из реализаций — подсистеме WMI.

Архитектура WMI

Подсистема WMI реализует сложную многоуровневую архитектуру, которая вовсе не ограничивается ставшим уже привычным набором удобных COM-объектов. Эта архитектура в общем виде изображена на рис. 2. Упомянутые COM-объекты, в сущности, являются скорее вершиной айсберга, стандартизирующей механизмы доступа. С точки зрения модулей и драйверов режима ядра WMI представляет собой механизм, позволяющий предоставить информацию приложениям пользовательского режима. В отличие от описанной выше технологии SCH, разработчикам приложений режима ядра нет необходимости придумывать коды ввода/вывода или разрабатывать приложения, предоставляющие пользователю доступ к этой информации.

Кроме того, поддержка WMI дает возможность настраивать модули ядра, используя стандартный интерфейс. Таким образом, доступ к драйверам через механизм DeviceIoControl оказывается скрыт от пользователя — разработчика приложений режима пользователя. Также предоставляется стандартный интерфейс, позволяющий получать уведомления о происходящих событиях, без необходимости низкоуровневого программирования.

Подсистема WMI предоставляет унифицированный механизм доступа к информации от модулей режима ядра. Подсистема основывается на модели описания информации Common Information Model (CIM), которая является индустриальным стандартом, выработанным комитетом Distributed Management Task Force (DMTF).

Подсистема WMI образует трехуровневую архитектуру для сбора и распространения информации. Эта архитектура включает:

  • стандартный механизм для сбора информации, реализованный как CIM-совместимое хранилище данных;
  • стандартный протокол для получения и распространения информации, который описывает как механизмы предоставления информации потребителю, например через COM/DCOM, так и механизмы взаимодействия между WMI и другими компонентами, в частности драйверами режима ядра;
  • стандартные модули предоставления информации — провайдеры WMI. Эти модули представляют собой библиотеки DLL, которые обеспечивают доступ к поддерживаемой ими информации CIM. Провайдер WMI для WDM осуществляет интерфейс между модулями режима ядра и системой WMI режима пользователя. Кроме того, компонент WMI для WDM предоставляет другим модулям режима ядра службы, позволяющие им предлагать свою информацию.

Напомню, что драйверы режима ядра, которые удовлетворяют требованиям Windows Driver Model, называются WDM-драйверами. Так вот, модуль WMI/WDM представляет собой обычный WDM-драйвер. Поэтому он может использоваться другими драйверами и модулями режима ядра для реализации простой поддержки WMI.

Начиная с версии Windows 2000 все WDM-драйверы будут обрабатывать запросы IRP_MJ_SYSTEM_CONTROL вне зависимости от того, поддерживает драйвер механизмы WMI или нет. Если драйвер их не поддерживает, запрос должен быть передан драйверу более низкого уровня. Запросы могут быть переданы подсистемой WMI в любой момент. Но, как правило, это происходит в ситуации, когда приложение режима пользователя передает запрос на информацию.

Формат данных, передаваемых драйвером, определяется разработчиком. Как же другие приложения могут интерпретировать информацию? Для описания структуры информации используется язык MOF (Managed Object Format). Разработчик драйвера может создать файл MOF, откомпилировать его компилятором MOF, mofcomp и либо включить его в качестве ресурса в бинарный файл драйвера, либо включить откомпилированное описание в качестве ресурса в другой бинарный файл, например DLL. В этом случае путь к файлу, содержащему описание, должен быть помещен в значение реестра MofImagePath, которое будет добавлено к описанию драйвера в реестре. Также и описание структуры может возвращаться динамически, в ответ на запрос WMI.

Обсудим теперь более подробно программный интерфейс Event Tracing, который является одним из механизмов, предоставляемых WMI.

Event Tracing API

Несмотря на то что программный интерфейс трассировки событий Event Tracing описан в разделе MSDN Performance Monitoring (http://msdn.microsoft. com/library/default.asp?url=/library/en-us/perfmon/base/ performance_monitoring_architecture.asp), он имеет отношение в первую очередь к WMI. Собственно говоря, это низкий уровень доступа к упомянутым выше событиям WMI из программ пользовательского режима. Посмотрим на рис. 3, иллюстрирующий данный программный интерфейс.

Как на самом деле осуществляется доступ к обсуждаемым вызовам функций системного реестра, известно только разработчикам Windows. Можно, конечно, предположить, что соответствующая информация собирается непосредственно в системных модулях Windows и передается зарегистрированным приложениям. Но ряд соображений заставляет думать иначе. Во-первых, в DDK описан специальный тип драйверов режима ядра, о котором говорилось выше, позволяющий осуществлять мониторинг. Во-вторых, в заголовочном файле EvnTrace.h в качестве комментариев к событиям системного реестра указаны имена вызываемых функций Native API. Следует предположить, что эти события возникают в результате перехвата вызовов. В третьих, этот режим используют некоторые специальные приложения, а реализация встроенного механизма приведет к дополнительным накладным расходам. Также очевидно, что Event Tracing не использует провайдеры WMI или механизмы CIM. Для того чтобы в этом убедиться, достаточно загрузить в CIM Studio какое-нибудь приложение, использующее Event Tracing (например, демонстрационную программу к данной статье), и сравнить списки загружаемых модулей. Программа, использующая Event Tracing, загружает лишь базовые модули: ntdll.dll, kernel32.dll, advapi32.dll, rpccrt4.dll, tcappcmp.dll и msvcrt.dll (модуль tcappcmp.dll под Windows 2000 не загружается). Это позволяет утверждать, что Event Tracing взаимодействует с модулями режима ядра напрямую, как показано на рис. 3.

Так или иначе, существует стандартный механизм получения информации о различных событиях (проявлениях активности) в объектах ядра. К ним относятся:

  • события настройки аппаратуры;
  • события создания файла (вызов NtFileCreate);
  • события дискового ввода/вывода;
  • чтение образа исполняемых файлов;
  • ошибки доступа к памяти;
  • доступ по TCP и UDP;
  • создание и завершение процессов;
  • события доступа к системному реестру;
  • создание и завершение потоков.

Мы говорим здесь об уже реализованных классах событий. Безусловно, разработчик может реализовать собственный драйвер, провайдер WMI и предоставить доступ к информации, не предусмотренной разработчиками Microsoft.

Полученные события могут быть записаны в файл специфического формата, а могут поставляться в режиме реального времени. Интерфейс Event Tracing оперирует объектами четырех видов.

Event tracing session (сессия). События записываются сессиями. Система поддерживает максимум 32 сессии. Заметим, что в Windows 2000 две, а в Windows XP три из них резервируются под нужды операционной системы.

Controllers (контроллеры). Контроллером является программа, которая запускает, останавливает сессии и управляет ими. Под управлением подразумевается определение числа буферов, сбор статистики при получении событий, а также определение пути к log-файлу, если он используется, и его размера.

Event Trace Provider (провайдер). Как уже отмечалось выше, провайдером является приложение Win32, предоставляющее ту или иную информацию. Приложение регистрирует класс событий, который оно может поставлять, используя для идентификации значение UUID. После регистрации приложения-контроллеры могут управлять поставляемыми событиями. Вообще говоря, провайдер в терминологии Event Tracing и WMI — понятия разные. То есть провайдером в терминах Event Tracing может быть любое приложение. Но провайдеры WMI непосредственно не являются поставщиками информации для приложений Event Tracing. Хотя, конечно, реализовать механизм доставки информации от провайдеров WMI в Event Tracing можно — для этого необходим соответствующий провайдер Event Tracing.

Event Trace Consumer (получатель). Получателем информации являются приложения Win32, либо зарегистрировавшие функции для получения информации в режиме реального времени, либо анализирующие записываемый log-файл.

Обсудим теперь механизмы доставки информации в режиме реального времени. Конечно, мы знаем, что операционные системы семейства Windows NT/2000/XP/Server 2003 не являются системами реального времени. Поэтому здесь под термином «режим реального времени» мы подразумеваем доставку информации после возникновения события в течение достаточно короткого промежутка времени.

Итак, цикл получения информации разделяется на две части: инициализацию и собственно процесс получения событий. Важно понимать, что инициализация процесса сбора информации может быть вызвана любым приложением. Она соответствует созданию сессии. При создании сессии контроллер указывает, какой тип мониторинга предполагается — запись в файл, доставка в реальном времени или и то и другое. Также сообщается информация о размере файла, числе буферов и т.д. Два раза создать одну и ту же сессию невозможно. Но приложение, получающее информацию о событиях — CONSUMER, также не зависит от того, кто создал сессию. Достаточно просто сообщить необходимый идентификатор. События доставляются через механизм обратного вызова (call-back). Все функции, обслуживающие call-back, имеют одинаковый формат, что удобно для унификации программы. Сессию может закрыть приложение — контроллер. Но совсем необязательно, чтобы это было то приложение, которое сессию создало.

Рассмотрим теперь демонстрационную программу, получающую информацию о некоторых событиях режима ядра.

Мониторинг доступа

Мы уже можем реализовать мониторинг доступа к реестру, не разрабатывая драйверы режима ядра. В нем задействованы два механизма — запуск трассировщика в режиме реального времени (создавать файл не требуется) и собственно регистрация функций и получение событий. Программа собрана в Visual C++ 6.0 с использованием Platform SDK и Windows DDK (из последнего пакета применяется заголовочный файл wmiguid.h, в котором определены идентификаторы событий ядра) и протестирована под Windows 2000 Professional SP2, Windows XP Professional и Windows Server 2003. В последних версиях Platform SDK определены константы KERNEL_LOGGER_NAME, а для XP и более поздних версий Windows несколько изменена структура EVENT_TRACE_HEADER — поле ULONGLONG ThreadId заменено на два — ULONG ThreadId и ULONG ProcessId. Но в примере для обеспечения совместимости последнее поле не используется, и идентификаторы процесса получаются по идентификаторам потоков (см. врезку «Определение процесса, которому принадлежит поток»).

Создание сессии. Разработанное приложение совмещает в себе функции контроллера сессии и получателя событий. Для создания сессии используется функция StartTarce (см. листинг 1). Ей передаются три параметра. Первый — это указатель на значение типа TRACEHANDLE, в котором будет возвращен идентификатор сессии. Во втором передается имя сессии. В нашем случае, соответствующем получению событий ядра, это имя фиксированное. Оно определено в KERNEL_LOGGER_NAME и имеет значение _T(«NT Kernel Logger»). Третий параметр определяет параметры создаваемой сессии.

Получение событий. Теперь мы предполагаем, что сессия создана и процесс перехвата событий включен. Остается зарегистрировать функции call-back, открыть существующую сессию и инициализировать процесс мониторинга. В листинге 2 показаны первые два шага. Регистрирует функцию call-back вызов функции SetTraceCallback. У функции два аргумента. Первый — это указатель на GUID, идентифицирующий событие, второй — собственно адрес функции.

Открывает сессию для получения событий функция OpenTrace. У нее единственный параметр — указатель на структуру EVENT_TRACE_LOGFILE. В передаваемой переменной описываются события, информацию о которых необходимо доставлять. Функция OpenTrace возвращает значение типа TRACEHANDLE, которое будет использоваться для запуска мониторинга.

Для того чтобы зарегистрированные функции начали вызываться в ответ на происходящие события, необходимо вызвать функцию ProcessTrace. Ей передается массив, содержащий идентификаторы открытых сессий — значений типа TRACEHANDLE, число элементов в этом массиве и два указателя на структуру FILETIME, в которых будет возвращено время начала и окончания процесса мониторинга (см. листинг 3). Эти два значения могут быть равны NULL.

Закрытие сессии. После окончания мониторинга вызывается функция CloseTrace для каждой открытой ранее с помощью функции OpenTrace сессии. Заметим, что управление из вызова ProcessTrace не возвращается, пока мониторинг не завершен. Как это сделать корректно? В тестовом примере завершение реализовано неверно. Для корректного завершения сессии необходимо определить функцию BufferCallBack, которая прерывает мониторинг, возвращая значение FALSE, и продолжает его, возвращая значение TRUE.

Функции, зарегистрированные как call-back, получают один параметр типа PEVENT_TRACE, содержащий информацию о событии (см. листинг 4).

Структура содержит большое число полей. Первое из них — это структура EVENT_TRACE_HEADER, содержащая описание события. Его тип указан в поле Class.Type. Как я заметил выше, для разных версий операционной системы структура EVENT_TRACE_HEADER различна. Во врезке «Определение процесса, которому принадлежит поток» показано, как можно унифицировать приложение, получая идентификатор процесса, вызвавшего событие по идентификатору потока, который передается во всех версиях. Это может быть необходимо, так как многие события создаются собственно программой мониторинга. Иногда такие события нужно игнорировать.

Поле Flags структуры EVENT_TRACE_HEADER содержит набор флагов. Если выставлен флаг WNODE_FLAG_USE_MOF_PTR, то в поле MofData структуры PEVENT_TRACE имеется дополнительная информация о событии. В рассматриваемом случае это имя параметра реестра, с которым связано событие.

В структуре, указатель на которую содержится в поле MofData, находится информация о событии. Но как ее получить? Ведь структура определяется провайдером и нигде не описана! Это, как уже говорилось выше, не совсем так. Косвенно структура данных, возвращаемая провайдером доступа к реестру, описана в Platform SDK. Она представлена в виде MOF-файлов, но ее легко отобразить на обычные структуры. Замечу, что для Windows 2000 и XP/Server 2003 структуры разные. Оба варианта отображены в листинге 5.

Полная версия программы rtTrace, исходные тексты которой можно получить у меня по электронной почте, реализует мониторинг большего числа классов событий. Она позволяет, кроме событий доступа к системному реестру, осуществлять мониторинг файловой системы, запуска и остановки процессов. Подробнее об Event Tracing рассказано на сайте MSDN.

Чего не может Event Tracing

Не говоря о мелких недостатках, например о невозможности получить все аргументы вызываемых функций, у Event Tracing есть и более существенные. Наша программа может лишь регистрировать события. Технология, предложенная Марком Руссиновичем и развиваемая другими авторами, значительно более гибкая. Она позволяет запретить выполнение тех или иных функций. Рассмотрим приложение, которое должно на низком уровне зашифровывать некоторую папку, используя вместо механизмов шифрования Crypto API свой, реализованный, например, с помощью смарт-карты. Мы пишем драйвер режима ядра, который перехватывает функции работы с файлами. Он проверят наличие смарт-карты. Если ее нет, драйвер не делает ничего. А если есть, считывает с карты имя папки, которую надо защищать. Теперь остается при открытии файла расшифровывать его с использованием смарт-карты, а при закрытии — зашифровывать. При удалении карты из ридера все незакрытые файлы можно уничтожить. Описанный алгоритм, конечно, лишь схематично демонстрирует возможности, предоставляемые технологией system call hooking, но сами возможности, по-моему, выглядят весьма впечатляюще.

Литература
  1. Lorch J. R., Jay Smith А. The VTrace Tool: Building a System Tracer for Windows NT and Windows 2000 // MSDN magazine, October 2000.
  2. Russinovich M., Cogswell B. Windows NT System Call Hooking // Dr. Dobb?s Journal, January 1997.
  3. Russinovich M. Inside the Native API, 1998.
  4. Russinovich M. Inside NT Utilities // Windows NT Magazine, February 1999.
  5. Solomon D. A., Russinovich M. E. Inside for Microsoft Windows 2000 // Third Edition Microsoft Press(Соломон Д., Руссинович М. Внутреннее устройство Microsoft Windows 2000. Русская Редакция, 2001).
  6. Schreiber S. B. Undocumented Windows 2000 secrets. Addison-Wesley, 2001. (Шрайбер С. Недокументированные возможности Windows 2000. Питер, 2002).
  7. Эпштейн А. Tool Help // Windows 2000 & .NET Magazine/RE, №4 2001.
  8. Эпштейн А. Smart Cards... // Windows 2000 & .NET Magazine/RE.
  9. Kernel-Mode drivers. Supporting WMI // Microsoft(r) Drver Developer Kit (DDK).
  10. Microsoft(r) Developer Network (MSDN), «Event Tracing».
  11. Hughes K., Wohlferd D. Say Goodbye to Quirky APIs: Building a WMI Provider to Expose Your Object Info // MSDN magazine.
  12. Cooperstein J. Windows Management Instrumentation: Administering Windows and Applications Across Your Enterprise // MSDN magazine.

Определение процесса, которому принадлежит поток

В разрабатываемой программе, наверное, не имеет смысла показывать события доступа к реестру, генерируемые самой программой rtTrace. Я сделал это с помощью функций библиотеки Tool Help Library, которая была описана в статье «Библиотека Tool Help для Windows 2000/9х», опубликованной в № 4 нашего журнала за 2001 год. С помощью этого API можно получить список всех потоков в системе. А перебрав их, найти поток с нужным идентификатором. Далее остается сравнить идентификаторы процессов, соответствующих двум потокам. В листинге А приведен код функции, которая проверяет, выполняется ли поток с заданным ID в адресном пространстве текущего процесса.


Александр Эпштейн — независимый консультант и разработчик программного обеспечения. С ним можно связаться по электронной почте alex_ep@hotmail.com