Внутреннее устройство Windows. 7-е изд.

Глава 3. Процессы и задания

В этой главе рассматриваются структуры данных и алгоритмы, относящиеся к процессам и заданиям Windows. Сначала мы опишем создание процесса в целом. Затем рассмотрим внутренние структуры, составляющие процесс. Далее вы узнаете о защищенных процессах и о том, чем они отличаются от процессов незащищенных. Далее мы опишем действия по созданию процесса (и его исходного потока). Глава завершается описанием заданий.

Процессы связаны со многими компонентами Windows, поэтому многие термины и структуры данных (рабочие наборы, потоки, объекты и дескрипторы, кучи системной памяти и т.д.) упоминаются в этой главе, но подробно рассматриваются в других главах. Чтобы в полной мере понять эту главу, вы должны знать термины и концепции, представленные в главе 1 «Концепции и средства» и главе 2 «Архитектура системы» — в частности, понимать различия между процессами и потоками, знать структуру виртуального адресного пространства Windows и представлять суть различий между пользовательским режимом и режимом ядра.

Создание процесса

Windows API предоставляет несколько функций для создания процессов. Простейшая из них — функция CreateProcess — пытается создать процесс с таким же маркером доступа, как у процесса-создателя. Если необходимо использовать другой маркер, можно воспользоваться функцией CreateProcessAsUser, которая получает дополнительный аргумент — дескриптор объекта маркера, который уже был получен каким-то образом (например, при вызове LogonUser).

К числу других функций создания процессов относятся функции CreateProcessWithTokenW и CreateProcessWithLogonW (обе принадлежат advapi32.Dll). Функция CreateProcessWithTokenW похожа на CreateProcessAsUser, но они различаются уровнем необходимых привилегий для вызывающей стороны. (За подробностями обращайтесь к документации Windows SDK.) CreateProcessWithLogonW представляет собой удобный сокращенный способ входа с учетными данными указанного пользователя и создания процесса с полученным маркером за один вызов. Обе функции обращаются к службе вторичного входа (seclogon.dll с хостом SvcHost.Exe), выдавая вызов RPC для фактического создания процесса.

SecLogon выполняет вызов в своей внутренней функции SlrCreateProcessWithLogon, и если все прошло нормально, в итоге вызывает CreateProcessAsUser. Служба SecLogon по умолчанию настраивается для ручного запуска, поэтому при первом вызове CreateProcessWithTokenW или CreateProcessWithLogonW происходит запуск службы. Если попытка запуска завершается неудачей (например, администратор может отключить службу), вызовы функций не проходят. Также эти функции используются программой командной строки runas — вероятно, вам знакомой.

На рис. 3.1 изображена диаграмма вызовов, описанных выше.

348315.png 

Рис. 3.1. Функции создания процесса. Внутренние функции помечены пунктирными рамками

Все функции, документированные выше, рассчитаны на файл PE (Portable Executable) с правильной структурой (хотя расширение EXE не обязательно), пакетный файл или 16-разрядное COM-приложение. В остальном они не знают, как связать файл с определенным расширением (например, .txt) с исполняемым файлом (например, приложением Блокнот). Эта функциональность предоставляется Оболочкой Windows в таких функциях, как ShellExecute и ShellExecuteEx. Эти функции могут получить произвольный файл (не только исполняемые файлы) и попытаться найти исполняемый файл на основании расширения и настроек реестра в HKEY_CLASSES_ROOT. (См. главу 9 «Механизмы управления» части 2.)

Затем ShellExecute(Ex) вызывает CreateProcess с подходящим исполняемым файлом и добавляет в командную строку аргументы для реализации намерения пользователя (например, имени файла TXT для его редактирования в Блокноте).

В конечном итоге все ветви выполнения ведут к общей внутренней функции CreateProcessInternal, которая начинает реальную работу по созданию процесса Windows пользовательского режима. Если все проходит нормально, CreateProcessInternal вызывает NtCreateUserProcess из Ntdll.dll, чтобы передать управление в режим ядра и продолжить процесс создания режима ядра в одноименной функции (NtCreateUserProcess), части исполняющей системы.

Аргументы функций CreateProcess*

Стоит обсудить аргументы семейства функций CreateProcess*, часть которых будет рассматриваться при описании логики выполнения CreateProcess. Процесс, созданный из пользовательского режима, всегда создается с одним потоком. Этот поток в конечном итоге будет выполнять главную функцию исполняемого файла. Важные аргументы функций CreateProcess*:

• Для CreateProcessAsUser и CreateProcessWithTokenW — дескриптор маркера, под которым будет выполняться новый процесс. Аналогичным образом для CreateProcessWithLogonW требуется имя пользователя, домен и пароль.

• Путь к исполняемому файлу и аргументы командной строки.

• Дополнительные атрибуты безопасности, применяемые к новому процессу и создаваемому объекту потока.

• Флаг, указывающий, должны ли все дескрипторы текущего (создающего) процесса, помеченные как наследуемые, наследоваться (копироваться) в новый процесс. (Подробнее о дескрипторах и наследовании дескрипторов см. в главе 8 «Системные механизмы».)

• Различные флаги, влияющие на создание процессов. Несколько примеров (за полным списком обращайтесь к документации Windows SDK):

• CREATE_SUSPENDED — исходный поток нового процесса создается в приостановленном состоянии. Последующий вызов ResumeThread заставляет поток начать выполнение;

• DEBUG_PROCESS — создающий процесс объявляет, что он является отладчиком, создающим новый процесс под своим контролем;

• EXTENDED_STARTUPINFO_PRESENT — вместо STARTUPINFO (см. ниже) передается расширенная структура STARTUPINFOEX.

• Необязательный блок среды нового процесса (с указанием переменных среды). Если среда не задана, она наследуется от создающего процесса.

• Необязательный текущий каталог нового процесса. (Если он не задан, используется текущий каталог создающего процесса.) Созданный процесс позднее может установить другой текущий каталог вызовом SetCurrentDirectory. Текущий каталог процесса используется в различных вариантах поиска с неполным путем (например, при загрузке DLL с указанием только имени файла).

• Структура STARTUPINFO или STARTUPINFOEX с дополнительными данными конфигурации для создания процесса. STARTUPINFOEX содержит дополнительное непрозрачное поле, представляющее список атрибутов процесса и потока (фактически массив пар «ключ/значение»). Эти атрибуты заполняются вызовами UpdateProcThreadAttributes для каждого необходимого атрибута. Некоторые атрибуты не документированы и используются во внутренней реализации, как при создании приложений Магазина (см. следующий раздел).

• Структура PROCESS_INFORMATION, содержащая выходные данные успешного создания процесса. Структура содержит новый уникальный идентификатор процесса, новый уникальный идентификатор потока, дескриптор нового процесса и дескриптор нового потока. Значения дескрипторов могут пригодиться создающему процессу, если он хочет выполнить какие-то операции с новым процессом или потоком после создания.

Создание современных процессов Windows

В главе 1 были описаны новые типы приложений, появившиеся в Windows 8 и Windows Server 2012. Термины для обозначения таких приложений меняются со временем, но мы будем называть их современными приложениями (а также приложениями UWP или иммерсивными процессами), чтобы отличить их от классических (также называемых настольными) приложений.

Создание процесса современного приложения не сводится к простому вызову CreateProcess с правильным путем к исполняемому файлу; также появляются некоторые обязательные аргументы командной строки. Другое требование заключается в добавлении недокументированного атрибута процесса (с использованием UpdateProcThreadAttribute) с ключом PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME и значением, которому присваивается полное имя пакета приложения Магазина. Хотя атрибут не документирован, существуют и другие способы (с точки зрения API) выполнения приложений Магазина. Например, Windows API включает интерфейс COM с именем IApplicationActivationManager, который реализуется классом COM с идентификатором CLSID CLSID_ApplicationActivationManager. Один из методов этого интерфейса, ActivateApplication, может использоваться для запуска приложения Магазина после получения идентификатора AppUserModelId из полного имени пакета вызовом GetPackageApplicationIds. (За дополнительной информацией об этих API обращайтесь к Windows SDK.)

Имена пакетов и типичный механизм создания процесса приложения Магазина с момента нажатия пользователя на плитке современного приложения до итогового вызова CreateProcess описаны в главе 9 части 2.

Создание других разновидностей процессов

Хотя приложения Windows запускают либо классические, либо современные приложения, исполняющая система включает поддержку других видов процессов, которые должны запускаться в обход Windows API, таких как собственные процессы, минимальные процессы или процессы Pico. Например, в главе 2 упоминалось о существовании диспетчера сеансов Smss, который является примером машинного образа. Так как он создается ядром, очевидно, процесс создания не использует API CreateProcess; вместо этого NtCreateUserProcess вызывается напрямую. Аналогичным образом, когда Smss создает Autochk (программа проверки диска) или Csrss (процесс подсистемы Windows), функции Windows API также недоступны, и вместо них необходимо использовать NtCreateUserProcess. Кроме того, собственные процессы не могут создаваться на базе приложений Windows, так как функция CreateProcessInternal отвергает образы с типом обычной подсистемы. Для устранения этих сложностей библиотека Ntdll.dll включает экспортированную вспомогательную функцию RtlCreateUserProcess, которая предоставляет более простую и удобную обертку для NtCreateUserProcess.

Как подсказывает имя, функция NtCreateUserProcess используется для создания процессов пользовательского режима. Однако, как мы узнали из главы 2, Windows также включает ряд процессов пользовательского режима, например процессы System и Memory Compression (которые являются минимальными процессами), а также возможность управления процессами Pico со стороны поставщиков, например подсистемой Windows для Linux. Вместо этого созданием таких процессов занимается системная функция NtCreateProcessEx, часть возможностей которой зарезервирована исключительно для вызовов из режима ядра (например, созданием минимальных процессов).

Наконец, поставщики Pico вызывают вспомогательную функцию, которая обеспечивает как создание минимальных процессов, так и инициализацию контекста поставщика Pico — PspCreatePicoProcess. Эта функция не экспортируется и доступна только поставщикам Pico через специальный интерфейс.

Как будет показано далее в этой главе, хотя NtCreateProcessEx и NtCreate­UserProcess формально являются разными системными функциями, для выполнения работы используются одни и те же внутренние функции: PspAllocateProcess и PspInsertProcess. Все возможные способы создания процессов, упоминавшиеся до настоящего момента, и любые возможные способы, которые можно себе представить, от командлетов WMI PowerShell до драйвера режима ядра, в конечном итоге приходят в эту точку.

Внутреннее устройство процессов

В этом разделе описаны ключевые структуры данных процессов Windows, поддерживаемые различными компонентами системы, а также различные механизмы и средства для анализа этих данных.

Каждый процесс Windows представляется структурой EPROCESS (Executive Process). Кроме многих атрибутов, относящихся к процессу, EPROCESS содержит ряд других взаимосвязанных структур данных и указателей на них. Например, каждый процесс содержит один или несколько потоков, каждый из которых представлен структурой ETHREAD (Executive Thread). (Структуры данных потоков рассматриваются в главе 4 «Потоки».)

Структура EPROCESS и большинство связанных с ней структур данных существует в системном адресном пространстве. Исключение составляет блок PEB (Process Environment Block), который существует в адресном пространстве процесса (пользовательском), потому что он содержит информацию, доступную для кода пользовательского режима. Кроме того, некоторые структуры данных процесса, используемые при управлении памятью (например, список рабочих наборов), действительны только в контексте текущего процесса, поскольку хранятся в системном пространстве, принадлежащем процессу. (Подробнее об адресном пространстве процессов см. в главе 5 «Управление памятью».)

Для каждого процесса, выполняющего программу Windows, процесс подсистемы Windows (Csrss) поддерживает параллельную структуру с именем CSR_PROCESS. Кроме того, часть подсистемы Windows, относящаяся к режиму ядра (Win32k.sys), поддерживает структуру данных уровня процесса W32PROCESS, которая создается при первом вызове из потока функции Windows USER или GDI, реализованной в режиме ядра. Это происходит сразу же после загрузки библиотеки User32.dll. Типичные функции, инициирующие загрузку этой библиотеки, — CreateWindow(Ex) и GetMessage.

Так как часть подсистемы Windows режима ядра интенсивно использует графику с аппаратным ускорением на базе DirectX, инфраструктура компонента GDI (Graphics Device Interface) заставляет графическое ядро DirectX (Dxgkrnl.sys) инициализировать собственную структуру DXGPROCESS. Эта структура содержит информацию для объектов DirectX (поверхности, шейдеры и т.д.), а также счетчики и параметры политики, относящиеся к GPGPU, для планирования как вычислений, так и управления памятью.

За исключением процесса Idle, каждая структура EPROCESS инкапсулируется в виде объекта процесса диспетчером объектов (см. главу 8 части 2). Поскольку процессы не являются именованными объектами, они не отображаются в программе WinObj (из пакета Sysinternals). Впрочем, в каталоге \ObjectTypes виден объект Type с именем Process (в WinObj). Дескриптор процесса предоставляет (через связанные с процессом API) доступ к некоторым данным структуры EPROCESS и некоторым из связанных с ней структур.

Многие другие драйверы и компоненты системы, регистрируя уведомления о создании процессов, могут создавать собственные структуры данных для хранения информации на уровне процессов. (Функции PsSetCreateProcessNotifyRoutine(Ex, Ex2), документированные в WDK, предоставляют такую возможность.) При анализе затрат памяти процесса часто приходится учитывать размер таких структур, хотя получить точное числовое значение практически невозможно. Кроме того, некоторые из этих функций позволяют таким компонентам запрещать (блокировать) создание процессов. Таким образом, производителям средств защиты от вредоносных программ предоставляется архитектурный механизм включения усовершенствований безопасности в операционную систему — с применением черных списков на базе хешей либо других средств.

Для начала сосредоточимся на объекте Process. На рис. 3.2 изображены ключевые поля структуры EPROCESS.

386635.png 

Рис. 3.2. Важнейшие поля структуры EPROCESS

348487.png 

Рис. 3.3. Важнейшие поля структуры KPROCESS

Мы уже показывали, как API и компоненты ядра разбиваются на изолированные многоуровневые модули с собственными соглашениями имен. Структуры данных процесса строятся по похожему принципу. Как видно из рис. 3.2, первое поле структуры процесса называется PCB (Process Control Block). Это структура типа KPROCESS (Kernel Process). Хотя функции исполняющей системы хранят информацию в EPROCESS, диспетчер, планировщик и код учета прерываний/времени — части ядра операционной системы — используют KPROCESS. Это позволяет ввести абстрактную прослойку между высокоуровневой функциональностью исполняющей системы и низкоуровневой реализацией некоторых функций, а также предотвратить нежелательные зависимости между уровнями. На рис. 3.3 показаны ключевые поля структуры KPROCESS.

Эксперимент: вывод формата структуры EPROCESS

Чтобы получить список полей, образующих структуру EPROCESS, и их смещений в шестнадцатеричной форме, введите команду dt nt!_eprocess в отладчике ядра. (Подробнее об отладчике ядра и выполнении отладки режима ядра в локальной системе см. в главе 1.) Вывод (сокращенный для экономии места) в 64-разрядной системе Windows 10 выглядит так:

lkd> dt nt!_eprocess

    +0x000 Pcb              : _KPROCESS

    +0x2d8 ProcessLock      : _EX_PUSH_LOCK

    +0x2e0 RundownProtect   : _EX_RUNDOWN_REF

    +0x2e8 UniqueProcessId  : Ptr64 Void

    +0x2f0 ActiveProcessLinks : _LIST_ENTRY

...

    +0x3a8 Win32Process     : Ptr64 Void

    +0x3b0 Job              : Ptr64 _EJOB

...

    +0x418 ObjectTable      : Ptr64 _HANDLE_TABLE

    +0x420 DebugPort        : Ptr64 Void

    +0x428 WoW64Process     : Ptr64 _EWOW64PROCESS

...

    +0x758 SharedCommitCharge : Uint8B

    +0x760 SharedCommitLock : _EX_PUSH_LOCK

    +0x768 SharedCommitLinks : _LIST_ENTRY

    +0x778 AllowedCpuSets : Uint8B

    +0x780 DefaultCpuSets : Uint8B

    +0x778 AllowedCpuSetsIndirect : Ptr64 Uint8B

    +0x780 DefaultCpuSetsIndirect : Ptr64 Uint8B

Первое поле структуры (Pcb) содержит вложенную структуру типа KPROCESS. В этой структуре хранятся данные планирования и учета времени. Формат структуры KPROCESS выводится так же, как и формат структуры EPROCESS:

lkd> dt nt!_kprocess

    +0x000 Header           : _DISPATCHER_HEADER

    +0x018 ProfileListHead  : _LIST_ENTRY

    +0x028 DirectoryTableBase : Uint8B

    +0x030 ThreadListHead   : _LIST_ENTRY

    +0x040 ProcessLock      : Uint4B

    ...

    +0x26c KernelTime       : Uint4B

    +0x270 UserTime         : Uint4B

    +0x274 LdtFreeSelectorHint : Uint2B

    +0x276 LdtTableLength   : Uint2B

    +0x278 LdtSystemDescriptor : _KGDTENTRY64

    +0x288 LdtBaseAddress : Ptr64 Void

    +0x290 LdtProcessLock : _FAST_MUTEX

    +0x2c8 InstrumentationCallback : Ptr64 Void

    +0x2d0 SecurePid      : Uint8B

Команда dt также позволяет просмотреть содержимое одного или нескольких полей; для этого введите их имена после имени структуры. Например, команда dt nt!_eprocess UniqueProcessId выводит поле с идентификатором процесса. В случае если поле представляет структуру (как поле Pcb структуры EPROCESS, которое содержит вложенную структуру KPROCESS), добавьте точку после имени поля, чтобы отладчик вывел вложенную структуру. Например, для просмотра KPROCESS также можно ввести команду dt nt!_eprocess Pcb. Рекурсию можно продолжить; добавьте новые имена полей (из KPROCESS) и т.д. Наконец, ключ -r команды dt позволяет выполнить рекурсивный обход по всем вложенным структурам. Число, добавленное после ключа, управляет глубиной рекурсии при работе команды.

Команда dt в приведенном ранее виде выводит формат выбранной структуры, но не содержимое конкретного экземпляра этого типа структуры. Чтобы вывести экземпляр конкретного процесса, укажите адрес структуры EPROCESS в аргументе команды dt. Для получения адресов почти всех структур EPROCESS в системе используйте команду !process 0 0 (исключением является процесс простоя системы). Так как структура KPROCESS находится в самом начале EPROCESS, адрес EPROCESS также подойдет в качестве адреса KPROCESS в команде dt _kprocess.

Эксперимент: использование команды !process отладчика ядра

Команда !process отладчика ядра выводит подмножество информации, хранящейся в объекте процесса и в связанных с ним структурах. Вывод для каждого процесса делится на две части. Сначала выводится информация о процессе, как показано ниже. Если идентификатор или адрес процесса не указан, то команда !process выводит информацию о процессе — владельце потока, выполняемого на процессоре 0. В однопроцессорной системе им будет сам отладчик WinDbg (или livekd, если он используется вместо WinDbg).

lkd> !process

PROCESS ffffe0011c3243c0

    SessionId: 2 Cid: 0e38 Peb: 5f2f1de000 ParentCid: 0f08

    DirBase: 38b3e000 ObjectTable: ffffc000a2b22200 HandleCount:

    <Data Not Accessible>

    Image: windbg.exe

    VadRoot ffffe0011badae60 Vads 117 Clone 0 Private 3563. Modified 228.

    Locked 1.

    DeviceMap ffffc000984e4330

    Token                             ffffc000a13f39a0

    ElapsedTime                       00:00:20.772

    UserTime                          00:00:00.000

    KernelTime                        00:00:00.015

    QuotaPoolUsage[PagedPool]         299512

    QuotaPoolUsage[NonPagedPool]      16240

    Working Set Sizes (now,min,max)  (9719, 50, 345) (38876KB, 200KB, 1380KB)

    PeakWorkingSetSize                9947

    VirtualSize                       2097319 Mb

    PeakVirtualSize                   2097321 Mb

    PageFaultCount                    13603

    MemoryPriority                    FOREGROUND

    BasePriority                      8

    CommitCharge                      3994

    Job                               ffffe0011b853690

После вывода основной информации о процессе следует список потоков в процессе. Этот вывод объясняется в разделе «Эксперимент: использование команды !thread отладчика ядра» главы 4.

Также к числу команд, отображающих информацию о процессе, относится коман­да !handle, которая выводит таблицу дескрипторов процесса (см. раздел «Дескрипторы объектов и таблица дескрипторов процесса» главы 8 части 2). Структуры процесса и безопасности потоков описаны в главе 7 «Безопасность».

Обратите внимание: в выходных данных приводится адрес PEB. Его можно использовать с командой !peb, описанной в следующем эксперименте, для просмотра удобного представления блока PEB произвольного процесса; также вы можете использовать обычную команду dt со структурой _PEB. Но поскольку блок PEB находится в адресном пространстве пользовательского режима, адрес действителен только в контексте его процесса. Чтобы обратиться к блоку PEB другого процесса, необходимо сначала переключить WinDbg на этот процесс. Для этого можно воспользоваться командой .process /P, за которой следует указатель на EPROCESS.

Если вы используете последнюю версию Windows 10 SDK, в обновленной версии WinDbg под адресом PEB размещается гиперссылка; щелчок на этой ссылке автоматически выполняет команду .process и команду !peb.

Блок PEB размещается в адресном пространстве пользовательского режима описываемого им процесса. Он содержит информацию, необходимую для загрузчика образов, диспетчера кучи и других компонентов Windows, которые должны обращаться к нему из пользовательского режима; предоставлять доступ ко всей этой информации через системные вызовы было бы слишком затратно. Структуры EPROCESS и KPROCESS доступны только из режима ядра. Важнейшие поля PEB изображены на рис. 3.4 и подробно рассматриваются позднее в этой главе.

348565.png 

Рис. 3.4. Важнейшие поля структуры PEB

Эксперимент: просмотр PEB

Структуру PEB можно вывести командой !peb отладчика ядра. Эта команда выводит PEB процесса, который является владельцем потока, выполняемого в настоящее время на процессоре 0. Информация из предыдущего эксперимента также позволяет использовать указатель на PEB в аргументе команды.

lkd> .process /P ffffe0011c3243c0 ; !peb 5f2f1de000

PEB at 0000003561545000

    InheritedAddressSpace:    No

    ReadImageFileExecOptions: No

    BeingDebugged:            No

    ImageBaseAddress:         00007ff64fa70000

    Ldr                       00007ffdf52f5200

    Ldr.Initialized:          Yes

    Ldr.InInitializationOrderModuleList: 000001d3d22b3630 . 000001d3d6cddb60

    Ldr.InLoadOrderModuleList:           000001d3d22b3790 . 000001d3d6cddb40

    Ldr.InMemoryOrderModuleList:         000001d3d22b37a0 . 000001d3d6cddb50

                    Base TimeStamp                     Module

            7ff64fa70000 56ccafdd Feb 23 21:15:41 2016 C:\dbg\x64\windbg.exe

            7ffdf51b0000 56cbf9dd Feb 23 08:19:09 2016 C:\WINDOWS\SYSTEM32\

                                  ntdll.dll

            7ffdf2c10000 5632d5aa Oct 30 04:27:54 2015 C:\WINDOWS\system32\

                                  KERNEL32.DLL

    ...

Структура CSR_PROCESS содержит информацию о процессах, относящихся к подсистеме Windows (Csrss). Соответственно, структура CSR_PROCESS связывается только с приложениями Windows (например, у Smss ее нет). Кроме того, поскольку каждый сеанс содержит собственный экземпляр подсистемы Windows, структуры CSR_PROCESS поддерживаются процессом Csrss в каждом отдельном сеансе. Базовая структура CSR_PROCESS изображена на рис. 3.5, а более подробное описание приведено далее в этой главе.

348624.png 

Рис. 3.5. Поля структуры CSR_PROCESS

Эксперимент: анализ CSR_PROCESS

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

Начнем с вывода списка существующих процессов Csrss:

lkd> !process 0 0 csrss.exe

PROCESS ffffe00077ddf080

    SessionId: 0  Cid: 02c0    Peb: c4e3fc0000  ParentCid: 026c

    DirBase:   ObjectTable: ffffc0004d15d040  HandleCount: 543.

    Image: csrss.exe

 

PROCESS ffffe00078796080

    SessionId: 1  Cid: 0338    Peb: d4b4db4000  ParentCid: 0330

    DirBase:   ObjectTable: ffffc0004ddff040  HandleCount: 514.

    Image: csrss.exe

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

lkd> .process /r /P ffffe00078796080

Implicit process is now ffffe000'78796080

Loading User Symbols

.............

Ключ /p меняет контекст процесса отладчика на переданный объект процесса (EPROCESS), а ключ /r запрашивает загрузку символических имен пользовательского режима. Теперь вы можете просмотреть список модулей командой lm или просмотреть структуру CSR_PROCESS:

lkd> dt csrss!_csr_process

   +0x000 ClientId         : _CLIENT_ID

   +0x010 ListLink         : _LIST_ENTRY

   +0x020 ThreadList       : _LIST_ENTRY

   +0x030 NtSession        : Ptr64 _CSR_NT_SESSION

   +0x038 ClientPort       : Ptr64 Void

   +0x040 ClientViewBase   : Ptr64 Char

   +0x048 ClientViewBounds : Ptr64 Char

   +0x050 ProcessHandle    : Ptr64 Void

   +0x058 SequenceNumber   : Uint4B

   +0x05c Flags            : Uint4B

   +0x060 DebugFlags       : Uint4B

   +0x064 ReferenceCount   : Int4B

   +0x068 ProcessGroupId   : Uint4B

   +0x06c ProcessGroupSequence : Uint4B

   +0x070 LastMessageSequence : Uint4B

   +0x074 NumOutstandingMessages : Uint4B

   +0x078 ShutdownLevel    : Uint4B

   +0x07c ShutdownFlags    : Uint4B

   +0x080 Luid             : _LUID

   +0x088 ServerDllPerProcessData : [1] Ptr64 Void

W32PROCESS — последняя из системных структур данных процессов, которые будут рассмотрены в этой главе. Она содержит всю информацию, необходимую коду управления графикой и окнами в ядре (Win32k) для поддержания информации состояния процессов GUI (которые определялись ранее как процессы, выполнившие хотя бы один системный вызов USER/GDI). Базовая структура W32PROCESS показана на рис. 3.6. К сожалению, так как информация типа для структур Win32k недоступна в виде общедоступных символических имен, мы не сможем легко продемонстрировать эксперимент с выводом этой информации. Как бы то ни было, обсуждение структур данных и концепций, относящихся к графике, выходит за рамки темы книги.

348679.png 

Рис. 3.6. Поля структуры процесса Win32k

Защищенные процессы

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

