Изучаем ETW и извлекаем профиты

Давненько у нас в бложеке не появлялось новых статей! Что ж, ETW, настало твое время!

Возможно, кто-то из вас и раньше сталкивался с этим сокращением - ETW. Что же это такое? Расшифровывается эта аббревиатура как Event Tracing for Windows. ETW - это системный компонент ОС Windows, который используется для диагностики, отладки и исследования производительности тех или иных частей ОС, а также приложений. Появился он где-то с Windows 2000, но полноценно его использовать стало возможным примерно с Windows Vista.

- А мне какое до этого дело? - спросите вы. А вот какое: в Windows несколько сотен (в Win10 - около тысячи) провайдеров событий, которые жаждут вам что-нибудь рассказать о своей работе, нужно лишь подписаться! Вот, например, как можно штатными средствами Windows посмотреть, что Internet Explorer отсылает по HTTP. А вот эти ребята сумели с помощью ETW поснифать, что пользователь набирает на своей USB-клавиатуре. И это все - безо всяких хуков, инжектов и драйверов! ETW просто пронизывает Windows, его использует и Event Viewer, и такие утилиты, как xPerf, и Visual Studio во время отладки приложения, чтобы отображать вам статистику потребляемой памяти и процессорного времени, и даже штатный Resource Monitor, который вы можете открыть из таскменеджера!

Уже интересно?

Тогда сначала быстренько рассмотрим архитектуру ETW, его плюсы и минусы. Вот что из себя представляет этот компонент:

Как показано на картинке, в системе зарегистрировано некоторое количество провайдеров событий (можно зарегистрировать свой провайдер). Провайдером может быть ядро системы, драйвера или user-mode приложения. Существуют контроллеры ETW, которые могут создавать, модифицировать и удалять сессии логирования. Сессии ETW - это глобальные объекты ОС Windows. Сессия потребляет события каких-либо ETW-провайдеров, после чего записывает их в файл или же выдает всем заинтересованным потребителям (consumers) в реальном времени. Цель ETW как API - быть максимально быстрым (чтобы даже драйвера и ядро могли вести свои логи, не сильно напрягаясь), не нагружать провайдер, если никто не подписан на его ивенты, и иметь возможность логировать события даже на самых ранних этапах загрузки ОС. Далее я приведу достоинства и недостатки ETW с точки зрения потребителя событий.

Плюсы ETW:

  • Позволяет единообразно потреблять события из сотен источников.
  • Полноценно работает на Windows Vista (или даже Win 7) и новее.
  • Не требует хуков, инжектов, драйверов. Все события можно получать в user mode.

Минусы ETW:

  • Требует привилегий администратора. Увы, не получится работать с объектами ядра (сессиями) от имени простого пользователя.
  • Не будет работать на Windows XP. Также, не все провайдеры, которые доступны в более новых ОС, присутствуют в более старых.
  • Созданные вами сессии будут палиться в Computer Management'е (описано далее), а также любое приложение сможет перечислить имеющиеся в системе сессии; их можно будет остановить и удалить извне. Разумеется, для этого тоже нужны будут права администратора.

В этой статье я буду рассматривать только создание потребителей событий ETW. Провайдеры - это отдельная большая тема, с которой при желании можно познакомиться, почитав соответствующие статьи в MSDN.

Само ETW API - не сахар. Оно неочевидное, не слишком хорошо документировано, многие вещи приходится додумывать или искать в Интернете примеры использования той или иной функции. Статей на эту тему написано не так много (некоторые дополнительные ссылки я приведу в конце статьи). Я постараюсь прояснить большинство вопросов, которые у нас возникнут по ходу разбора этого API.

Перед тем, как я перейду к коду, который позволяет создавать сессии и получать события, я опишу несколько штатных системных утилит Windows, которые помогут нам в работе с ETW. Вначале, конечно, неплохо было бы изучить список провайдеров, доступных на конкретной ОС. Для этого можно использовать следующую команду:

Запускать нужно, как я уже говорил, от имени администратора. Команда выдаст нам полный список доступных в системе провайдеров ETW. Вы увидите что-то вроде этого:

Есть также возможность посмотреть, какие провайдеры подключены к тому или иному процессу, чтобы узнать, какую информацию через ETW-события этот самый процесс может нам предоставлять:

