Автор: Артем Шишкин, исследовательский центр Positive Research.
А вы никогда не задумывались над тем, что же происходит с операционной системой в тот момент, когда она рисует свой логотип и говорит «Starting Windows»? И вообще, почему она долго загружается? Ведь при старте системы уж точно не решаются никакие задачи, сложные с вычислительной точки зрения!
Что тогда подразумевает под собой загрузка операционной системы? По большей части это проецирование в память исполняемых модулей и инициализация служебных структур данных. Структуры данных живут в памяти, поэтому операции с ними по идее должны быть быстрыми. Все наталкивает на мысль о том, что время съедается именно процессом загрузки исполняемых модулей в память.
Давайте интереса ради разберемся, какие модули, в каком количестве и в каком порядке загружаются при старте ОС. Чтобы выяснить это, можно, например, получить лог загрузки системы. Подопытная ОС в моем случае — Windows 7 Enterprise x64. Логировать процесс загрузки будем при помощи отладчика ядра. Существует несколько вариантов отладчиков ядра, лично я предпочитаю WinDbg. Также нам понадобятся некоторые вспомогательные средства для волшебного превращения лога в нечто более приятное глазу.
Mining and crafting
Настройка отладки хорошо гуглится, поэтому описывать подробно этот процесс я не буду. Поскольку нас интересует все происходящее с момента старта системы, нам нужно отметить пункт «Cycle Initial Break», с помощью чего отладчик остановится, как только в отлаживаемой системе будет загружена подсистема отладки ядра. Дублирование вывода в файл можно осуществить при помощи команд «.logopen» и «.logclose», это просто. Другая полезная команда — «.cls». Она очищает экран команд, и да, только экран команд.
Интересующая нас функция — «MiCreateImageFileMap». Это внутренняя функция менеджера памяти, проецирующая исполняемый файл в память. Проецирование в память происходит при создании секции, например, при запуске исполняемого файла. Однако учтите, что если исполняемый файл проецируется в память, это не гарантия того, что будет выполнен его код! Эта функция просто создает проекцию, чаще всего «про запас», чтобы, если кто-то надумает запустить модуль на исполнение, можно было сэкономить время его загрузки. На эту функцию поставим логирующую точку останова.
Если у вас достаточно маны, вводите следующую команду:
bu nt!MiCreateImageFileMap "dt nt!_EPROCESS -d ImageFileName @$proc; dt nt!_FILE_OBJECT -d FileName @rcx; g"
Магическая строчка буквально означает следующее:
- bu (Set Unresolved Breakpoint) — установить неразрешенную точку останова. Не то чтобы кто-то или что-то не разрешал, просто для ее установки необходимо определиться, по какому адресу ее ставить. Дело в том, что заранее не известно, по какому адресу она должна располагаться. При загрузке любого модуля проверяется присутствие в нем необходимой функции, и если такая функция найдена, точка останова устанавливается автоматически. Такой способ установки незаменим при включенном ASLR — рандомизации адресного пространства, поскольку модули будут загружаться каждый раз по разным адресам, и точка останова, установленная по фиксированному адресу, с большой вероятностью окажется не у дел.
- nt!MiCreateImageFileMap — символ, на котором нужно останавливаться. В WinDbg принята запись в форме ‘module_name!function_name’. В данном случае nt является предопределенным псевдонимом для ntoskrnl.exe.
- далее следует часть WinDbg-скрипта, которая будет выполняться каждый раз при остановке на этой функции. «dt nt!_EPROCESS -d ImageFileName @$proc» по-русски означает «отобразить поле ImageFileName структуры _EPROCESS из модуля nt при условии ее отображения по адресу, определенному в псевдорегистре «текущий процесс»». Следующая после разделителя «;» команда означает примерно то же самое, только адрес структуры берется из регистра rcx, в котором в Microsoft x64 ABI передается первый параметр функции. «g» означает «go», т.е. продолжить исполнение.
Небольшая рекомендация по использованию логирующих точек останова: старайтесь не использовать расширения отладчика (команды, начинающиеся с «!»), поскольку в таком случае логирование будет выполняться на порядок медленнее.
Поехали! Отжимаем тормоз точки останова и ждем. Я ждал, пока не прогрузится рабочий стол, т.е. я залогинился. Полученный «урожай» немного редактируется, обрезается все лишнее для удобства дальнейшей обработки и скармливается дружище питону. Не будем заострять внимание на парсинге лога. Отметим только, что граф укладывался в форму спирали Архимеда с дальнейшей коррекцией вручную, поскольку происходило наложение узлов друг на друга. В полученном графе учитывается порядок загрузки библиотек. К сожалению, пришлось пожертвовать учетом порядка загрузки исполняемых файлов относительно библиотек в угоду удобочитаемости графа.
Карта звездного неба
Условно выделим несколько групп загрузки.
Начинается работа OC в модуле ntoskrnl.exe, являющимся ядром ОС. А если еще конкретнее — с функции KiSystemStartup(). Вместе с загружаемыми системными компонентами она формирует фундамент ОС: разделение режимов работы, базовые сервисы для пользовательских приложений и т.п. В эту же группу входят драйверы, отмеченные для загрузки во время старта системы. В двух словах, в этой ракушке зарождается ОС Windows.
Следующий узел — менеджер сессий (session manager). Его представляет первый после системного процесс, стартующий в Windows — smss.exe. Процесс примечателен тем, что является родным (native) процессом Windows, то есть он не использует подсистему Win32, которая в общем-то еще не загружена. Этот процесс использует только нативные сервисы операционной системы посредством ntdll.dll, представляющей собой интерфейс режима пользователя для сервисов ОС. Также этот процесс является доверенным компонентом операционной системы и обладает исключительными правами, например, он может создавать маркеры безопасности (security tokens). Но главное его предназначение — создание сеансов и инициализация подсистем, как графической, так и различных исполняемых (Windows, POSIX). Эта ракушка воздает каждому по потребностям.
Последним отмечу любимый всеми explorer.exe. Примечательно, что к моменту его запуска все используемые им модули уже загружены в память. В скриншот также попал некий vcredist_x64.exe — бедолага лежал на рабочем столе подопытной виртуальной машины и был прогружен в память проводником.
Вообще способов оказаться загруженным в память у модуля много. Например, достаточно запросить информацию из ресурсов исполняемого файла, в том числе его иконку. Конкретно в данном примере проводник проверял, является ли эта программа требующей повышенных привилегий, т.е. стоит ли дорисовывать к иконке соответствующий рисуночек с желто-голубым щитом. Еще раз отмечу, что загрузка модуля в память не означает выполнение его кода!
Лично я держу получившуюся картинку под боком. По ней хорошо прослеживаются зависимости, например, драйверов. Также в паре с утилитой Sysinternals Autoruns можно увидеть, на каком этапе загрузки подтягиваются те или иные модули.
Граф загрузки был построен для ОС Windows 7 Enterprise x64, установленной на виртуальной машине VMware. Ниже приведены векторное изображение графа и непосредственно файл в формате gml, с которым можно поиграться в любом редакторе графов.
Граф в формате GML
Векторное изображение графа
Внимание! Бонус!
Граф загрузки для чистой ОС Windows 8 Enterprise x64 на живой машине 😉
Граф в формате GML
Векторное изображение графа
Ингредиенты:
WinDbg @ msdn.microsoft.com/en-us/windows/hardware/gg463009.aspx
Python @ www.python.org/
NetworkX @ networkx.lanl.gov/
yEd Graph Editor @ www.yworks.com/en/products_yed_about.html
Руки @ Плечи