Статья ASM. В ядре Windows(1) – объекты KERNEL, и таблица дескрипторов.

Marylin

Mod.Assembler
Red Team
05.06.2019
315
1 416
BIT
517
В статье рассматриваются такие сущности ОС как: «объекты» ядра, «дескрипторы» объектов, а так-же «идентификаторы» процессов. Читатель ознакомится с их внутренним устройством, представлением на уровне системы, и основным назначением. Тема окунёт нас в самые закрома системы безопасности, куда закрыт доступ не только смертным юзерам, но и в некоторых случаях драйверам. Поняв общие принципы можно будет сделать вывод, на чём основывается работа античитов и антивирусов, а главное определить вектор атаки вредоносных руткитов для защиты от них.

Для тех, кто захочет пощупать всё это дело ручками, придётся установить отладчик . На выбор предлагается там 2 варианта – или в составе пакета для разработчиков SDK, или-же как отдельный продукт «Debugging Tools for Windows». Чтобы не было лишних проблем, желательно выбрать Web-установщик, который сам проверит версию вашей ОС и утянет с репозитория майков подходящий продукт.


1. Процессы и потоки

Придерживаясь общих правил начнём с того, как создаются пользовательские процессы, и какой объём работы при этом совершает ядро ОС Windows. Обычный процесс от своего имени мы создаём функцией Kernel32!CreateProcess(), но предусмотрена возможность через CreateProcessAsUser() стартовать процессы и от чужого имени, когда в системе зареганы несколько пользователей. Бонусом идёт и более упрощённый вариант последней CreateProcessWithLogon(), но не в этом суть, так-что будем считать, что создаём (или запускаем уже готовый экзешник) от своего имени.

Под катом пройдя всевозможные проверки по цепочке Kernel32-->Ntdll.dll, код процедуры создания переходит из юзермоды в функцию ядра Nt/ZwCreateFile(), где и осуществляется регистрация нового процесса. При этом исполнительная подсистема ядра создаёт основную структуру клиента _EPROCESS (Executive), которая включает в себя ещё одну для планировщика потоков _KPROCESS (Kernel). На данном этапе новоиспечённой тушке выделяется память из общего пула свободной, происходит ещё много чего (здесь нас некасающегося), и наконец назначается личный идентификатор процесса РID, что подразумевает «Process Identifier». Систему не интересует, какое там имя носит процесс – она находит и контролирует все процессы исключительно по их идентификаторам. Вся эта информация сохраняется в _EPROCESS, которая является паспортом любого процесса.

Однако сам процесс не может исполнять код программы – это просто бокс для хранения нужных системе данных. Рабочими лошадками являются один или несколько потоков процесса, а потому ядро создаёт привязанные к конкретной _EPROCESS структуры _ETHREAD (у каждого потока своя), которые так-же содержат в себе _KTHREAD для планировщика. Идентификатор потока назвали TID, от слова Thread (в народе тред).

Две эти структуры одного процесса имеют в своих тушках указатели друг на друга, чтобы среди огромного числа чужих потоков случайно не превратиться в заблудшую овцу. Поскольку родительская _EPROCESS одна, то каждой структуре _ETHREAD достаточно одного указателя. Зато у _EPROCESS может быть 1000 и более резвящихся в системной памяти дочерних тредов, поэтому она собирает указатели на них в кольцевой список LIST_ENTRY – это намного удобней.

По окончании, ядро Ntoskrnl.exe через своего подопечного Ntdll.dll в пользовательском пространстве возвращает PID созданного процесса, а Ntdll.dll в ответ оформляет для себя структуру PEB (Process Environment Block, окружение процесса), а так-же для каждого из его потоков структуры TEB. Эти структуры по назначению аналогичны ядерным и служат для того, чтобы код юзера по каждым пустякам не переходил в ядро за информацией. В режиме х32 на текущий TEB всегда указывает сегментный регистр FS, а в режиме х64 регистр GS. Получив указатель на TEB, мы всегда сможем найти в нём линк и на структуру РЕВ своего процесса. Здесь и далее нас не будут интересовать PEB/TEB юзера, а сделаем акцент исключительно на структурах ядра.

Запустим любой свой процесс на исполнение (у меня это HandleInfo.exe), и в отладчике WinDbg соберём о нём данные.
Структура _EPROCESS на моей х64 Win7 имеет размер 1.2КБ, и чтобы не искать в этой портянке лога интересующие нас поля, я просто выдерну их из контекста:

Код:
0: kd> !process 0 0 HandleInfo.exe
PROCESS fffffa8002104b10     <---------------- указатель на _EPROCESS
    SessionId: 1   Cid: 0928  Peb: 7fffffd3000  ParentCid: 066c
    DirBase: 1e1d3000  ObjectTable: fffff8a001a3a360  HandleCount: 8.
    Image: HandleInfo.EXE