Вот, например, вывод для Notepad++ на Win10:

Провайдеры, которые попали в процесс из загруженных в него внешних DLL-модулей, также будут включены в список. Просмотрев список провайдеров ОС или конкретного процесса, можно найти что-нибудь интересное. Для себя я выбрал Microsoft-Windows-Kernel-Process, логично предположив, что он нам может рассказать что-то о процессах в системе. На нем мы и будем тренироваться получать и парсить события. Выполняем следующую команду:

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

Здесь value - это идентификатор события, он нам потребуется далее, чтобы отличать полученные события друг от друга. Хочу заметить, что многие провайдеры также содержат недокументированные события (без каких-либо сообщений), а также существуют полностью недокументированные (безымянные) провайдеры. Это очень большое поле для исследований, но пока что мы остановимся на том, что хорошо описано.

Теперь неплохо было бы получить эти события и посмотреть, что же в них содержится. К сожалению, я не нашел годного и быстрого способа просмотреть метаданные событий без того, чтобы их получать (возможно, такой способ все же есть, если вы о нем знаете, напишите комментарий). На самом деле, провайдер может предоставлять события в куче различных форматов (MOF, WPP, Manifest-based и появившийся в Win10 TraceLogging; подробнее о них здесь). Пока это нас не сильно интересует: мы воспользуемся стандартными средствами Windows, чтобы создать сессию ETW и получить события в файл журнала. Итак, начнем. Сначала запустим оснастку "Computer Management" / "Управление компьютером" (Win+R -> compmgmt.msc) и перейдем в раздел "Performance" -> "Data Collector Sets" -> "Event Trace Sessions" ("Производительность" -> "Группы сборщиков данных" -> "Сеансы отслеживания событий"). Здесь мы увидим список имеющихся сессий ETW:

Давайте создадим свою для провайдера Microsoft-Windows-Kernel-Process. Правой кнопкой мыши кликнем на свободном поле в списке сессий, выберем "New" -> "Data Collector Set" ("Создать" -> "Группа сборщиков данных"). Откроется окно создания сессии. Укажем имя сессии и выберем "Create manually (Advanced)" ("Создать вручную (для опытных)", мы же с вами опытные ребята):

Теперь пришло время добавить интересующие нас провайдеры в сессию. У нас он всего один, выбираем его из списка, нажав "Add..." ("Добавить..."). Указываем интересующие нас ключевые слова в поле "Keywords(Any)" ("Ключевые слова(Любые)"). Помните, раньше мы уже видели список ключевых слов провайдера, когда запускали wevtutil? Я добавлю всего одно ключевое слово 0x10 - события, имеющие отношение к процессу:

Если, опять-таки, посмотреть на вывод wevtutil, то мы увидим, что этому ключевому слову соответствует всего два события: старт и остановка процесса (ID 1 и 2). Для полноценного исследования абсолютно всех событий провайдера можно указать 0xFFFFFFFFFFFFFFFF. Далее указываем уровень логирования (опять-таки, для надежности можно указать 0xFF, чтобы ничего не пропустить, но мы укажем 0x4 - Informational). Есть также возможность указать прочие настройки сессии (например фильтр), но нам пока что хватит того, что мы уже настроили, чтобы получить интересующие нас события.

Нажимаем "Next" ("Далее"). Выбираем путь, по которому будет сохраняться файл журнала сессии. Снова нажимаем "Next". Выбираем флажок "Start this data collector set now" ("Запустить группу сборщиков данных сейчас") и нажимаем "Finish" ("Готово"):

