Новости

15.04.2021

Книга «Работа с ядром Windows»

Павел Йосифович поможет вам справиться с этой сложной задачей: пояснения и примеры кода превратят концепции и сложные сценарии в пошаговые инструкции, доступные даже начинающим.

В книге рассказывается о создании драйверов Windows. Однако речь идет не о работе с конкретным «железом», а о работе на уровне операционной системы (процессы, потоки, модули, реестр и многое другое).

Вы начнете с базовой информации о ядре и среде разработки драйверов, затем перейдете к API, узнаете, как создавать драйвера и клиентские приложения, освоите отладку, обработку запросов, прерываний и управление уведомлениями.

Глава 8

Уведомления потоков и процессов

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

В этой главе:

  • Уведомления процессов
  • Реализация уведомления процессов
  • Передача данных в пользовательский режим
  • Уведомления потоков
  • Уведомления о загрузке образов
  • Упражнения

 

Уведомления процессов


Каждый раз, когда в системе создается или уничтожается процесс, ядро может уведомить об этом факте заинтересованные драйверы. Это позволяет драйверам отслеживать состояние процессов (возможно, связывая с процессами некоторые данные). Как минимум это позволяет драйверам отслеживать создание/уничтожение процессов в реальном времени. Под «реальным временем» я имею в виду, что уведомления отправляются в оперативном режиме как часть создания процесса; драйвер не пропустит никакие процессы при создании и уничтожении.

При создании процесса драйвер также получает возможность остановить создание процесса и вернуть ошибку стороне, инициировавшей создание процесса. Эта возможность доступна только в режиме ядра.

Windows предоставляет другие механизмы уведомления о создании или уничтожении процессов. Например, с механизмом ETW (Event Tracing for Windows) такие уведомления могут приниматься процессами пользовательского режима (работающими с повышенными привилегиями). Впрочем, предотвратить создание процесса при этом не удастся. Более того, у ETW существует внутренняя задержка уведомлений около 1–3 секунд (по причинам, связанным с быстродействием), так что процесс с коротким жизненным циклом может завершиться до получения уведомления. Если в этот момент будет сделана попытка открыть дескриптор для созданного процесса, произойдет ошибка.

Основная функция API для регистрации уведомлений процессов PsCreateSetProcessNotifyRoutineEx определяется так:

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

В первом аргументе передается функция обратного вызова драйвера, прототип которой выглядит так:

Второй аргумент PsCreateSetProcessNotifyRoutineEx указывает, что делает драйвер — регистрирует обратный вызов или отменяет его регистрацию (FALSE — первое). Обычно драйвер вызывает эту функцию с аргументом FALSE в своей функции DriverEntry, а потом вызывает ту же функцию с аргументом TRUE в своей функции выгрузки.

Аргументы функции уведомления:

  • Process — объект создаваемого или уничтожаемого процесса.
  • ProcessId — уникальный идентификатор процесса. Хотя аргумент объявлен с типом HANDLE, на самом деле это идентификатор.
  • CreateInfo — структура с подробной информацией о создаваемом процессе. Если процесс уничтожается, то этот аргумент равен NULL.


При создании процесса функция обратного вызова драйвера выполняется создающим потоком. При выходе из процесса функция обратного вызова выполняется последним потоком, выходящим из процесса. В обоих случаях обратный вызов вызывается в критической секции (с блокировкой нормальных APC-вызовов режима ядра).

В Windows 10 версии 1607 появилась другая функция для уведомлений процессов: PsCreateSetProcessNotifyRoutineEx2. Эта «расширенная» функция создает обратный вызов, сходный с предыдущим, но обратный вызов также активизируется для процессов Pico. Процессы Pico используются хост-процессами Linux для WSL (Windows Subsystem for Linux). Если драйвер заинтересован в таких процессах, он должен регистрироваться с расширенной функцией.
У драйвера, использующего эти обратные вызовы, должен быть установлен флаг IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY в заголовке PE (Portable Executable). Без установки флага вызов функции регистрации возвращает STATUS_ACCESS_DENIED (значение не имеет отношения к режиму тестовой подписи драйверов). В настоящее время Visual Studio не предоставляет пользовательского интерфейса для установки этого флага. Он должен задаваться в параметрах командной строки компоновщика ключом /integritycheck. На рис. 8.1 показаны свойства проекта при указании этого ключа.

 

Структура данных, предоставляемая для создания процесса, определяется следующим образом:

Описание важнейших полей этой структуры:

  • CreatingThreadId — комбинация идентификаторов потока и процесса, вызывающего функцию создания процесса.
  • ParentProcessId — идентификатор родительского процесса (не дескриптор). Этот процесс может быть тем же, который предоставляется CreateThreadId.UniqueProcess, но может быть и другим, так как при создании процесса может быть передан другой родитель, от которого будут наследоваться некоторые свойства.
  • ImageFileName — имя файла с исполняемым образом; доступен при установленном флаге FileOpenNameAvailable.
  • CommandLine — полная командная строка, используемая для создания процесса. Учтите, что он может быть равен NULL.
  • IsSubsystemProcess — этот флаг устанавливается, если процесс является процессом Pico. Это возможно только в том случае, если драйвер регистрируется PsCreateSetProcessNotifyRoutineEx2.
  • CreationStatus — статус, который будет возвращен вызывающей стороне. Драйвер может остановить создание процесса, поместив в это поле статус ошибки (например, STATUS_ACCESS_DENIED).

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

Реализация уведомлений процессов


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

Наш драйвер будет называться SysMon (хотя он никак не связан с программой SysMon из пакета Sysinternals). Он будет хранить всю информацию о создании/уничтожении в связном списке (с использованием структур LIST_ENTRY). Так как к связному списку могут одновременно обращаться несколько потоков, необходимо защитить его мьютексом или быстрым мьютексом; мы воспользуемся быстрым мьютексом, так как он более эффективен.

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

Приведенное выше определение перечисления ItemType использует новую возможность C++ 11 — перечисления с областью видимости (scoped enums). В таких перечислениях значения имеют область видимости (ItemType в данном случае). Также размер этих перечислений может быть отличен от int — short в данном случае. Если вы работаете на C, используйте классические перечисления или даже #define.

Структура ItemHeader содержит информацию, общую для всех типов событий: тип события, время события (выраженное в виде 64-разрядного целого числа) и размер полезных данных. Размер важен, так как каждое событие имеет собственную информацию. Если позднее вы захотите упаковать массив таких событий и (допустим) предоставить его клиенту пользовательского режима, клиент должен знать, где заканчивается каждое событие и начинается новое.

При наличии такого общего заголовка можно создать другие структуры данных для конкретных событий. Начнем с простейшего — выхода из процесса:

Для события выхода из процесса существует только один интересный фрагмент информации (кроме заголовка) — идентификатор завершаемого процесса.

Если вы работаете на C, наследование вам недоступно. Впрочем, его можно имитировать — создайте первое поле типа ItemHeader, а затем добавьте конкретные поля; структура памяти остается одинаковой.

Для идентификатора процесса используется тип ULONG. Использовать тип HANDLE не рекомендуется, так как в пользовательском режиме он может создать проблемы. Кроме того, тип DWORD не используется, хотя в заголовках пользовательского режима тип DWORD (32-разрядное целое без знака) встречается часто. В заголовках WDK тип DWORD не определен. И хотя определить его явно нетрудно, лучше использовать тип ULONG — он означает то же самое, но определяется в заголовках как пользовательского режима, так и режима ядра.

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

В новом файле с именем SysMon.h определяется параметризованная структура, в которой хранится поле LIST_ENTRY с основной структурой данных:

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

Также возможно наследовать от LIST_ENTRY, а затем добавить структуру ProcessExitInfo. Но такое решение менее элегантно, так как наши данные не имеют никакого отношения к LIST_ENTRY, поэтому расширение — искусственный прием, которого следует избегать.

Тип FullItem избавляет от хлопот с созданием этих отдельных типов.

Если вы используете C, то, естественно, решение с шаблонами будет недоступно, поэтому вам придется применить представленный структурный подход. Не буду снова упоминать C — всегда существует обходное решение, которым можно воспользоваться в случае необходимости.

Заголовок связанного списка должен где-то храниться. Мы создадим структуру данных для хранения всего глобального состояния драйвера (вместо набора отдельных переменных). Определение структуры выглядит так:

В определении используется тип FastMutex, который был разработан в главе 6. Также в определении встречается RAII-обертка AutoLock на C++ (тоже из главы 6).

С полным содержанием статьи можно ознакомиться на сайте "Хабрахабр":

https://habr.com/ru/company/piter/blog/550822/


Комментарии: 0

Пока нет комментариев


Оставить комментарий






CAPTCHAОбновить изображение

Наберите текст, изображённый на картинке

Все поля обязательны к заполнению.

Перед публикацией комментарии проходят модерацию.