В статье рассматриваются такие сущности ОС как: «объекты» ядра, «дескрипторы» объектов, а так-же «идентификаторы» процессов. Читатель ознакомится с их внутренним устройством, представлением на уровне системы, и основным назначением. Тема окунёт нас в самые закрома системы безопасности, куда закрыт доступ не только смертным юзерам, но и в некоторых случаях драйверам. Поняв общие принципы можно будет сделать вывод, на чём основывается работа античитов и антивирусов, а главное определить вектор атаки вредоносных руткитов для защиты от них.
Для тех, кто захочет пощупать всё это дело ручками, придётся установить отладчик
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 всегда указывает сегментный регистр
Запустим любой свой процесс на исполнение (у меня это HandleInfo.exe), и в отладчике WinDbg соберём о нём данные.
Структура _EPROCESS на моей х64 Win7 имеет размер 1.2КБ, и чтобы не искать в этой портянке лога интересующие нас поля, я просто выдерну их из контекста:
Значит сначала я запросил общую информацию о своём процессе, чтобы получить указатель на _EPROCESS. Отладчик вернул мне не только адрес
2. Понятия “Object, Handle, Identifier”
Теперь поговорим о более возвышенном, и дадим определения таким понятиям как «Объект», дескриптор «Handle», и идентификатор «PID». В этой тройке кроится вся сила и мощь ОС Windows, а потому уделим им чуть больше внимания.
Инженеры спроецировали систему так, что в глазах ядра весь мир состоит только из объектов. То есть всё, с чем имеет дело ядро – считается объектом. Все объекты (аля ресурсы системы) принято разделять на KERNEL, USER и GDI, причём 2 последних пользовательские, а потому пока нам не интересны. Начиная с Vista, в ядре числится 43 базовых типов объектов, каждый из которых может иметь десятки тысяч своих экземпляров. Наиболее часто используемые типы с кратким описанием представлены в таблице ниже:
Таким образом, процессы и их потоки по сути являются объектами. Когда мы запускаем новый процесс на исполнение, системный диспетчер создаёт экземпляр базового объекта «Process», и возвращает нам не указатель на экземпляр, а ссылку на него в виде дескриптора «Handle». Этот дескриптор не представляет никакой опасности для ядра, т.к. фактически является просто целым числом. Но с другой стороны это индекс в таблице дескрипторов процесса – здесь и начинается самое интересное. Аналогичную ситуацию наблюдаем и со всеми остальными объектами ядра. Например при вызове функции CreateFile(), на откуп ядро возвращает нам дескриптор объекта «File», и т.д.
Утилита "WinObj" из пакета
3. Исследование объектов
На физическом уровне объект представляет собой обычную структуру данных в системной памяти. Он начинается с одинакового для всех заголовка
Диспетчер объектов поддерживает два счётчика 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» позволяет заблокировать доступ к структуре
Как видим, тело объекта «Body» лежит в хвосте и начинается со-смещения 30h. Дело в том, что большинство API драйверов возвращают указатели только на само тело объекта, скрывая тем-самым доступ к заголовку. А ведь в нём лежит важное поле «SecurityDescriptor», где хранятся такие биты как DACL/ACE, да и всё остальное. Поэтому когда мы получим от драйвера адрес объекта, то отняв от него значение 30h в виде смещения тела «Body», получим нелегальный доступ к заголовку. Запушим пока этот факт в черепную коробку..
Самый простой способ найти структуры объектов (аля их тела) – это передать команде
Возьмём к примеру объект любого драйвера.. пусть это будет сетевой NDIS.SYS (Network Driver Interface).
Получив на первом этапе адрес структуры его тела, отнимем 30h от головы, чтобы сразу прочесть и заголовок:
В штате диспетчера объектов присутсвуют ещё 5 необязательных заголовков, о наличии которых сигнализирует поле «InfoMask». Каждый бит этого значения является флагом присутствия/отсутствия заголовка, т.е. информативны только 5 младших бит. Например выше InfoMask=2, а это в бинах 0000.0010. Таким образом, в наличии только одна структура под №2. Запросить у отладчика названия необязательных заголовков можно так (правда в них нет ничего для нас интересного):
Флаги атрибутов для самого объекта хранятся в поле «Flags» заголовка. Это сумма значений из таблицы ниже, и если в данном случае имеем Flags=0x12, значит объект имеет флаги
4. Поиск таблицы дескрипторов процесса
На этапе создания процесса, система выделяет ему несколько страниц вирт.памяти под «Таблицу дескрипторов». Это означает, что у каждого процесса в системе имеется личный свой пул экземпляров объектов, дескрипторы которых собираются в спец.таблицу
По этой схеме видно, что повествование выше касалось лишь конца цепочки, где расположена структура заголовка
По сути, указатель
Перед нами основная информация о таблице дескрипторов процесса.
PID совпадает, счётчик дескрипторов(8) тоже (см.первый лог). Но если всего 8 дескрипторов, тогда почему «FirstFreeHandle» (первый свободный) имеет значение 0x24=36? Дело в том, что значения любых дескрипторов всегда кратны 4, при этом нулевой лежит в резерве и не используется (см.рис.выше). Итого получаем 8*4=32, а 36-ой уже свободный. Когда процессу понадобится очередной дескриптор, диспетчер объектов будет знать, куда именно в таблицу его прописать.
Сама-же таблица состоит из 16-байтых записей «Entry», которые имеют официальную структуру
Как упоминалось выше, размер одной записи «Entry» равен 16-байт, при этом первые 8-байт хранят указатель на уже знакомый нам заголовок
Возьмём из таблицы, к примеру, последний дескриптор(0x20) по адресу
Так мы получили заголовок объекта, и теперь остаётся узнать, какому из 43-х возможных объектов он принадлежит, ведь иначе тело «Body» мы просто не сможем опознать. Эта информация зарыта в поле «TypeIndex» заголовка, только вычисляется этот тип весьма своеобразным способом. Дело в том, что все поддерживаемые диспетчером типы объектов прописаны в специальной таблице индексов. На эту таблицу указывает неэкспортируемая переменная
Но сейчас у нас в руках отладчик, так-что получить значение этой переменной (да и сразу вывести часть содержимого таблицы индексов) можно так. Обратите внимание на адрес
В предыдущем
Отметим, что это глобальная структура системы и лично к моему/подопытному процессу HandleInfo.exe она никак не касается. Из неё я просто выудил для себя тип дескриптора, чтобы идентифицировать тушку после заголовка. Для объектов типа «Process» телом является структура
5. Глобальная таблица идентификаторов процессов
По аналогии с таблицей дескрипторов процесса, в системе имеется и глобальная таблица идентификаторов PID. Отличие их в том, что если первая создаётся для каждого из процессов, то вторая существует в единственном экземпляре, и хранит список идентификаторов буквально всех активных на данный момент процессов. Как только процесс заканчивает свою работу, система тут-же удаляет его PID из этой базы. Что примечательно, у них даже структуры одинаковы, а потому будет легче понять принцип организации.
Переменная ядра
Значит по младшему байту «TableCode» делаем вывод, что эта таблица дескрипторов уровня(2).
Сбрасываем 3 младших бита в нуль, и выводим дамп:
Видим всего 4 указателя – это (под)таблицы, и каждая занимает одну 4К-байтную страницу вирт.памяти.
Проверим первую запись в первой – мусор. Тогда добавив 0x10 (размер одной записи) пробуем вторую – в точку:
Теперь имеем адрес
Таким образом, первый PID в таблице идентификаторов принадлежит EPROCESS процесса "System", далее берём сл. PID и повторяем:
Заключение
Подводя черту под вышесказанным хотелось-бы пояснить, зачем всё это надо.
Во-первых, прогулки по тёмным переулкам ядра без транспорта в виде Native-API позволяют обходить антивирусы, у которых хоть и есть эвристики на все случаи жизни, но по большей части они контролируют всё-же вызовы API. Если не модифицировать структуры самого ядра, то и системный PatchGuard будет молчать, поскольку до пользовательской кухни ему нет никакого дела. Так-что путём правки дескрипторов можно снимать защиту от записи в память своенравных процессов, входить в доверенную зону, и многое другое. Во-вторых, если изменить свой PID в системной таблице идентификаторов, можно скрыть процесс от всех, включая саму систему. Да.. вскормить свой дров 64-битной оси проблематично из-за обязательной подписи, но никто не отменял уже функционирующие в системе уязвимые драйвера. В общем здесь есть над чем подумать.
Для тех, кто захочет пощупать всё это дело ручками, придётся установить отладчик
Ссылка скрыта от гостей
. На выбор предлагается там 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 базовых типов объектов, каждый из которых может иметь десятки тысяч своих экземпляров. Наиболее часто используемые типы с кратким описанием представлены в таблице ниже:
Таким образом, процессы и их потоки по сути являются объектами. Когда мы запускаем новый процесс на исполнение, системный диспетчер создаёт экземпляр базового объекта «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):4. Поиск таблицы дескрипторов процесса
На этапе создания процесса, система выделяет ему несколько страниц вирт.памяти под «Таблицу дескрипторов». Это означает, что у каждого процесса в системе имеется личный свой пул экземпляров объектов, дескрипторы которых собираются в спец.таблицу
HANDLE_TABLE
. Такая схема позволяет менеджеру объектов быстрее находить дескрипторы конкретно взятого процесса, чем если-бы они были беспорядочно разбросаны по всему периметру системной памяти. Алгоритм поиска дескрипторов своего/чужого процесса представлен на рис.ниже:По этой схеме видно, что повествование выше касалось лишь конца цепочки, где расположена структура заголовка
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-х битах указателя кодируются флаги, а потому для получения фактического адреса нужно сбросить их в нуль (как и в предыдущем случае). Значения этих бит представлены ниже:Возьмём из таблицы, к примеру, последний дескриптор(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
хранит указатель, по которому можно будет найти базовый адрес этой таблицы в памяти. Получив адрес, мы сразу упираемся всё в ту-же структуру 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-битной оси проблематично из-за обязательной подписи, но никто не отменял уже функционирующие в системе уязвимые драйвера. В общем здесь есть над чем подумать.