0: kd> dt _eprocess fffffa8002104b10  <----- запрос содержимого структуры
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x180 UniqueProcessId  : 0x00000000`00000928 Void
   +0x200 ObjectTable      : 0xfffff8a0`01a3a360 _HANDLE_TABLE
   +0x270 BaseAddress      : 0x00000000`00400000 Void
   +0x290 ParentProcessId  : 0x00000000`0000066c Void
   +0x2e0 ImageFileName    : [15] "HandleInfo.EXE"
   +0x2ef PriorityClass    : 2
   +0x308 ThreadListHead   : _LIST_ENTRY [ 0xfffffa80`01fc6f78 - 0xfffffa80`01fc6f78 ]
   +0x328 ActiveThreads    : 1
   +0x338 Peb              : 0x000007ff`fffd3000 _PEB
   +0x448 VadRoot          : _MM_AVL_TABLE

Значит сначала я запросил общую информацию о своём процессе, чтобы получить указатель на _EPROCESS. Отладчик вернул мне не только адрес fffffa8002104b10, но прицепом ID сессии в которой я нахожусь, Cid (от client id, это тот-же pid), указатель на РЕВ, ParentCid или Pid родителя (кто запустил процесс), линк на каталог выделенных страниц памяти DirBase, указатель на таблицу объектов процесса, и число открытых дескрипторов. Как видим, запрошенные напрямую из структуры _EPROCESS данные совпадают в первым логом, и это гуд.


2. Понятия “Object, Handle, Identifier”

Теперь поговорим о более возвышенном, и дадим определения таким понятиям как «Объект», дескриптор «Handle», и идентификатор «PID». В этой тройке кроится вся сила и мощь ОС Windows, а потому уделим им чуть больше внимания.

Инженеры спроецировали систему так, что в глазах ядра весь мир состоит только из объектов. То есть всё, с чем имеет дело ядро – считается объектом. Все объекты (аля ресурсы системы) принято разделять на KERNEL, USER и GDI, причём 2 последних пользовательские, а потому пока нам не интересны. Начиная с Vista, в ядре числится 43 базовых типов объектов, каждый из которых может иметь десятки тысяч своих экземпляров. Наиболее часто используемые типы с кратким описанием представлены в таблице ниже:

Obj.png

Таким образом, процессы и их потоки по сути являются объектами. Когда мы запускаем новый процесс на исполнение, системный диспетчер создаёт экземпляр базового объекта «Process», и возвращает нам не указатель на экземпляр, а ссылку на него в виде дескриптора «Handle». Этот дескриптор не представляет никакой опасности для ядра, т.к. фактически является просто целым числом. Но с другой стороны это индекс в таблице дескрипторов процесса – здесь и начинается самое интересное. Аналогичную ситуацию наблюдаем и со всеми остальными объектами ядра. Например при вызове функции CreateFile(), на откуп ядро возвращает нам дескриптор объекта «File», и т.д.

Утилита "WinObj" из пакета М.Руссиновича позволяет просматривать всё пространство имён объектов, и даже возвращает их свойства, что пригодится нам далее. Альтернативный пакет системных утилит под названием AllTools предлагает и Павел Йосифович, так-что здесь есть из чего выбирать. В свою очередь WinDbg способен проникнуть буквально во все потайные закрома системы, и на запрос ниже предоставляет исчерпывающие сведения об объектах, включая их реальные адреса, а не дескрипторы Handle.

Код:
0: kd> !object \ObjectTypes
Object: fffff8a000006730  Type: (fffffa8001830f30) Directory
    ObjectHeader: fffff8a000006700 (new version)
    HandleCount: 0    PointerCount: 44
    Directory Object: fffff8a000004640  Name: ObjectTypes

    Hash Address           Type     Name
    ---- -------           ----     ----
     00  fffffa80018b5600  Type     TmTm
     01  fffffa80018f9220  Type     Desktop
         fffffa800182af30  Type     Process
     03  fffffa800182a950  Type     DebugObject
     04  fffffa80018b5f30  Type     TpWorkerFactory
     05  fffffa80018b5de0  Type     Adapter
         fffffa8001830b90  Type     Token
     08  fffffa80018f9de0  Type     EventPair
     09  fffffa80026b0c50  Type     PcwObject
         fffffa80018ec3a0  Type     WmiGuid
     11  fffffa80018ed3a0  Type     EtwRegistration
     12  fffffa80018b7a40  Type     Session
         fffffa80018ebde0  Type     Timer
     13  fffffa80018eb670  Type     Mutant
     16  fffffa80018b58a0  Type     IoCompletion
     17  fffffa80018f9370  Type     WindowStation
         fffffa80018ebc90  Type     Profile
     18  fffffa80018b5750  Type     File
     21  fffffa80018ebf30  Type     Semaphore
     23  fffffa80018ee3a0  Type     EtwConsumer
     25  fffffa80018b6a40  Type     TmTx
         fffffa8001830de0  Type     SymbolicLink
     26  fffffa800234f8a0  Type     FilterConnectionPort
         fffffa80018bb260  Type     Key
         fffffa80018f9080  Type     KeyedEvent
         fffffa80018eb520  Type     Callback
     28  fffffa800182ac90  Type     UserApcReserve
         fffffa8001830970  Type     Job
     29  fffffa80018b5c90  Type     Controller
         fffffa800182ab40  Type     IoCompletionReserve
     30  fffffa80018b5b40  Type     Device
         fffffa8001830f30  Type     Directory
     31  fffffa80018b6650  Type     Section
         fffffa80018b67a0  Type     TmEn
         fffffa800182ade0  Type     Thread
     32  fffffa8001825270  Type     Type
     33  fffffa8002345d40  Type     FilterCommunicationPort
         fffffa80018c0830  Type     PowerRequest
     35  fffffa80018b68f0  Type     TmRm
         fffffa80018f9f30  Type     Event
     36  fffffa80018bf9a0  Type     ALPC Port
         fffffa80018b59f0  Type     Driver
0: kd>


3. Исследование объектов

На физическом уровне объект представляет собой обычную структуру данных в системной памяти. Он начинается с одинакового для всех заголовка OBJECT_HEADER, после которого следует непосредственно тело «Body». Поскольку все объекты разные, то и тушки у них специфичные. Вот как выглядит стандартный заголовок любого объекта:

Код:
0: kd> dt _object_header
nt!_OBJECT_HEADER
   +0x000 PointerCount       : Int8B
   +0x008 HandleCount        : Int8B
   +0x010 Lock               : _EX_PUSH_LOCK
   +0x018 TypeIndex          : UChar
   +0x019 TraceFlags         : UChar
   +0x01a InfoMask           : UChar
   +0x01b Flags              : UChar
   +0x020 ObjectCreateInfo   : Ptr64 _OBJECT_CREATE_INFORMATION
   +0x028 SecurityDescriptor : Ptr64 Void
   +0x030 Body               : _QUAD

Диспетчер объектов поддерживает два счётчика Count: дескрипторов(H) и указателей(P). Инкремент/декремент счётчика Handle автоматом воздействует и на счётчик Pointer, но не наоборот. Pointer живёт своей жизнью, и его значение может намного превышать счётчик открытых дескрипторов.

Например, когда чз CreateFile() мы открываем файл, диспетчер создаёт экземпляр файлового объекта и возвращает нам его Handle. При этом оба счётчика тут-же получают +1. Затем для чтения из файла мы передаём этот Handle в ReadFile(), а в фоне диспетчер кэша файловой системы тоже использует наш Handle для своих нужд, и следовательно счётчик обращений Pointer увеличивается на 1 без воздействия на Handle. То-есть получаем Н1:Р2. Теперь, когда по CloseHandle() мы прочитав закрываем файл, счётчик(H) уменьшается на 1, что приводит к авто-декременту и счётчика(P). Однако ссылка диспетчера кэша FS всё ещё остаётся действительной Н0:Р1, а потому диспетчер объектов не может удалить (по сути уже не нужный) файловый объект – он периодически будет чекать состояние счётчиков в надежде увидеть там Н0:Р0, пока диспетчер кэша FS не соизволит освободить свой счётчик(Р).

Поле «Lock» позволяет заблокировать доступ к структуре OBJECT_HEADER из вне, обычно перед тем как вносятся какие-либо изменения. «TypeIndex» играет важную роль – по нему диспетчер определяет тип объекта, которому принадлежит данный заголовок. Индекс берётся из специальной таблицы, где хранятся адреса (дочерних к заголовку) структур OBJECT_TYPE.

Как видим, тело объекта «Body» лежит в хвосте и начинается со-смещения 30h. Дело в том, что большинство API драйверов возвращают указатели только на само тело объекта, скрывая тем-самым доступ к заголовку. А ведь в нём лежит важное поле «SecurityDescriptor», где хранятся такие биты как DACL/ACE, да и всё остальное. Поэтому когда мы получим от драйвера адрес объекта, то отняв от него значение 30h в виде смещения тела «Body», получим нелегальный доступ к заголовку. Запушим пока этот факт в черепную коробку..

Самый простой способ найти структуры объектов (аля их тела) – это передать команде dt отладчика подстановочный знак(*) с маской поиска, ..т.е. «Гоу искать все совпадения в указанном модуле». На своей Win7 я получилил следущие 9 объектов, а если учесть, что телом объекта «Process» является уже знакомая нам EPROCESS, то с тредами получается 11, чего за глаза хватает для большинства типичных задач.

Код:
0: kd> dt nt!*_object
          ntkrnlmp!_DEVICE_OBJECT
          ntkrnlmp!_FILE_OBJECT
          ntkrnlmp!_DRIVER_OBJECT
          ntkrnlmp!_CALLBACK_OBJECT
          ntkrnlmp!_DUMMY_FILE_OBJECT
          ntkrnlmp!_SEGMENT_OBJECT
          ntkrnlmp!_SECTION_OBJECT
          ntkrnlmp!_LPCP_PORT_OBJECT
          ntkrnlmp!_ADAPTER_OBJECT
0: kd>

Возьмём к примеру объект любого драйвера.. пусть это будет сетевой NDIS.SYS (Network Driver Interface).
Получив на первом этапе адрес структуры его тела, отнимем 30h от головы, чтобы сразу прочесть и заголовок:

Код:
0: kd> !drvobj ndis
Driver object (fffffa80026b2a10) is for: \Driver\NDIS
Device object list:  fffffa80026b22b0

0: kd> dt _driver_object fffffa80026b2a10   <---- тушка объекта
nt!_DRIVER_OBJECT
   +0x000 Type              : 0n4
   +0x002 Size              : 0n336
   +0x008 DeviceObject      : 0xfffffa80`026b22b0 _DEVICE_OBJECT
   +0x010 Flags             : 0x12
   +0x018 DriverStart       : 0xfffff880`014d1000
   +0x020 DriverSize        : 0xf3000
   +0x028 DriverSection     : 0xfffffa80`01824010
   +0x030 DriverExtension   : 0xfffffa80`026b2b60 _DRIVER_EXTENSION
   +0x038 DriverName        : _UNICODE_STRING "\Driver\NDIS"
   +0x048 HardwareDatabase  : _UNICODE_STRING "\Registry\Machine\Hardware\Description\System"
   +0x050 FastIoDispatch    : (null)
   +0x058 DriverInit        : 0xfffff880`015aa06c
   +0x060 DriverStartIo     : (null)
   +0x068 DriverUnload      : (null)
   +0x070 MajorFunction     : [28] 0xfffff880`01571b80

0: kd> dt _object_header fffffa80026b2a10-0x30  <---- отняв 30h получаем заголовок
nt!_OBJECT_HEADER
   +0x000 PointerCount      : 0n26
   +0x008 HandleCount       : 0n0
   +0x010 Lock              : _EX_PUSH_LOCK
   +0x018 TypeIndex         : 0x1a ''
   +0x019 TraceFlags        : 0 ''
   +0x01a InfoMask          : 0x2 ''
   +0x01b Flags             : 0x12 ''
   +0x020 ObjectCreateInfo  : 0x00000000`00000001 _OBJECT_CREATE_INFORMATION
   +0x028 SecurityDescriptor: 0xfffff8a0`002d3263
   +0x030 Body              : _QUAD

0: kd>

В штате диспетчера объектов присутсвуют ещё 5 необязательных заголовков, о наличии которых сигнализирует поле «InfoMask». Каждый бит этого значения является флагом присутствия/отсутствия заголовка, т.е. информативны только 5 младших бит. Например выше InfoMask=2, а это в бинах 0000.0010. Таким образом, в наличии только одна структура под №2. Запросить у отладчика названия необязательных заголовков можно так (правда в них нет ничего для нас интересного):

Код:
0: kd> dt nt!_object_header_*
          ntkrnlmp!_OBJECT_HEADER_QUOTA_INFO
          ntkrnlmp!_OBJECT_HEADER_PROCESS_INFO
          ntkrnlmp!_OBJECT_HEADER_HANDLE_INFO
          ntkrnlmp!_OBJECT_HEADER_NAME_INFO
          ntkrnlmp!_OBJECT_HEADER_CREATOR_INFO
0: kd>

Флаги атрибутов для самого объекта хранятся в поле «Flags» заголовка. Это сумма значений из таблицы ниже, и если в данном случае имеем Flags=0x12, значит объект имеет флаги INHERIT + PERMANENT (см. ntdef.h):

Flags.png


4. Поиск таблицы дескрипторов процесса

На этапе создания процесса, система выделяет ему несколько страниц вирт.памяти под «Таблицу дескрипторов». Это означает, что у каждого процесса в системе имеется личный свой пул экземпляров объектов, дескрипторы которых собираются в спец.таблицу HANDLE_TABLE. Такая схема позволяет менеджеру объектов быстрее находить дескрипторы конкретно взятого процесса, чем если-бы они были беспорядочно разбросаны по всему периметру системной памяти. Алгоритм поиска дескрипторов своего/чужого процесса представлен на рис.ниже:

Find_object.png

По этой схеме видно, что повествование выше касалось лишь конца цепочки, где расположена структура заголовка OBJECT_HEADER. Так как-же до неё добраться, когда прикрывшись драйвером мы находимся в ядре? Для этого запустим любой процесс на исполнение, и получив указатель на его EPROCESS запросим у отладчика поля Pid и ObjectTable:

Код:
0: kd> !process 0 0 HandleInfo.exe
PROCESS fffffa8001cfb060
    SessionId: 1  Cid: 061c    Peb: 7fffffdf000  ParentCid: 014c
    DirBase: 3ad84000  ObjectTable: fffff8a004840660  HandleCount: 8
    Image: HandleInfo.EXE

0: kd> dt _eprocess fffffa8001cfb060 UniqueProcessId ObjectTable
nt!_EPROCESS
   +0x180 UniqueProcessId : 0x00000000`0000061c Void
   +0x200 ObjectTable     : 0xfffff8a0`04840660 _HANDLE_TABLE

По сути, указатель 0xfffff8a0`04840660 на таблицу объектов текущего процесса можно было взять сразу из первого лога, но в реальной ситуации никакого первого лога не будет, и нужно брать этот линк именно из структуры EPROCESS. Теперь двигаемся по цепочке дальше:

