EreTIk's Box » Cтатьи, исходники » Описатели объектов Windows: взгляд из ядра



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


Итак, что такое описатель в NT-based ОС? Описатель (handle) это элемент таблицы, содержащий маску предоставленного доступа (granted access) и указатель на объект ядра. Таблица описателей для каждого процесса своя: указатель на таблицу содержится в поле nt!_EPROCESS.ObjectTable. Из этого следует, что описатель содержит в себе информацию о предоставленном конкретному процессу доступе к объекту ядра.


Теперь проследим путь описателя при вызове системного сервиса, например NtReadFile. Для корректного вызова этого сервиса необходимо передать в параметрах описатель на файловый объект с полученным доступом FILE_READ_DATA. Для проверки корректности описателя и получения указателя на nt!_FILE_OBJECT NtReadFile вызывает экспортируемую функцию ядра ObReferenceObjectByHandle. В самом начале этой функции стоит очень важное ветвление: если


if ((LONG)(ULONG_PTR) Handle < 0) { ...
                

То (*1) ObReferenceObjectByHandle(...) обрабатывает специальные случаи:

  • значение описателя совпадает с -1: это псевдо-описатель, макрос NtCurrentProcess(). Он, как несложно догадаться, всегда указывает на текущий процесс
  • значение описателя совпадает с -2: это псевдо-описатель, макрос NtCurrentThread(). Он всегда указывает на текущую нить

Если же выполняется условие *1 и это не текущие процесс/нить, то значит переданный нам описатель – описатель ядра (Kernel Handle). Тут, по ходу кода функции, сразу же проверяется что (AccessMode == KernelMode). Если вызывающий код пытается получить объект по kernel-описателю, указав AccessMode == UserMode, то сразу же возвращается ошибка STATUS_INVALID_HANDLE. Это не документированная особенность описателей, которая приводит к частой ошибке тех, кто начинает писать код в ядре и не понимает отличий в Zw-функций от Nt-функций. Пример, так сказать, просто в яблочко: ветка NtWriteFile STATUS_INVALID_HANDLE на форуме WASM’а. Обратимся к WRK и получим довольно прозрачное определение описателя ядра:


    #define KERNEL_HANDLE_MASK ((ULONG_PTR)((LONG)0x80000000)) 
    #define IsKernelHandle(H,M)                                \
    (((KERNEL_HANDLE_MASK & (ULONG_PTR)(H)) == KERNEL_HANDLE_MASK) && \
     ((M) == KernelMode) &&                                \
     ((H) != NtCurrentThread())&&                         \
     ((H) != NtCurrentProcess()))
                

Фактически: кernel-описатель это описатель, принадлежащий процессу SYSTEM (nt!PsInitialSystemProcess). Таблица описателей системного процесса создается один раз при старте системы, и ее адрес расположен в не экспортируемой глобальной переменной ядра nt!ObpKernelHandleTable. Если же идет работа с обычным описателем (не kernel), то используется таблица описателей текущего процесса.


А теперь рассмотрим разницу вызовов в ядре Zw-функций и Nt-функций. В ntdll.dll вообще нет разницы между этими функциями, собственно экспорты этой библиотеки указывают на один и тот же код. Наверное, это и порождает ошибки при использовании вызовов системных сервисов из кода режима ядра. В ядре это разные функции: конечно, они выполняют в итоге один и тот же код, но все Zw-функции это вызов Nt-функций через специальный wrap’ер, меняющий режим вызова текущей нити. Поле PreviousMode структуры nt!_KTHREAD текущей нити принудительно меняется на KernelMode. После возврата восстанавливается старое значение. То есть любой вызов функции nt!ExGetPreviousMode(), внутри этого wrap'ера, будет возвращать KernelMode. На самом деле wrap'ер намного сложнее, чем просто смена PreviousMode, в чем можно убедиться взлянув на функцию nt!KiServiceInternal. Смена режима вызова текущей нити на amd64 Win7 выглядит следующим образом (nt!KiServiceInternal+2A):


    mov     rbx, gs:188h
    prefetchw byte ptr [rbx+1D8h]
    movzx   edi, [rbx+_KTHREAD.PreviousMode]
    mov     [rbp+0E8h+var_140], dil
    mov     [rbx+_KTHREAD.PreviousMode], 0
                

Об этом можно прочесть в MSDN'овской статье Using Nt and Zw Versions of the Native System Services Routines. Теперь рассмотрим пути создания kernel-описателя. Самый простой и прозрачный способ это создать описатель на объект в нити процесса SYSTEM. Ваш код исполняется в системной нити в следующих случаях:

  • В DriverEntry (точке входа) драйвера. Даже если загрузку драйвера инициировал пользовательский процесс, система вызовет DriverEntry к нити процесса SYSTEM
  • Очевидный вариант: из нити, созданной функцией PsCreateSystemThread(..., ProcessHandle == NULL, ...)
  • В коде функции Work Item’а: все обработчики, указные при инициализации, вызываются только в нитях процесса SYSTEM

Это, конечно, не полный список, но об этих ситуациях нужно помнить в первую очередь. Но можно создать kernel-описатель в любой нити любого процесса. Для этого нужно в атрибутах создаваемого объекта (nt!_OBJECT_ATTRIBUTES.Attributes) указать флаг OBJ_KERNEL_HANDLE. Это и будет сигнализировать ядру о том, что запрашиваемый описатель должен быть создан в таблице процесса SYSTEM. И в результате будет возвращен описатель с маской KERNEL_HANDLE_MASK.


Механизм описателей показался разработчикам из kernel team довольно удобным. Это отразилось, к примеру, в том, что идентификаторы процессов и нитей (PID и TID) являются описателями специальной таблицы nt!PspCidTable. Именно поэтому функция PsGetCurrentProcessId() возвращает HANDLE, а не DWORD, как в user mod’е. В этом достаточно просто убедиться, если заглянуть в WRK в код функций PspCreateThread и PspCreateProcess соответственно:


    ... 

    CidEntry.Object = Thread;
    CidEntry.GrantedAccess = 0;
    Thread->Cid.UniqueThread = ExCreateHandle (PspCidTable, &CidEntry);

    ... 

    CidEntry.Object = Process;
    CidEntry.GrantedAccess = 0;
    Process->UniqueProcessId = ExCreateHandle (PspCidTable, &CidEntry);

    ... 
                

ΞρεΤΙκ