Это поведение (гарантирующее, что полный доступ к запущенному коду будет только у администраторов) выглядит логично, но оно противоречит поведению системы, соответствующему требованиям по управлению цифровыми правами, установленным медиаиндустрией для операционных систем компьютеров. Эти права поддерживают воспроизведение современного высококачественного цифрового контента, например, записанного на носителях Blu-ray. Для поддержки надежного и защищенного проигрывания такого контента в Windows Vista и Windows Server 2008 появились защищенные процессы. Такие процессы сосуществуют с обычными процессами Windows, но накладывают существенные ограничения на права доступа, которые могут запрашиваться другими процессами системы (даже запущенными с привилегиями администратора).

Защищенные процессы могут быть созданы любым приложением, но операционная система разрешит процессу быть защищенным, только если файл образа был снабжен цифровой подписью со специальным сертификатом Windows Media Certificate. В Windows защищенный путь к носителю — Protected Media Path (PMP) — использует защищенные процессы для защиты дорогостоящих цифровых материалов, и разработчики приложений (например, DVD-проигрывателей) могут воспользоваться защищенными процессами с помощью API-функций Media Foundation.

Процесс Audio Device Graph (Audiodg.exe) является защищенным, потому что через него может быть декодирован защищенный музыкальный контент. С ним непосредственно связан защищенный конвейер Media Foundation (Mfpmp.exe), который тоже является защищенным процессом по аналогичным причинам (по умолчанию он не запускается). Принадлежащий системе отчета об ошибках Windows Error Reporting (WER; см. главу 8 части 2) клиентский процесс (Werfaultsecure.exe) также может быть запущен в защищенном режиме, поскольку ему нужно иметь доступ к защищенным процессам на случай аварии одного из них. И наконец, сам процесс System защищен, потому что часть расшифрованной информации генерируется драйвером Ksecdd.sys и сохраняется в его памяти пользовательского режима. Процесс System также защищен для обеспечения целостности всех дескрипторов ядра (потому что таблица дескрипторов процесса System содержит все имеющиеся в системе дескрипторы ядра). Поскольку другие драйверы также могут отображать память из адресного пространства пользовательского режима процесса System (например, сертификат целостности кода и данные каталога), это еще одна причина для защиты процесса.

Поддержка защищенных процессов на уровне ядра ведется по двум направлениям: во-первых, во избежание атак внедрения кода основная часть создания процесса происходит в режиме ядра. (Последовательность действий при создании защищенных и стандартных процессов описана в следующем разделе.) Во-вторых, у защищенных процессов (и их усовершенствованных «родственников» PPL, описанных в следующем разделе) в структуре EPROCESS устанавливаются специальные биты, которые изменяют поведение процедур, связанных с мерами безопасности в диспетчере процессов для отказа в некоторых правах доступа, которые обычно предоставляются администраторам. Фактически, в отношении защищенных процессов предоставляются только следующие права: на запрос процесса и установку ограниченной информации — PROCESS_QUERY/SET_LIMITED_INFORMATION, на завершение процесса — PROCESS_TERMINATE и на приостановку и возобновление процесса — PROCESS_SUSPEND_RESUME. Определенные права доступа отключаются также и в отношении потоков, запущенных внутри защищенных процессов, эти права доступа будут рассмотрены позже в разделе «Внутреннее устройство потоков» главы 4.

Поскольку в Process Explorer для запроса информации о внутренних данных процесса используются стандартные API-функции пользовательского режима, некоторые операции не могут выполняться с защищенными процессами. С другой стороны, такое инструментальное средство, как WinDbg, в режиме отладки ядра использующее для получения этой информации инфраструктуру режима ядра, будет в состоянии вывести полную информацию. Поведение Process Explorer при работе с такими защищенными процессами, как Audiodg.exe, описано в эксперименте раздела «Внутреннее устройство потоков» главы 4.

ПРИМЕЧАНИЕ Как уже упоминалось в главе 1, для выполнения локальной отладки ядра нужно загрузить систему в режиме отладки (который включается командой bcdedit /debug on или путем использования расширенных настроек загрузки в средстве Msconfig). Это оградит от атак на защищенные процессы и на PMP (Protected Media Path), основанные на использовании режима отладки. При загрузке в режиме отладки проигрывание содержимого высокого разрешения работать не будет.

Надежное ограничение прав доступа позволяет ядру оградить защищенный процесс от доступа к нему из пользовательского режима. С другой стороны, поскольку признаком защищенного процесса служит флаг в структуре EPROCESS, администратор все же может загрузить драйвер режима ядра, снимающий бит этого флага. Но это будет нарушением PMP-модели и будет считаться потенциально опасным действием, и такой драйвер, скорее всего, будет в конечном итоге заблокирован от загрузки на 64-разрядной системе, поскольку действующая в режиме ядра политика цифровой подписи кода запретит снабжать подписью вредоносный код. Кроме того, защита режима ядра PatchGuard (описанная в главе 7), а также драйвер защищенной среды и аутентификации (Peauth.sys) распознают такие попытки и сообщают о них. Даже на 32-разрядных системах драйвер должен быть распознан PMP-политикой, иначе воспроизведение будет остановлено. Эта политика реализована компанией Microsoft и обнаруживается не на всяком ядре. Блокировка потребует вмешательства Microsoft по идентификации подписи, определения вредоносности кода и обновления ядра.

Облегченные защищенные процессы (PPL)

Как вы только что видели, исходная модель защищенных процессов ориентировалась на цифровую защиту контента. Начиная с Windows 8.1 и Windows Server 2012 R2, появилось расширение модели защищенных процессов — так называемые облегченные защищенные процессы (PPL, Protected Process Light).

Процессы PPL защищены так же, как и классические защищенные процессы: код пользовательского режима (даже выполняемый с повышенными привилегиями) не может проникнуть в эти процессы посредством внедрения потоков или получения подробной информации о загруженных DLL. Тем не менее модель PPL добавляет в качество защиты новое измерение: значения атрибутов. Разные подписывающие стороны имеют различные уровни доверия, а это приводит к тому, что некоторые PPL имеют более высокую (или низкую) защиту, чем другие.

Поскольку поддержка управления цифровыми правами (DRM) прошла путь от простой защиты мультимедийного контента до лицензирования Windows и приложений Магазина Windows, стандартные защищенные процессы также теперь различаются в зависимости от подписывающей стороны. Наконец, различные признанные подписывающие стороны также определяют, какие права доступа будут запрещены для менее защищенных процессов. Например, обычно разрешены только маски доступа PROCESS_QUERY/SET_LIMITED_INFORMATION и PROCESS_SUSPEND_RESUME. Право завершения PROCESS_TERMINATE не разрешено для некоторых подписывающих сторон PPL.

В табл. 3.1 перечислены допустимые значения флага защиты, хранимого в структуре EPROCESS.

Таблица 3.1. Допустимые значения защиты для процессов

Внутреннее символическое имя уровня защиты процесса

Тип защиты

Подписывающая сторона

PS_PROTECTED_SYSTEM (0x72)

Защищенный

WinSystem

PS_PROTECTED_WINTCB (0x62)

Защищенный

WinTcb

PS_PROTECTED_WINTCB_LIGHT (0x61)

Защищенный облегченный

WinTcb

PS_PROTECTED_WINDOWS (0x52)

Защищенный

Windows

PS_PROTECTED_WINDOWS_LIGHT (0x51)

Защищенный облегченный

Windows

PS_PROTECTED_LSA_LIGHT (0x41)

Защищенный облегченный

Lsa

PS_PROTECTED_ANTIMALWARE_LIGHT (0x31)

Защищенный облегченный

Средства защиты от вредоносных программ

PS_PROTECTED_AUTHENTICODE (0x21)

Защищенный

Authenticode

PS_PROTECTED_AUTHENTICODE_LIGHT (0x11)

Защищенный облегченный

Authenticode

PS_PROTECTED_NONE (0x00)

Нет

Нет

Как видно из табл. 3.1, определено несколько подписывающих сторон с убыванием приоритета. WinSystem обладает наивысшим приоритетом и используется для процесса System и минимальных процессов (таких, как процесс Memory Compression). Для процессов пользовательского режима наивысшим приоритетом обладает WinTCB (Windows Trusted Computer Base); он используется для защиты критических процессов, которые хорошо известны ядру, — для таких процессов ядро может немного опустить свою «планку безопасности». Анализируя приоритеты процессов, помните, что защищенные процессы всегда превосходят PPL, а процессы подписывающих сторон с более высоким приоритетом всегда имеют доступ к низкоприоритетным, но не наоборот. В табл. 3.2 перечислены уровни подписывающих сторон (высокие значения обозначают более высокий приоритет) и приведены примеры их использования. Для вывода их в отладчике используется тип _PS_PROTECTED_SIGNER.

Таблица 3.2. Подписывающие стороны и уровни защиты

Имя (PS_PROTECTED_SIGNER)

Уровень

Применение

PsProtectedSignerWinSystem

7

Системные и минимальные процессы (включая процессы Pico)

PsProtectedSignerWinTcb

6

Критические компоненты Windows (с запретом PROCESS_TERMINATE)

PsProtectedSignerWindows

5

Важные компоненты Windows для работы с конфиденциальными данными

PsProtectedSignerLsa

4

Lsass.exe (при настройке для защищенного выполнения)

PsProtectedSignerAntimalware

3

Службы и процессы, предназначенные для защиты от вредоносных программ, включая сторонние (с запретом PROCESS_TERMINATE)

PsProtectedSignerCodeGen

2

NGEN (.NET Native Code Generation)

PsProtectedSignerAuthenticode

1

Работа с контентом DRM или загрузка шрифтов пользовательского режима

PsProtectedSignerNone

0

Защита отсутствует

Казалось бы, что мешает вредоносному процессу заявить, что он является защищенным процессом, и оградиться от средств борьбы с вредоносными программами? Поскольку для запуска в качестве защищенного процесса сертификат Windows Media DRM уже не требуется, компания Microsoft расширила свой модуль целостности кода и включила в него поддержку двух специальных вариантов расширенного использования ключа (EKU, Enhanced Key Usage), которые могут кодироваться в сертификате цифровой подписи кода: 1.3.6.1.4.1.311.10.3.22 и 1.3.6.4.1.311.10.3.20. При наличии одного из этих EKU жестко закодированные строки подписывающей стороны и издателя в сертификате, в сочетании с дополнительными возможными EKU, затем связываются с различными значениями защищенной подписывающей стороны. Например, издатель сертификата Microsoft Windows может предоставить значение защищенной подписывающей стороны PsProtectedSignerWindows, но только в том случае, если вместе с ним также присутствует EKU проверки системного компонента Windows (1.3.6.1.4.1.311.10.3.6). Так, на рис. 3.7 показан сертификат процесса Smss.exe, которому разрешен запуск в режиме WinTcb-Light.

Наконец, учтите, что уровень защиты процесса также влияет на то, какие DLL ему будет разрешено загружать, — в противном случае из-за ошибки логики или простой замены файла совершенно законный защищенный процесс можно было бы заставить загрузить стороннюю или вредоносную библиотеку, которая теперь будет выполняться на том же уровне защиты, что и процесс. Для реализации этой проверки каждому процессу предоставляется «уровень подписи», который хранится в поле SignatureLevel структуры EPROCESS и используется для поиска по внутренней таблице соответствующего «уровня подписи DLL», хранящегося в поле SectionSignatureLevel в EPROCESS. Все DLL-библиотеки, загружаемые в процессе, будут проверяться компонентом целостности кода по тем же правилам, по которым проверяется основной исполняемый файл. Например, процесс с подписывающей стороной «WinTcb» будет загружать только DLL-библиотеки с подписью «Windows» или уровнем выше.

FIGURE%203-7.tif 

Рис. 3.7. Сертификат Smss

В Windows 10 и Windows Server 2016 следующие процессы снабжаются подписью PPL уровня WinTcb-Lite: smss.exe, csrss.exe, services.exe и wininit.exe. Lsass.exe выполняется как PPL в Windows на базе ARM (таких, как Windows Mobile 10) и может выполняться как PPL в x86/x64, если соответствующая настройка будет включена параметром реестра или политики (подробнее см. в главе 7). Кроме того, некоторые службы могут настраиваться для выполнения в форме Windows PPL или защищенного процесса — как, например, sppsvc.exe (Software Protection Platform). Также на этом уровне защиты работают некоторые процессы-хосты служб (Svchost.exe), поскольку многие службы — например, служба развертывания AppX и подсистема Windows для Linux — также выполняются как защищенные. Подробнее о таких защищенных службах см. в главе 9 части 2.

Тот факт, что эти базовые системные двоичные модули работают с защитой TCB, критичен для безопасности системы. Например, Csrss.exe имеет доступ к некоторым закрытым API, реализуемым диспетчером окон (Win32k.sys), что может предоставить атакующему административный доступ к чувствительным частям ядра. Аналогичным образом Smss.exe и Wininit.exe реализуют логику запуска и управления системой, критичную для выполнения без возможного вмешательства со стороны администратора. Windows гарантирует, что эти двоичные модули всегда выполняются с правами WinTCB-Lite, поэтому, например, никто не сможет запустить их без указания правильного уровня защиты процесса в атрибутах процесса при вызове CreateProcess. Эта гарантия, известная как минимальный список TCB, заставляет любые процессы из системного пути, имена которых присутствуют в табл. 3.3, обладать минимальным уровнем защиты и/или уровнем подписи независимо от данных, переданных вызывающей стороной.

Таблица 3.3. Минимальный уровень TCB

Имя процесса

Минимальный уровень подписи

Минимальный уровень защиты

Smss.exe

Определяется по уровню защиты

WinTcb-Lite

Csrss.exe

Определяется по уровню защиты

WinTcb-Lite

Wininit.exe

Определяется по уровню защиты

WinTcb-Lite

Services.exe

Определяется по уровню защиты

WinTcb-Lite

Werfaultsecure.exe

Определяется по уровню защиты

WinTcb-Full

Sppsvc.exe

Определяется по уровню защиты

Windows-Full

Genvalobj.exe

Определяется по уровню защиты

Windows-Full

Lsass.exe

SE_SIGNING_LEVEL_WINDOWS

0

Userinit.exe

SE_SIGNING_LEVEL_WINDOWS

0

Winlogon.exe

SE_SIGNING_LEVEL_WINDOWS

0

Autochk.exe

SE_SIGNING_LEVEL_WINDOWS*

0

* Только в системах с микропрограммами UEFI

3-118.tif

Эксперимент: просмотр защищенных процессов в Process Explorer

В этом эксперименте вы увидите, как в Process Explorer отображаются защищенные процессы (любого типа). Запустите Process Explorer и установите флажок Protection на вкладке Process Image для включения в список столбца Protection:

 

Теперь отсортируйте столбец Protection по убыванию и прокрутите список в начало. Вы увидите в нем все защищенные процессы с указанием типа защиты. Следующий снимок экрана сделан на машине с Windows 10 для x64:

3-119.tif 

Если выбрать защищенный процесс и посмотреть на нижнюю часть в режиме настройки просмотра DLL, вы ничего не увидите. Дело в том, что Process Explorer для получения информации о загруженных модулях использует API пользовательского режима, а для этого необходим уровень доступа, не предоставляемый для обращения к защищенным процессам. Наиболее заметное исключение — процесс System, который является защищенным, однако Process Explorer показывает для него список загруженных модулей ядра (в основном драйверов), потому что в системных процессах DLL-библиотек нет. Для получения информации используется EnumDeviceDrivers — системная API-функция, которой не нужен дескриптор процесса.

Переключившись на представление Handle, вы получите полную информацию о дескрипторах. Причина та же: Process Explorer использует недокументированную API-функцию, которая возвращает все дескрипторы в системе, а для ее работы конкретный дескриптор процесса не нужен. Process Explorer может идентифицировать процесс просто потому, что в эту информацию включается значение PID, связанное с каждым дескриптором.

Сторонняя поддержка PPL

Механизм PPL выводит средства защиты процессов за рамки исполняемых файлов, создаваемых исключительно Microsoft. Один из примеров — программные продукты для борьбы с вредоносными программами (AM, Anti-Malware). Типичный AM-продукт состоит из трех основных компонентов.

• Драйвер ядра, который перехватывает запросы ввода/вывода к файловой системе и/или сети и реализует средства блокировки с использованием объектов, процессов и обратных вызовов потоков.

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

• GUI-процесс пользовательского режима, который передает информацию пользователю и (возможно) позволяет пользователю принимать решения там, где это уместно.

Один из возможных путей атаки вредоносных программ на систему основан на внедрении кода в процесс, работающий с повышенными привилегиями. А еще лучше — прямо в службу, предназначенную для борьбы с вредоносными программами. Таким образом они пытаются заблокировать ее работу. Но если AM-служба будет выполняться как PPL, внедрение кода станет невозможным, а завершение процесса будет запрещено; таким образом обеспечивается улучшенная защита AM-программ от вредоносных программ, не использующих эксплойты уровня ядра.

Чтобы такой сценарий стал возможен, драйвер ядра AM-программы, описанный выше, должен иметь соответствующий драйвер ELAM (Early-Launch Anti Malware). Подробное описание ELAM приведено в главе 7, но ключевое различие заключается в том, что таким драйверам необходим специальный сертификат, предоставленный компанией Microsoft (после проверки издателя программного продукта). После того как такой драйвер будет установлен, его основной исполняемый (PE) файл может содержать дополнительный ресурсный раздел с именем ELAMCERTIFICATEINFO. Этот раздел может описывать до трех дополнительных подписывающих сторон (определяемых их открытыми ключами), каждая из которых имеет до трех дополнительных EKU (идентифицируемых значениями OID).

Если система целостности кода распознает любой файл, подписанный одной из этих трех сторон и содержащий один из трех EKU, она разрешает процессу запросить PPL с уровнем PS_PROTECTED_ANTIMALWARE_LIGHT (0x31). Каноническим примером служит собственный AM-продукт компании Microsoft — Защитник Windows. Его служба в Windows 10 (MsMpEng.exe) подписана сертификатом для улучшения защиты от вредоносного кода, атакующего как сам AM-продукт, так и его сетевой сервер (NisSvc.exe).

Минимальные процессы и процессы Pico

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

Минимальные процессы

Если функции NtCreateProcessEx передается особый флаг, а вызывающая сторона находится в режиме ядра, работа функции слегка изменяется: в ней вызывается API-функция PsCreateMinimalProcess.

Это приводит к тому, что процесс создается без многих структур, представленных ранее, а именно:

• не создается адресное пространство имен пользовательского режима, поэтому блок PEB и связанные с ним структуры не существуют;

• в процесс не отображается ни NTDLL, ни информация загрузчика/наборов API-функций;

• никакой объект раздела не связывается с процессом; это означает, что файл с исполняемым образом не будет связан с выполнением или с именем процесса (которое может быть пустым или содержать произвольную строку);

• во флагах EPROCESS устанавливается флаг Minimal, с которым все потоки становятся минимальными потоками, и для них не создаются структуры пользовательского режима (такие, как TEB или стек пользовательского режима). Подробнее о TEB см. в главе 4.

Как было показано в главе 2, Windows 10 содержит не менее двух минимальных процессов: процесс System и процесс Memory Compression; также он может содержать третий процесс — Secure System, если включены средства безопасности на основе виртуализации (см. главу 2 и главу 7).

Наконец, есть еще один способ создания минимальных процессов в системе Windows 10: включение необязательной подсистемы Windows для Linux (WSL), которая также была описана в главе 2. Это приводит к установке поставщика Pico, состоящего из драйверов Lxss.sys и LxCore.sys.

Процессы Pico

Минимальные процессы имеют ограниченную область применения касательно доступа к виртуальному адресному пространству из компонентов ядра и его защиты. Процессы Pico играют более важную роль: для этого они разрешают специальному компоненту, называемому поставщиком Pico, управлять большинством аспектов своего выполнения с позиций операционной системы. Такая степень контроля позволяет поставщику эмулировать поведение совершенно другого ядра операционной системы таким образом, что двоичный образ понятия не имеет о том, что он выполняется в операционной системе на базе Windows. По сути это реализация проекта Drawbridge от Microsoft Research, которая также используется для аналогичной поддержки SQL Server (хотя и с использованием Library OS на базе Windows поверх ядра Linux).

Чтобы система поддерживала процессы Pico, в ней должен присутствовать поставщик. Такой поставщик регистрируется API-функцией PsRegisterPicoProvider, но по очень специфическому правилу: поставщик Pico должен быть загружен до того, как будут загружены сторонние драйверы (включая драйверы загрузки).

Вызов этой API-функции разрешен только ограниченному набору из десятка или около того основных драйверов, причем эти драйверы должны быть снабжены сертификатом Microsoft и EKU компонента Windows. В системах Windows с включенным необязательным компонентом WSL основной драйвер называется Lxss.sys; он служит заглушкой до того момента, как будет загружен другой драйвер LxCore.sys, который берет на себя обязанности поставщика Pico посредством перевода различных таблиц диспетчеризации на себя. Кроме того, учтите, что на момент написания книги только один основной драйвер мог зарегистрироваться в качестве поставщика Pico.

Когда поставщик Pico вызывает API-функцию регистрации, он получает набор указателей на функции, которые использует для создания процессов Pico и управления ими:

• функция для создания процесса Pico и функция для создания потока Pico;

• функция для получения контекста (произвольного указателя, который может использоваться поставщиком для хранения произвольных данных) процесса Pico, функция для назначения контекста и еще пара аналогичных функций для потоков Pico. Эти данные используются для заполнения поля PicoContext в ETHREAD и/или EPROCESS;

• функция для чтения структуры контекста процессора (CONTEXT) потока Pico и функция для записи этой структуры;

• функция для изменения сегментов FS и/или GS потока Pico; обычно эти функции используются кодом пользовательского режима для хранения указателей на локальную структуру потока (например, TEB в Windows);

• функция для завершения потока Pico и аналогичная функция для процесса Pico;

• функция для приостановки потока Pico и функция для возобновления его выполнения.

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

• для вызова системной функции потоком Pico с использованием команды SYSCALL;

• для инициирования исключений потоками Pico;

• для ошибок страниц при выполнении операций пробирования и блокировки со списками дескрипторов памяти (MDL, Memory Descriptor List) в потоках Pico;

• для запроса имени процесса Pico вызывающей стороной;

• для запроса трассировки стека пользовательского режима процесса Pico из WETW (Event Tracing for Windows);

• для попытки приложения открыть дескриптор для процесса Pico или потока Pico;

• для запроса на завершение процесса Pico;

• для неожиданного завершения потока Pico или процесса Pico.

Кроме того, поставщик Pico также использует защиту от модификации ядра KPP (Kernel Patch Protection), описанную в главе 7, для защиты как своих обратных вызовов, так и системных вызовов с целью предотвращения регистрации вредоносных поставщиков Pico поверх нормальных.

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

386661.png 

Рис. 3.8. Типы процессов

Итак, поставщики Pico по сути представляют собой специально написанные модули ядра, реализующие необходимые обратные вызовы для реакции на список возможных событий (см. выше), которые могут инициироваться процессами Pico. Так у WSL появляется возможность выполнения немодифицированных двоичных файлов Linux ELF в пользовательском режиме, которая ограничивается только полнотой эмуляции системных функций и сопутствующей функциональности.

Чтобы картина сравнения обычных процессов NT с минимальными процессами и процессами Pico была более полной, на рис. 3.8 представлены структуры каждой разновидности процессов.

Трастлеты (безопасные процессы)

Как упоминалось в главе 2, в Windows появились новые средства обеспечения безопасности на основе виртуализации (VBS, Virtualization-Based Security) — Device Guard и Credential Guard. Они повышают безопасность данных операционной системы и пользователя за счет применения гипервизора. Вы видели, как одна из таких функций, Credential Guard (более подробно рассматривается в главе 7), работает в новой среде изолированного пользовательского режима (IUM). Последняя хотя и является непривилегированной (кольцо 3), имеет виртуальный уровень доверия 1 (VTL 1), что дает ей защиту от обычного мира VTL 0, в котором существуют как ядро NT (кольцо 0), так и приложения (кольцо 3). Посмотрим, как ядро организует выполнение таких процессов и какие структуры данных используются такими процессами.

Структура трастлета

Прежде всего, хотя трастлеты представляют собой файлы в традиционном для Windows формате PE (Portable Executable), они обладают рядом дополнительных свойств, специфических для IUM.

• Они могут импортировать только из ограниченного набора системных DLL Windows (C/C++ Runtime, KernelBase, Advapi, RPC Runtime, CNG Base Crypto и NTDLL) из-за ограниченного числа системных функций, доступных для трастлетов. Математические DLL, работающие только со структурами данных (такие, как NTLM, ASN.1 и т.д.), тоже доступны, поскольку они не вызывают никаких системных функций.

• Возможно импортирование из доступной для них системной DLL-библиотеки, специфической для IUM. Эта библиотека (Iumbase) предоставляет интерфейс Base IUM System API с поддержкой слотов сообщений, блоков хранения данных, криптографии и т.д. Библиотека в конечном итоге обращается с вызовом к Iumdll.dll — версии Ntdll.dll для VTL 1, которая содержит безопасные системные функции (системные функции, реализованные безопасным ядром и не передаваемые обычному ядру VTL 0).

• Они содержат раздел PE с именем .tPolicy, в котором находится экспортированная глобальная переменная с именем s_IumPolicyMetadata. Она содержит метаданные, на основе которых безопасное ядро реализует параметры политики, связанные с разрешением доступа к трастлету из VTL 0 (возможность отладки, поддержка аварийного дампа и т.д.).

• Они подписываются сертификатом, содержащим EKU изолированного пользовательского режима (1.3.6.1.4.1.311.10.3.37). На рис. 3.9 представлены данные сертификата для файла LsaIso.exe, в которых отображается его IUM EKU.

Кроме того, для запуска трастлета при использовании CreateProcess должен быть указан конкретный атрибут процесса — как для запроса выполнения в IUM, так и для задания конкретных свойств запуска. Ниже приводится описание метаданных политики и атрибутов процесса.

FIGURE%203-9.tif 

Рис. 3.9. EKU сертификат трастлета

Метаданные политики трастлетов

Метаданные политики включают различные параметры для настройки того, насколько «доступен» будет трастлет из VTL 0. Они описываются структурой, присутствующей в упоминавшемся ранее экспорте s_IumPolicyMetadata, которая содержит номер версии (в настоящее время равный 1) и идентификатор трастлета — уникальное число, которое однозначно определяет этот конкретный трастлет среди существующих (например, BioIso.exe присвоен идентификатор трастлета 4). Наконец, метаданные содержат массив параметров политики. В настоящее время поддерживаются параметры, перечисленные в табл. 3.4. Поскольку эти политики являются частью подписанных данных, любая попытка изменить их приведет к нарушению подписи IUM, и выполнение станет невозможным.

Таблица 3.4. Параметры политики трастлетов

Политика

Описание

Дополнительная информация

ETW

Включает или отключает ETW

 

Отладка

Настраивает отладку

Отладка может быть включена постоянно, только при отключении SecureBoot или при использовании механизма «запрос/ответ» по требованию

Аварийный дамп ядра

Включает или запрещает аварийный дамп

 

Ключ аварийного дампа

Задает открытый ключ для шифрования аварийного дампа

Дампы могут отправляться в группу продукта из компании Microsoft. У этой группы имеется закрытый ключ для расшифровки

GUID аварийного дампа

Задает идентификатор для ключа аварийного дампа

Открывает возможность использования/идентификации группой продукта нескольких ключей

Родительский дескриптор безопасности

Формат SDDL

Используется для проверки ожидаемого владельца/родительского процесса

Версия родительского дескриптора безопасности

Идентификатор версии формата SDDL

Используется для проверки ожидаемого владельца/родительского процесса

SVN

Версия безопасности

Уникальное число, которое может использоваться трастлетом (наряду с его идентификатором) при шифровании сообщений AES256/GCM

Идентификатор устройства

Идентификатор PCI безопасного устройства

Трастлет может взаимодействовать только с безопасным устройством (Secure Device), имеющим совпадающий идентификатор PCI

Возможность

Активизирует возможности VTL 1

Открывает доступ к API создания безопасных разделов, DMA, MMIO-доступ пользовательского режима к безопасным устройствам и API безопасного хранения

Идентификатор сценария

Идентификатор сценария для этого двоичного файла

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

Атрибуты трастлета

Запуск трастлета требует правильного использования атрибута PS_CP_SECURE_PROCESS, который сначала используется для проверки того, что вызывающая сторона действительно хочет создать трастлет, а также что выполняется именно тот трастлет, который хочет выполнить вызывающая сторона. Для этого в атрибут встраивается идентификатор трастлета, который должен совпадать с идентификатором трастлета, указанным в метаданных политики. Кроме того, может быть задан один или несколько атрибутов, представленных в табл. 3.5.

Таблица 3.5. Атрибуты трастлетов

Атрибут

Описание

Дополнительная информация

Ключ почтового ящика

Используется для получения данных почтового ящика

Почтовые ящики (mailbox) используются трастлетами для обмена данными со средой VTL 0 (для этого должен быть известен ключ трастлета)

Идентификатор взаимодействия

Задает идентификатор взаимодействия, который должен использоваться при работе с IUM API безопасного хранилища

Безопасное хранилище позволяет трастлетам обмениваться данными друг с другом — при условии, что они имеют одинаковый идентификатор взаимодействия (Collaboration ID). Если идентификатор отсутствует, вместо него используется идентификатор экземпляра трастлета

Идентификатор сеанса TK

Определяет идентификатор сеанса, используемый с Crypto

 

Встроенные системные трастлеты

На момент написания книги Windows 10 содержит пять разных трастлетов, которые определяются идентификаторами, перечисленными в табл. 3.6. Обратите внимание: идентификатор трастлета 0 представляет само безопасное ядро.

Таблица 3.6. Встроенные трастлеты

Имя двоичного файла (идентификатор трастлета)

Описание

Параметры политики

Lsalso.exe (1)

Трастлет защиты учетных данных и ключей

Разрешить ETW, Запретить отладку, Разрешить шифрование аварийного дампа

Vmsp.exe (2)

Рабочий процесс безопасной виртуальной машины (трастлет vTPM)

Разрешить ETW, Запретить отладку, Запретить аварийный дамп, Разрешить возможность безопасного хранилища, Проверить родительский дескриптор безопасности S-1-5-83-0 (NT VIRTUAL MACHINE\Virtual Machines)

Unknown (3)

Трастлет регистрации ключей vTPM

Неизвестно

BioIso.exe (4)

Трастлет безопасной биометрии

Разрешить ETW, Запретить отладку, Разрешить шифрование аварийного дампа

FsIso.exe (5)

Трастлет сервера безопасных фреймов

Запретить ETW, Разрешить отладку, Разрешить возможность создания безопасных разделов, Использовать идентификатор сценария

{AE53FC6E-8D89-4488-9D2E-4D008731C5FD}

Идентификация трастлета

У трастлетов есть несколько механизмов идентификации, которые могут использоваться системой.

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

• Экземпляр трастлета. Криптографически безопасное 16-байтовое случайное число, сгенерированное безопасным ядром. Без использования идентификатора взаимодействия экземпляр трастлета гарантирует, что API безопасного хранилища разрешит только одному экземпляру трастлета читать/записывать данные в двоичный объект хранения данных.

• Идентификатор взаимодействия. Используется, когда трастлет хочет разрешить другим трастлетам с тем же идентификатором или другими экземплярами того же трастлета предоставить доступ к тому же двоичному объекту хранения данных. Если идентификатор присутствует, идентификатор экземпляра трастлета будет игнорироваться при вызове API-функций чтения и записи.

• Версия безопасности (SVN, Security Version). Используется для трастлетов, требующих сильного криптографического доказательства происхождения подписанных или зашифрованных данных. Используется при шифровании данных AES256/GCM механизмами Credential и Key Guard, а также используется службой Cryptograph Report.

• Идентификатор сценария. Используется для трастлетов, создающих именованные (основанные на идентификационных данных) безопасные объекты ядра, такие как безопасные разделы. Это значение GUID подтверждает, что трастлет создает такие объекты, как часть заранее определенного сценария (для этого объекты помечаются в пространстве имен этим кодом GUID). Соответственно, другие трастлеты, желающие открыть объекты с одинаковыми именами, должны иметь одинаковый идентификатор сценария. Также обратите внимание на то, что присутствовать могут сразу несколько идентификаторов сценариев, но трастлеты не могут использовать более одного из них.

Изолированные службы пользовательского режима

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

• Безопасные устройства (IumCreateSecureDevice, IumDmaMapMemory, IumGetDmaEnabler, IumMapSecureIo, IumProtectSecureIo, IumQuerySecureDeviceInformation, IopUnmapSecureIo, IumUpdateSecureDeviceState). Предоставляют доступ к безопасным устройствам ACPI и/или PCI, недоступным из VTL 0 и монопольно принадлежащим безопасному ядру (и его вспомогательным механизмам безопасного слоя HAL и безопасного PCI). Трастлеты с соответствующими возможностями (см. «Метаданные политики трастлетов» ранее в этой главе) могут отображать регистры таких устройств в VTL 1 IUM, а также выполнять пересылку данных средствами DMA (Direct Memory Access). Кроме того, трастлеты могут служить драйверами устройств пользовательского режима для такого оборудования, используя инфраструктуру SDF (Secure Device Framework) в SDFHost.dll. Эта функциональность используется средствами безопасной биометрии для Windows Hello, такими как безопасные смарт-карты USB (через PCI) или веб-камера/сканера отпечатков пальцев (через ACPI).

• Безопасные разделы (IumCreateSecureSection, IumFlushSecureSectionBuffers, IumGetExposedSecureSection, IumOpenSecureSection). Предоставляют возможность совместного использования физических страниц с применением драйвера VTL 0 (который использует VslCreateSecureSection) через безопасные разделы, а также позволяют организовать совместный доступ к данным исключительно с VTL 1 в форме именованных безопасных разделов (с использованием механизма, описанного ранее в разделе «Идентификация трастлета») с другими трастлетами или экземплярами того же трастлета. Для использования этой функциональности трастлетам необходима возможность безопасных разделов (см. раздел «Метаданные политики трастлетов»).

• Почтовые ящики (IumPostMailbox). Позволяют трастлету совместно использовать до 8 слотов, содержащих около 4 Кбайт данных, с компонентом нормального (VTL 0) ядра, которое может вызывать VslRetrieveMailbox с передачей идентификатора слота и секретного ключа почтового ящика. Например, Vid.sys в VTL 9 использует его для получения различных секретов, используемых механизмом vTPM из трастлета Vmsp.exe.

• Идентификационные ключи (IumGetIdk). Позволяют трастлету получить либо уникальный ключ дешифрования, либо ключ подписи. Эти ключи уникальны для машины и могут быть получены только от трастлета. Они являются важнейшей частью механизма Credential Guard, в котором они используются для однозначной проверки машины и того, что учетные данные поступили от IUM.

• Криптографические функции (IumCrypto). Позволяют трастлету шифровать и дешифровать данные с локальным и/или действующим на уровне загрузки сеансовым ключом, сгенерированным безопасным ядром и доступным только для IUM, для получения дескрипторов привязки, для получения режима FIPS безопасного ядра и для получения затравки генератора случайных чисел (ГСЧ), генерируемой только безопасным ядром для IUM. Также позволяет трастлету генерировать отчеты, снабженные подписью IDK, хешированием SHA-2 и временной пометкой, которые содержат идентификационные данные и SVN трастлета, дамп его метаданных политики, признак его подключения к отладчику, а также любые другие запрашиваемые данные, находящиеся под управлением трастлета. Такие данные могут использоваться как своего рода метрика трастлета, аналогичная TPM и доказывающая, что трастлет не подвергался злонамеренному вмешательству.

• Безопасное хранилище (IumSecureStorageGet, IumSecureStoragePut). Позволяет трастлетам использовать возможность безопасного хранилища (описанную ранее в разделе «Метаданные политики трастлетов») для сохранения больших двоичных объектов (BLOB, Binary Large Object) произвольного размера и их последующего чтения либо по уникальному экземпляру трастлета, либо по тому же идентификатору взаимодействия, как у другого трастлета.

Системные функции, доступные для трастлетов

В своем стремлении к минимизации поверхности атаки безопасное ядро пытается предоставить небольшое подмножество (менее 50) сотен системных функций, которые могут вызываться обычными приложениями (VTL 0). Эти системные функции представляют минимум, необходимый для совместимости с системными DLL-библиотеками, которые могут использоваться трастлетами (см. раздел «Структура трастлета») и сервисами, необходимыми для поддержки исполнительной среды RPC (Rpcrt4.dll) и трассировки ETW.

• API рабочих фабрик и потоков. Поддержка API пула потоков (используемого RPC) и слотов TLS, используемых загрузчиков.

• API информации процесса. Поддержка слотов TLS и выделения памяти в стеке потоками.

• API событий, семафоров, ожидания и завершения. Поддержка пула потоков и синхронизации.

• API расширенного локального вызова процедур (ALPC). Поддержка локальных вызовов RPC на основе транспорта ncalrpc.

• API информации о системе. Поддержка чтения информации безопасной загрузки, основной системной информации и информации NUMA для Kernel32.dll и масштабирования пула потоков, быстродействия и подмножеств информации времени.

• API маркеров. Минимальная поддержка олицетворения RPC.

• API выделения виртуальной памяти. Поддержка выделения памяти диспетчером кучи пользовательского режима.

• API разделов. Поддержка функциональности загрузчика (для образов DLL) и функциональности безопасных разделов (созданных/открытых безопасными системными функциями, описанными выше).

• API управления трассировкой. Поддержка ETW.

• API исключений и продолжения работы. Поддержка структурированной обработки исключений (Structured Exception Handling, SEH).

Из списка очевидно, что поддержка таких операций, как операции ввода/вывода с устройствами (с файлами или физическими устройствами), невозможна (прежде всего из-за отсутствия API CreateFile), как поддержка операций ввода/вывода с реестром. Также отсутствует поддержка создания других процессов и использование графических API (в VTL 1 нет драйвера Win32k.sys). Как следствие, трастлеты должны быть изолированными служебными рабочими процессами (в VTL 1) для своих сложных «напарников», взаимодействующих с пользователем (в VTL 0); вся передача данных осуществляется только через ALPC или безопасные разделы (дескриптор которого будет передан через ALPC). В главе 7 «Безопасность» мы подробнее обсудим реализацию конкретного трастлета — LsaIso.exe, предоставляющего механизмы Credential и Key Guard.

Эксперимент: идентификация безопасных процессов

Кроме идентификации по имени безопасные процессы могут быть идентифицированы в отладчике ядра. Во-первых, каждый безопасный процесс имеет безопасный идентификатор PID, представляющий дескриптор в таблице дескрипторов безопасного ядра. Он используется нормальным (VTL 0) ядром при создании потоков в процессе и запросах на его завершение. Во-вторых, с самими потоками связывается потоковый объект cookie, который представляет их индекс в таблице потоков безопасного ядра.

Попробуйте ввести следующие команды в отладчике ядра:

lkd> !for_each_process .if @@(((nt!_EPROCESS*)${@#Process})->Pcb.SecurePid) {

.printf "Trustlet: %ma (%p)\n", @@(((nt!_EPROCESS*)${@#Process})->ImageFileName),

@#Process }

Trustlet: Secure System (ffff9b09d8c79080)

Trustlet: LsaIso.exe (ffff9b09e2ba9640)

Trustlet: BioIso.exe (ffff9b09e61c4640)

lkd> dt nt!_EPROCESS ffff9b09d8c79080 Pcb.SecurePid

   +0x000 Pcb           :

      +0x2d0 SecurePid     : 0x00000001'40000004

lkd> dt nt!_EPROCESS ffff9b09e2ba9640 Pcb.SecurePid

   +0x000 Pcb           :

      +0x2d0 SecurePid     : 0x00000001'40000030

lkd> dt nt!_EPROCESS ffff9b09e61c4640 Pcb.SecurePid

   +0x000 Pcb           :

      +0x2d0 SecurePid     : 0x00000001'40000080

lkd> !process ffff9b09e2ba9640 4

PROCESS ffff9b09e2ba9640

    SessionId: 0  Cid: 0388    Peb: 6cdc62b000  ParentCid: 0328

    DirBase: 2f254000  ObjectTable: ffffc607b59b1040  HandleCount:  44.

    Image: LsaIso.exe

        THREAD ffff9b09e2ba2080  Cid 0388.038c  Teb: 0000006cdc62c000 Win32Thread:

0000000000000000 WAIT

lkd> dt nt!_ETHREAD ffff9b09e2ba2080 Tcb.SecureThreadCookie

   +0x000 Tcb                    :

      +0x31c SecureThreadCookie     : 9

Порядок работы функции CreateProcess

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

Создание процесса Windows состоит из нескольких этапов, выполняемых тремя частями операционной системы: Windows-библиотекой Kernel32.dll, работающей на стороне клиента (реальная работа начинается с вызова CreateProcessInternalW), исполняющей системой Windows и процессом подсистемы Windows (Csrss). Поскольку архитектура подсистем Windows рассчитана на разные варианты окружения, создание объекта процесса исполняющей системы (который может использоваться другими подсистемами) отделено от работы, выполняемой при создании процесса подсистемы Windows. Поэтому, хотя следующее описание порядка выполнения Windows-функции CreateProcess выглядит довольно сложным, нужно иметь в виду, что часть работы характерна для семантики, добавленной подсистемой Windows, в отличие от основной работы, необходимой для создания объекта процесса исполняющей системы.

В следующем списке собраны основные этапы создания процесса с помощью Windows-функций CreateProcess*. Операции, выполняемые на каждом этапе, подробно рассматриваются в следующих разделах.

ПРИМЕЧАНИЕ Многие шаги CreateProcess связаны с конфигурацией виртуального адресного пространства процесса, а следовательно, относятся ко многим понятиям управления памятью и структурами, описанными в главе 5.

1. Проверка приемлемости параметров; преобразование флагов и настроек подсистемы Windows в их внутренние аналоги; разбор, проверка и преобразование списка атрибутов во внутренний аналог.

2. Открытие файла образа (.exe), предназначенного для выполнения внутри процесса.

3. Создание объекта процесса исполняющей системы.

4. Создание исходного потока (стека, контекста и объекта потока исполняющей системы Windows).

5. Инициализация процесса, специфическая для подсистемы Windows, выполняемая после его создания.

6. Начало выполнения исходного потока (если только не был установлен флаг создания приостановленного потока CREATE_SUSPENDED).

7. Завершение в контексте нового процесса и потока инициализации адресного пространства (например, загрузка требуемых DLL-библиотек) и передача управления в точку входа программы.

На рис. 3.10 изображена диаграмма действий Windows при создании процесса.

371591.png 

Рис. 3.10. Основные этапы создания процесса

Этап 1. Преобразование и проверка параметров и флагов

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

1. Класс приоритета нового процесса задается независимыми битами в параметре CreationFlags функции CreateProcess*. Поэтому для одного вызова CreateProcess* можно указать более одного класса приоритетов. Чтобы назначить процессу класс приоритета, Windows выбирает для него самый низкий класс приоритета.

Всего определены шесть классов приоритета процессов, каждый из которых связывается с определенным числом:

• простой (Idle) или низкий (Low) (4);

• ниже обычного (Below Normal) (6);

• обычный (Normal) (8);

• выше обычного (Above Normal) (10);

• высокий (High) (13);

• приоритет реального времени (Real-time) (24).

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

2. Если класс приоритета для нового процесса не указан, по умолчанию для него устанавливается класс приоритета Normal. Если для нового процесса указан класс приоритета Real-time и код, вызвавший процесс, не имеет привилегии на повышение приоритета при планировании (SE_INC_BASE_PRIORITY_NAME), то вместо него используется класс приоритета High. Иначе говоря, попытка создания процесса не завершается неудачей только потому, что у вызывающего кода были недостаточные привилегии для создания процесса с классом приоритета Real-time; просто у нового процесса не будет такого высокого приоритета, как Real-time.

3. Если флаги создания определяют, что процесс будет подвергаться отладке, Kernel32 приступает к подключению к исходному коду отладки в Ntdll.dll путем вызова функции DbgUiConnectToDbg и получает дескриптор объекта отладки из блока переменных окружения (TEB) текущего потока.

4. Библиотека Kernel32.dll устанавливает по умолчанию жесткий режим ошибки, если таковой указан во флагах создания.

5. Указанный пользователем список атрибутов преобразуется из формата подсистемы Windows в собственный формат, и к нему добавляются внутренние атрибуты. Возможные атрибуты, которые могут быть добавлены к списку атрибутов, перечислены в табл. 3.7; туда включены и их документированные аналоги из Windows API, если таковые имеются.

ПРИМЕЧАНИЕ Список атрибутов, переданный при вызове CreateProcess*, позволяет вернуть на сторону вызова информацию, выходящую за рамки простого кода статуса, например адрес TEB исходного потока или сведения о разделе образа. Это необходимо при использовании защищенных процессов, поскольку родитель не сможет запросить эту информацию после создания дочернего процесса.

Таблица 3.7. Атрибуты процесса

Исходный атрибут

Эквивалентный атрибут Windows

Тип

Описание

PS_CP_PARENT_PROCESS

PROC_THREAD_ATTRIBUTE_

PARENT_PROCESS. Также

используется при повышении привилегий

Входной

Дескриптор родительского процесса

PS_CP_DEBUG_OBJECT

Отсутствует — применяется при использовании флага DEBUG_PROCESS

Входной

Объект отладки, если начинается отладка процесса

PS_CP_PRIMARY_TOKEN

Отсутствует — применяется при использовании CreateProcessAsUser/

WithTokenW

Входной

Маркер доступа процесса, если была использована процедура CreateProcessAsUser

PS_CP_CLIENT_ID

Отсутствует — возвращается в виде параметра Win32 API (PROCESS_INFORMATION)

Выходной

Возвращение TID и PID исходного потока и процесса

PS_CP_TEB_ADDRESS

Отсутствует — предназначается для внутреннего использования

Выходной

Возвращение адреса TEB исходного потока

PS_CP_FILENAME

Отсутствует — используется в качестве параметра API-функций CreateProcess

Входной

Имя создаваемого процесса

PS_CP_IMAGE_INFO

Отсутствует — предназначается для внутреннего использования

Выходной

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

PS_CP_MEM_RESERVE

Отсутствует — предназначается для внутреннего использования SMSS и CSRSS

Входной

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

PS_CP_PRIORITY_CLASS

Отсутствует — передается в параметре API-функции CreateProcess

Входной

Класс приоритета, который будет назначен процессу

PS_CP_ERROR_MODE

Отсутствует — передается через флаг CREATE_

DEFAULT_ERROR_

MODE

Входной

Жесткий режим обработки ошибок для процесса

PS_CP_STD_HANDLE_INFO

Отсутствует — предназначается для внутреннего использования

Входной

Выбор между дублированием стандартных дескрипторов или созданием новых дескрипторов

PS_CP_HANDLE_LIST

PROC_THREAD_ATTRIBUTE_HANDLE_LIST

Входной

Список дескрипторов, принадлежащих родительскому процессу, который должен быть унаследован новым процессом

PS_CP_GROUP_AFFINITY

PROC_THREAD_ATTRIBUTE_GROUP_AFFINITY

Входной

Группа (группы) процессоров, на которой разрешается выполнение потока

PS_CP_PREFERRED_NODE

PROC_THREAD_ATTRIBUTES_PREFERRED_NODE

Входной

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

PS_CP_IDEAL_PROCESSOR

PROC_THREAD_ATTTRIBUTE_IDEAL_PROCESSOR

Входной

Предпочитаемый (идеальный) процессор, на котором должно планироваться выполнение потока

PS_CP_UMS_THREAD

PROC_THREAD_ATTRIBUTE_UMS_THREAD

Входной

Содержит UMS-атрибуты, список завершения и контекст

PS_CP_MITIGATION_OPTIONS

PROC_THREAD_MITIGATION_POLICY

Входной

Содержит информацию о том, какие средства устранения рисков (SEHOP, ATL Emulation, NX) должны быть включены/отключены для процесса

PS_CP_PROTECTION_LEVEL

PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL

Входной

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

PS_CP_SECURE_PROCESS

Отсутствует — предназначается для внутреннего использования

Входной

Означает, что процесс должен выполняться как трастлет изолированного пользовательского режима (IUM.) Подробнее см. в главе 2 части 2

PS_CP_JOB_LIST

Отсутствует — предназначается для внутреннего использования

Входной

Включает процесс в список заданий

PS_CP_CHILD_PROCESS_POLICY

PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY

Входной

Указывает, разрешено ли новому процессу создавать дочерние процессы прямо или косвенно (например, с использованием WMI)

PS_CP_ALL_APPLICATION_PACKAGES_POLICY

PROC_THREAD_ATTRIBUTE_ALL_APPLICATION_PACKAGES_POLICY

Входной

Указывает, должен ли маркер контейнера приложения быть исключен из проверок ACL, включающих группу ALL APPLICATION PACKAGES. Вместо нее будет использоваться группа ALL RESTRICTED APPLICATION PACKAGES

PS_CP_WIN32K_FILTER

PROC_THREAD_ATTRIBUTE_WIN32K_FILTER

Входной

Указывает, должны ли вызовы процессом многих системных функций GDI/USER к Win32k.sys отфильтровываться (блокироваться), или же они должны быть разрешены, но подвергнуты аудиту. Используется браузером Microsoft Edge для сокращения поверхности атаки

PS_CP_SAFE_OPEN_PROMPT_ORIGIN_CLAIM

Отсутствует — предназначается для внутреннего использования

Входной

Используется функциональностью Mark of the Web для обозначения того, что файл поступил от непроверенного источника

PS_CP_BNO_ISOLATION

PROC_THREAD_ATTRIBUTE_BNO_ISOLATION

Входной

Связывает первичный маркер процесса с изолированным каталогом BaseNamedObjects. (Подробнее об именованных объектах см. в главе 8 части 2.)

PS_CP_DESKTOP_APP_POLICY

PROC_THREAD_ATTRIBUTE_DESKTOP_APP_POLICY

Входной

Указывает, будет ли разрешено современному приложению запускать традиционные настольные приложения, и если разрешено — то как именно

Отсутствует — предназначается для внутреннего использования

PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES

Входной

Указатель на структуру SECURITY_CAPABILITIES,которая используется для создания маркера контейнера приложения для процесса перед вызовом NtCreateUserProcess

6. Если процесс является частью объекта задания, но флаги создания запрашивают отдельную виртуальную машину DOS (VDM), то флаг игнорируется.

7. Атрибуты безопасности процесса и исходного потока, переданные функции CreateProcess, преобразуются в их внутреннее представление (структуры OBJECT_ATTRIBUTES, документированные в WDK).

8. CreateProcessInternalW проверяет, должен ли процесс создаваться как современное приложение. Это происходит в том случае, если этот режим задается атрибутом (PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME) с полным именем пакета или сам создатель является современным (и родительский процесс не был явно задан атрибутом PROC_THREAD_ATTRIBUTE_PARENT_PROCESS). В таком случае вызывается внутренняя функция BasepAppXExtension для получения дополнительной контекстной информации о параметрах современного приложения, которая описывается структурой APPX_PROCESS_CONTEXT. В структуре хранится такая информация, как имя пакета (во внутренней терминологии — моникер пакета), возможности, связанные с приложением, текущий каталог процесса и флаг полного доверия. Возможность создания современных приложений с полным доверием не предоставляется для открытого использования; она резервируется для приложений, обладающих современным оформлением, но выполняющих операции системного уровня. Классический пример — приложение Параметры (Settings) в Windows 10 (SystemSettings.exe).

9. Если процесс создается как современный, то возможности безопасности (если они предоставляются PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES) сохраняются для исходного создания маркера вызовом внутренней функции BasepCreateLowBox. Суффикс LowBox относится к «песочнице» (контейнеру приложения), в которой будет выполняться процесс. Хотя создание современных процессов прямым вызовом CreateProcess не поддерживается (вместо этого следует использовать интерфейсы COM, упоминавшиеся ранее), в Windows SDK и MSDN документирована возможность создания традиционных настольных приложений для контейнера приложения с передачей этого атрибута.

10. Если создается современный процесс, устанавливается флаг, который приказывает ядру пропустить обнаружение встроенного манифеста. У современных процессов встроенного манифеста быть не должно, потому что он просто не нужен. (Современное приложение имеет собственный манифест, не связанный со встроенным манифестом, о котором идет речь.)

11. Если установлен флаг отладки (DEBUG_PROCESS), то параметр Debugger в разделе реестра Image File Execution Options (упоминаемый в следующем разделе) для исполняемого файла помечается для игнорирования. В противном случае отладчик никогда не сможет создать отлаживаемый процесс, так как создание зациклится (попытки создания процесса отладки будут происходить снова и снова).

12. Все окна связаны с рабочими столами — графическими представлениями рабочей области. Если рабочий стол не указан в структуре STARTUPINFO, то процесс связывается с текущим рабочим столом вызывающей стороны.

ПРИМЕЧАНИЕ Поддержка виртуального рабочего стола в Windows 10 не использует множественные объекты рабочих столов (в смысле объектов ядра). Рабочий стол только один, но окна отображаются и скрываются по мере необходимости. В этом виртуальный рабочий стол отличается от программы desktop.exe из пакета Sysinternals, которая действительно создает до четырех объектов рабочего стола. Различия видны при попытке перемещения окна с одного рабочего стола на другой. В случае desktops.exe это сделать не удастся, поскольку в Windows такая операция не поддерживается. С другой стороны, виртуальный рабочий стол Windows 10 позволяет это сделать, так как реальное «перемещение» при этом не происходит.

13. Приложение и аргументы командной строки CreateProcessInternalW анализируются, путь к исполняемому файлу преобразуется во внутреннее имя NT (например, c:\temp\a.exe преобразуется в строку вида \device\harddiskvolume1\temp\a.exe), потому что некоторым функциям нужен именно такой формат.

14. Большая часть собранной информации преобразуется в одну большую структуру типа RTL_USER_PROCESS_PARAMETERS.

Как только эти действия завершатся, функция CreateProcessInternalW выполняет начальный вызов NtCreateUserProcess для попытки создания процесса. Поскольку к этому моменту времени Kernel32.dll еще не знает, соответствует ли имя образа приложения Windows-приложению, пакетному файлу (.bat или .cmd) или 16-разрядному приложению (приложению DOS), вызов может окончиться неудачей, в таком случае функция CreateProcessInternalW ищет причину ошибки и пытается выправить ситуацию.

Этап 2. Открытие образа, предназначенного для исполнения

На данный момент создающий поток переключился в режим ядра и продолжает работу в реализации системной функции NtCreateUserProcess.

1. NtCreateUserProcess сначала проверяет аргументы и формирует внутреннюю структуру для хранения всей информации создания. Повторная проверка аргументов гарантирует, что вызов не поступил от вредоносной программы, которой удалось имитировать переход Ntdll.dll в режим ядра при помощи фиктивных аргументов.

2. Как показано на рис. 3.11, далее функция NtCreateUserProcess ищет соответствующий Windows-образ, который запустит исполняемый файл, указанный вызывающему коду, и создаст объект раздела, чтобы затем отобразить его на адресное пространство нового процесса. Если по какой-нибудь причине вызов потерпит неудачу, управление вернется функции CreateProcessInternalW со статусом сбоя (см. табл. 3.8), что заставит CreateProcessInternalW повторить попытку выполнения.

3. Если процесс должен быть создан как защищенный, также проверяется политика подписывания.

4. Если процесс должен быть создан как современный, выполняется проверка, которая подтвердит, что процесс лицензирован и ему разрешено выполнение. Если приложение было заранее установлено с Windows, ему разрешается выполнение независимо от лицензии. Если разрешена загрузка неопубликованных приложений (настраиваемая в приложении Параметры (Settings)), то будет выполнено любое подписанное приложение, а не только приложение из магазина.

349440.png 

Рис. 3.11. Выбор активируемого Windows-образа

5. Если процесс является трастлетом, то объект раздела должен создаваться со специальным флагом, разрешающим его использование безопасным ядром.

6. Если указанный исполняемый файл является Windows-файлом в формате EXE, функция NtCreateUserProcess пытается открыть файл и создать для него объект раздела. Объект пока не отображается на память, но он открывается. Успешное открытие объекта раздела еще не означает, что файл является действительным Windows-образом; это может быть DLL-библиотека или исполняемый POSIX-файл. Если это исполняемый POSIX-файл, вызов завершается неудачей, потому что POSIX более не поддерживается. Если файл является DLL-библиотекой, функция CreateProcess также дает сбой.

7. После того как функция NtCreateUserProcess нашла настоящий исполняемый Windows-образ, она обращается в раздел реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options в поисках подраздела с именем файла и расширением исполняемого образа (но без каталога и информации о пути, например Notepad.exe). Если такой подраздел существует, функция PspAllocateProcess ищет в этом подразделе параметр Debugger. Если такой параметр существует, образ, предназначенный для запуска, становится строкой в этом параметре, и функция CreateProcessInternalW перезапускается с этапа 1.

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

8. Если образ не является Windows-файлом в формате EXE (например, если это приложение MS-DOS или Win16), функция CreateProcessInternalW выполняет действия по поиску вспомогательного Windows-образа, чтобы его запустить. Этот процесс необходим, потому что приложения, не являющиеся Windows-приложениями, не запускаются напрямую — вместо этого Windows использует один из нескольких специальных вспомогательных образов, которые отвечают за реальный запуск программ, не являющихся Windows-программами. Например, при попытке запустить приложение MS-DOS или Win16 (только в 32-разрядных версиях Windows) запускаемым образом станет исполняемый Windows-файл Ntvdm.exe. Короче говоря, вам не удастся напрямую создать процесс, не являющийся Windows-процессом. Если Windows не сможет решить, какой образ нужно активировать в качестве Windows-процесса (как показано в табл. 3.8), вызов CreateProcessInternalW завершается неудачей.

Таблица 3.8. Дерево принятия решений для стадии 1 функции CreateProcess

Если образ...

Код состояния

Будет выполнен образ…

…со следующим результатом

Приложение MS-DOS с расширением .exe, .com или .pif

PsCreateFailOnSectionCreate

Ntvdm.exe

CreateProcessInternalW перезапускается с этапа 1

Приложение Win16

PsCreateFailOnSectionCreate

Ntvdm.exe

CreateProcessInternalW перезапускается с этапа 1

Приложение Win64 на 32-разрядной системе (или двоичный файл PPC, MIPS или Alpha)

PsCreateFailMachineMismatch

CreateProcessInternalW завершается неудачей

Имеет параметр Debugger с другим именем образа

PsCreateFailExeName

Имя, ука­занное в параметре Debugger

CreateProcessInternalW перезапускается с этапа 1

Недопустимый или поврежденный Windows EXE

PsCreateFailExeFormat

CreateProcessInternalW завершается неудачей

Не открывается

PsCreateFailOnFileOpen

CreateProcessInternalW завершается неудачей

Командная процеду­ра (приложение с расширением .bat или .cmd)

PsCreateFailOnSectionCreate

Cmd.exe

CreateProcessInternalW перезапускается с этапа 1

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

• Если образ является приложением MS-DOS с расширением .exe, .com или .pif, а Windows является 32-разрядной версией на платформе x86, подсистеме Windows отправляется сообщение о проверке на наличие созданного для этого сеанса вспомогательного процесса MS-DOS (Ntvdm.exe, указанного в параметре реестра HKLM\SYSTEM\CurrentControlSet\Control\WOW\cmdline). Если вспомогательный процесс был создан, он используется для запуска приложения MS-DOS. (Подсистема Windows отправляет процессу VDM (виртуальной DOS-машины) сообщение о запуске нового образа.) Функция CreateProcessInternalW возвращает управление. Если поддерживающий процесс создан не был, запускаемый образ изменяется на Ntvdm.exe, и функция CreateProcessInternalW перезапускается с этапа 1.

• Если у запускаемого файла расширение .bat или .cmd, запускаемым образом становится Cmd.exe (окно командной строки Windows), и функция CreateProcessInternalW перезапускается с этапа 1. (Имя пакетного файла передается Cmd.exe во втором параметре с ключом /c.)

• Если образ является исполняемым файлом Win16 (Windows 3.1) в системе на платформе x86, функция CreateProcessInternalW должна решить, должен ли для его запуска быть создан новый VDM-процесс или должен использоваться исходный VDM-процесс, предназначенный для всего сеанса (который к этому моменту может быть еще не создан). Этим решением управляют флаги функции CreateProcessInternalW с именами CREATE_SEPARATE_WOW_VDM и CREATE_SHARED_WOW_VDM. Если эти флаги не определены, поведение по умолчанию диктуется значением параметра HKLM\SYSTEM\CurrentControlSet\Control\WOW\DefaultSeparateVDM. Если приложение должно запускаться на отдельной машине VDM, запускаемый образ меняется на ntvdm.exe, за которым следуют некоторые параметры дополнительной настройки и имя 16-разрядного процесса, и функция CreateProcessInternalW перезапускается с этапа 1. В противном случае подсистема Windows отправляет сообщение, чтобы понять, существует ли общий VDM-процесс и может ли он быть использован. (Если VDM-процесс запущен на другом рабочем столе или он не запущен на том же уровне безопасности, что и вызывающий код, он не может быть использован и должен быть создан новый VDM-процесс.) Если может использоваться общий VDM-процесс, подсистема Windows отправляет ему сообщение, чтобы запустить новый образ, и функция CreateProcessInternalW возвращает управление. Если VDM-процесс еще не был создан (или если он существует, но не может использоваться), запускаемый образ должен поменяться на образ поддержки VDM, и функция CreateProcessInternalW перезапускается с этапа 1.

Этап 3. Создание объекта процесса исполняющей системы Windows

На данный момент функция NtCreateUserProcess открыла подходящий исполняемый файл Windows и создала объект раздела для отображения его на адресное пространство нового процесса. Затем она создает объект процесса исполняющей системы Windows для запуска образа путем вызова внутренней системной функции PspAllocateProcess. Создание объекта процесса исполняющей системы (осуществляемое путем создания потока) включает в себя следующие подэтапы:

• 3A — настройка объекта EPROCESS;

• 3Б — создание исходного адресного пространства процесса;

• 3В — инициализация находящейся в ядре структуры процесса (KPROCESS);

• 3Г — завершение настройки адресного пространства процесса;

• 3Д — настройка PEB;

• 3Е — завершение настройки объекта процесса.

ПРИМЕЧАНИЕ Единственный случай отсутствия родительского процесса относится к инициализации системы (при создании процесса System). После нее для предоставления контекста безопасности для нового процесса всегда требуется родительский процесс.

Этап 3А. Настройка объекта EPROCESS

Этот подэтап включает в себя следующие действия.

1. Наследование сходства (affinity) родительского процесса, если только оно не было явно установлено в ходе создания процесса (через список атрибутов).

2. Выбор идеального узла NUMA, указанного в списке атрибутов (если он был задан).

3. Наследование приоритета ввода/вывода и приоритета страниц (page priority) от родительского процесса. Если родительского процесса не было, используется исходный приоритет страниц (5) и ввода/вывода (Normal).

4. Установка для статуса выхода из нового процесса значения STATUS_PENDING.

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

6. Сохранение идентификатора родительского процесса в поле Inherited­FromUniqueProcessId объекта нового процесса.

7. Запрос раздела реестра Image File Execution Options с целью проверки, должен ли процесс отображаться с использованием больших страниц (значение UseLargePages в разделе IFEO), если только процесс не предназначен для выполнения под управлением WoW64; в этом случае большие страницы не используются. Также запрашиваются данные для проверки того, включена ли библиотека NTDLL в список DLL-библиотек, которые должны отображаться с помощью больших страниц внутри этого процесса.

8. Запрос параметра реестра из IFEO (PerfOptions, если он существует), который может состоять из любого количества следующих возможных значений: IoPriority, PagePriority, CpuPriorityClass и WorkingSetLimitInKB.

9. Если процесс будет работать в WoW64, выделяется вспомогательная структура WoW64 (EWOW64PROCESS), которая присваивается полю Wow64Process структуры EPROCESS.

10. Если процесс должен создаваться в контейнере приложения (как у большинства современных приложений), проверяется, что маркер был создан в режиме LowBox. (Контейнеры приложений более подробно рассматриваются в главе 7.)

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

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

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

14. Установка блока квот нового процесса на адрес блока квот его родительского процесса и увеличение показания счетчика ссылок для родительского блока квот. Если процесс был создан с помощью функции CreateProcessAsUser, это действие пропускается. Вместо него будет создана квота по умолчанию или квота, соответствующая выбранному профилю пользователя.

15. Установка максимального и минимального значений рабочего набора в соответствии со значениями PspMinimumWorkingSet и PspMaximumWorkingSet. Эти значения могут быть заменены, если в подразделе PerfOptions раздела Image File Execution Options были указаны настройки производительности; в таком случае максимальное значение рабочего набора берется оттуда. Следует заметить, что по умолчанию для рабочего набора устанавливаются мягкие ограничения (soft limits), являющиеся по сути рекомендательными, а максимум рабочего набора в PerfOptions является жестким ограничением (т. е. рабочему набору не будет разрешено расти выше указанного числа).

16. Инициализация адресного пространства процесса (см. этап 3Б) и открепление от целевого сеанса, если он отличался от текущего.

17. Выбор для процесса группового сходства, если не использовалось наследование сходства. Исходное групповое сходство либо наследуется от родителя, если ранее было установлено распространение NUMA-узла (будет использована группа, владеющая NUMA-узлом), либо будет назначена по кругу. Если система находится в принудительном режиме осведомленности о группах и алгоритмом выбора была выбрана группа 0, вместо нее выбирается группа 1, если она существует.

18. Инициализация KPROCESS как части объекта процесса (см. этап 3В).

19. Назначение процессу маркера доступа.

20. Назначение процессу нормального класса приоритета, если только родительским процессом не использовался класс Idle или класс Below Normal; в таком случае наследуется класс приоритета родительского процесса.

21. Инициализация таблицы дескрипторов процесса. Если для родительского процесса установлен флаг наследования дескрипторов, все наследуемые дескрипторы копируются из таблицы дескрипторов родительского объекта в новый процесс (см. главу 8 части 2). Также можно использовать атрибуты процесса, но только лишь для указания поднабора дескрипторов; такая возможность может пригодиться при использовании функции CreateProcessAsUser для ограничения тех объектов, которые должны быть унаследованы дочерним процессом.

22. Применение настроек производительности, если таковые были указаны в подразделе PerfOptions. В подраздел PerfOptions включаются переопределения ограничений рабочего набора, приоритета ввода/вывода, приоритета страниц и класса приоритета центрального процессора, применяемые для процесса.

23. Вычисление и установка окончательного класса приоритета процесса и квот по умолчанию для его потоков.

24. Чтение и назначение различных параметров устранения рисков, содержащихся в разделе IFEO (в виде одного 64-разрядного значения с именем Mitigation). Если процесс выполняется в контейнере приложения, добавляется флаг TreatAsAppContainer.

25. Применяются все остальные флаги устранения рисков.

Этап 3Б. Создание исходного адресного пространства процесса

Исходное адресное пространство процесса состоит из следующих страниц:

• каталога страниц (возможно, и не одного для систем с более чем двухуровневыми каталогами страниц, например для систем x86 в PAE-режиме или для 64-разрядных систем);

• страницы гиперпространства;

• страницы битового массива VAD;

• списка рабочего набора.

Для создания этих страниц предпринимаются следующие действия.

1. Для отображения исходных страниц создаются записи в соответствующих таблицах страниц.

2. Количество страниц вычитается из значения переменной ядра MmTotalCommittedPages и добавляется к значению переменной MmProcessCommit.

3. Используемый по умолчанию общесистемный размер минимального рабочего набора процесса (PsMinimumWorkingSet) вычитается из MmResidentAvailablePages.

4. Создание страниц таблицы страниц для глобального системного пространства. (То есть помимо страниц, относящихся к процессу, которые только что были рассмотрены, и за исключением памяти, относящейся к конкретному сеансу.)

Этап 3В. Создание находящейся в ядре структуры процесса

На следующем этапе работы функции PspAllocateProcess инициализируется структура KPROCESS (из поля Pcb структуры EPROCESS). Эта работа выполняется функцией KeInitializeProcess, которая делает следующее.

1. Инициализирует двусвязный список, который соединяет все потоки, являющиеся частью процесса (изначально пуст).

2. Назначает исходное (или получаемое при сбросе) значение кванта времени процесса, используемое по умолчанию (более подробно рассмотрен далее в разделе «Планирование потоков»), которому жестко задается значение 6 до последующей инициализации с помощью PspComputeQuantumAndPriority.

ПРИМЕЧАНИЕ Величина кванта по умолчанию различается для клиентских и серверных систем Windows. Подробнее о квантах потоков см. в разделе «Планирование потоков» главы 4.

3. Базовый приоритет процесса устанавливается на основе вычислений, сделанных на этапе 3А.

4. Задается сходство процессора для потоков процесса в формате группового сходства. Групповое сходство вычисляется на этапе 3А или наследуется от родителя.

5. Процессу назначается резидентное (resident) состояние выгрузки.

6. Затравка (seed) потока назначается в зависимости от идеального процессора, который выбран ядром для этого процесса (этот выбор основывается на идеальном процессоре ранее созданного процесса, что фактически означает случайный выбор на циклической основе). Создание нового процесса приведет к обновлению затравки в KeNodeBlock (в исходном блоке NUMA-узла), чтобы следующий новый процесс получил другую затравку идеального процессора.

7. Если процесс является безопасным (Windows 10 и Server 2016), то его безопасный идентификатор создается вызовом HvlCreateSecureProcess.

Этап 3Г. Завершение настройки адресного пространства процесса

Процедура подготовки адресного пространства нового процесса достаточно сложна, поэтому давайте рассмотрим все по порядку. Чтобы извлечь из этого раздела максимум пользы, нужно иметь некоторое представление о внутреннем устройстве диспетчера памяти Windows, описанном в главе 5.

Большую часть работы по настройке адресного пространства выполняет функция MmInitializeProcessAddressSpace. Она также поддерживает клонирование адресного пространства из другого процесса. Эта возможность была полезна для реализации системной функции POSIX fork; возможно, в будущем она также будет использоваться для поддержки других разновидностей fork в стиле Unix (в частности, так fork реализуется в подсистеме Windows для Linux в Redstone 1). Следующее описание не учитывает функциональность клонирования адресного пространства, а ограничивается нормальной инициализацией адресного пространства процесса.

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

2. Диспетчер памяти инициализирует список рабочего набора процесса — теперь становятся возможными ошибки обращения к страницам.

3. Раздел (созданный при открытии файла образа) теперь отображается на адресное пространство нового процесса, а базовый адрес раздела процесса устанавливается на базовый адрес образа.

4. Создается и инициализируется блок окружения процесса (PEB) — см. описание этапа 3Д.

5. Библиотека Ntdll.dll отображается на процесс; если это процесс WoW64, то отображается также и 32-разрядная библиотека Ntdll.dll.

6. При наличии соответствующего запроса для процесса создается новый сеанс. Это особое действие реализовано главным образом для облегчения работы диспетчера сеанса — Session Manager (Smss) при инициализации нового сеанса.

7. Стандартные дескрипторы дублируются, и новые значения записываются в структуру параметров процесса.

8. Обрабатываются любые фиксированные распределения памяти (memory reservations), перечисленные в списке атрибутов. Кроме того, два флага позволяют зарезервировать первый 1 Мбайт или первые 16 Мбайт адресного пространства. Эти флаги используются внутри системы для отображения, к примеру, кода постоянного запоминающего устройства и векторов реального режима (должны быть в нижних диапазонах виртуального адресного пространства, где обычно располагаются куча и другие структуры процесса).

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

10. Информация о сходстве записывается в PEB.

11. На процесс отображается набор перенаправления MinWin API, а указатель на него сохраняется в PEB.

12. Определяется и сохраняется уникальный идентификатор процесса. Ядро не различает идентификаторы и дескрипторы отдельных процессов и потоков. Идентификаторы процессов и потоков (дескрипторы) хранятся в глобальной таблице дескрипторов (PspCidTable), которая не связана ни с каким процессом.

13. Если процесс безопасен (т. е. он выполняется в IUM), то он инициализируется и связывается с объектом ядра, представляющим процесс.

Этап 3Д. Настройка PEB

Функция NtCreateUserProcess вызывает функцию MmCreatePeb, которая сначала отображает таблицы общесистемной поддержки национальных языков — NLS (National Language Support) — на адресное пространство процесса. Затем эта функция вызывает функцию MiCreatePebOrTeb для выделения страницы для PEB с последующей инициализацией нескольких полей, значения большинства из которых основаны на значениях внутренних переменных, которые были настроены через реестр, например через значения параметров MmHeap*, MmCriticalSectionTimeout и MmMinimumStackCommitInBytes. Некоторые из этих полей могут быть заменены настройками связанных исполняемых образов, например Windows-версией PE-заголовка или маской родственности в каталоге конфигурации загрузки PE-заголовка.

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

Этап 3Е. Завершение настройки объекта процесса исполняющей системы

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

1. Если включен общесистемный аудит процессов (либо в результате настроек локальной политики, либо в результате настроек групповой политики из контроллера домена), в журнале событий безопасности делается запись о создании процесса.

2. Если родительский процесс входил в задание, то это задание восстанавливается из набора уровня заданий родительского процесса, а затем привязывается к сеансу вновь созданного процесса. И наконец, новый процесс добавляется к заданию.

3. Объект нового процесса вставляется в конец Windows-списка активных процессов (PsActiveProcessHead). Теперь процесс становится доступным для таких функций, как EnumProcess и OpenProcess.

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

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

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

Этап 4. Создание исходного потока, а также его стека и контекста

К этому времени настройка объекта процесса исполняющей системы Windows полностью завершена. Но потоков пока еще нет, поэтому что-либо сделать процесс не может. Пора заняться этой работой. Обычно за все аспекты создания потока отвечает функция PspCreateThread, и при создании нового потока именно она и вызывается функцией NtCreateThread. Но поскольку исходный поток создается ядром внутри системы без ввода из пользовательского режима, вместо нее вызываются две вспомогательные процедуры, от которых зависят функции PspCreateThread: PspAllocateThread и PspInsertThread. PspAllocateThread управляет собственно созданием и инициализацией самого объекта потока исполняющей системы, а PspInsertThread управляет созданием дескриптора потока и атрибутов безопасности, а также вызовом функции KeStartThread для превращения объекта исполняющей системы в планируемый поток системы. Но поток пока еще ничего не будет делать, он создан в приостановленном состоянии и не возобновит выполнение, пока процесс не будет полностью проинициализирован (см. описание этапа 5).

ПРИМЕЧАНИЕ Параметром потока (который не может быть указан в CreateProcess, но может быть определен в CreateThread) является адрес PEB. Этот параметр будет использоваться кодом инициализации, который запускается в контексте этого нового потока (см. описание этапа 6).

Функция PspAllocateThread выполняет следующие действия.

1. Запрещает потокам, работа которых планируется в пользовательском режиме UMS (User Mode Scheduling), создаваться в процессах WoW64, а также не позволяет вызывающим процедурам пользовательского режима создавать потоки в системном процессе.

2. Создает и инициализирует объект потока исполняющей системы.

3. Если для системы включена оценка энергопотребления (всегда отключено для XBOX), создает и инициализирует структуру THREAD_ENERGY_VALUES, на которую указывает объект ETHREAD.

4. Инициализирует различные списки, используемые LPC, системой управления вводом/выводом и исполняющей системой.

5. Устанавливает для потока время создания и создает его идентификатор потока TID (Thread ID).

6. Перед тем как поток сможет выполняться, ему нужен стек и контекст, в котором он будет запущен, поэтому она настраивает и то и другое. Размер стека для исходного потока берется из образа, способа указать другой размер не существует. Если процесс является процессом WoW64, будет также инициализирован контекст потока WoW64.

7. Выделяет для нового потока его блок переменных окружения — TEB (Thread Environment Block).

8. Сохраняет стартовый адрес потока пользовательского режима в ETHREAD (в поле StartAddress). Это адрес предоставленной системой функции запуска потока в библиотеке Ntdll.dll (RtlUserThreadStart). Указанный пользователем стартовый адрес Windows сохраняется в ETHREAD в другом месте (поле Win32StartAddress), чтобы такое средство отладки, как Process Explorer, могло запросить информацию.

9. Вызывает для настройки структуры KTHREAD функцию KeInitThread. Для исходного приоритета потока и текущего базового приоритета устанавливаются значения базового приоритета процесса, а для их сходства и кванта времени устанавливаются такие же значения, как и у процесса. Затем функция KeInitThread выделяет для потока стек ядра и инициализирует для потока аппаратный контекст, зависящий от машины, включая фреймы контекста, системных прерываний и исключений. Контекст потока устанавливается таким образом, чтобы поток запускался в режиме ядра в функции KiThreadStartup. И наконец, функция KeInitThread устанавливает состояние потока в Initialized (Инициализирован) и возвращает управление функции PspAllocateThread.

10. Если это UMS-поток, вызывается функция PspUmsInitThread для инициализации UMS-состояния.

Как только эта работа завершится, функция NtCreateUserProcess вызывает функцию PspInsertThread для выполнения следующих действий.

1. Инициализируется идеальный процессор потока, если он был задан атрибутом.

2. Инициализируется групповое сходство потока, если оно было задано атрибутом.

3. Если процесс является частью задания, проводится проверка того, что групповое сходство потока не нарушает ограничений задания (которые были описаны ранее).

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

5. Если поток является частью безопасного процесса (IUM), создается и инициализируется объект безопасного потока.

6. Путем вызова функции KeStartThread инициализируется структура KTHREAD как часть объекта потока. Здесь предполагается наследование у процесса-владельца настроек планирования, выбор идеального узла и процессора, обновление группового сходства, назначение базового и динамического приоритета (копированием из процесса), назначение кванта потока и вставка потока в список процесса, который ведется структурой KPROCESS (отдельный список от того, который хранится в EPROCESS).

7. Если процесс находится в состоянии глубокой заморозки (запрещено выполнение любых потоков, включая новые потоки), этот поток также замораживается.

8. В системах, отличных от x86, если поток является первым в процессе (а процесс не является процессом Idle), процесс вставляется в другой общесистемный список процессов, который хранится в глобальной переменной KiProcessListHead.

9. Увеличивается счетчик потоков в объекте процесса и наследуются приоритет ввода/вывода процесса-владельца и приоритет страниц. Если достигнуто наивысшее количество когда-либо имевшихся у процесса потоков, обновляется также наивысший показатель счетчика потоков. Если поток был вторым для процесса, замораживается первичный маркер доступа (т. е. он не может быть больше изменен).

10. Поток вставляется в список потоков процесса, и поток приостанавливается, если создающий его процесс требует этого.

11. Объект потока вставляется в таблицу дескрипторов процесса.

12. Если это был первый поток процесса (и поэтому операции проводились как часть выполнения CreateProcess*), вызываются также зарегистрированные функции обратного вызова процесса ядра. Затем вызываются все зарегистрированные функции обратного вызова потока. Если какой-либо обратный вызов блокирует создание, попытка завершается неудачей, а вызывающей стороне возвращается соответствующий код статуса.

13. Если был передан список заданий (с использованием атрибута) и поток является первым в процессе, то процесс включается во все задания из списка.

14. Поток подготавливается к выполнению с помощью вызова функции KeReadyThread. Он входит в отложенное состояние (подробнее о состояниях потоков см. в главе 4).

Этап 5. Выполнение инициализации, относящейся к подсистеме Windows

Если функция NtCreateUserProcess вернет управление с кодом успешного завершения работы, значит, все необходимые объекты процессов и потоков исполняющей системы были созданы. Затем CreateProcessInternalW выполняет различные операции, связанные с операциями, характерными для подсистемы Windows, для завершения инициализации процесса.

1. В первую очередь проводятся различные проверки того, должна ли Windows позволить запуститься исполняемому файлу. В их число входят проверка версии образа в заголовке и проверка, не заблокирован ли процесс сертификацией Windows-приложения (через групповую политику). В специализированных выпусках Windows Server 2012 R2, таких как Windows Storage Server 2012 R2, проводятся дополнительные проверки для определения того, не импортировало ли приложение какие-нибудь запрещенные API-функции.

2. Если в отношении программного обеспечения действуют ограничительные политики, для нового процесса создается маркер с ограничениями. После этого отправляется запрос базе данных совместимости приложения, чтобы определить, есть ли для процесса запись либо в реестре, либо в базе данных системного приложения. Оболочки совместимости на этой стадии не применяются; информация будет сохранена в PEB, как только начнется выполнение исходного потока (на этапе 6).

3. CreateProcessInternalW вызывает некоторые внутренние функции (для незащищенных процессов) для получения SxS-информации (см. раздел «Разрешение имен DLL и перенаправление» далее в этой главе) — такой, как файлы манифестов и пути перенаправлений DLL, а также другой информации (например, является ли носитель, на котором находится EXE-файл, съемным, а также флаги обнаружения установки). Для иммерсивных процессов также возвращается информация версии и целевая платформа из манифеста пакета.

4. На основании собранной информации строится сообщение к подсистеме Windows, предназначенное для отправки Csrss. Сообщение включает в себя следующую информацию:

• путь и путь SxS;

• дескрипторы процесса и потока;

• дескриптор сеанса;

• дескриптор маркера доступа;

• информация о носителе;

• данные AppCompat и оболочки совместимости;

• информация иммерсивного процесса;

• адрес PEB;

• различные флаги (например, является ли процесс защищенным или должен ли он выполняться с повышенными правами);

• флаг, показывающий, принадлежит ли процесс Windows-приложению (чтобы Csrss мог определить, нужно или нет показывать курсор запуска);

• информация о языке пользовательского интерфейса;

• флаги DLL-перенаправления и .local (см. раздел «Загрузчик образа» далее в этой главе);

• информация файла манифеста.

При получении этого сообщения подсистема Windows выполняет следующие действия.

1. Функция CsrCreateProcess создает дубликат дескриптора для процесса и потока. Здесь же значение счетчика использования процесса и потока увеличивается со значения 1 (установленного во время создания) до значения 2.

2. Выделяет память для структуры процесса Csrss (CSR_PROCESS).

3. Устанавливает порт исключения нового процесса в качестве общего функционального порта для подсистемы Windows, чтобы эта подсистема получала сообщение, когда в процессе возникнет исключение второго шанса. (Подробнее об обработке исключений см. в главе 8 части 2.)

4. Если новая группа процессов создается с новым процессом в качестве корня (флаг CREATE_NEW_PROCESS_GROUP в CreateProcess), то он задается в CSR_PROCESS. Группа процессов удобна для отправки управляющего события совокупности процессов, совместно использующих консоль. За дополнительной информацией обращайтесь к описаниям CreateProcess и GenerateConsoleCtrlEvent в документации Windows SDK.

5. Выделяет память и инициализирует структуру потока Csrss (CSR_THREAD).

6. Функция CsrCreateThread вставляет поток в список потоков процесса.

7. Увеличивает значение счетчика процессов в сеансе.

8. Устанавливает для уровня завершения процесса значение 0x280 (уровень завершения процесса по умолчанию, дополнительные сведения можно найти в описании SetProcessShutdownParameters в документации Windows SDK).

9. Вставляет структуру нового Csrss-процесса в список процессов, относящихся ко всей подсистеме Windows.

После того как Csrss выполнит эти действия, функция CreateProcess проверяет, не был ли процесс запущен с повышенными правами (запущен через функцию ShellExecute и повышен в правах службой AppInfo после того, как пользователь ответил на соответствующий запрос). Сюда включена проверка того, не был ли процесс программой установки. Если был, маркер процесса открыт, и флаг виртуализации установлен так, чтобы приложение было виртуализировано (UAC и виртуализация рассматриваются в главе 7). Если приложение содержит оболочки совместимости для повышения прав или имело в своем манифесте запрос на повышение уровня прав, процесс уничтожается, а запрос на повышение прав отправляется службе AppInfo.

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

Этап 6. Начало выполнения исходного потока

К этому моменту уже определено окружение процесса, выделены ресурсы для использования его потоками, у процесса есть поток, и подсистема Windows знает о новом процессе. Если вызывающий код не установил флаг создания приостановленного процесса — CREATE_SUSPENDED, то теперь исходный поток приведен в состояние готовности и может приступить к работе, выполнив оставшуюся часть работы по инициализации процесса, которая осуществляется в контексте нового процесса (этап 7).

Этап 7. Выполнение инициализации процесса в контексте нового процесса

Новый поток начинает свою жизнь с выполнения в режиме ядра процедуры запуска потока KiThreadStartup. Эта процедура снижает IRQL-уровень потока с уровня DPC (Deferred Procedure Call) до уровня APC, а затем вызывает системную процедуру исходного потока PspUserThreadStartup. Этой процедуре в качестве параметра передается определенный пользователем стартовый адрес потока. PspUserThreadStartup выполняет следующие действия.

1. Устанавливает цепочку исключений в архитектуре x86. (Другие архитектуры в этом отношении работают иначе, как показано в главе 8 части 2.)

2. IRQL опускается до уровня PASSIVE_LEVEL (0 — единственный уровень IRQL, на котором разрешена работа пользовательского кода).

3. Отключает возможность замены первичного маркера процесса во время выполнения.

4. Если поток был уничтожен при запуске (независимо от причины), он завершается, и никакие дальнейшие действия не предпринимаются.

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

6. Вызывает функцию DbgkCreateThread, которая проверяет, отправлялись ли уведомления образов для нового процесса. Если они не отправлялись, а уведомления включены, сначала уведомление отправляется для процесса, а затем для загрузки образа Ntdll.dll.

ПРИМЕЧАНИЕ Это делается на данном этапе, а не в момент изначального отображения образов, потому что к тому времени идентификатор процесса (необходимый для внешних вызовов ядра) еще не был назначен.

7. После завершения этих проверок выполняется еще одна проверка того, не подвергается ли процесс отладке. Если подвергается и если уведомления отладчика еще не отправлялись, сообщение о создании процесса отправляется через объект отладки (если таковой имеется), чтобы событие отладки запуска процесса (CREATE_PROCESS_DEBUG_INFO) могло быть отправлено соответствующему процессу отладчика. Далее следует аналогичное событие отладки запуска потока и еще одно событие отладки для загрузки образа Ntdll.dll. ­Затем функция DbgkCreateThread ждет ответа от отладчика (через функцию ContinueDebugEvent).

8. Процедура проверяет, разрешена ли в системе предварительная выборка приложения, и, если разрешена, вызывает механизм предварительной выборки (и Superfetch) для обработки файла инструкции предвыборки (если таковой имеется) и осуществления предварительной выборки страниц, на которые были ссылки в течение первых 10 секунд последнего запуска процесса. (Подробнее о предварительной выборке и Superfetch см. в главе 5.)

9. Затем функция PspUserThreadStartup проверяет, был ли уже установлен общесистемный объект cookie в структуре SharedUserData. Если нет, функция генерирует его на основе хеша системной информации, такой как количество обработанных прерываний, доставок DPC и ошибок обращения к страницам, времени прерывания и на случайном числе. Этот общесистемный объект cookie используется во внутреннем декодировании и кодировании указателей, например, в диспетчере кучи для защиты от определенных классов эксплойтов. (Подробнее о безопасности диспетчера кучи см. в главе 5.)

10. Если процесс является безопасным (процесс IUM), то вызывается функция HvlStartSecureThread, которая передает управление безопасному ядру для запуска выполнения потока. Функция возвращает управление только при выходе из потока.

11. И наконец, функция PspUserThreadStartup устанавливает контекст исходного преобразователя (initial thunk) для запуска процедуры инициализации загрузчика образа (LdrInitializeThunk в Ntdll.dll), а также общесистемной заглушки запуска потока — thread startup stub (RtlUserThreadStart в Ntdll.dll). Эти действия совершаются путем редактирования контекста потока на месте с последующим вызовом выхода из операции системной службы, которая загружает специально созданный контекст пользователя. Процедура LdrInitializeThunk инициализирует загрузчик, диспетчер кучи, NLS-таблицы, массивы локальной памяти потока — TLS (Thread-Local Storage) и локальной памяти волокна — FLS (Fiber-Local Storage), а также структуры критического раздела. Затем она загружает любые требуемые библиотеки DLL и вызывает с помощью функцио­нального кода DLL_PROCESS_ATTACH точки входа DLL.

Как только функция вернет управление, процедура NtContinue возвращает контекст нового пользователя и возвращается в пользовательский режим — вот теперь действительно запускается выполнение потока.

Функция RtlUserThreadStart использует адрес фактической точки входа образа и пусковой параметр и вызывает код точки входа приложения. Адрес и параметр уже были помещены в стек ядром. Эта довольно сложная череда событий преследует две цели.

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

• Когда все потоки начинаются в общей процедуре, это позволяет заключить их в конструкцию обработки исключений, чтобы при их сбое библиотека Ntdll.dll знала об этом и могла вызвать фильтр необработанного исключения внутри библиотеки Kernel32.dll. Это позволяет также скоординировать выход потока по возвращении из стартовой процедуры потока и выполнение различной завершающей работы. Разработчики приложений могут также вызвать функцию SetUnhandledExceptionFilter, чтобы добавить их собственный код обработки необработанных исключений.

Эксперимент: отслеживание запуска процесса

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

Хотя этот эксперимент не дает полной картины всех рассмотренных нами внутренних действий, вы сможете посмотреть на некоторые части системы в действии, а именно: на предвыборку и Superfetch, на варианты выполнения файла-образа и на другие проверки совместимости, а также на отображение DLL-библиотеки загрузчика образа.

Мы возьмем простой исполняемый файл — Notepad.exe — и запустим его из окна командной строки (Cmd.exe). Существенно то, что мы рассмотрим операции как внутри Cmd.exe, так и внутри Notepad.exe. Вспомним, что большой объем работы в пользовательском режиме выполняется функцией CreateProcessInternalW, которая вызывается родительским процессом перед тем, как ядро создаст новый объект процесса.

Чтобы все правильно настроить, выполните следующие действия.

1. Добавьте два фильтра в Process Monitor: один для Cmd.exe и один для Notepad.exe — нужно включить только эти два процесса. Убедитесь в том, что не имеется никаких текущих запущенных экземпляров, чтобы иметь полную уверенность в том, что наблюдаются нужные события. Окно фильтров должно выглядеть так:

3-150.tif 

2. Убедитесь в том, что отключена регистрация событий (снят флажок захвата событий — Capture Events в меню File), и запустите окно командной строки.

3. Включите регистрацию событий (откройте меню File и выберите команду Event Logging, или просто нажмите клавиши CTRL+E, или щелкните на значке с увеличительным стеклом на панели инструментов), а затем наберите Notepad.exe и нажмите клавишу Ввод. На обычной Windows-системе вы должны увидеть появление от 500 до 1500 событий.

4. Остановите отслеживание и скройте столбцы Sequence (Последовательность) и Time Of Day (Время дня), чтобы сосредоточиться на интересующих нас столбцах. Окно должно приобрести следующий вид:

3-150.1.tif 

Согласно описанию этапа 1 хода выполнения функции CreateProcess, в первую очередь нужно заметить, что перед запуском процесса и созданием первого потока Cmd.exe читает значение параметра реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\Notepad.exe. Поскольку никаких настроек для образа выполнения, связанного с Notepad.exe, нет, процесс создается в исходном виде.

Как с этим, так и с любым другим событием в журнале Process Monitor, вы можете просмотреть стек событий и увидеть, в каком режиме (пользовательском или режиме ядра) выполнялась каждая часть создания процесса и с помощью каких процедур. Для этого дважды щелкните на событии RegOpenKey и перейдите на вкладку Stack (Стек). На следующей копии экрана показан стандартный стек на 64-разрядной машине с Windows 10:

3-150.2.tif 

Этот стек показывает, что вы уже достигли той части создания процесса, которая выполняется в режиме ядра (посредством функции NtCreateUserProcess), и что за данную проверку отвечает вспомогательная процедура PspAllocateProcess.

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

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

За этой проверкой следуют несколько считываний разделов Side-By-Side, Manifest и MUI/Language, являющихся частью упомянутой ранее структуры сборки.

Файловый ввод/вывод в отношении одного или нескольких файлов с расширением .sdb, которые представляют в системе базы данных совместимости приложений. Этот ввод/вывод осуществляется с целью дополнительных проверок, не нужно ли для этого приложения активизировать механизм оболочек совместимости. Поскольку Блокнот (Notepad) является программой Microsoft с обычным поведением, ему не нужны никакие оболочки совместимости.

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

3-152.tif 

Затем свою лепту вносит механизм предвыборки, который ищет файл своей базы данных, который уже был сгенерирован для Notepad. (Подробнее о предварительной выборке см. в главе 5.) В системе, в которой программа Блокнот уже запускалась хотя бы один раз, эта база данных будет в наличии, и механизм предвыборки начнет выполнение указанных в ней команд. Если это так, опустившись ниже, вы увидите несколько прочитанных и запрошенных DLL-библиотек. В отличие от обычной загрузки DLL-библиотеки, которая осуществляется загрузчиком образа в пользовательском режиме после просмотра таблиц импорта или когда приложение самостоятельно загружает DLL-библиотеку, эти события сгенерированы механизмом предвыборки, который уже знает о тех библиотеках, которые потребуются приложению Блокнот. Обычно за этим следует загрузка образа требуемых DLL-библиотек, и вы увидите события, подобные показанным на следующем скриншоте:

3-153.tif 

Теперь эти события сгенерированы из кода, запущенного внутри пользовательского режима, который был вызван, как только завершила работу функция оболочки режима ядра. Поэтому это первые события, исходящие из процедуры LdrpInitializeProcess, которая вызывается LdrInitializeThunk для первого потока в процессе. Вы можете сами в этом убедиться, взглянув на стек этих событий — например, на стек события загрузки образа kernel32.dll, показанный на следующем скриншоте.

3-154.tif 

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

Завершение процесса

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

Процесс может корректно завершить свою работу вызовом функции ExitProcess. Для многих процессов (в зависимости от настроек компоновщика) стартовый код первого потока в процессе вызывает ExitProcess от имени процесса, когда поток возвращает управление из своей главной функции. Под «корректным завершением» следует понимать, что DLL-библиотеки, загруженные в процесс, получают возможность выполнить некоторые действия по уведомлению о выходе из процесса, для которого используется вызов их функции DllMain с флагом DLL_PROCESS_DETACH.

Функция ExitProcess может вызываться только самим процессом, запрашивающим свое завершение. Возможно некорректное завершение процесса с использованием функции TerminateProcess, которая может вызываться и за пределами процесса. (Например, Process Explorer и диспетчер задач используют эту функцию по запросу пользователя.) Функции TerminateProcess передается дескриптор процесса, открытый с маской доступа PROCESS_TERMINATE, — может оказаться, что в нем будет отказано. Вот почему бывает нелегко (или невозможно) завершать некоторые процессы (например, Csrss) — дескриптор с необходимой маской доступа не может быть получен пользователем, выдавшим запрос. Под «некорректностью» здесь понимается то, что DLL не получает возможности выполнить код (DLL_PROCESS_DETACH не отправляется), а все потоки завершаются моментально. В некоторых случаях это может привести к потере данных — например, если файловый кэш не получил возможности записать данные в файл.

Как бы ни прекращалось существование процесса, никаких утечек быть не может. Другими словами, вся закрытая память процесса автоматически освобождается ядром, адресное пространство уничтожается, закрываются все дескрипторы объектов ядра и т.д. Если открытые дескрипторы процесса все еще существуют (все еще существует структура EPROCESS), то другие процессы все равно могут получить доступ к информации управления процессом — например, коду завершения (GetExitCodeProcess). После закрытия этих дескрипторов структура EPROCESS будет уничтожена, и от процесса действительно ничего не остается.

Несмотря на это, если сторонние драйверы выделяют память в режиме ядра (допустим, из-за IOCTL или просто из-за уведомления процесса), они должны самостоятельно освободить любую такую память. Windows не отслеживает и не зачищает память ядра, принадлежащую процессу (кроме памяти, занимаемой объектами из-за дескрипторов, созданных процессом). Обычно это делается при помощи уведомлений IRP_MJ_CLOSE или IRP_MJ_CLEANUP, которые сообщают драйверу о закрытии объекта устройства, или через уведомление о завершении процесса. (Подробнее о IOCTL см. в главе 6.)

Загрузчик образов

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

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

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

• инициализация состояния приложения в пользовательском режиме, например создание исходной кучи (динамически размещаемой структуры данных) и подготовка слотов локальной памяти потока (TLS) и локальной памяти волокна (FLS);

• разбор таблицы импорта адресов — IAT (Import Table) — приложения для поиска всех необходимых приложению DLL-библиотек (а затем рекурсивный разбор IAT каждой DLL), за которым следует разбор экспортной таблицы DLL-библиотек, чтобы убедиться в том, что функция уже присутствует (специальные записи продвижения, forwarder entries, могут также перенаправить экспорт на другую DLL-библиотеку);

• загрузка и выгрузка DLL-библиотек во время выполнения приложения, а также по требованию, и ведение списка всех загруженных модулей (базы данных модулей);

• обработка файлов манифеста, необходимых для поддержки Windows SxS (Side-by-Side), а также файлов и ресурсов многоязыкового интерфейса (MUI, Multiple Language User Interface);

• чтение базы данных совместимости приложения для любых оболочек совместимости и загрузка, если требуется, DLL-библиотеки механизма оболочек совместимости;

• включение поддержки API-наборов и API-перенаправлений — важной части функциональности OneCore, открывающей возможность создания приложений UWP (Universal Windows Platform);

• разрешение динамического снижения рисков совместимости в процессе выполнения с использованием механизма SwitchBack, а также взаимодействие с механизмами оболочек совместимости и Application Verifier.

Как видите, большинство этих задач играет важную роль в разрешении приложению запускать его код. Без них все, начиная с вызова внешних функций и заканчивая использованием кучи, приведет к немедленному отказу. После того как процесс создан, загрузчик вызывает специальную встроенную API-функцию NtContinue для продолжения выполнения на основе фрейма исключения, находящегося в стеке. Этот фрейм исключения, созданный ядром, содержит фактическую точку входа в приложение. Следовательно, поскольку загрузчик не использует стандартный вызов или передачу управления в запущенное приложение, функции инициализации загрузчика никогда не отображаются в дереве вызовов в трассировке стека потока.

Эксперимент: наблюдение за работой загрузчика образов

В этом эксперименте глобальные флаги используются для включения функции отладки, которая называется снимками загрузчика (loader snaps). Это позволит просмотреть вывод отладки из загрузчика образов при отладке запуска приложения.

1. Из каталога, в котором находится отладчик WinDbg, запустите приложение Gflags.exe, а затем щелкните на вкладке Image File.

2. В поле Image наберите Notepad.exe, а затем нажмите клавишу Tab. Это даст возможность устанавливать флаги. Установите флаг Show Loader Snaps, а затем щелкните на OK или Apply.

3. Запустите WinDbg, откройте меню File, выберите Choose Executable и перей­дите к файлу c:\windows\system32\notepad.exe, чтобы запустить его. Вы увидите пару экранов отладочной информации, которые выглядят примерно так:

0f64:2090 @ 02405218 - LdrpInitializeProcess - INFO: Beginning execution of

notepad.exe (C:\WINDOWS\notepad.exe)

    Current directory: C:\Program Files (x86)\Windows Kits\10\Debuggers\

    Package directories: (null)

0f64:2090 @ 02405218 - LdrLoadDll - ENTER: DLL name: KERNEL32.DLL

0f64:2090 @ 02405218 - LdrpLoadDllInternal - ENTER: DLL name: KERNEL32.DLL

0f64:2090 @ 02405218 - LdrpFindKnownDll - ENTER: DLL name: KERNEL32.DLL

0f64:2090 @ 02405218 - LdrpFindKnownDll - RETURN: Status: 0x00000000

0f64:2090 @ 02405218 - LdrpMinimalMapModule - ENTER: DLL name: C:\WINDOWS\

System32\KERNEL32.DLL

ModLoad: 00007fff'5b4b0000 00007fff'5b55d000 C:\WINDOWS\System32\KERNEL32.DLL

0f64:2090 @ 02405218 - LdrpMinimalMapModule - RETURN: Status: 0x00000000

0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-

rtlsupport-l1-2-0.dll was redirected to C:\WINDOWS\SYSTEM32\ntdll.dll by API set

0f64:2090 @ 02405218 - LdrpFindKnownDll - ENTER: DLL name: KERNELBASE.dll

0f64:2090 @ 02405218 - LdrpFindKnownDll - RETURN: Status: 0x00000000

0f64:2090 @ 02405218 - LdrpMinimalMapModule - ENTER: DLL name: C:\WINDOWS\

System32\KERNELBASE.dll

ModLoad: 00007fff'58b90000 00007fff'58dc6000 C:\WINDOWS\System32\KERNELBASE.dll

0f64:2090 @ 02405218 - LdrpMinimalMapModule - RETURN: Status: 0x00000000

0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-win-

eventing-provider-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\

kernelbase.dll by API set

0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-

apiquery-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ntdll.dll by API set

4. Со временем отладчик прервет выполнение где-то внутри кода загрузчика, в специальном месте, где загрузчик образов проверяет, не присоединен ли к нему отладчик, и сделает из него контрольную точку. Если для продолжения выполнения нажать клавишу g, вы увидите дополнительные сообщения, поступившие от загрузчика, и на экране появится программа Блокнот.

5. Попробуйте поработать в Блокноте, и тогда вы увидите, как те или иные операции приводят к пробуждению загрузчика. Хорошо было бы открыть диалоговое окно сохранения или открытия файла. Тем самым будет продемонстрировано, что загрузчик запускается не только при запуске программы, но и постоянно реагирует на запросы потока, что может привести к отложенным загрузкам других модулей (которые затем могут быть выгружены после использования).

Ранняя стадия инициализации процесса

Поскольку загрузчик находится в библиотеке Ntdll.dll, которая является встроенной DLL-библиотекой, не связанной ни с какой конкретной подсистемой, для всех процессов поведение загрузчика практически одинаково (с некоторыми незначительными отличиями). Ранее мы подробно рассмотрели этапы, приводящие к созданию процесса в режиме ядра, а также некоторую работу, производимую Windows-функцией создания процесса CreateProcess. А здесь будет рассмотрена вся остальная работа, совершаемая в пользовательском режиме, независимо от какой-либо подсистемы, как только начнет выполняться первая инструкция пользовательского режима.

При запуске процесса загрузчик выполняет следующие действия.

1. Проверяет, было ли LdrpProcessInitialized уже присвоено значение 1 или в TEB установлен флаг SkipLoaderInit. В этом случае загрузчик пропускает всю инициализацию и ожидает три секунды, пока кто-нибудь вызовет Ldrp­ProcessInitializationComplete. Эта возможность используется в тех случаях, когда отражение процесса используется механизмом Windows Error Reporting, или при других попытках ветвления, при которых инициализация загрузчика не нужна.

2. Флагу LdrInitState присваивается значение 0, означающее, что загрузчик не был инициализирован. Также флагам ProcessInitializing в PEB и RanProcessInit в TEB присваивается значение 1.

3. Инициализирует блокировку загрузчика в PEB.

4. Инициализирует таблицу динамических функций, используемую для поддержки раскрутки стека/исключений в коде JIT.

5. Инициализирует раздел MRDATA (Mutable Read Only Heap), используемый для хранения глобальных переменных, относящихся к безопасности, которые не должны изменяться эксплойтами (подробнее см. в главе 7).

6. Инициализирует базу данных загрузчика в PEB.

7. Инициализирует таблицы поддержки национальных языков (NLS) для процесса.

8. Строит полное имя образа для приложения.

9. Сохраняет обработчики исключений SEH для раздела .pdata и строит внутренние таблицы исключений.

10. Сохраняет преобразователи системных вызовов для пяти критических функций загрузчика: NtCreateSection, NtOpenFile, NtQueryAttributesFile, NtOpenSection и NtMapViewOfSection.

11. Читает параметры устранения рисков для приложения (которые передаются ядром через экспортируемую переменную LdrSystemDllInitBlock). Эти параметры более подробно описаны в главе 7.

12. Запрашивает содержимое раздела реестра Image File Execution Options (IFEO) для приложения. Здесь хранятся такие параметры, как глобальные флаги (хранящиеся в GlobalFlags), параметры отладки кучи (DisableHeapLookaside, ShutdownFlags и FrontEndHeapDebugOptions), параметры загрузчика (UnloadEventTraceDepth, MaxLoaderThreads, UseImpersonatedDeviceMap), параметры ETW (TracingFlags), а также ряд других параметров, включая MinimumStackCommitInBytes и MaxDeadActivationContexts. В процессе этой работы инициализируется пакет Application Verifier и сопутствующие DLL-библиотеки, а из CFGOptions читаются настройки CFG (Control Flow Guard).

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

14. Загружает любые конфигурационные настройки, указанные в справочнике конфигурации загрузки образа исполняемого файла. Эти настройки, которые разработчик может определить при компиляции приложения (и которые используются компилятором и компоновщиком для реализации некоторых средств безопасности и устранения рисков — таких, как CFG), управляют поведением исполняемого файла.

15. Выполняет минимальную инициализацию FLS и TLS.

16. Задает параметры отладки для критических разделов, создает базу данных трассировки стека пользовательского режима при установке соответствующего глобального флага и запрашивает значение StrackTraceDatabaseSizeInMb из раздела IFEO.

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

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

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

20. Инициализирует пакет пула потоков, поддерживающий API пула потоков. При этом запрашивается и принимается во внимание информация NUMA.

21. Инициализирует и преобразует блок окружения и блок параметров, особенно при необходимости поддержки процессов WoW64.

22. Открывает каталог объектов \KnownDlls и строит путь к DLL. Для процессов WoW64 используется каталог \KnownDlls32.

23. Для приложения магазина читаются параметры политики модели приложения, закодированные в утверждениях WIN://PKG и WP://SKUID маркера (подробнее см. в разделе «Контейнеры приложений» главы 7).

24. Определяет текущий каталог процесса, системный путь и путь загрузки по умолчанию (используемый при загрузке образов и открытии файлов), а также правил, касающихся порядка поиска DLL по умолчанию. При этом читаются текущие настройки политики для универсальных приложений (UWP), приложений Desktop Bridge (Centennial) и Silverlight (Windows Phone 8) (или служб).

25. Строит первую запись таблицы данных загрузчика для Ntdll.dll и вставляет ее в базу данных модулей.

26. Строит таблицу истории раскрутки.

27. Инициализирует параллельный загрузчик, который используется для загрузки всех зависимостей (не имеющих перекрестных зависимостей) с использованием пула потоков и параллельных потоков.

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

29. При необходимости выполняет перемещение главного исполняемого образа.

30. Инициализирует средство Application Verifier, если эта функция включена.

31. Инициализирует ядро WoW64, если это процесс WoW64. В этом случае 64-разрядный загрузчик завершит инициализацию, а 43-разрядный загрузчик перехватит управление и перезапустит многие операции, описанные до настоящего момента.

32. Если это образ .NET, загрузчик проверяет его, загружает Mscoree.dll (оболочка совместимости среды .NET) и получает главную точку входа исполняемого файла (_CorExeMain); при этом заменяется запись исключения с назначением этой точки входа вместо обычной функции main.

33. Инициализирует слоты TLS процесса.

34. Для приложений подсистемы Windows загрузчик вручную загружает Kernel32.dll и Kernelbase.dll независимо от фактического состояния импортирования процесса. При необходимости эти библиотеки используются для инициализации механизмов SRP/Safer (Software Restriction Policies). Наконец, он разрешает все зависимости набора API-функций, которые существуют конкретно между этими двумя библиотеками.

35. Инициализирует механизм оболочек совместимости и анализирует базу данных оболочек совместимости.

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

37. Переменной LdrInitState присваивается значение 1, означающее «выполняется загрузка импортирования».

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

В более новых версиях Windows загрузчик вместо этого заранее строит карту зависимостей, конкретные узлы которой описывают одну DLL-библиотеку и ее граф зависимостей; таким образом определяются узлы, которые могут загружаться параллельно. В некоторые моменты рабочая очередь пула потоков «сливается», что служит точкой синхронизации. Одна из таких точек располагается перед вызовом всех функций инициализации DLL для всех статических импортируемых функций (одна из последних стадий работы загрузчика). Когда это будет сделано, вызываются все статические инициализаторы TLS. Наконец, для Windows-приложений между этими двумя шагами сначала вызывается функция преобразователя инициализации ядра Kernel32 (BaseThreadInitThunk), а потом функция Kernel32, вызываемая в конце инициализации процесса.

Разрешение имен DLL-библиотек и перенаправление

Разрешение (resolution) имен — процесс, с помощью которого система преобразует имя двоичного файла PE-формата в имя физического файла в ситуациях, когда вызывающий модуль не указал файл или не может однозначно его идентифицировать. Поскольку размещение различных каталогов (каталога приложения, системного каталога и т.д.) во время компоновки не может быть жестко задано, этот процесс включает разрешение всех двоичных зависимостей, а также операций LoadLibrary, в которых вызывающий модуль не указал полный путь.

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

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

1. Каталог, из которого было запущено приложение.

2. Основной системный каталог Windows (например, C:\Windows\System32).

3. 16-разрядный системный каталог Windows (например, C:\Windows\System).

4. Каталог Windows (например, C:\Windows).

5. Текущий каталог на момент запуска приложения.

6. Любые каталоги, указанные в переменной среды окружения %PATH%.

Для каждой последующей операции загрузки DLL-библиотеки путь поиска DLL вычисляется заново. Алгоритм вычисления пути поиска аналогичен алгоритму, используемому для вычисления пути поиска по умолчанию, но приложение может изменять конкретные элементы пути, изменяя значение переменной %PATH% с помощью API-функции SetEnvironmentVariable, сменяя текущий каталог с помощью API-функции SetCurrentDirectory или указывая для процесса каталог DLL-библиотеки с помощью API-функции SetDllDirectory. При указании каталога DLL-библиотеки этот каталог заменяет в пути поиска текущий каталог, и загрузчик игнорирует безопасный режим поиска DLL, установленный для процесса.

Вызывающие модули могут также изменять путь поиска DLL для конкретных операций загрузки, вызывая API-функцию LoadLibraryEx с флагом LOAD_WITH_ALTERED_SEARCH_PATH. Если предоставлен этот флаг и в предоставленном APL-функции имени DLL указывается полная строка поиска, при вычислении пути поиска для операции вместо каталога приложения используется путь, содержащий DLL-файл. Учтите, что для относительных путей такое поведение становится неопределенным и создает потенциальную угрозу. При загрузке приложений Desktop Bridge (Centennial) этот флаг игнорируется.

Также при вызове LoadLibraryEx приложения могут указывать флаги LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_APPLICATION_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32 и LOAD_LIBRARY_SEARCH_USER_DIRS вместо флага LOAD_WITH_ALTERED_SEARCH_PATH. Каждый из этих флагов изменяет порядок поиска так, чтобы поиск производился только в конкретном каталоге (или каталогах), описываемом флагом, или же флаги могут объединяться для поиска в нескольких местах. Например, объединение каталога приложения, System32 и пользовательского каталога дает режим LOAD_LIBRARY_SEARCH_DEFAULT_DIRS. Более того, эти флаги могут устанавливаться глобально при помощи API-функции SetDefaultDllDirectories, которая влияет на все загрузки библиотек с этого момента.

На порядок поиска также можно повлиять другим способом: если приложение является пакетным или же не является пакетной службой или старым приложением Silverlight 8.0 для Windows Phone. В таких условиях порядок поиска DLL не использует традиционный механизм и API, а ограничивается поиском по пакетному графу. Это же происходит и при использовании API-функции LoadPackagedLibrary вместо обычной функции LoadLibraryEx. Пакетный граф вычисляется на основании записей <PackageDependency> в разделе <Dependencies> файла манифеста приложения UWP и гарантирует, что никакие посторонние DLL-библиотеки не будут случайно загружены в пакете.

Кроме того, при загрузке пакетного приложения, которое не является приложением Desktop Bridge, все API-функции изменения пути поиска DLL (вроде тех, которые были представлены выше) блокируются, и используется только системное поведение по умолчанию (в сочетании с поиском пакетных зависимостей для большинства приложений UWP).

К сожалению, даже с режимом безопасного поиска и стандартными алгоритмами поиска для старых приложений, которые всегда начинают с каталога приложения, двоичный файл все равно может быть скопирован из своего обычного каталога в другой, доступный для пользователя (например, из c:\windows\system32\notepad.exe в c:\temp\notepad.exe; такая операция не требует административных прав). В такой ситуации атакующий может поместить специально созданную DLL-библиотеку в один каталог с приложением, и с учетом описанного порядка такая библиотека подменит системную DLL-библиотеку. После этого подмененная библиотека может использоваться для влияния на работу приложения, которое может быть привилегированным (особенно если пользователь, не подозревая об изменениях, повысит привилегии приложения через UAC). Для защиты от подобных атак процессы и/или администраторы могут использовать политику устранения рисков процесса Prefer System32 Images (подробнее см. в главе 7), которая меняет порядок пунктов 1 и 2 в приведенном выше списке.

Перенаправление имен DLL

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

• Перенаправление набора API-функций MinWin. Механизм набора API-функций разработан с той целью, чтобы дать возможность команде Windows вносить изменения в двоичный файл, экспортирующий системную API-функцию, способом, который был бы прозрачен для приложений. Работа механизма основана на концепции контрактов. Этот механизм был кратко затронут в главе 2, а ниже приводится его более подробное объяснение.

• .LOCAL-перенаправление. Механизм .LOCAL-перенаправления позволяет приложениям перенаправлять все загрузки, связанные с указанным базовым именем DLL, независимо от того, был ли указан полный путь, на локальную копию DLL в каталоге приложения. Это делается либо путем создания копии DLL с таким же базовым именем, за которым следует расширение .local (например, MyLibrary.dll.local), либо путем создания файловой папки с именем .local в каталоге приложения и помещения копии локальной DLL-библиотеки в эту папку (например, C:\Program Files\My App\.LOCAL\MyLibrary.dll). DLL-библиотеки, загрузка которых перенаправлена с помощью механизма .LOCAL, обрабатываются точно так же, как и те, для которых использовался механизм перенаправления SxS. (См. следующий пункт списка.) Загрузчик принимает .LOCAL-перенаправление загрузки DLL-библиотек, только когда исполняемый файл не имеет связанного с ним манифеста (встроенного либо внешнего). По умолчанию этот механизм отключен. Чтобы включить его глобально, добавьте параметр DevOverrideEnable с типом DWORD в базовый раздел IFEO (HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Image File Execution Options) и присвойте ему значение 1.

• Перенаправление Fusion (SxS). Fusion (также SxS) — расширение модели Windows-приложений, которое позволяет компонентам более подробно выразить информацию о зависимостях исполняемых двоичных файлов (обычно она касается версий этих файлов) путем встраивания двоичных ресурсов, называемых манифестами. Механизм Fusion был впервые использован с таким расчетом, чтобы приложения могли загрузить правильную версию пакета общих элементов управления Windows (comctl32.dll) после того, как исполняемые двоичные файлы были разбиты на разные версии, которые могут быть параллельно установлены в системе; с тех пор другим двоичным файлам версии стали назначаться аналогичным образом. Начиная с Visual Studio 2005, приложения, созданные с помощью компоновщика Microsoft, стали применять Fusion для нахождения соответствующей версии C-библиотек времени выполнения, тогда как Visual Studio 2015 и выше используют перенаправление наборов API-функций для реализации идеи универсальной среды CRT.

Инструментарий времени выполнения Fusion читает встроенную информацию о зависимостях из раздела ресурсов исполняемого двоичного файла, используя загрузчик ресурсов Windows, и упаковывает информацию о зависимостях в поисковые структуры, известные как контексты активации (activation contexts). Система при своей загрузке и при запуске процесса создает, соответственно, на уровне системы и процесса исходные контексты активации; кроме этого у каждого потока есть связанный с ним стек контекста активации, со структурой контекста активации на вершине того стека, который считается активным. Стек контекста активации, имеющийся у каждого потока, управляется как явным образом посредством API-функций ActivateActCtx и DeactivateActCtx, так и неявным образом системой в определенные моменты, например, когда вызывается основная процедура DLL, являющаяся исполняемым двоичным файлом со встроенной информацией о зависимостях. Когда в рамках перенаправления осуществляется Fusion-поиск имени DLL, система ищет информацию о перенаправлении в контексте активации на вершине принадлежащего потоку стека контекста активации, а затем в контекстах активации процесса и системы; при наличии информации о перенаправлении для операции загрузки используется файл, идентифицируемый контекстом активации.

• Перенаправление известных DLL. Известные DLL-библиотеки — механизм, который отображает определенные базовые имена DLL на файлы в системном каталоге, препятствуя замене DLL альтернативной версией, находящейся в другом месте.

Одним из особых случаев алгоритма поиска пути DLL является проверка версии DLL, осуществляемая в 64-разрядных приложениях и в приложениях WoW64. Если DLL с соответствующим базовым именем обнаружена, но впоследствии определена как скомпилированная для неподходящей машинной архитектуры, например 64-разрядный образ в 32-разрядном приложении, загрузчик игнорирует ошибку и продолжает операцию поиска пути, начиная с элемента пути, который находится после элемента, использовавшегося для обнаружения неподходящего файла. Такой стиль поведения был разработан, чтобы дать возможность приложениям указывать в глобальной переменной среды окружения %PATH% записи как для 64-разрядных, так и для 32-разрядных вариантов.

Эксперимент: наблюдение за порядком поиска при загрузке DLL

Для наблюдения за тем, как загрузчик ищет DLL-библиотеки, можно воспользоваться программой Process Monitor из пакета Sysinternals. Когда загрузчик попытается разрешить DLL-зависимость, вы увидите, что он вызывает CreateFile, чтобы проверить каждое место поисковой последовательности до тех пор, пока не будет найдена указанная DLL, либо загрузка потерпит неудачу.

Ниже приведен снимок экрана для поиска загрузчиком исполняемого файла OneDrive.exe. Чтобы повторить эксперимент, выполните следующие действия.

1. Если программа OneDrive.exe выполняется, закройте ее со значка системной панели. Не забудьте закрыть все окна Проводника, в которых просматривается содержимое OneDrive.

2. Откройте Process Monitor и добавьте фильтры для отображения только процесса OneDrive.exe. Также можно отобразить только операции CreateFile.

3. Перейдите в каталог %LocalAppData%\Microsoft\OneDrive и запустите OneDrive.exe или OneDrive Personal.cmd (который запускает OneDrive для «персонального», а не для «коммерческого» использования). Ниже показан примерный результат (обратите внимание: в данном случае OneDrive — 32-разрядный процесс, который выполняется в 64-разрядной системе):

3-164.tif 

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

известные DLL загружаются из системного каталога (ole32.dll на снимке);

LoggingPlatform.Dll загружается из подкаталога версии — возможно, потому что OneDrive вызывает SetDllDirectory для перенаправления поиска на новейшую версию (17.3.6743.1212 на снимке);

MSVCR120.dll (исполнительная среда MSVC версии 12) ищется в каталоге исполняемого файла, где ее найти не удается. Затем поиск проводится в подкаталоге версий, и этот вариант поиска оказывается успешным;

Wsock32.Dll (Winsock) ищется в каталоге исполняемого файла, затем в подкаталоге версии и наконец находится в системном каталоге (SysWow64). Обратите внимание: эта DLL-библиотека не относится к числу известных.

База данных загруженных модулей

Загрузчик ведет список всех модулей (DLL-библиотек, а также основных исполняемых файлов), которые были загружены процессом. Эта информация хранится в PEB — а именно, в подструктуре, имеющей идентификатор Ldr, которая называется PEB_LDR_DATA. В этой структуре загрузчик ведет три двусвязных списка, в которых содержится одна и та же, но по-разному выстроенная информация: по порядку загрузки, по размещению в памяти или по порядку инициализации. Эти списки содержат структуры, называемые записями таблицы данных загрузчика (LDR_DATA_TABLE_ENTRY), с которых хранится информация о каждом модуле.

Кроме того, поскольку поиск в связанных списках обходится алгоритмически дорого (за линейное время), загрузчик также поддерживает два красно-черных дерева — эффективные структуры данных для бинарного поиска. Первое дерево сортируется по базовому адресу, а второе — по хешу имени модуля. С такими деревьями алгоритм поиска может работать за логарифмическое время, что гораздо более эффективно и намного ускоряет создание процессов в Windows 8 и далее. Кроме того, для обеспечения безопасности корни этих двух деревьев (в отличие от связанных списков) недоступны в PEB. Это усложняет их нахождение из кода оболочки, который работает в среде с включенной рандомизацией структуры адресного пространства (ASLR, Address Space Layout Randomization). (Подробнее об ASLR см. в главе 5.)

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

Таблица 3.9. Поля в записи таблицы данных загрузчика

Поле

Значение

BaseAddressIndexNode

Связывание записи с узлом красно-черного дерева, отсор­тированного по базовому адресу

BaseDllName/BaseNameHashValue

Имя самого модуля без полного пути к нему. Второе поле содержит его хеш, полученный RtlHashUnicodeString

DdagNode/NodeModuleLink

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

DllBase

Хранит базовый адрес, по которому был загружен модуль

EntryPoint

Содержит начальную процедуру модуля (например, DllMain)

EntryPointActivationContext

При вызове инициализаторов содержит контекст активации SxS/Fusion

Flags

Флаги состояния загрузчика для этого модуля. (Описание флагов приводится в табл. 3.10)

ForwarderLinks

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

FullDllName

Полное имя модуля

HashLinks

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

ImplicitPathOptions

Используется для хранения флагов поиска пути, которые задаются API-функцией LdrSetImplicitPathOptions или наследуются на основании пути DLL

List Entry Links

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

LoadContext

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

ObsoleteLoadCount

Счетчик ссылок для модуля (т. е. количество загрузок). Не содержит полезных данных, которые были перемещены в структуру DDAG

LoadReason

Содержит значение перечисляемого типа, которое объясняет, почему DLL-библиотека была загружена (динамически, статически, при перенаправлении, как зависимость отложенной загрузки и т.д.)

LoadTime

Хранит показания системного времени на момент загрузки этого модуля

MappingInfoIndexNode

Ссылка для узла красно-черного дерева, отсортированного по хешу имени

OriginalBase

Хранит исходный базовый адрес модуля (установленный компоновщиком) до ASLR и перемещения для ускорения обработки записей импорта с модифицируемыми адресами

ParentDllBase

В случае статических зависимостей (или перенаправления, или отложенной загрузки) хранит адрес DLL-библиотеки, которая зависит от этой

SigningLevel

Хранит уровень подписи образа (подробнее об инфраструктуре целостности кода см. в главе 8 части 2)

SizeOfImage

Размер модуля в памяти

SwitchBackContext

Используется технологией SwitchBack (см. далее) для хранения GUID текущего контекста Windows, связанного с этим модулем, и других данных

TimeDateStamp

Отметка времени, записанная компоновщиком при сборке модуля, которую загрузчик получает из PE-заголовка образа модуля

TlsIndex

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

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

Эксперимент: вывод дампа базы данных загруженных модулей

Перед началом этого эксперимента выполните те же действия, которые производились в двух предыдущих экспериментах по запуску программы Notepad.exe с использованием отладчика WinDbg. Когда дойдете до первого приглашения на ввод данных (там, где до этого момента вам предписывалось ввести команду g), выполните следующие инструкции.

1. PEB-блок текущего процесса можно просмотреть с помощью команды !peb. Но теперь нас интересуют только выводимые на экран данные Ldr.

0:000> !peb

PEB at 000000dd4c901000

    InheritedAddressSpace:    No

    ReadImageFileExecOptions: No

    BeingDebugged:            Yes

    ImageBaseAddress:         00007ff720b60000

    Ldr                       00007ffe855d23a0

    Ldr.Initialized:          Yes

    Ldr.InInitializationOrderModuleList: 0000022815d23d30 . 0000022815d24430

    Ldr.InLoadOrderModuleList:           0000022815d23ee0 . 0000022815d31240

    Ldr.InMemoryOrderModuleList:         0000022815d23ef0 . 0000022815d31250

                    Base TimeStamp                     Module

            7ff720b60000 5789986a Jul 16 05:14:02 2016 C:\Windows\System32\

notepad.exe

            7ffe85480000 5825887f Nov 11 10:59:43 2016 C:\WINDOWS\SYSTEM32\

ntdll.dll

            7ffe84bd0000 57899a29 Jul 16 05:21:29 2016 C:\WINDOWS\System32\

KERNEL32.DLL

            7ffe823c0000 582588e6 Nov 11 11:01:26 2016 C:\WINDOWS\System32\

KERNELBASE.dll

...

2. Адрес в строке Ldr — указатель на ранее рассмотренную структуру PEB_LDR_DATA. Обратите внимание: WinDbg показывает вам адрес трех списков и выводит дамп списка модулей в порядке их инициализации, где показан полный путь, отметка времени и базовый адрес каждого модуля.

3. Можно также проанализировать отдельно запись каждого модуля путем прохода по списку модулей с последующим выводом дампа данных по каждому адресу, отформатированных в виде структуры LDR_DATA_TABLE_ENTRY. Но вместо того чтобы делать это для каждой записи, WinDbg может проделать основную часть работы, использовав расширение !list и следующий синтаксис:

!list –x "dt ntdll!_LDR_DATA_TABLE_ENTRY" @@C++(&@$peb->Ldr-

>InLoadOrderModuleList)

Затем вы сможете просмотреть записи для каждого модуля:

+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000228'15d23d10 -

0x00007ffe'855d23b0 ]

   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000228'15d23d20 -

0x00007ffe'855d23c0 ]

   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000'00000000 -

0x00000000'00000000 ]

   +0x030 DllBase          : 0x00007ff7'20b60000 Void

   +0x038 EntryPoint       : 0x00007ff7'20b787d0 Void

   +0x040 SizeOfImage      : 0x41000

   +0x048 FullDllName      : _UNICODE_STRING "C:\Windows\System32\notepad.

exe"

   +0x058 BaseDllName      : _UNICODE_STRING "notepad.exe"

   +0x068 FlagGroup        : [4]  "???"

   +0x068 Flags            : 0xa2cc

Хотя в этом разделе рассматривается загрузчик пользовательского режима из библиотеки Ntdll.dll, обратите внимание на то, что ядро также использует свой собственный загрузчик для драйверов и зависимых DLL-библиотек, с похожей структурой данных KLDR_DATA_TABLE_ENTRY. Точно так же у загрузчика режима ядра есть своя собственная база данных каждой записи, которая напрямую доступна через глобальную переменную PsLoadedModuleList. Для вывода дампа базы данных модулей, загруженных в ядро, можно воспользоваться такой же командой !list, которая была показана в предыдущем эксперименте, заменив указатель в конце команды строкой nt!PsLoadedModuleList и указав новое имя структуры/модуля: !list-x" dt nt!_KLDR_DATA_TABLE_ENTRY nt!PsLoadedModuleList.

Просмотр списка в низкоуровневом формате позволяет еще глубже разобраться во внутренностях загрузчика, таких как поле Flags, флагов, содержащее информацию о состоянии, которую сама по себе команда !peb не показывает. Значения флагов показаны в табл. 3.10. Поскольку эту структуру используют оба загрузчика, и ядра и пользовательского режима, некоторые флаги применимы только к драйверам режима ядра, в то время как другие флаги применимы только к приложениям пользовательского режима (например, связанные с .NET-состоянием).

Таблица 3.10. Флаги записей таблицы данных загрузчика

Флаг

Значение

Пакетный двоичный модуль (0x1)

Модуль является частью пакетного приложения (устанавливается только в главном модуле пакета AppX)

Помечено для удаления (0x2)

Модуль будет выгружен сразу же после того, как будут закрыты все ссылки на него (например, из выполняемого рабочего потока)

DLL-образ (0x4)

Модуль представляет собой DLL образа (а не данных или исполняемого файла)

Уведомления загрузки отправлены (0x8)

Зарегистрированные получатели уже получили уведомления об этом образе

Данные телеметрии обработаны (0x10)

Данные телеметрии уже были обработаны для этого образа

Статический импорт процесса (0x20)

Модуль является статическим импортом главного двоичного модуля приложения

В унаследованных списках (0x40)

Запись образа содержится в двусвязных списках загрузчика

В индексах (0x80)

Запись образа содержится в красно-черных деревьях

DLL оболочки совместимости (0x100)

Запись образа представляет DLL-часть базы данных оболочки совместимости/приложения

В таблице исключений (0x200)

Обработчики исключений .pdata модулей были сохранены в инвертированной таблице функций загрузчика

Выполняется загрузка (0x800)

Модуль загружается в настоящее время

Конфигурация загрузки обработана (0x1000)

Каталог конфигурации загрузки образа был найден и обработан

Запись обработана (0x2000)

Загрузчик завершил обработку этого модуля

Защита отложенной загрузки (0x4000)

Механизм Control Flow Guard для этого двоичного ­модуля запросил защиту IAT отложенной загрузки. За дополнительной информацией обращайтесь к главе 7

Вызов присоединения процесса (0x20000)

Уведомление DLL_PROCESS_ATTACH было уже отправлено DLL

Сбой при попытке присоединения процесса (0x40000)

Функция DllMain инициировала сбой уведомления DLL_PROCESS_ATTACH

Не вызывать для потоков (0x80000)

Не отправлять DLL уведомления DLL_THREAD_ATTACH. Может устанавливаться вызовом DisableThreadLibraryCalls

Отложенная проверка COR (0x100000)

COR (Common Object Runtime) проверит образ .NET позднее

Образ COR (0x200000)

Модуль является приложением .NET

Не перемещать (0x400000)

Образ не должен перемещаться или подвергаться рандомизации

Только COR IL (0x800000)

Библиотека содержит только код промежуточного языка (IL) без кода языка ассемблера

База данных совместимости обработана (0x40000000)

Механизм оболочки совместимости обработал DLL

Анализ импорта

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

1. Загружает каждую DLL, на которую есть ссылка в таблице импорта исполняемого процессом образа.

2. Проверяет, не была ли DLL уже загружена, просматривая для этого базу данных модуля. Если библиотека не будет найдена в списке, загрузчик открывает DLL и отображает ее в памяти.

3. В ходе операции отображения загрузчик сначала просматривает различные пути, где он должен попытаться найти эту DLL, а также выясняет, не является ли эта библиотека «известной»; это будет означать, что система уже загрузила ее во время запуска и предоставила для доступа к ней глобальный файл, отображенный в памяти. Также возможны определенные отклонения от стандартного алгоритма поиска, связанные либо с использованием файла с расширением .local (что заставит загрузчик использовать DLL-библиотеки в локальном пути), либо с файлом манифеста, который может указать на необходимость использования DLL, находящейся в другом месте, чтобы гарантировать использование конкретной версии.

4. После того как DLL найдена на диске и отображена, загрузчик проверяет, не загрузило ли ядро ее в какое-нибудь другое место — это называется перемещением (relocation). Если загрузчик обнаружил перемещение, он проводит анализ информации о перемещении в DLL и выполняет требуемые операции. Если информация о перемещении отсутствует, происходит сбой загрузки DLL.

5. Затем загрузчик создает запись таблицы данных загрузчика для этой DLL и вставляет ее в базу данных.

6. После того как DLL была отображена, процесс повторяется для этой DLL с целью анализа ее таблицы импорта и всех ее зависимостей.

7. После загрузки каждой DLL загрузчик проводит анализ IAT с целью поиска конкретных импортированных функций. Обычно это делается по именам, но также может делаться и по порядку (по порядковому номеру). Для каждого имени загрузчик проводит анализ экспортной таблицы импортированной DLL и пытается найти соответствие. Если соответствие найдено не будет, операция прерывается.

8. Таблица импорта, принадлежащая образу, может также быть связанной. Это означает, что в процессе компоновки разработчики уже назначили статические адреса, указывающие на импортируемые функции во внешних DLL-библиотеках. Это снимает необходимость в поиске каждого имени, но предполагает, что те DLL-библиотеки, которые будут использоваться приложением, будут всегда находиться по одним и тем же адресам. Поскольку в Windows используется рандомизация адресного пространства (за информацией об ASLR обращайтесь к главе 5), к системным приложениям и библиотекам это, как правило, не относится.

9. Экспортная таблица импортированной DLL может использовать запись продвижения данных (forwarder entry), означающую, что текущая функция реализована в другой DLL. По сути это должно рассматриваться как импорт или зависимость, поэтому после анализа экспортной таблицы загружается также и каждая DLL, на которую ссылалась запись продвижения данных, и загрузчик возвращается к пункту 1.

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

FIGURE%203-12.tiff 

Рис. 3.12. Диалоговое окно, которое отображается при отсутствии необходимой импортированной функции в DLL

Инициализация процесса после импортирования

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

1. Все действия начинаются с присваивания LdrInitState значения 2; это означает, что весь импорт был загружен.

2. В этот момент при использовании отладчика (например, WinDbg) будет достигнута исходная контрольная точка отладчика. Именно здесь для продолжения выполнения в ранее рассмотренных экспериментах вам приходилось вводить команду g.

3. Проверяет, является ли процесс приложением подсистемы Windows; в этом случае функция BaseThreadInitThunk должна быть зафиксирована на ранних этапах инициализации процесса. На этой стадии она вызывается и проверяется на успех. Аналогичным образом вызывается функция TermsrvGetWindowsDirectoryW, которая должна быть зафиксирована ранее (в системах с поддержкой терминальных служб), что приводит к сбросу путей к системному каталогу и каталогу Windows.

4. По распределенному графу перебирает все зависимости и запускает инициализаторы для всех статических импортов образов. На этом шаге вызывается функция DllMain каждой DLL (чтобы каждая библиотека могла провести свою инициализацию, которая может включать даже загрузку новых DLL во время выполнения), а также обрабатываются инициализаторы TLS всех DLL. Это один из последних этапов, в которых при загрузке приложения может произойти сбой. Все загруженные DLL должны вернуть код успешного завершения после выполнения функций DllMain, иначе загрузчик отменяет запуск приложения.

5. Если образ использует слоты TLS, вызывает его инициализатор TLS.

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

7. Запускает функцию, вызываемую после завершения инициализации процесса, соответствующей подсистемы, зарегистрированную в PEB. Например, для приложений Windows эта функция выполняет проверки, относящиеся к службам терминалов.

8. Записывает событие ETW, сообщающее об успешной загрузке процесса.

9. Если существует минимальный размер фиксации (commit) стека, то коснитесь стека потоков, чтобы заставить встроенную страницу зафиксировать страницы.

10. Переменной LdrInitState присваивается значение 3, которое сообщает о том, что инициализация завершена. Полю ProcessInitializing в PEB снова присваивается значение 0, после чего обновляется переменная LdrpProcessInitialized.

Технология SwitchBack

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

Например, если разработчик хочет воспользоваться усовершенствованиями, добавленными в Windows 10 в данные API-функции, он должен включить в свой манифест GUID-идентификатор Windows 10, а если у разработчика имеется устаревшее приложение, сориентированное на поведение, характерное для Windows 7, он должен поместить в манифест GUID-идентификатор Windows 7.

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

Значения GUID для SwitchBack

В настоящее время в Windows определены следующие GUID-идентификаторы, представляющие установки совместимости для всех версий Windows, начиная с Windows Vista:

• {e2011457-1546-43c5-a5fe-008deee3d3f0} для Windows Vista;

• {35138b9a-5d96-4fbd-8e2d-a2440225f93a} для Windows 7;

• {4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38} для Windows 8;

• {1f676c76-80e1-4239-95bb-83d0f6d0da78} для Windows 8.1;

• {8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a} для Windows 10.

Эти GUID-идентификаторы должны присутствовать в файле манифеста приложения — в атрибуте ID элемента <SupportedOS>, присутствующего в записи атрибута совместимости. (Если в манифесте приложения не содержится GUID, в качестве режима совместимости по умолчанию выбирается Windows Vista.) В диспетчере задач можно включить столбец Контекст ОС (Operating System Context) на вкладке Подробности (Details); он покажет, работают ли какие-либо приложения в конкретном контексте ОС (пустое значение в столбце обычно означает, что они работают в режиме Windows 10).

На рис. 3.13 показан пример таких приложений, работающих в режиме Windows Vista и Windows 7 в системе с Windows 10.

FIGURE%203-13.tiff 

Рис. 3.13. Процессы, работающие в режиме совместимости

Пример записи манифеста, устанавливающей совместимость с Windows 10:

  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">

    <application>

      <!-- Windows 10 -->

      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />

    </application>

  </compatibility>

Режимы совместимости SwitchBack

Рассмотрим несколько примеров того, что делает SwitchBack. При выполнении в контексте Windows 7:

• RPC-компоненты вместо закрытой реализации используют пул потоков Windows;

• блокировка DirectDraw Lock не может быть получена на первичном буфере;

• копирование битового массива (blitting) на рабочем столе не допускается без отсечения (clipping) окна;

• исправляется ситуация гонки в GetOverlappedResult;

• вызовам CreateFile может передаваться «понижающий» флаг для получения монопольного открытия даже в том случае, если вызывающая сторона не имеет привилегий записи (при этом NtCreateFile не передается флаг FILE_DISALLOW_EXCLUSIVE).

С другой стороны, запуск в режиме Windows 10 оказывает нетривиальное влияние на работу LFH (Low Fragmentation Heap): подсегменты LFH полностью фиксируются, а все операции выделения памяти дополняются блоком заголовка при отсутствии GUID Windows 10. Кроме того, в Windows 10 при использовании функции устранения рисков «инициировать исключение при закрытии недействительного дескриптора» (см. главу 7) это поведение будет поддерживаться функциями CloseHandle и RegCloseKey. В более ранних операционных системах, если к процессу не присоединен отладчик, это поведение будет блокироваться перед вызовом NtClose и снова включаться после вызова.

Другой пример: механизм проверки правописания будет возвращать NULL для языков, для которых такая проверка не поддерживается, тогда как в Windows 8.1 возвращается «пустое» средство проверки. Аналогичным образом реализация функции IShellLink::Resolve при передаче относительного пути в режиме совместимости Windows 8 будет возвращать E_INVALIDARG, а в режиме Windows 7 эта проверка отсутствует.

Кроме того, вызовы GetVersionEx или эквивалентных функций из NtDll (таких, как RtlVerifyVersionInfo) будут возвращать максимальный номер версии, соответствующий заданному GUID контекста SwitchBack.

ПРИМЕЧАНИЕ Эти API-функции считаются устаревшими. Вызов GetVersionEx будет возвращать 6.2 во всех версиях Windows 8 и выше, если не был задан более высокий  SwitchBack GUID.

Поведение SwitchBack

Когда Windows API подвергается изменениям, которые могут нарушить совместимость, код входа в функции вызывает функцию SbSwitchProcedure, чтобы запустить SwitchBack-логику. По указателю управление передается в таблицу модулей SwitchBack, в которой содержится информация о механизмах SwitchBack, задействованных в модуле. В таблице также содержится указатель на массив записей для каждой точки SwitchBack. В этой таблице содержится дескриптор каждой точки ветвления, который идентифицирует ее с символическим именем и с полным описанием, а также со связанной меткой устранения рисков совместимости. Обычно в модуле имеется несколько точек ветвления: одна для поведения в Windows Vista, одна для поведения в Windows 7 и т.д.

Для каждой точки ветвления дается требуемый контекст SwitchBack — тот самый контекст, который определяет, какая из двух (или более) ветвей будет выбрана во время выполнения приложения. Наконец, в каждом из этих дескрипторов содержится указатель на функцию с реальным кодом, который должен выполняться при каждом ветвлении. Если приложение запускается с Windows 10 GUID, этот код будет частью его SwitchBack-контекста, и API-функция SbSelectProcedure после анализа таблицы модулей выполнит соответствующую операцию. Она находит дескриптор записи модуля для контекста и вызывает функцию по указателю в дескрипторе.

SwitchBack использует ETW для отслеживания выбора SwitchBack-контекстов и точек ветвления, и снабжает данными регистратор Windows AIT (Application Impact Telemetry — телеметрия влияния на приложение). Эти данные периодически собираются компанией Microsoft для определения степени использования каждой записи совместимости, идентификации того приложения, которое ее использует (в журнале предоставляется полная трассировка стека), и для уведомления сторонних поставщиков.

Как уже упоминалось, уровень совместимости приложения хранится в его манифесте. Во время загрузки загрузчик анализирует файл манифеста, создает структуру контекстных данных и кэширует ее в поле pContextData блока PEB. В этих данных контекста содержатся соответствующие GUID совместимости, под которыми выполняется данный процесс, и определяется, какая версия точек ветвления у вызванных API-функций, которые будут выполняться используемой технологией SwitchBack.

Наборы API-функций

Несмотря на то что SwitchBack для определенных сценариев совместимости приложений использует API-перенаправление, для всех приложений в Windows используется гораздо более распространенный механизм перенаправления, который называется наборами API-функций. Он предназначен для более точного деления на категории API-функций Windows на DLL-библиотеки вместо создания больших многоцелевых DLL-библиотек, охватывающих тысячи API-функций, которые могут не понадобиться всем типам ныне существующих и будущих Windows-систем. Эта технология, разработанная в основном для поддержки перестройки самых нижних уровней архитектуры Windows для отделения ее от более высоких уровней, идет рука об руку с разбиением Kernel32.dll и Advapi32.dll (наряду со всем остальным) на несколько виртуальных DLL-файлов.

Например, из рис. 3.14 видно, что Kernel32.dll, основная библиотека Windows, ведет импорт из множества других DLL-библиотек, начиная с API-MS-WIN. В каждой из этих DLL-библиотек содержится небольшой поднабор API-функций, которые обычно предоставляются библиотекой Kernel32, но все вместе они составляют все пространство API, предоставляемое библиотекой Kernel32.dll. Например, библиотека CORE-STRING содержит только основные строковые функции Windows.

FIGURE%203-14.tiff 

Рис. 3.14. Наборы API-функций для kernel32.dll

Разбиение функций на отдельные файлы достигает двух целей: во-первых, это позволяет будущим приложениям компоноваться только с теми API-библиотеками, которые предоставляют нужную им функциональность. Во-вторых, если Microsoft создаст версию Windows, не поддерживающую, к примеру, локализацию (скажем, для встроенной системы, предназначенной только для англоязычных стран), она сможет просто удалить соответствующие подчиненные DLL-библиотеки и изменить схему набора API-функций. В результате будет получен более компактный двоичный код Kernel32, а любое приложение, не требующее локализации, будет работать по-прежнему.

В рамках этой технологии была определена (реализована на уровне исходного кода) «базовая» Windows-система под названием «MinWin» с минимальным набором сервисных функций, куда было включено ядро и основные драйверы (включая файловую систему, основные системные процессы, такие как CSRSS и диспетчер управления службами (Service Control Manager) и еще небольшой перечень Windows-служб). Семейство Windows Embedded имеет специальный инструмент для построения ядра операционной системы, который называется Platform Builder, который на первый взгляд делает примерно то же: построители системы могут удалять отдельные компоненты Windows (оболочка, сетевой стек и т.д.). Но удаление компонентов из Windows оставляет зависшие зависимости — пути выполнения кода, при попытке выполнения которых произойдет сбой, поскольку они зависят от исключенных компонентов. С другой стороны, зависимости MinWin являются полностью самодостаточными.

При инициализации диспетчера процессов вызывается функция PspInitializeApiSetMap, отвечающая за создание объекта раздела (используя стандартный объект раздела) для таблицы перенаправления API-набора, которая хранится в %SystemRoot%\System32\ApiSetSchema.dll. Эта библиотека не содержит исполняемого кода, но в ней есть раздел .apiset с данными отображения виртуальных DLL-библиотек API-набора на логические DLL-библиотеки, содержащие реализацию API-функций. При запуске нового процесса диспетчер процессов отображает объект раздела на адресное пространство процесса и записывает в поле ApiSetMap в PEB-блоке процесса базовый адрес, куда был отображен объект раздела.

В свою очередь, функция загрузчика LdrpApplyFileNameRedirection, которая обычно отвечает за перенаправление рассмотренного ранее манифеста SxS/Fusion и .local, также проверяет перенаправление данных API-набора, когда загружается новая импортируемая библиотека, имя которой начинается с «API-» (как в динамическом, так и в статическом режиме). Таблица API-набора выстраивается библиотекой таким образом, что в каждой записи есть описание, в какой логической DLL-библиотеке может быть найдена та или иная функция; эта DLL-библиотека и будет загружена. Хотя у данных схемы двоичный формат, вы можете вывести дамп строк этих данных с помощью программы Strings из пакета Sysinternals, чтобы посмотреть, какие DLL-библиотеки определены на данный момент:

C:\Windows\System32>strings apisetschema.dll

...

api-ms-onecoreuap-print-render-l1-1-0

printrenderapihost.dllapi-ms-onecoreuap-settingsync-status-l1-1-0

settingsynccore.dll

api-ms-win-appmodel-identity-l1-2-0

kernel.appcore.dllapi-ms-win-appmodel-runtime-internal-l1-1-3

api-ms-win-appmodel-runtime-l1-1-2

api-ms-win-appmodel-state-l1-1-2

api-ms-win-appmodel-state-l1-2-0

api-ms-win-appmodel-unlock-l1-1-0

api-ms-win-base-bootconfig-l1-1-0

advapi32.dllapi-ms-win-base-util-l1-1-0

api-ms-win-composition-redirection-l1-1-0

...

api-ms-win-core-com-midlproxystub-l1-1-0

api-ms-win-core-com-private-l1-1-1

api-ms-win-core-comm-l1-1-0

api-ms-win-core-console-ansi-l2-1-0

api-ms-win-core-console-l1-1-0

api-ms-win-core-console-l2-1-0

api-ms-win-core-crt-l1-1-0

api-ms-win-core-crt-l2-1-0

api-ms-win-core-datetime-l1-1-2

api-ms-win-core-debug-l1-1-2

api-ms-win-core-debug-minidump-l1-1-0

...

api-ms-win-core-firmware-l1-1-0

api-ms-win-core-guard-l1-1-0

api-ms-win-core-handle-l1-1-0

api-ms-win-core-heap-l1-1-0

api-ms-win-core-heap-l1-2-0

api-ms-win-core-heap-l2-1-0

api-ms-win-core-heap-obsolete-l1-1-0

api-ms-win-core-interlocked-l1-1-1

api-ms-win-core-interlocked-l1-2-0

api-ms-win-core-io-l1-1-1

api-ms-win-core-job-l1-1-0

...

Задания

Задание (job) представляет собой именованный объект ядра, у которого может быть механизм защиты и механизм совместного использования и который позволяет контролировать один или несколько процессов, сведенных в группу. Основной функцией объекта задания является предоставление возможности управления группой процессов как единым целым и работы с этим объединением. Процесс может входить только в один объект задания. По умолчанию связь процесса с объектом задания не может быть разорвана, и все созданные им процессы и их потомки также связаны с тем же объектом задания — если только дочерние процессы не были созданы с флагом CREATE_BREAKAWAY_FROM_JOB, а само задание не ограничило такую возможность. Объект задания также записывает основную учетную информацию для всех процессов, связанных с заданием, и для всех процессов, которые были связаны с заданием, но работа которых уже была завершена.

Задания также могут быть связаны с объектом порта завершения ввода/вывода, в ожидании которого могут находиться другие потоки, с использованием Windows-функции GetQueuedCompletionStatus или API пула потоков (функция TpAllocJobNotification). Это позволяет заинтересованным сторонам (обычно создателю задания) следить за нарушением лимита и за событиями, которые могут влиять на безопасность задания (например, это может быть создание нового процесса или аварийное завершение процесса).

Задания играют важную роль во многих системных механизмах.

• Они управляют современными приложениями (процессами UWP). За подробностями обращайтесь к главе 9 части 2. Более того, каждое современное приложение работает в составе задания. Вы можете убедиться в этом при помощи программы Process Explorer, как описано в эксперименте «Просмотр объекта задания» далее в этой главе.

• Они используются для реализации поддержки контейнеров Windows через механизм серверных участков, рассмотренный позднее в этом разделе.

• Они лежат в основе того, как DAM (Desktop Activity Moderator) управляет регулированием, виртуализацией таймеров, замораживанием таймеров и другим поведением, приводящим к бездействию, в приложениях и службах Win32. DAM описывается в главе 8 части 2.

• Они делают возможным определение и управление группами планирования для планирования DFSS (Dynamic Fair-Share Scheduling), описанного в главе 4.

• Они лежат в основе спецификации нестандартного разбиения памяти, необходимого для использования API секционирования памяти, описанного в главе 5.

• Они становятся одним из ключевых факторов для реализации таких функций, как запуск от имени (вторичный вход), изоляция приложений и помощник совместимости программ.

• Они предоставляют часть песочницы безопасности для таких приложений, как Google Chrome и Microsoft Office Document Converter, а также устранения рисков от атак отказа в обслуживании (DoS) через запросы WMI (Windows Management Instrumentation).

Ограничения заданий

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

• Максимальное количество активных процессов. Ограничивает количество параллельно выполняемых процессов в задании.

• Ограничение времени работы центрального процессора в пользовательском режиме на уровне задания. Ограничения максимального количества времени работы центрального процессора в пользовательском режиме, которое может быть потреблено процессами в задании (включая те процессы, которые уже выполнены и из них осуществлен выход). По достижении этого лимита все процессы в задании завершаются с кодом ошибки, и никакие новые процессы в задании не могут быть созданы (если только лимит не будет перезапущен). Об объекте задания дается сигнал, поэтому любые потоки, ожидающие задание, будут освобождены. Вы можете изменить поведение по умолчанию вызовом функции SetInformationJobObject для установки поля EndOfJobTimeAction структуры JOBOBJECT_END_OF_JOB_TIME_INFORMATION, переданной с информационным классом JobObjectEndOfJobTimeInformation в классе запроса, вместо отправки уведомления через порт завершения задания.

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

• Сходство процессоров для задания. Устанавливает маску сходства процессоров для каждого процесса в задании. (Отдельные потоки могут изменять свое сходство на любой поднабор сходства задания, но процессы изменять свои установки сходства процесса не могут.)

• Групповое сходство для задания. Устанавливает список групп, которым могут быть назначены процессы в задании. Затем любые изменения сходства сводятся к выбору группы. Это считается устаревшей групповой версией ограничения сходства процессоров для задания, которое не рекомендовано к использованию.

• Класс приоритета для процессов задания. Устанавливает класс приоритета для каждого процесса в задании. Потоки не могут повысить свой приоритет относительно класса (обычно они могли это делать). Попытки повышения приоритета потока игнорируются. (Ошибка при вызове функции SetThreadPriority не возвращается, но повышение не происходит.)

• Минимум и максимум рабочего набора по умолчанию. Определяет указанный минимум и максимум рабочего набора для каждого процесса в задании. (Эта настройка не распространяется сразу на все задание, у каждого процесса есть свой собственный рабочий набор с одинаковыми минимальными и максимальными значениями.)

• Лимит зафиксированной виртуальной памяти для процесса и задания. Определяет максимальный размер виртуального адресного пространства, которое может быть зафиксировано либо отдельным процессом, либо всем заданием.

• Управление процессорным временем. Определяет максимальную величину процессорного времени, которое разрешено использовать заданию перед тем, как оно столкнется с фиксированным регулированием. Используется как часть поддержки группового планирования, описанного в главе 4.

• Управление скоростью передачи данных для сетевого канала. Определяет максимальную скорость передачи данных, после которой в действие вступает регулирование. Также позволяет назначить метку DSCP (Differentiated Services Code Point) в контексте QoS для каждого сетевого пакета, отправляемого заданием. Может задаваться только для одного задания в иерархии, влияет на задание и все дочерние задания.

• Управление скоростью передачи данных для дискового ввода/вывода. То же, что и управление скоростью передачи данных для сетевого канала, но относится к дисковому вводу/выводу и может управлять либо самой скоростью передачи данных, либо количеством операций ввода/вывода в секунду (IOPS). Может задаваться как для конкретного тома, так и для всех томов в системе.

Для многих ограничений владелец задания может установить пороговые значения, при которых будут отправляться уведомления (или, если уведомление не зарегистрировано, задание будет просто уничтожаться). Также средства управления скоростью передачи позволяют назначать диапазоны допустимых отклонений — например, разрешить процессу превышать свои 20 % пропускной способности канала каждые 5 минут не более чем на 10 секунд. Эти уведомления осуществляются ­записью соответствующего сообщения в порт завершения ввода/вывода для задания. (За подробностями обращайтесь к документации Windows SDK.)

Наконец, для процессов в задании можно установить ограничения, касающиеся пользовательского интерфейса. Такие ограничения включают запрет на открытие процессами дескрипторов окон, владельцами которых являются потоки за пределами задания, запрет чтения и/или записи в буфер обмена и на изменения многих параметров системы пользовательского интерфейса через Windows-функцию SystemParametersInfo. Этими ограничениями, касающимися пользовательского интерфейса, управляет драйвер GDI/USER Win32k.sys, относящийся к подсистеме Windows, и они приводятся в исполнение через одну из специальных выносок (callouts), зарегистрированных с помощью диспетчера процессов. Вы можете предоставить доступ всем процессам задания к конкретным пользовательским дескрипторам (например, дескрипторам окна) вызовом функции UserHandleGrantAccess; эта функция может вызываться только процессом, который не является частью соответствующего задания, что вполне естественно.

Работа с заданиями

Объект задания создается API-функцией CreateJobObject. Только что созданное задание не содержит никаких процессов. Чтобы добавить процесс в задание, вызовите функцию AssignProcessToJobObject, которая может вызываться многократно для добавления процессов в задание и даже для включения одного процесса в разные задания. В последнем варианте создается вложенное задание (см. следующий раздел). Другой способ добавления процесса в задание основан на ручном указании дескриптора объекта задания при помощи атрибута PS_CP_JOB_LIST, упоминавшегося ранее в этой главе. Можно указать один или несколько объектов заданий.

Самая интересная API-функция для заданий, SetInformationJobObject, позволяет задавать различные ограничения и параметры, упомянутые в предыдущем разделе, а также содержит внутренние информационные классы, используемые такими механизмами, как контейнеры, DAM или приложения Windows UWP. Эти значения могут читаться функцией QueryInformationJobObject, при помощи которой заинтересованные стороны получают информацию об ограничениях, установленных для задания. Также ее необходимо вызывать при установке уведомлений о превышении ограничений (см. предыдущий раздел), чтобы вызывающая сторона точно знала, какие ограничения были нарушены. Также нередко используется функция TerminateJobObject, которая завершает все процессы в задании (так, как если бы для каждого процесса была вызвана функция TerminateProcess).

Вложенные задания

До выхода Windows 7 и Windows Server 2008 R2 процесс мог быть связан только с одним заданием, отчего задания приносили меньше пользы, чем могли бы, так как в некоторых случаях приложение не может заранее знать, будет ли процесс, которым ему предстоит управлять, принадлежать заданию или нет. Начиная с Windows 8 и Windows Server 2012, процесс может быть связан с несколькими заданиями; фактически это приводит к созданию иерархии заданий.

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

Ограничения для дочернего задания не могут быть менее жесткими, чем у родителя, но могут быть более жесткими. Например, если родительское задание устанавливает ограничение по памяти 100 Мбайт, никакое дочернее задание не сможет установить более высокое ограничение (такие попытки будут завершаться неудачей). Однако дочернее задание может установить более жесткое ограничение для своих процессов (и любых дочерних заданий) — например, 80 Мбайт. Любые уведомления, предназначенные для порта завершения ввода/вывода задания, будут отправляться заданию и всем его предкам. (Для отправки уведомлений предкам само задание не обязано иметь порт завершения ввода/вывода.)

Учет ресурсов родительского задания включает агрегированные ресурсы, используемые его непосредственно управляемыми процессами и всеми процессами дочерних заданий. При завершении задания (TerminateJobObject) все процессы в задании и его дочерних заданиях завершаются, начиная с дочерних заданий на нижнем уровне иерархии.

На рис. 3.15 показаны четыре процесса, управляемые иерархией заданий.

350230.png 

Рис. 3.15. Иерархия заданий

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

1. Добавить процесс П1 в задание 1.

2. Добавить процесс П1 в задание 2. При этом создается первое вложение.

3. Добавить процесс П2 в задание 1.

4. Добавить процесс П2 в задание 3. При этом создается второе вложение.

5. Добавить процесс П3 в задание 2.

6. Добавить процесс П4 в задание 1.

Эксперимент: просмотр объекта задания

Для просмотра именованных объектов заданий можно воспользоваться Системным монитором (обратитесь к категориям Job Object и Job Object Details). Безымянные задания также можно просмотреть с помощью команд отладчика ядра !job или dt nt!_ejob.

Чтобы увидеть, не связан ли процесс с заданием, можно воспользоваться коман­дой отладчика ядра !process или средством Process Explorer. Для создания и просмотра безымянного объекта задания выполните следующие действия.

1. Из окна командной строки воспользуйтесь командой runas для создания процесса, запускающего окно командной строки (Cmd.exe). Например, наберите runas /user:<домен>\<имя_пользователя> cmd.

2. Команда запросит ваш пароль. Введите свой пароль, и появится окно команд­ной строки. Служба Windows, выполняющая команду runas, создает безымянное задание для содержания всех процессов (чтобы она могла завершить все эти процессы во время выхода из системы).

3. Запустите Process Explorer, откройте меню Options, выберите команду Configure Colors и категорию Jobs. Обратите внимание: процесс Cmd.exe и его дочерний процесс ConHost.exe выделены как часть задания, как показано ниже:

3-180.tif 

4. Дважды щелкните либо на процессе Cmd.exe, либо на процессе ConHost.exe, чтобы открыть диалоговое окно свойств. Щелкните на вкладке Job, чтобы просмотреть информацию о задании, частью которого является данный процесс:

3-181.tif 

5. Запустите из окна командной строки программу Блокнот: Notepad.exe.

6. Откройте процесс Блокнота и просмотрите его вкладку Job. Блокнот выполняется в составе того же задания. Это объясняется тем, что cmd.exe не использует флаг создания CREATE_BREAKAWAY_FROM_JOB. В случае вложенных заданий на вкладке Job перечислены процессы задания, которому непосредственно принадлежит процесс, и все процессы в дочерних заданиях.

7. Запустите отладчик ядра в работающей системе и введите команду !process, чтобы найти notepad.exe и вывести основную информацию об этом процессе:

lkd> !process 0 1 notepad.exe

PROCESS ffffe001eacf2080

    SessionId: 1  Cid: 3078    Peb: 7f4113b000  ParentCid: 05dc

    DirBase: 4878b3000  ObjectTable: ffffc0015b89fd80  HandleCount: 188.

    Image: notepad.exe

    ...

    BasePriority                      8

    CommitCharge                      671

    Job                               ffffe00189aec460

8. Обратите внимание на указатель Job, отличный от нуля. Чтобы получить сводку задания, введите команду отладчика !job:

lkd> !job ffffe00189aec460

Job at ffffe00189aec460

  Basic Accounting Information

    TotalUserTime:             0x0

    TotalKernelTime:           0x0

    TotalCycleTime:            0x0

    ThisPeriodTotalUserTime:   0x0

    ThisPeriodTotalKernelTime: 0x0

    TotalPageFaultCount:       0x0

    TotalProcesses:            0x3

    ActiveProcesses:           0x3

    FreezeCount:               0

    BackgroundCount:           0

    TotalTerminatedProcesses:  0x0

    PeakJobMemoryUsed:         0x10db

    PeakProcessMemoryUsed:     0xa56

  Job Flags

  Limit Information (LimitFlags: 0x0)

  Limit Information (EffectiveLimitFlags: 0x0)

9. Обратите внимание: поле ActiveProcesses содержит значение 3 (cmd.exe, conhost.exe и notepad.exe). После команды !job можно передать флаг 2, чтобы просмотреть список процессов, входящих в задание:

lkd> !job ffffe00189aec460 2

...

Processes assigned to this job:

    PROCESS ffff8188d84dd780

        SessionId: 1  Cid: 5720    Peb: 43bedb6000  ParentCid: 13cc

        DirBase: 707466000  ObjectTable: ffffbe0dc4e3a040  HandleCount:

<Data Not Accessible>

        Image: cmd.exe

 

    PROCESS ffff8188ea077540

        SessionId: 1  Cid: 30ec    Peb: dd7f17c000  ParentCid: 5720

        DirBase: 75a183000  ObjectTable: ffffbe0dafb79040  HandleCount:

<Data Not Accessible>

        Image: conhost.exe

 

    PROCESS ffffe001eacf2080

        SessionId: 1  Cid: 3078    Peb: 7f4113b000  ParentCid: 05dc

    DirBase: 4878b3000  ObjectTable: ffffc0015b89fd80  HandleCount: 188.

    Image: notepad.exe

10. Чтобы вывести объект задания, можно также воспользоваться командой dt и просмотреть дополнительные поля, относящиеся к заданию, — например, уровень вхождения этого задания и его отношения с другими заданиями в случае вложения (родительское задание, одноуровневые задания, корневое задание):

lkd> dt nt!_ejob ffffe00189aec460

   +0x000 Event            : _KEVENT

   +0x018 JobLinks         : _LIST_ENTRY [ 0xffffe001'8d93e548 -

0xffffe001'df30f8d8 ]

   +0x028 ProcessListHead  : _LIST_ENTRY [ 0xffffe001'8c4924f0 -

0xffffe001'eacf24f0 ]

   +0x038 JobLock          : _ERESOURCE

   +0x0a0 TotalUserTime    : _LARGE_INTEGER 0x0

   +0x0a8 TotalKernelTime  : _LARGE_INTEGER 0x2625a

   +0x0b0 TotalCycleTime   : _LARGE_INTEGER 0xc9e03d

   ...

   +0x0d4 TotalProcesses   : 4

   +0x0d8 ActiveProcesses  : 3

   +0x0dc TotalTerminatedProcesses : 0

...

   +0x428 ParentJob        : (null)

   +0x430 RootJob          : 0xffffe001'89aec460 _EJOB

...

   +0x518 EnergyValues     : 0xffffe001'89aec988 _PROCESS_ENERGY_VALUES

   +0x520 SharedCommitCharge : 0x5e8

Контейнеры Windows (серверные участки)

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

Именно для удовлетворения таких потребностей создавались такие технологии, как Docker. Такие технологии, по сути, позволяют развернуть «изолированное приложение» из одного дистрибутива Linux в другом, не беспокоясь о сложном развертывании локальной установки или потреблении ресурсов виртуальной машины. Хотя технология изначально предназначалась только для Linux, компания Microsoft способствовала включению Docker в Windows 10 в составе обновления Anniversary Update. Технология может работать в двух режимах.

• Приложение развертывается в тяжеловесном, но полностью изолированном контейнере Hyper-V, который поддерживается как в клиентском, так и в серверном сценарии.

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

Последняя технология, которая будет рассмотрена в этом разделе, вызвала глубокие изменения в операционной системе, необходимые для ее поддержки. Как упоминалось ранее, возможность создания контейнеров серверных участков (server silos) для клиентских систем существует, но в настоящее время она отключена. В отличие от контейнеров Hyper-V, использующих среду с полноценной виртуализацией, контейнер серверного участка предоставляет второй «экземпляр» всех компонентов пользовательского режима, работающих поверх того же ядра и драйверов. За счет некоторого снижения безопасности формируется гораздо менее затратная среда контейнера.

Объекты заданий и участки

Возможность создания участков связана с некоторыми недокументированными субклассами API-функции SetJobObjectInformation. Другими словами, участок по сути является «суперзаданием» с дополнительными правилами и возможностями, выходящими за пределы того, что вы видели ранее. Более того, объект задания может использоваться как для изоляции и управления ресурсами, так и для создания участка. Такие задания называются гибридными (hybrid).

На практике объекты заданий могут обслуживать два типа участков: участки приложений (которые в настоящее время используются для реализации технологии Desktop Bridge и не рассматриваются в этом разделе — см. главу 9 части 2) и серверные участки, которые используются поддержкой контейнеров Docker.

Изоляция участков

Первый элемент, определяющий серверный участок — специальный объект корневого каталога (\) диспетчера объектов. (Диспетчер объектов рассматривается в главе 8 части 2.) И хотя этот механизм еще не рассматривался, достаточно сказать, что все именованные объекты, видимые приложению (файлы, разделы реестра, события, мьютексы, порты RPC и т.д.), размещаются в корневом пространстве имен, что позволяет приложениям создавать, находить и совместно использовать эти объекты.

Возможность существования у серверного участка собственного корня означает возможность контроля над обращениями к любому именованному объекту. Контроль может осуществляться тремя разными способами;

• созданием новой копии существующего объекта для предоставления альтернативного доступа к нему из участка;

• созданием символической ссылки на существующий объект для предоставления прямого доступа к нему;

• созданием совершенно нового объекта, который существует только в пределах участка (как объекты, используемые контейнеризованным приложением).

Эта способность объединяется с функциональностью Virtual Machine Compute (Vmcompute) (используемой Docker), которая взаимодействует с другими компонентами для предоставления полного уровня изоляции.

• Файл с базовым образом Windows (WIM) — базовый образ ОС. Этот компонент предоставляет отдельную копию операционной системы. На момент написания книги компания Microsoft предоставляет образы Server Core и Nano Server.

• Библиотека Ntdll.dll ведущей ОС. Переопределяет библиотеку из базового образа ОС. Это объясняется тем фактом, что серверные участки, как уже было сказано, используют то же ядро и драйверы. Поскольку Ntdll.dll обрабатывает системные функции, это единственный компонент пользовательского режима, который должен использоваться управляющей ОС.

• Виртуальная файловая система «песочницы», предоставляемая драйвером-фильтром Wcifs.sys. Позволяет вносить временные изменения в файловой системе контейнера без модификации диска NTFS. Такие изменения могут быть потеряны при закрытии контейнера.

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

• Диспетчер сеансов (Smss.exe). Сейчас используется для создания дополнительных сеансов служб или консольных сеансов — новая возможность, необходимая для поддержки контейнеров. При этом Smss расширяется для поддержки не только дополнительных пользовательских сеансов, но и сеансов, необходимых для каждого запущенного контейнера.

На рис. 3.16 изображена архитектура таких контейнеров с этими компонентами.

386680.png 

Рис. 3.16. Архитектура контейнеров

Границы изоляции участков

Упомянутые компоненты формируют среду изоляции пользовательского режима. Однако при использовании управляющего компонента Ntdll.dll, взаимодействующего с управляющим ядром и драйверами, важно иметь дополнительные границы изоляции. Ядро предоставляет такие границы для того, чтобы отличать один участок от другого. Соответственно, каждый серверный участок содержит собственные изолированные версии.

• Общие пользовательские микроданные (SILO_USER_SHARED_DATA в символических именах). Здесь хранится нестандартный системный путь, идентификатор сеанса, PID переднего плана и тип/семейство продукта. Все эти элементы исходных данных KUSER_SHARED_DATA не могут поступить от управляющей ОС, так как они содержат ссылки на информацию, относящуюся к образу управляющей ОС — вместо образа базовой ОС, которая должна использоваться вместо нее. Различные компоненты и API модифицируются так, чтобы при чтении таких данных использовались совместные общие данные участка (вместо пользовательских общих данных). При этом исходная структура KUSER_SHARED_DATA остается по своему обычному адресу со своим исходным представлением подробной информации хоста; это один из способов, которыми состояние управляющей системы «проникает» в состояние контейнера.

• Корневое пространство имен каталога объектов. Участок имеет собственную символическую ссылку \SystemRoot, каталог \Device (через который все компоненты пользовательского режима обращаются к драйверам устройств опосредованно), карту устройства и отображения устройств DOS (например, так приложения пользовательского режима обращаются к сетевым отображенным драйверам), каталог \Sessions и т.д.

• Отображение набора API. Базируется на схеме набора API образа базовой ОС, а не на том, который хранится в файловой системе управляющей ОС. Как упоминалось ранее, загрузчик использует отображения наборов API для определения того, какая DLL-библиотека реализует ту или иную функцию. Эти библиотеки могут различаться в зависимости от выпуска ОС, и приложениям должен быть виден выпуск базовой, а не управляющей ОС.

• Сеанс входа. Связывается с локально-уникальным идентификатором (LUID) SYSTEM и Anonymous, а также с LUID виртуальной учетной записи, описывающей пользователя в участке. Фактически представляет маркер служб и приложения, которые будут работать в контейнерном сеансе службы, созданном Smss. Подробнее о LUID и сеансах входа см. в главе 7.

• Контексты трассировки ETW и средства ведения журнала. Предназначены для изоляции операций ETW с участком без предоставления доступа или утечки данных состояния между контейнерами и/или самой управляющей ОС. (Подробнее о ETW см. в главе 9 части 2.)

Контексты участков

Хотя существуют изоляционные границы, предоставляемые ядром управляющей ОС, другие компоненты внутри ядра, а также драйверы (включая сторонние) могут добавлять контекстные данные в участки с использованием API-функции PsCreateSiloContext для записи специализированных данных, связанных с участком, или связыванием существующего объекта с участком. Каждый такой контекст участка использует индекс слота участка, который будет вставляться во все работающие (и будущие) серверные участки, хранящие указатель на контекст. Система предоставляет 32 встроенных общесистемных индекса слотов системного уровня и 256 слотов расширения, предоставляющих множество средств расширения.

При создании серверного участка он получает собственный массив SLS (Silo-Local Storage) по аналогии с тем, как у потоков имеется собственная память TLS (Thread-Local Storage). Внутри массива разные элементы будут соответствовать индексам слотов, выделенным для хранения контекстов участков. Разные участки будут хранить разные указатели на один индекс слота, но по этому индексу всегда хранится один контекст. (Например, драйверу Foo всегда принадлежит индекс 5 на всех участках, и он всегда может использовать его для хранения своего указателя/контекста в каждом участке.) В некоторых случаях встроенные компоненты ядра (такие, как диспетчер объектов, монитор безопасности (SRM) и диспетчер конфигурации) используют такие слоты, хотя другие слоты используются встроенными драйверами (такими, как вспомогательный функциональный драйвер для Winsock, Afd.sys).

Как и при работе с общими пользовательскими данными серверных участков, различные компоненты и API были обновлены: теперь они обращаются к данным из соответствующих контекстов участков, а не из используемых ранее глобальных переменных ядра. Например, поскольку каждый контейнер теперь содержит собственный процесс Lsass.exe, а SRM ядра нужен дескриптор для процесса Lsass.exe (подробнее о Lsass и SRM см. в главе 7), он уже не может быть одиночным объектом, хранящимся в глобальной переменной. Соответственно, SRM теперь обращается к дескриптору посредством запроса контекста участка активного серверного участка, а переменная читается из возвращенной структуры данных.

Возникает интересный вопрос: что происходит с процессом Lsass.exe, работающим в самой управляющей ОС? Как SRM будет обращаться к дескриптору, если для этого набора процессов и сеанса (т. е. для самого сеанса 0) не существует серверного участка? Чтобы разрешить эту проблему, ядро теперь реализует корневой управляющий участок. Другими словами, сама управляющая ОС теперь также считается частью участка! Она не является участком в полном смысле слова; скорее это хитрый трюк, благодаря которому запросы контекстов для текущего участка работают даже в том случае, если самого текущего участка нет. Задача решается хранением глобальной переменной ядра с именем PspHostSiloGlobals, имеющей собственный массив SLS, а также других контекстов участков, используемых встроенными компонентами ядра. При вызове различных API-функций участков с указателем NULL этот указатель интерпретируется как «участок отсутствует — использовать участок управляющей ОС».

Эксперимент: вывод контекста участка SRM для управляющего участка

Итак, хотя система Windows 10 может не управлять серверными участками (особенно если это клиентская система), все равно существует управляющий участок, который содержит изолированные контексты, используемые ядром. У отладчика Windows существует расширение !silo, которое может использоваться с параметрами Host в формате !silo -g Host. Вывод должен выглядеть примерно так:

lkd> !silo -g Host

Server silo globals fffff801b73bc580:

                        Default Error Port: ffffb30f25b48080

                        ServiceSessionId  : 0

                        Root Directory    : 00007fff00000000 ''

                        State             : Running

В вашем выводе указатель на глобальные переменные участка должен быть оформлен в виде гиперссылки. Щелчок на ней приводит к выполнению следую­щей команды:

lkd> dx -r1 (*((nt!_ESERVERSILO_GLOBALS *)0xfffff801b73bc580))

(*((nt!_ESERVERSILO_GLOBALS *)0xfffff801b73bc580))                 [Type: _

ESERVERSILO_GLOBALS]

[+0x000] ObSiloState       [Type: _OBP_SILODRIVERSTATE]

[+0x2e0] SeSiloState       [Type: _SEP_SILOSTATE]

[+0x310] SeRmSiloState     [Type: _SEP_RM_LSA_CONNECTION_STATE]

[+0x360] CmSiloState       : 0xffffc308870931b0 [Type: _CMP_SILO_CONTEXT *]

[+0x368] EtwSiloState      : 0xffffb30f236c4000 [Type: _ETW_SILODRIVERSTATE *]

...

Теперь щелкните на поле SeRmSiloState. В открывшейся структуре, среди прочего, содержится указатель на процесс Lsass.exe:

lkd> dx -r1 ((ntkrnlmp!_SEP_RM_LSA_CONNECTION_STATE *)0xfffff801b73bc890)

((ntkrnlmp!_SEP_RM_LSA_CONNECTION_STATE *)0xfffff801b73bc890)       :

0xfffff801b73bc890 [Type: _SEP_RM_LSA_CONNECTION_STATE *]

    [+0x000] LsaProcessHandle : 0xffffffff80000870 [Type: void *]

    [+0x008] LsaCommandPortHandle : 0xffffffff8000087c [Type: void *]

    [+0x010] SepRmThreadHandle : 0x0 [Type: void *]

    [+0x018] RmCommandPortHandle : 0xffffffff80000874 [Type: void *]

Мониторы участков

Если драйверы ядра обладают возможностью добавлять собственные контексты участков, как они узнают, какие участки выполняются и какие новые участки создаются при запуске контейнеров? Ответ лежит в мониторе участков, предоставляющем набор API-функций для получения уведомлений о создании и/или завершении серверных участков (PsRegisterSiloMonitor, PsStartSiloMonitor, PsUnregisterSiloMonitor), а также уведомлений для любых уже существующих участков. Затем каждый монитор участка может получить свой индекс слота вызовом PsGetSiloMonitorContextSlot, который затем может использоваться функциями PsInsertSiloContext, PsReplaceSiloContext и PsRemoveSiloContext при необходимости. Вы можете создавать дополнительные слоты вызовом PsAllocSiloContextSlot, но это потребуется только в том случае, если компонент по какой-то причине захочет сохранить два контекста. Кроме того, драйверы могут использовать API-функцию PsInsertPermanentSiloContext или PsMakeSiloContextPermanent для создания «долгосрочных» контекстов участков, которые не учитываются при подсчете ссылок и не привязываются к сроку жизни серверного участка или количеству операций чтения контекстов участков. После сохранения такие контексты участков могут читаться методами PsGetSiloContext и/или PsGetPermanentSiloContext.

Эксперимент: мониторы и контексты участков

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

lkd> dps poi(afd!AfdPodMonitor)

ffffe387'a79fc120  ffffe387'a7d760c0

ffffe387'a79fc128  ffffe387'a7b54b60

ffffe387'a79fc130  00000009'00000101

ffffe387'a79fc138  fffff807'be4b5b10 afd!AfdPodSiloCreateCallback

ffffe387'a79fc140  fffff807'be4bee40 afd!AfdPodSiloTerminateCallback

Обратите внимание на слот (9 в данном примере) из управляющего участка. Участки хранят свои SLS в поле с именем Storage, которое содержит массив структур данных (элементов слотов); каждая структура содержит указатель и некоторые флаги. Для получения смещения правильного элемента слота индекс умножается на 2, после чего мы обращаемся ко второму полю (+1) для получения указателя на следующий указатель:

lkd> r? @$t0 = (nt!_ESERVERSILO_GLOBALS*)@@masm(nt!PspHostSiloGlobals)

lkd> ?? ((void***)@$t0->Storage)[9 * 2 + 1]

void ** 0xffff988f'ab815941

Обратите внимание: флаг долгосрочного хранения включен в указатель операцией OR. Исключите его при помощи маски, а затем используйте расширение !object для подтверждения того, что это действительно контекст участка.

lkd> !object (0xffff988f'ab815941 & -2)

Object: ffff988fab815940 Type: (ffff988faaac9f20) PsSiloContextNonPaged

Создание серверного участка

При создании серверного участка сначала используется объект задания, потому что, как упоминалось выше, участки реализуются на основе объектов заданий. Для этого используется стандартная API-функция CreateJobObject, которая была модифицирована в составе обновления Anniversary Update так, что в нее включается идентификатор задания (JID, Job ID). JID берется из того же пула чисел, что и идентификаторы процесса и потока (PID и TID), т.е. из таблицы клиентских идентификаторов (CID, Client ID). Соответственно, идентификаторы JID уникальны не только между другими заданиями, но также между другими процессами и потоками. Так же автоматически создается GUID контейнера.

Затем используется API-функция SetInformationJobObject с классом информации создания участка. Это приводит к установке флага Silo в объекте исполняющей системы EJOB, представляющем задание, а также к размещению в памяти массива слотов SLS, который вы уже видели ранее в поле Storage объекта EJOB. К этому моменту создается участок приложения.

Затем создается пространство имен корневого каталога объектов с другим информационным классом и вызовом SetInformationJobObject. Новый класс требует привилегии TCB (Trusted Computing Base). Так как участки обычно создаются только службой Vmcompute, это делается для того, чтобы предотвратить злонамеренное использование пространств имен виртуальных приложений для введения приложений в заблуждение и потенциальное нарушение их работы. При создании этого пространства имен диспетчер объектов создает или открывает новый каталог Silos в реальном корне управляющей ОС (\) и присоединяет JIT для создания нового виртуального корня (например, \Silos\148\). Затем создаются объекты KernelObjects, ObjectTypes, GLOBALROOT и DosDevices. Далее корень сохраняется как контекст участка с индексом слота из PsObjectDirectorySiloContextSlot, выделенным диспетчером объектов при загрузке.

Следующий шаг — преобразование участка в серверный участок, что требует еще одного вызова SetInformationJobObject и еще одного информационного класса. Затем выполняется функция PspConvertSiloToServerSilo в ядре, которая инициализирует структуру ESERVERSILO_GLOBALS, которая была представлена ранее как часть эксперимента с выводом PspHostSiloGlobals командой !silo. Функция инициализирует пользовательские общие данные участка, отображение набора API, SystemRoot и различные контексты участка (например, тот, который использовался SRM для идентификации процесса Lsass.exe). В ходе преобразования мониторы участков, которые зарегистрировали и запустили свои обратные вызовы, будут получать уведомления, что позволит им добавить собственные контекстные данные участка.

Остается сделать последний шаг: «запустить» серверный участок, инициализируя новый сеанс службы для него. Считайте, что это своего рода сеанс 0, но для серверного участка. Для этого используется сообщение ALPC, отправляемое Smss SmApiPort, которое содержит дескриптор объекта задания, созданного Vmcompute, который теперь становится объектом задания серверного участка. Как и при создании реального пользовательского сеанса, Smss клонирует собственную копию, но на этот раз копия будет связана с объектом задания в момент создания. Таким образом, новая копия Smss присоединяется ко всем контейнеризованным элементам серверного участка. Smss будет считать, что это сеанс 0, и выполнит свои обычные действия: запуск Csrss.exe, Wininit.exe, Lsass.exe и т.д. Процесс «запуска» будет продолжаться как обычно: Wininit.exe запустит диспетчер служб (Services.exe), который в свою очередь запустит все службы с автоматическим режимом запуска, и т.д. Теперь в серверном участке могут выполняться новые приложения, которые будут запускаться с сеансом входа, связанным с LUID виртуальной учетной записи службы, как описано выше.

Вспомогательная функциональность

Возможно, вы заметили, что короткое описание, приведенное выше, не приведет к реальному успеху процесса «загрузки». Например, если в процессе инициализации потребуется создать именованный канал с именем ntsvcs, это потребует взаимодействия с объектом \Device\NamedPipe или, как его видит Services.exe, — \Silos\JID\Device\NamedPipe. Но такой объект устройства не существует!

Соответственно, для того чтобы драйвер устройства мог функционировать, драйверы должны получить информацию и зарегистрировать собственные мониторы участков, которые затем используют уведомления для создания собственных объектов устройств уровня участка. Ядро предоставляет API-функцию PsAttachSiloToCurrentThread (и парную функцию PsDetachSiloFromCurrentThread), которая временно сохраняет в поле Silo объекта ETHREAD переданный объект задания. Это приводит к тому, что все обращения (например, к диспетчеру объектов) будут рассматриваться как исходящие от участка.

Например, драйвер именованного канала может использовать эту функциональность для создания объекта NamedPipe в пространстве имен \Device, который становится частью \Silos\JID\.

Другой вопрос: если приложения запускаются фактически в сеансе «службы», как они могут взаимодействовать с пользователем, обрабатывать ввод и вывод? Во-первых, важно заметить, что при запуске в контейнере Windows графический интерфейс невозможен и не разрешен, а попытки использовать удаленный рабочий стол (Remote Desktop, RDP) для обращения к контейнеру тоже невозможны. Соответственно, выполняться могут только приложения командной строки. Но даже таким приложениям обычно нужен «интерактивный» сеанс. Как они могут функционировать? Секрет кроется в специальном управляющем процессе CExecSvc.exe, который реализует службу исполнения контейнера. Служба через именованный канал взаимодействует со службами Docker и Vmcompute управляющей ОС и используется для запуска контейнеризованных приложений в сеансе. Она также используется для эмуляции консольной функциональности, которая обычно предоставляется Conhost.exe, передачи ввода и вывода по именованному каналу окну командной строки (или PowerShell), которое изначально использовалось для выполнения команды docker в управляющей ОС. Служба также используется при выполнении таких команд, как docker cp (передача файлов контейнеру или из контейнера).

Шаблоны контейнеров

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

Если участок хочет воспроизвести музыку на звуковой карте, он не должен использовать отдельный объект устройства для звуковой карты, к которой будут обращаться все остальные участки, а также управляющая ОС. Это было бы необходимо только в том случае, если бы, допустим, требовалось реализовать изоляцию объектов на уровне участков. Другой пример — драйвер AFD. Хотя он использует монитор участка, это делается для идентификации службы пользовательского режима, которая является хостом клиента DNS. Эта информация нужна драйверу для взаимодействия с запросами DNS режима ядра, которая ведется на уровне участка, без создания отдельных объектов \Silos\JID\Device\Afd, так как в системе существует единый стек сети /Winsock.

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

Для поддержки всех этих потребностей пространство имен участка, реестр и файловая система определяются специализированным контейнерным шаблоном, который по умолчанию хранится в файле %SystemRoot%\System32\Containers\wsc.def после включения поддержки контейнеров Windows в диалоговом окне Включение или отключение компонентов Windows (Add/Remove Windows Features). Этот файл описывает пространство имен диспетчера объектов и реестра и связанные с ним правила, позволяющие определять по мере надобности символические ссылки для «настоящих» объектов управляющей ОС. Кроме того, он также описывает, какой объект задания, точки монтирования томов и политики сетевой изоляции должны использоваться. Теоретически в будущем при использовании объектов участков в операционной системе Windows можно будет выбирать другие файлы шаблонов для создания других видов контейнеризованных сред. Ниже приведен фрагмент файла wsc.def в системе, в которой включена поддержка контейнеров:

<!-- Файл определения участка для cmdserver.exe -->

<container>

    <namespace>

        <ob shadow="false">

            <symlink name="FileSystem" path="\FileSystem" scope="Global" />

            <symlink name="PdcPort" path="\PdcPort" scope="Global" />

            <symlink name="SeRmCommandPort" path="\SeRmCommandPort" scope="Global" />

            <symlink name="Registry" path="\Registry" scope="Global" />

            <symlink name="Driver" path="\Driver" scope="Global" />

            <objdir name="BaseNamedObjects" clonesd="\BaseNamedObjects" shadow="false"/>

            <objdir name="GLOBAL??" clonesd="\GLOBAL??" shadow="false">

                <!-- Needed to map directories from the host -->

                <symlink name="ContainerMappedDirectories" path="\

ContainerMappedDirectories" scope="Local" />

                <!-- Допустимые ссылки на \Device -->

                <symlink name="WMIDataDevice" path="\Device\WMIDataDevice" scope="Local"

/>

                <symlink name="UNC" path="\Device\Mup" scope="Local" />

...

            </objdir>

            <objdir name="Device" clonesd="\Device" shadow="false">

                <symlink name="Afd" path="\Device\Afd" scope="Global" />

                <symlink name="ahcache" path="\Device\ahcache" scope="Global" />

                <symlink name="CNG" path="\Device\CNG" scope="Global" />

                <symlink name="ConDrv" path="\Device\ConDrv" scope="Global" />

...

        <registry>

            <load

                key="$SiloHivesRoot$\Silo$TopLayerName$Software_Base"

                path="$TopLayerPath$\Hives\Software_Base"

                ReadOnly="true"

                />

...

                <mkkey

                    name="ControlSet001"

                    clonesd="\REGISTRY\Machine\SYSTEM\ControlSet001"

                    />

                <mkkey

                    name="ControlSet001\Control"

                    clonesd="\REGISTRY\Machine\SYSTEM\ControlSet001\Control"

                    />

Заключение

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

Показать оглавление

Комментариев: 6

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

  1. JamesCen
    scottsdale health care studiomerliniortodonzia.it/cgi-bin/antibiotici.htm health care communities
  2. Franksog
    home remedy gingivitis studiomerliniortodonzia.it/cgi-bin/antibiotici.htm infant fever remedies
  3. KennethLib
    knee ache remedies studiomerliniortodonzia.it/cgi-bin/testosterone.htm lifetime health care
  4. KennethLib
    knee ache remedies studiomerliniortodonzia.it/cgi-bin/testosterone.htm lifetime health care
  5. KennethLib
    knee ache remedies studiomerliniortodonzia.it/cgi-bin/testosterone.htm lifetime health care
  6. KennethLib
    knee ache remedies studiomerliniortodonzia.it/cgi-bin/testosterone.htm lifetime health care