Код:
0: kd> dt _handle_table 0xfffff8a0`04840660
nt!_HANDLE_TABLE
   +0x000 TableCode             : 0xfffff8a0`02a0f000  <-------------
   +0x008 QuotaProcess          : 0xfffffa80`01cfb060 _EPROCESS
   +0x010 UniqueProcessId       : 0x00000000`0000061c Void
   +0x018 HandleLock            : _EX_PUSH_LOCK
   +0x020 HandleTableList       : _LIST_ENTRY [ 0xfffff8a0`0239fef0 - 0xfffff8a0`043cc380 ]
   +0x030 HandleContentionEvent : _EX_PUSH_LOCK
   +0x038 DebugInfo             : (null)
   +0x040 ExtraInfoPages        : 0n0
   +0x044 Flags                 : 0
   +0x048 FirstFreeHandle       : 0x24
   +0x050 LastFreeHandleEntry   : 0xfffff8a0`02a0fff0 _HANDLE_TABLE_ENTRY
   +0x058 HandleCount           : 8      <-----------
   +0x05c NextHandleNeedingPool : 0x400
   +0x060 HandleCountHighWtmark : 8
0: kd>

Перед нами основная информация о таблице дескрипторов процесса.
PID совпадает, счётчик дескрипторов(8) тоже (см.первый лог). Но если всего 8 дескрипторов, тогда почему «FirstFreeHandle» (первый свободный) имеет значение 0x24=36? Дело в том, что значения любых дескрипторов всегда кратны 4, при этом нулевой лежит в резерве и не используется (см.рис.выше). Итого получаем 8*4=32, а 36-ой уже свободный. Когда процессу понадобится очередной дескриптор, диспетчер объектов будет знать, куда именно в таблицу его прописать.

Сама-же таблица состоит из 16-байтых записей «Entry», которые имеют официальную структуру HANDLE_TABLE_ENTRY. Указатель на таблицу лежит в поле «TableCode» и в данном случае имеет значение 0xfffff8a0`02a0f000. Поскольку процесс с хорошим аппетитом на системные ресурсы может иметь просто огромное число открытых дескрипторов, таблица многоуровневая. Сначала система выделяет для неё одну вирт.страницу памяти размером 4КБ, и если размер одной записи равен 16-байт, то в этой странице имеется резерв всего для 4096/16=256 дескрипторов. Когда упрёмся в потолок, система динамически выделит вторую страницу и т.д. Так вот номер текущей страницы указывается в младшем байте адреса – он не несёт для нас никакой информации, а потому прочитав, этот байт нужно сбросить в нуль. В данном случае он и так сброшен, значит перед нами первый уровень таблицы дескрипторов. Так выглядят в памяти 8 наших хэндлов, не считая резервный:

Код:
0: kd> dq 0xfffff8a0`02a0f000 L12
fffff8a0`02a0f000  00000000`00000000 00000000`fffffffe  <------- Резерв
fffff8a0`02a0f010  fffff8a0`047b2d21 00000000`00000009  <-- Handle 0x04
fffff8a0`02a0f020  fffff8a0`00bce2d1 00000000`00000003  <-- Handle 0x08
fffff8a0`02a0f030  fffffa80`01cd51a1 00000000`00100020  <-- Handle 0x0c
fffff8a0`02a0f040  fffffa80`01a2dee1 00000000`001f0003  <-- Handle 0x10
fffff8a0`02a0f050  fffffa80`020089c1 00000000`001f0001  <-- Handle 0x14
fffff8a0`02a0f060  fffffa80`01ae07d1 00000000`001f0001  <-- Handle 0x18
fffff8a0`02a0f070  fffff8a0`04881911 00000000`00020019  <-- Handle 0x1c
fffff8a0`02a0f080  fffffa80`01cfb033 00000000`00000000  <-- Handle 0x20
0: kd>

Как упоминалось выше, размер одной записи «Entry» равен 16-байт, при этом первые 8-байт хранят указатель на уже знакомый нам заголовок OBJECT_HEADER, а вторые – это маска доступа к объекту «GrantedAccess». В младших 3-х битах указателя кодируются флаги, а потому для получения фактического адреса нужно сбросить их в нуль (как и в предыдущем случае). Значения этих бит представлены ниже:

Entry_scheme.png

Возьмём из таблицы, к примеру, последний дескриптор(0x20) по адресу fffff8a0`02a0f080, и расшифруем его значения в отладчике:

Код:
0: kd> dt _handle_table_entry fffff8a0`02a0f080
nt!_HANDLE_TABLE_ENTRY
   +0x000 Object             : 0xfffffa80`01cfb033  <--- сбросить 3 мл.бита!
   +0x008 GrantedAccess      : 0

0: kd> dt _object_header 0xfffffa80`01cfb030
nt!_OBJECT_HEADER
   +0x000 PointerCount       : 0n10
   +0x008 HandleCount        : 0n3
   +0x010 Lock               : _EX_PUSH_LOCK
   +0x018 TypeIndex          : 0x7    <-----------------
   +0x019 TraceFlags         : 0
   +0x01a InfoMask           : 0x8
   +0x01b Flags              : 0
   +0x020 ObjectCreateInfo   : 0xfffffa80`02cd5800  _OBJECT_CREATE_INFORMATION
   +0x028 SecurityDescriptor : 0xfffff8a0`02cffcb3  Void
   +0x030 Body               : _QUAD
0: kd>

Так мы получили заголовок объекта, и теперь остаётся узнать, какому из 43-х возможных объектов он принадлежит, ведь иначе тело «Body» мы просто не сможем опознать. Эта информация зарыта в поле «TypeIndex» заголовка, только вычисляется этот тип весьма своеобразным способом. Дело в том, что все поддерживаемые диспетчером типы объектов прописаны в специальной таблице индексов. На эту таблицу указывает неэкспортируемая переменная ObTypeIndexTable ядра Ntoskrnl.exe. В боевых условиях, единственный способ получить её адрес в памяти – это дизассемблировать функцию nt!ObGetObjectType() (см.аргумент в регистре rcx).

Код:
0: kd> uf nt!ObGetObjectType
nt!ObGetObjectType:
fffff800`02f3f974  0fb641e8        movzx   eax,byte ptr [rcx-18h]
fffff800`02f3f978  488d0d6162f0ff  lea     rcx,[nt!ObTypeIndexTable (fffff800`02e45be0)]
fffff800`02f3f97f  488b04c1        mov     rax,qword ptr [rcx+rax*8]
fffff800`02f3f983  c3              ret
0: kd>

Но сейчас у нас в руках отладчик, так-что получить значение этой переменной (да и сразу вывести часть содержимого таблицы индексов) можно так. Обратите внимание на адрес fffff800`02e45be0 в дизасм.листинге выше и в логе отладчика – он совпадает:

Код:
0: kd> dps nt!ObTypeIndexTable
fffff800`02e45be0  00000000`00000000  <-- индекс(0) резерв
fffff800`02e45be8  00000000`bad0b0b0  <-- индекс(1) резерв
fffff800`02e45bf0  fffffa80`01825270  <----- индекс(2)
fffff800`02e45bf8  fffffa80`01830f30  <----- индекс(3)
fffff800`02e45c00  fffffa80`01830de0
fffff800`02e45c08  fffffa80`01830b90
fffff800`02e45c10  fffffa80`01830970
fffff800`02e45c18  fffffa80`0182af30  <----- индекс(7)
fffff800`02e45c20  fffffa80`0182ade0
fffff800`02e45c28  fffffa80`0182ac90

В предыдущем OBJECT_HEADER поле TypeIndex=7, и поскольку теперь имеем соответствующий адрес fffffa80`0182af30 из таблицы типов, ничто не мешает вывести и саму структуру с описанием полученного нами объекта с дескриптором(0x20) – как видим это объект типа «Process»:

Код:
0: kd> dt nt!_OBJECT_TYPE fffffa80`0182af30
   +0x000 TypeList              : _LIST_ENTRY [ 0xfffffa80`0182af30 - 0xfffffa80`0182af30 ]
   +0x010 Name                  : _UNICODE_STRING "Process"  <--------------
   +0x020 DefaultObject         : (null)
   +0x028 Index                 : 0x7   <-----------------------------------
   +0x02c TotalNumberOfObjects  : 0x35
   +0x030 TotalNumberOfHandles  : 0x17f
   +0x034 HighWaterNumOfObjects : 0x3a
   +0x038 HighWaterNumOfHandles : 0x1f7
   +0x040 TypeInfo              : _OBJECT_TYPE_INITIALIZER
   +0x0b0 TypeLock              : _EX_PUSH_LOCK
   +0x0b8 Key                   : 0x636f7250
   +0x0c0 CallbackList          : _LIST_ENTRY [ 0xfffff8a0`003fb630 - 0xfffff8a0`003fb630 ]
0: kd>

Отметим, что это глобальная структура системы и лично к моему/подопытному процессу HandleInfo.exe она никак не касается. Из неё я просто выудил для себя тип дескриптора, чтобы идентифицировать тушку после заголовка. Для объектов типа «Process» телом является структура EPROCESS, и теперь по дескриптору я получил к ней доступ.

Код:
0: kd> dt _object_header 0xfffffa80`01cfb030
nt!_OBJECT_HEADER
   +0x000 PointerCount       : 0n10
   +0x008 HandleCount        : 0n3
   +0x010 Lock               : _EX_PUSH_LOCK
   +0x018 TypeIndex          : 0x7  <-----------// индекс типа объекта
   +0x019 TraceFlags         : 0
   +0x01a InfoMask           : 0x8
   +0x01b Flags              : 0
   +0x020 ObjectCreateInfo   : 0xfffffa80`02cd5800  _OBJECT_CREATE_INFORMATION
   +0x028 SecurityDescriptor : 0xfffff8a0`02cffcb3  Void
   +0x030 Body               : _QUAD

0: kd> dt _eprocess 0xfffffa80`01cfb030 + 0x30  <---// прыгаем от заголовка к телу Body
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x180 UniqueProcessId  : 0x00000000`0000061c Void
   +0x200 ObjectTable      : 0xfffff8a0`04840660 _HANDLE_TABLE
   +0x2e0 ImageFileName    : [15]  "HandleInfo.EXE"
   +0x2ef PriorityClass    : 0x2 ''
   +0x308 ThreadListHead   : _LIST_ENTRY [ 0xfffffa80`0364ab38 - 0xfffffa80`0364ab38 ]
   +0x338 Peb              : 0x000007ff`fffdf000 _PEB
............

0: kd> !sd 0xfffff8a0`02cffcb0  <---------// берём из заголовка дескриптор безопасности
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8814
            SE_SACL_PRESENT
            SE_SACL_AUTO_INHERITED
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-21-xxxxx-xxxxx-xxxxx-513

->Dacl    : ->Ace[0]: ->AceType : ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x0
->Dacl    : ->Ace[0]: ->AceSize : 0x18
->Dacl    : ->Ace[0]: ->Mask    : 0x001fffff
->Dacl    : ->Ace[0]: ->SID     : S-1-5-32-544

->Sacl    : ->Ace[0]: ->AceType : SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize : 0x14
->Sacl    : ->Ace[0]: ->Mask    : 0x00000003
->Sacl    : ->Ace[0]: ->SID     : S-1-16-12288


5. Глобальная таблица идентификаторов процессов

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

PspCidTable.png

Переменная ядра PspCidTable хранит указатель, по которому можно будет найти базовый адрес этой таблицы в памяти. Получив адрес, мы сразу упираемся всё в ту-же структуру HANDLE_TABLE. Далее всё под-копирку вышеизложенного, только записи «Entry» указывают теперь не на заголовки объектов, а непосредственно на структуры EPROCESS или на потоковые ETHREAD. Продемонстрируем это в отладчике:

Код:
0: kd> dt _handle_table poi(nt!PspCidTable)
ntdll!_HANDLE_TABLE
   +0x000 TableCode               : 0xfffff8a0`02498001
   +0x008 QuotaProcess            : (null)
   +0x010 UniqueProcessId         : (null)
   +0x018 HandleLock              : _EX_PUSH_LOCK
   +0x020 HandleTableList         : _LIST_ENTRY [ 0xfffff8a0`000047d0 - 0xfffff8a0`000047d0 ]
   +0x030 HandleContentionEvent   : _EX_PUSH_LOCK
   +0x038 DebugInfo               : (null)
   +0x040 ExtraInfoPages          : 0n0
   +0x044 Flags                   : 1
   +0x048 FirstFreeHandle         : 0xe10
   +0x050 LastFreeHandleEntry     : 0xfffff8a0`050733d0 _HANDLE_TABLE_ENTRY
   +0x058 HandleCount             : 0x280
   +0x05c NextHandleNeedingPool   : 0x1000
   +0x060 HandleCountHighWatermark: 0x32b
0: kd>

Значит по младшему байту «TableCode» делаем вывод, что эта таблица дескрипторов уровня(2).
Сбрасываем 3 младших бита в нуль, и выводим дамп:

Код:
0: kd> dps 0xfffff8a0`02498000
fffff8a0`02498000  fffff8a0`00005000
fffff8a0`02498008  fffff8a0`02499000
fffff8a0`02498010  fffff8a0`02b1c000
fffff8a0`02498018  fffff8a0`05073000
fffff8a0`02498020  00000000`00000000
fffff8a0`02498028  00000000`00000000

Видим всего 4 указателя – это (под)таблицы, и каждая занимает одну 4К-байтную страницу вирт.памяти.
Проверим первую запись в первой – мусор. Тогда добавив 0x10 (размер одной записи) пробуем вторую – в точку:

Код:
0: kd> dt _handle_table_entry fffff8a0`00005000
ntdll!_HANDLE_TABLE_ENTRY
   +0x000 Object           : (null)
   +0x008 GrantedAccess    : 0xfffffffe

0: kd> dt _handle_table_entry fffff8a0`00005010
ntdll!_HANDLE_TABLE_ENTRY
   +0x000 Object           : 0xfffffa80`0182bb11 Void
   +0x008 GrantedAccess    : 0
0: kd>

Теперь имеем адрес 0xfffffa80`0182bb11, но он может указывать как на структуру EPROCESS, так и на ETHREAD. Обнулив в адресе 3 мл.бита с флагами, конечно-же можно подбрасывать монетку и надеясь на удачу выбирать сначала первый, а затем второй вариант (предварительно создав какие-нибудь патерны для обнаружения структур). Но т.к. мы уже знаем, что EPROCESS это тело объекта «Process», а ETHREAD это тушка объекта «Thread», проще отнять от адреса 0x30 и прыгнуть таким образом на заголовок OBJECT_HEADER, где по значению «TypeIndex» уже точно определить владельца идентификатора PID. Для процессов Type всегда будет равен 7, а для тредов 8.

Код:
0: kd> dt _object_header 0xfffffa80`0182bb10-0x30
nt!_OBJECT_HEADER
   +0x000 PointerCount       : 0n119
   +0x008 HandleCount        : 0n4
   +0x010 Lock               : _EX_PUSH_LOCK
   +0x018 TypeIndex          : 0x7   <-------- тип(7) = EPROCESS
   +0x019 TraceFlags         : 0
   +0x01a InfoMask           : 0
   +0x01b Flags              : 0x2
   +0x020 ObjectCreateInfo   : 0xfffff800`02e20940 _OBJECT_CREATE_INFORMATION
   +0x028 SecurityDescriptor : 0xfffff8a0`000044d4 Void
   +0x030 Body               : _QUAD

0: kd> dt _eprocess 0xfffffa80`0182bb10
ntdll!_EPROCESS
   +0x000 Pcb                : _KPROCESS
   +0x180 UniqueProcessId    : 0x00000000`00000004
   +0x2d8 ImageFileName      : [15] ""

0: kd> db 0xfffffa80`0182bb10 + 0x2d8
fffffa80`0182bde8  00 00 00 00 00 00 00 00-53 79 73 74 65 6d 00 00  ........System..
fffffa80`0182bdf8  00 00 00 00 00 00 00 02-00 00 00 00 00 00 00 00  ................

Таким образом, первый PID в таблице идентификаторов принадлежит EPROCESS процесса "System", далее берём сл. PID и повторяем:

Код:
0: kd> dt _handle_table_entry fffff8a0`00005020
ntdll!_HANDLE_TABLE_ENTRY
   +0x000 Object           : 0xfffffa80`01892041 Void
   +0x008 GrantedAccess    : 0

0: kd> dt _object_header 0xfffffa80`01892040-0x30
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n1
   +0x008 HandleCount      : 0n0
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : 0x8   <------- на этот раз тип(8) = ETHREAD
............

0: kd> dt _ethread 0xfffffa80`01892040 Tcb.Process
ntdll!_ETHREAD
   +0x000 Tcb         :
      +0x210 Process     : 0xfffffa80`0182bb10 _KPROCESS  <--- линк на EPROCESS родителя


Заключение

Подводя черту под вышесказанным хотелось-бы пояснить, зачем всё это надо.
Во-первых, прогулки по тёмным переулкам ядра без транспорта в виде Native-API позволяют обходить антивирусы, у которых хоть и есть эвристики на все случаи жизни, но по большей части они контролируют всё-же вызовы API. Если не модифицировать структуры самого ядра, то и системный PatchGuard будет молчать, поскольку до пользовательской кухни ему нет никакого дела. Так-что путём правки дескрипторов можно снимать защиту от записи в память своенравных процессов, входить в доверенную зону, и многое другое. Во-вторых, если изменить свой PID в системной таблице идентификаторов, можно скрыть процесс от всех, включая саму систему. Да.. вскормить свой дров 64-битной оси проблематично из-за обязательной подписи, но никто не отменял уже функционирующие в системе уязвимые драйвера. В общем здесь есть над чем подумать.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!