Теперь сессия создана, запущена и собирает интересующие нас события. Для теста запустим notepad.exe, а потом закроем его. Остановим сессию, кликнув на ее имени правой кнопкой мыши и выбрав "Stop" ("Остановить") (сессию можно после этого удалить, выбрав "Delete" ("Удалить")). Перейдем в папку, которую мы указали для сохранения журнала сессии. Там будет находиться файл с именем имя_сессии.etl (в моем случае - my session.etl. Нам нужно его разобрать. Для этого существует еще одна стандартная утилита Windows. Запускаем ее следующим образом, предварительно перейдя в консоли в папку с etl-файлом:

Получим в той же папке файл в XML-формате result.xml. Можно открыть его в любом текстовом редакторе, чтобы увидеть интересующие нас события запуска и остановки notepad.exe:

Здесь мы уже видим имена и значения тех свойств, которые приходят с каждым ивентом (например, ProcessID, ExitCode и т.д.). К сожалению, здесь по-прежнему не видны типы данных этих свойств, и о них можно лишь догадываться, изучая значения этих свойств.

Что ж, теперь перейдем к коду. Как же сделать так, чтобы наше приложение получало события от нужных провайдеров в режиме реального времени, а затем выколупывало из них все интересующие нас свойства? Писать будем на C++14. Я сделал небольшую библиотеку, с помощью которой можно реализовать все перечисленное, далее я буду приводить выдержки кода из нее с комментариями. Полный код библиотеки вместе с тестовыми приложениями можно будет скачать в конце статьи. Наш код будет выступать в роли контроллера сессии и потребителя событий одновременно.

Для начала сделаем класс, который будет подготавливать структуру настроек для сессии, которую мы потом создадим.

Структура не совсем простая - после нее будет записано имя сессии, поэтому нам нужно будет выделить соответствующий объем памяти под это имя, помимо заполнения полей структуры. Копировать имя в выделенную память не нужно, это за нас будет делать ETW API. Теперь мы можем подготовить структуру, описывающую нашу сессию. Далее нужно эту самую сессию создать и запустить. Вот интерфейс класса сессии:

Начнем разбор имплементации с создания и завершения сессии:

Здесь я отмечу еще раз, что сессия - глобальный объект ОС Windows. Ее имя является ее же уникальным идентификатором. Логично, что сессия с тем именем, которое мы устанавливаем, может уже существовать в системе. Поэтому мы в этом случае закрываем старую сессию и пытаемся после этого еще раз создать новую. Стоит также помнить, что сессию после использования нужно обязательно закрывать (например, при выходе из приложения), потому что в противном случае сессия останется висеть до перезагрузки компьютера и потреблять его ресурсы.

Теперь перейдем к функции добавления провайдера к сессии:

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

Подробнее прочитать про использованные WinAPI-функции можно здесь: StartTrace, ControlTrace и EnableTraceEx2 (последняя доступна только на Windows 7 и новее, если требуется совместимость с Vista, то нужно обратить внимание на функцию EnableTraceEx).

Отмечу, что события real-time-сессии мы будем получать все же не мгновенно, как только они возникли, а в пределах одной секунды, когда сессия будет сбрасывать буферы событий. Ускорить этот таймер нельзя, замедлить - можно. Подробнее можно прочитать об этом в документации на параметр FlushTimer структуры EVENT_TRACE_PROPERTIES.

Теперь мы сможем создать и настроить сессию. Переходим к последней части работы с сессиями: потребление событий от запущенной real-time-сессии. Здесь мы меняем нашу роль с контроллера сессии на потребителя событий. Рассмотрим класс event_trace, который реализует сказанное. Здесь используется библиотека boost.signals2.

Вопрос, который мог возникнуть после чтения этого кода - зачем нам событие об остановке сессии, ведь мы сами знаем, когда ее останавливаем? Снова напоминаю, что так как сессия - глобальный объект, ее может остановить кто угодно с соответствующими правами. Поэтому нам стоит знать, если логирование вдруг остановилось извне.

Еще интересен метод on_trace_event, который, помимо прочих, принимает параметр USHORT event_id. Этот метод позволяет подписаться на конкретное событие конкретного провайдера. Однако, не все события будут иметь идентификатор. Если провайдер присылает события в формате Manifest-based, то этот идентификатор будет выставлен (см. комментарии к структуре EVENT_DESCRIPTOR). Если же события описаны в формате MOF (DecodingSourceWbem), то нужно смотреть на TRACE_EVENT_INFO::EventGuid (о том, как получить эту структуру, далее). Все системные провайдеры, на которые я подписывался, предоставляли события в формате Manifest-based (DecodingSourceXMLFile), поэтому в библиотеку я добавил удобную подписку по идентификаторам ивентов, которые в этом случае выставляются. Как вы сейчас уже осознаете, API для ETW действительно не самое стройное: оно предлагает нам огромные структуры, отдельные части из которых могут использоваться лишь в особых случаях.

Начнем разбор имплементации с кода создания потребителя событий. Для этого используется WinAPI-функция OpenTrace. Стоит отметить, что и OpenTrace, и StartTrace возвращают TRACEHANDLE, но их не нужно путать: первая имеет отношение к потребителю событий имеющейся сессии, а вторая создает саму сессию. Как я уже говорил, ETW - говнистое неочевидное API, но тут ничего не поделаешь.

Наконец, начинаем обработку приходящих к нам событий с помощью функции ProcessTrace:

Функция ProcessTrace блокируется до тех пор, пока кто-то не закроет сессию либо не освободит (CloseTrace) хендл потребителя событий. Поэтому класс event_trace предоставляет возможность получать события как синхронно, так и асинхронно.

Как же мы обрабатываем получаемые события? А вот так:

Вот, собственно, и вся суть класса event_trace. Мы можем теперь создать сессию, настроить ее, добавив к ней необходимые провайдеры, а потом получить события этой сессии. Но рано расслабляться, ведь это - только половина дела (а может, и меньше)! Нам же нужно еще разобрать полученные события, получив из них интересующие нас свойства. В этом нам поможет библиотека tdh.dll. Она позволяет унифицировано распарсивать события ETW. Единственный формат, с которым могут возникнуть сложности - это WPP (невозможно будет получить форматированные строки событий, потому что для парсинга необходимы tmf-файлы), но это нас не сильно волнует, потому что формат WPP используется только для отладки приложений, а все системные провайдеры предоставляют события в других форматах.

Устройство каждого ивента в рамках tdh.dll таково:

  • Событие может содержать единственную строку, и никаких свойств в нем не будет.
  • Событие может содержать одно или более именованных свойств разных типов.
  • Каждое свойство может быть массивом переменной длины (по сути, все свойства - это массивы, но у большинства из них единственный элемент).
  • Каждое свойство может быть структурой.
  • Структура содержит набор свойств разных типов (включая массивы переменной длины), но не другие структуры.

Со всем этим добром нам и придется работать. Я написал класс event_info, который извлекает разные параметры события, включая свойства (простые, массивы и структуры) в нетипизированном виде, аж 500 строк кода, и класс event_property, который представляет конкретное значение свойства (это значение обычного свойства, один из элементов массива-свойства, либо значение параметра свойства-структуры) еще на 700 строк (точнее, сам класс маленький, а вот код конвертации нетипизированных свойств в типизированные - длинный, это класс event_property_converter). Я не буду приводить этот код полностью. Для начала я приведу интерфейсы с описанием, затем - несколько примеров кода преобразования типов, а потом дам комментарии по поводу использованных WinAPI-функций. Скажу сразу: интерфейс для работы с ивентами, предоставляемый библиотекой tdh.dll, во многих отношениях еще более неочевидный и запутанный чем то, что мы уже изучили. Начнем с event_info:

Здесь целая куча всяких методов, но нам понадобится всего несколько. Шаблонные хелперы позволяют получить сразу типизированное значение свойства. Для конвертации используется класс event_property_converter, который определен рядом с event_property. Кратенько рассмотрим класс event_property:

- Какие еще типы на входе и выходе, окаянный ты демон? Что за размеры указателей?! - Вот что вы, возможно, думаете после прочтения этой части кода. Все по порядку! Итак, тип данных на входе - это, непосредственно, тип данных свойства. Такой, как INT32, UNICODESTRING или GUID. Он указывает, что содержится в значении raw_value_type value_. А вот тип на выходе нужен лишь для отображения свойства в виде строки. Этот тип - исключительно информативный, это скорее способ форматирования свойства. Например, тип на входе может быть UINT32 (т.е. 4 байта в little endian), а на выходе - PID, или IPv4, или, например, Win32Error. Это говорит потребителю о том, как форматировать этот самый 32-разрядный unsigned integer, чтобы отобразить его пользователю. Моя библиотека не поддерживает форматирование в соответствии с типами данных на выходе и форматирует их так, как сама считает нужным. Просмотреть список всех возможных типов на входе и соответствующих им типов на выходе можно здесь и здесь.

Теперь, что там у нас с указателем (is_wide_pointer)? Если тип свойства на входе - SIZET или POINTER, то is_wide_pointer указывает, сколько байтов занимает этот тип - 4 или 8. Логично предположить, что 64-разрядные приложения будут предавать 8-байтовые значения, а 32-разрядные - 4-байтовые.

Переходим к парсингу свойств, чтобы преобразовать их к конкретным C++-типам. Я написал шаблонный класс с целой кучей специализаций, который позволяет конвертировать нетипизированные свойства (объекты класса event_property) в типизированные значения C++:

Все эти специализации позволяют со всеми возможными проверками безопасно преобразовать класс event_property в понятное и пригодное для использования значение, например, std::uint64_t. Для этого типа соответствующая специализация проверит, какой тип свойство имеет на входе (нам в случае с std::uint64_t подходят TDH_INTYPE_UINT8, TDH_INTYPE_UINT16, TDH_INTYPE_UINT32 и TDH_INTYPE_UINT64 - все они помещаются в std::uint64_t без потери точности, и все они беззнаковые, как и std::uint64_t) и корректный ли размер оно имеет. Если все условия выполнены, то будет произведена конвертация, в противном случае мы получим исключение.

Для конвертации числовых и некоторых других типов я написал общую шаблонную функцию, которая сравнивает тип на входе переданного ей нетипизированного свойства (event_property) с теми типами, которые мы задали. Если тип совпадает, то проверяем размер переданного типизированного значения. Если все совпадает, производим конвертацию (с помощью обычного reinterpret_cast):

А вот как это применяется на примере конвертации в std::uint32_t:

А вот как это применяется на примере конвертации в std::uint32_t:

Аналогичным образом мы можем получить, скажем, float:

Разумеется, со строковыми типами такое не прокатит (а в событии строка может содержаться аж в восьми разных форматах), поэтому мне пришлось писать еще одну шаблонную функцию для конвертации строк в std::string / std::wstring. Ее я приводить не буду, код можно посмотреть, скачав солюшен в конце статьи. Еще отдельные функции написаны для конвертации типов SYSTEMTIME и FILETIME. Все остальное в той или иной мере использует описанные заготовки.

Нам осталось рассмотреть набор WinAPI-функций из библиотеки tdh.dll, которые я использовал. Предположим, что у нас изначально нет ничего, кроме указателя PEVENT_RECORD - это то, что мы получили в свой коллбэк после вызова ProcessTrace.

Есть отдельный случай, когда ивент содержит единственную строку, и ничего более. В этом случае в записи события (PEVENT_RECORD) будет выставлен соответствующий флаг EVENT_HEADER_FLAG_STRING_ONLY:

В этом случае нам не нужно ничего парсить. На практике я с такими событиями не сталкивался. Далее рассмотрим, что делать, если наш ивент всё же содержит набор свойств разных типов. Для начала нам необходимо получить некую дополнительную информацию о событии. Для этого используется функция TdhGetEventInformation. Первый раз мы ее вызываем, передав нулевой буфер, получаем на выходе необходимый размер буфера, выделяем буфер подходящего размера и вызываем эту функцию еще раз. Эта функция загружает необходимую для разбора события информацию. Если потребуется, она даже загрузит DLL-файл провайдера, в котором содержатся метаданные для разбираемого события, и считает их сама. Это достаточно удобно!

Полученная структура (TRACE_EVENT_INFO, ее я уже упоминал) содержит некоторые интересные нам свойства. Это, например, TopLevelPropertyCount - сколько свойств верхнего уровня содержится в событии. Напомню, что свойства верхнего уровня - это свойства, которые не вложены в структуру. Также она содержит имена всех свойств события. Получить их можно так:

Не самый красивый код, но и это неплохо. А как же нам получить данные какого-либо свойства? Допустим, мы уже знаем его индекс index. Но мы ведь еще не определили, является ли это свойство обычным значением или структурой? Содержится ли в этом свойстве всего одно значение, или их несколько (т.е. свойство - массив значений)? Вот как мы можем определить, структура ли свойство:

Здесь пока все понятно: если в наборе флагов имеется PropertyStruct, значит, мы имеем дело со структурой. Теперь нам нужно определить количество элементов свойства (ведь оно по-прежнему может быть массивом значений). А вот это уже сложнее:

Итак, количество элементов массива может храниться как в специальной переменной count, так и в отдельном свойстве типа UINT16 или UINT32, индекс которого лежит в countPropertyIndex. Мозг уже закипает, а ведь я еще не успел рассмотреть, как же получить нетипизированные данные свойства (которые содержат его значение)!

Сперва рассмотрим простой случай, когда у нас свойство - это обычное значение (не структура). Сначала необходимо заполнить массив структур PROPERTY_DATA_DESCRIPTOR. В нашем случае такая структура будет всего одна:

Теперь, подготовив структуру, мы можем вызвать TdhGetPropertySize и получить, наконец-то, этот злосчастный размер свойства:

Наконец-то в переменной property_size мы имеем размер свойства. Теперь мы можем подготовить буфер нужного размера, чтобы затем скопировать туда данные интересующего свойства с помощью TdhGetProperty.

Вот и всё! Осталась пара мелочей - получить типы свойства на входе и на выходе, а также размер указателя (на случай, если тип свойства - SIZET или POINTER), но это уже пустяки:

Фууух! Этот API действительно крепкий орешек. А что там со свойствами-структурами? Все еще интереснее! Вот тут нам и понадобится подготавливать массив из ДВУХ элементов PROPERTY_DATA_DESCRIPTOR. Но сперва я расскажу, как эти самые свойства-структуры хранятся в ивенте. По сути, структура - это диапазон индексов свойств. Например, у нас с индекса 4 по индекс 6 лежат три свойства разных типов. Структура объединяет эти три свойства в одно. Соответственно, эти индексы (4, 5, 6) не будут фигурировать в TopLevelPropertyCount, эти индексы не считаются индексами свойств верхнего уровня, но информация о них все равно хранится в событии (в массиве EventPropertyInfoArray). Таким образом, структура - это просто условная группировка нескольких обычных свойств под одним именем структуры. Вложенными структуры быть не могут (и на том спасибо!). Итак, как же нам получить диапазон этих самых индексов?

Здесь struct_start_index будет содержать первый индекс свойства, входящего в структуру, а member_count - количество свойств в структуре. Для наглядности приведу пример лейаута всего этого добра, чтобы было понятнее:

В этом примере свойства верхнего уровня имеют индекс с 0 до 3 включительно. Свойства 0, 1, 3 - это простые свойства, а вот 2 - структура. В структуре указан начальный индекс входящих в нее свойств (4) и количество свойств (3). Таким образом, свойства с индексами 4, 5, 6 (это уже не свойства верхнего уровня) включены в структуру с индексом 2.

Нужно помнить, что сама структура может быть массивом, плюс каждое свойство внутри структуры также может быть массивом! Теперь мы готовы заполнить PROPERTY_DATA_DESCRIPTOR:

Такие дела. Далее, в общем-то, все аналогично - вызываем сначала TdhGetPropertySize, передав туда этот массив структур PROPERTY_DATA_DESCRIPTOR и не забыв указать, что у нас их теперь две, а затем TdhGetProperty. При получении типа на входе и на выходе не забываем, что индекс теперь нужно использовать тот, который указывает не на структуру, а на то свойство структуры, которое мы получаем:

Вот теперь точно все! Очевидно, TDH API - не самое приятное из имеющихся в Windows, но оно на самом деле достаточно облегчает дело получения свойств из событий. Стоит упомянуть, что существует даже функция TdhFormatProperty, которая за вас может отформатировать свойство в нужный формат (она смотрит на тип свойства на выходе) и преобразовать его в строку. Останется только вывести на экран! Вот пример ее использования.

Мы подошли к концу изучения ETW API с точки зрения потребителя ивентов (consumer) и контроллера сессии (controller). Время переходить к примеру его использования. В конце-концов, я ведь хотел отслеживать процессы в системе. Я набросал простую консольную программку (используя свою библиотеку для работы с ETW), которая выводит получаемые ивенты, все их свойства с именами, значениями и типами. Посмотрим на ее код, а точнее, на самую его мякотку в функции main:

Из новенького в этом коде - получение GUID провайдера по его имени. Это делается с помощью перечисления всех провайдеров системы (функция TdhEnumerateProviders), код запрятан внутрь класса event_provider_list. Оператор << для класса event_info я перегрузил, он выводит все свойства события с их типами. После запуска программы я могу открыть, например, notepad.exe и увидеть в логе следующее:

Неплохо получилось! Мы видим, что запустился notepad.exe, потом, что в нем стартанул какой-то поток, затем видим, какие модули загружаются в этот процесс. Разумеется, это далеко не все созданные в процессе потоки, и модулей там тоже гораздо больше, но я это все опустил для краткости. Когда мы notepad.exe закроем, мы увидим, как все эти потоки останавливаются, модули выгружаются и, наконец, сам процесс завершается.

Для пущей наглядности я написал утилитку с GUI, которая мониторит в реальном времени все создаваемые процессы, смотрит, какие у них имеются потоки и какие модули в них загружены. Ее код - также в солюшене в конце статьи, а пока что - ее скриншот:

ETW Process Tracker

Написана она на чистом WinAPI, без каких-либо GUI-библиотек. Работоспособность утилиты проверена на Win7 x64 и Win10 x64. Конечно, эта утилита не покажет те процессы, которые были запущены до того, как она сама запустилась, но мы видим все те процессы, которые стартанули позже, а также всё, что происходит с их потоками и модулями.

Надеюсь, вы получили для себя какие-то новые знания об ОС Windows и работе с ETW! Конечно, я не покрыл и половины темы про Event Tracing for Windows (например, не рассказал про kernel logger, с которым работа ведется совсем по-другому, и ничего не пояснил про выдачу ивентов с точки зрения провайдера). Но эта тема очень обширна, и имеющегося материала уже хватило на огромную статью!

Напоследок приведу еще несколько полезных ссылок на всякие релевантные ресурсы помимо тех, которые указаны в самом начале статьи:

И, наконец, исходники моей библиотеки для разбора событий, создания сессий, получения событий, а также программ-примеров использования (консольный- и GUI-отслеживатель процессов): Скачать примеры и библиотеку для работы с ETW (исходники), Скачать примеры в собранном виде (пароль на архив - kaimi.io), Исходники всех проектов и библиотеки на GitHub.

Изучаем ETW и извлекаем профиты: 23 комментария

  1. Я года 2 назад пересел с с++ на c#, и глядя на код еще раз убедился что не зря ненавижу плюсы))
    Спасибо, статья действительно очень большая и сложная, но полезная. Честно говоря не знал о таком функционале до сегодняшнего дня. Читая статью поразился, как у человека столько знаний в голове помещается)) Мало того, хороший программист далеко не всегда хороший учитель, а тут еще и так подробно все расписано.
    Я так понимаю, частично информацию о событиях процесса можно посмотреть, когда например программа крешит, и в Управление компьютером -> Просмотр событий -> Сводка административных событий можно увидеть что-то типа такого:

    Имя сбойного приложения: Explorer.EXE, версия: 6.1.7601.17514, отметка времени: 0x4ce7a144
    Имя сбойного модуля: bcryptprimitives.dll, версия: 6.1.7601.17514, отметка времени 0x4ce7c4f0
    Код исключения: 0xc0000005
    Смещение ошибки: 0x0000000000007a94
    Идентификатор сбойного процесса: 0x8d4
    Время запуска сбойного приложения: 0x01d2a0a11f06420a
    Путь сбойного приложения: C:\Windows\Explorer.EXE

    Вся эта информация там появляется при помощи ETW?

    1. Спасибо за лестный комментарий :)

      По поводу некрависого кода - дело не в C++, а в низкоуровневом ETW API. Если писать обертки над этими API на C#, то лапша не меньше будет, чем на плюсах. Например, вот - тут и функции на 300 строк, и магические константы, и работа с указателями в unsafe-функциях... А конечный интерфейс для пользователя можно красивым сделать на любом языке.

      По поводу крешей - да, насколько я знаю, начиная с WinXP журнал событий Windows работает исключительно с ETW. События о падении приложений туда может отсылать ядро ОС. В списке ETW-сессий (как их посмотреть, описано в статье) на Win7 я вижу, например, EventLog-Application, EventLog-System и другие подобные. Думаю, что именно они используются для формирования журналов событий Windows.

  2. Мощная статья, всю не осилил, но взял на заметку, что в винде есть такое, и где, если понадобится, можно освежить память по теме.
    Спасибо.

  3. Привет, такой вопрос. Что думаете по поводу языка Golang. В нем можно делать много зеленых потоков и отличная работа с I/O. Нормально ли подойдет для для создания различных прожек рассыльщиков сообщений по сайтам, парсерам и т.д? Или лучше взять что-нибудь типо java. Но в Golang еще нормальная вещь, что можно нормально компилировать в бинарник и запускать на линукс серверах без дополнительного софта наподобие интерпретаторов.

    1. Привет, go только краем глаза видел, но ничего на нем не писал. Но, думаю, что для подобного софта подойдет. А в Java, конечно, готовых библиотек всяких разных побольше будет. Так что я бы сначала посмотрел, есть ли для языка написанные готовые либы, которые могут пригодиться, или придется их самому писать ручками.

    1. Обратись к http://blog.cr4.sh/p/blog-page.html, он тебе пояснит, что в Win10, с точки зрения безопасности, множество вещей реализовано лучше, чем в MacOS и типичных Linux дистрах.
      Та же Enterprise версия практически не содержит ненужного барахла и в ней легко отключается отправка данных, если волнует этот момент.

  4. Добрый день, подскажите, пожалуйста:
    1) как по имени сессии etw узнать, имя файла трассировки и путь до него
    2) как найти приложение (контроллер), который отвечает за данную сесиию?

    1. 1) Думаю, подойдёт функция ControlTrace. В TraceHandle можно передать NULL, а в InstanceName, соответственно, имя интересующей сессии. ControlCode должен быть, видимо, EVENT_TRACE_CONTROL_QUERY. В Properties заполнить Wnode.BufferSize, Wnode.Guid, LoggerNameOffset, and LogFileNameOffset. Далее после вызова функции по переданному смещению будет имя файла.

    2. 2). А вот тут, вероятно, никак. Сессия не принадлежит приложению и остаётся открытой и рабочей в том случае, если приложение уже закрылось, но не вызвало CloseTrace. Кроме того, одну сессию может слушать несколько приложений одновременно.

  5. Добрый день, dx, спасибо Вам большое за предыдущий ответ!
    Я решил посмотреть текущие сессии через xperf -loggers. Меня удивил тот факт, что не у каждой работающей сессии ETW есть файл журнала логирования (*.etl)
    Как так, ведь сессия работает?

    1. Сессия может быть настроена без журнала логирования. Приложение-пример, которое в статье разбирается, именно такую сессию и создает. Такая сессия используется только для потребления событий приложениями в реальном времени. Можно глянуть документацию на EVENT_TRACE_REAL_TIME_MODE.

  6. Доброго времени суток!

    Спасибо большое за статью, отличное введение в ETW.

    А не знаете как можно достать другую информацию о событии? Например, TimeCreated и Execution (картинку того, что имею ввиду прилагаю)
    https://yapx.ru/v/UL7vi

    1. Приветствую, эти данные находятся в структуре EVENT_HEADER. Туда можно добраться из EVENT_RECORD::EventHeader.
      TimeStamp - это, видимо, TimeCreated. Из Execution: KernelTime, UserTime, ThreadId, ProcessId. ProcessorId из Execution можно получить с помощью GetEventProcessorIndex (определена в evntcons.h):

      1. Спасибо) разобрался. А вот я по полученному ProcessId хотел еще имя процесса получать, допустим открыл я файл в ворде, и чтобы мне залогировалось, что это ворд открыл. Однако на все такие события, по ProcessId я получаю имя процесса "explorer.exe", а не конкретную программу. Можно ли через ETW выудить имя приложения, которое задействовало файл?

        1. Мой пример вроде бы корректно отображает имена процессов. Открыл ворд, вижу в списке отслеживаемых процессов WINWORD.exe, а explorer не вижу.
          process tracker

          1. Прошу прощения, не указал, что я использую Microsoft-Windows-Kernel-File провайдер.
            С помощью него я слежу за I/O операциями над определенным файлом. Но этот провайдер может лишь выдать Process ID. По этому ID я пытаюсь получить имя процесса, но к сожалению мне выдается только "explorer.exe". Можно ли в таком случае как то добыть имя процесса?

            1. Так может быть explorer.exe и выполняет эти I/O операции? Плюс, у многих ивентов в этом провайдере есть поле ThreadId / IssuingThreadId. Может быть, можно по нему определить, какой процесс выполнил операцию (вызвав, например, OpenThread и потом GetProcessIdOfThread. Или, воспользовавшись ToolHelp API, поддерживать актуальный список всех процессов и их потоков, и там искать по ID потока).

Добавить комментарий для Kaimi Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *