В предыдущей статье рассматривались объекты KERNEL, теперь-же ознакомимся с организацией пользовательских объектов USER и GDI, которые полностью удовлетворяют потребностям двумерной графики. Например один только объект типа «Window» охватывает буквально все элементы окна (кроме строки меню), а это: кнопки, поля ввода, всевозможные списки, чекбоксы, и многое другое. В свою очередь объекты GDI обрамляют окна рамками, рисуют простые фигуры, заливают элементы цветом, меняют шрифты, и прочее в том-же духе. В общем здесь есть о чём поговорить.
1. Пулы памяти Windows
Прежде, чем начать разговор о пользовательских объектах, определимся с расположением их в памяти.
Известно, что всей физической ОЗУ управляет системный диспетчер. Его аллокатор может распределять память как фиксированными страницами по 4КБ, так и произвольными блоками внутри этих страниц – это позволяет более экономно расходовать ресурсы. Для выделения больших объёмов юзеру доступна VirtualAlloc(), а запросы для мелких нужд обеспечивает HeapAlloc(). В конечном счёте, обе эти функции обращаются к ядерным NtAllocateVirtualMemory() и RtlAllocateHeap() соответственно. Таким образом Pool – это несколько областей распределённой на данный момент памяти.
В зависимости от назначения и свойств, пулы делятся на Paged (выгружаемый из физ.памяти на диск в pagefile.sys), и закреплённый в ОЗУ невыгружаемый NonPagedPool, к которому может приняться атрибут запрета на исполнение NХ (No eXecute). При старте системы диспетчер создаёт 5 базовых выгружаемых пула 0-4, которые позже могут расширяться динамически. Команда отладчика WinDbg
Каждого пользователя система запирает в свою Session, внутри каждой сессии находится одна или несколько рабочих станций WorkStation, причём интерактивной (с клавиатурой и мышью) может быть только одна «WinSta0». И наконец в станцию уже помещается один или несколько рабочих столов Desktop. Этой матрёшке была когда-то посвящена статья «Сессии, станции, рабочие столы», поэтому повторяться не будем. Сейчас важно понять, что у каждой сессии свой личный пул памяти, о чём свидетельствуют 2 записи в хвосте лога выше. Начиная с Win7, ОС создаёт закрытую от всех сессию(0) для своих служб «Services», чтобы оградить смертных юзеров от доступа к ним. В данном случае на своём узле зареган только я, а если добавить ещё и юзера с гостем, то сессий в логе было-бы уже 4. Такой расклад приводит к тому, что имеем три основных пула памяти, каждый из которых числится в системе под своим ID – при выделении памяти, драйвер передаёт тип пула в функцию ExAllocatePoolWithTag():
Обычно пул содержит в себе сотни мелких блоков памяти, общий объём которых может переполнить не одну 4КБ страницу. Поэтому диспетчер ведёт учёт не только каждого из пулов, но и каждой из его страниц, сохраняя инфу в своей структуре POOL_HEADER. Здесь видим индекс блока внутри страницы, его размер, перечисленный выше тип пула, 4-байтный тег для быстрого поиска, а так-же указатель на структуру EPROCESS хозяина:
Собрать информацию о пуле любой сессии можно командой
Учитывая вышеописанную иерархию Session-->WorkStation-->Desktop можно утверждать, что пользовательские объекты USER находятся в выделенной для рабочего стола кучи Heap, а для объектов GDI резервируется память из невыгружаемого пула NonPagedSession. Передаваемые в функцию ExAllocatePoolWithTag() теги (4 символа) способствуют быстрому поиску блоков внутри огромных пулов, и для объектов GDI они начинаются с буквы(G). Во-второй аргумент команды отладчика
Обратите внимание, что все эти блоки внутри пула сессии создал диспетчер окон win32k.sys, причём отчётливо видны объекты GDI типа: регион, шрифт, контекст устройства вывода DC (монитор или принтер), палитра и кисть. Аналогичную картину можно наблюдать и в окне софта П.Йосифовича «Kernel-Pool-Monitor». Память из невыгружаемого пула обычно выделяется драйверам режима ядра, но для объектов USER и GDI система делает исключение:
2. Объекты графики GDI
GDI это интерфейс подсистемы Win32, который позволяет рисовать в приложениях двумерные графические примитивы. К объектам GDI относятся: DC, Pen, Brush, Font, Bitmap, Region, Palette и Metafile, а библиотека Gdi32.dll предоставляет массу функций для работы с ними. В большинстве случаев предполагается, что эти объекты не живут долго – их следует создавать, использовать и немедленно освобождать. Причина в строгом ограничении доступных для каждого процесса объектов, о чём пойдёт речь далее.
В Win7+ объекты GDI хранятся в клиентской части модуля Gdi32.dll. Это означает, что любой объект действителен только в контексте приложения, которое его создало, т.к. у каждого процесса свой экземпляр системных библиотек. Например если создать новую кисть Brush и передать его дескриптор другому приложению, то выбор им нашего объекта через SelectObject() может вернуть OK, однако с вероятностью 99% вместо кисти в контекст устройства попадёт например карандаш Pen. Это потому, что дескрипторы GDI внутренне реализуются как смещения в «глобальной таблице дескрипторов», которая проецируется в пространство каждого процесса.
Указатель на таблицу система прописывает в поле «GdiSharedHandleTable» структуры PEB, а драйвер оконного менеджера на стороне ядра win32k.sys потом оперирует записями в её ячейках cell. Следующей последовательность команд отладчика можно вывести дамп этой таблицы, только подопытным обязательно должно быть приложение с графическим интерфейсом, импортируещее в свою тушку либу Gdi32.dll:
Перед нами фрагмент таблицы дескрипторов GDI текущего процесса. Каждый дескриптор описывает в нём оформленная в структуру запись GDI_TABLE_ENTRY размером 18h байт (полтора строки в дампе), а сама таблица состоит из массива таких записей. Важно отметить, что данная таблица имеет строго фиксированный размер, а не расширяется динамически как большинство системных таблиц. Именно поэтому использовав объект нужно сразу освобождать его. В глобальной таблице сессии зарезервировано место всего для 65.536 дескрипторов, хотя лимит для одного процесса итого меньше 16.384. В ветке реестра
Значит каждая такая структура описывает один объект, тип которого указывается в поле «Type». Информативны в этом поле только 5 мл.бит, поэтому нужно сбрасывать всё остальное маской
32-битное значение Handle, которое возвращает например функция CreateBitmap() состоит из двух 16-битных частей: старшее слово это как раз значение поля «Uniq», а младшее – это индекс (порядковый номер) записи в таблице. Таким образом, данная запись с индексом(1) принадлежит дескриптору Handle со значением
В поле «Count» лежит счётчик-ссылок на объект – при выборе объекта он взводится в единицу, а при освобождении сбрасывается в нуль, что означает свободную запись Entry. Поле «ProcessId» хранит идентификатор процесса-создателя. Использование чужих объектов GDI как-правило приводит к сбою, за исключением стандартных объектов. Такие объекты как: чёрное перо, белая кисть и системный шрифт являются (в терминологии gdi) стоковыми объектами, и выбираются специальной функцией gdi32!GetStockObject(). Они создаются один раз с предопределёнными свойствами, и могут использоваться во всех процессах – их Pid будет равен нуль.
И наконец в поле «KernelAddress» диспетчер прописывает указатель на объект, структура которого напрямую зависит от его типа. Драйвер win32k.sys держит все объекты у себя в ядерном пространстве, а в юзер-спэйс таблица лишь проецируется (мапится) с атрибутом «Только для чтения». После того-как объект создан в ядре, благодаря отображённой таблице код пользователя получает к нему мгновенный доступ, без дорогостоящих переходов в кернел. Раньше и сами структуры объектов мапились в память юзера, и поле «UserAddress» хранило как-раз линк на них. Но сейчас лавочку прикрыли, а потому адрес юзера будет всегда сброшен в нуль.
Программно обойти таблицу дескрипторов GDI можно следующим образом..
Значит берём из РЕВ поле «GdiSharedHandleTable», и с шагом 18h парсим структуры таблицы.
Если в первом аргументе функции user32!GetGuiResources() передать константу
3. Пользовательские объекты USER
C объектами USER дела обстоят аналогично, только есть несколько принципиальных отличий.
Все пользовательские объекты индексируются в таблице дескрипторов для каждого сеанса. Сама таблица хранится в ядре, и проецируется в каждый процесс с графическим интерфейсом GUI. Как и в случае с объектами GDI, такая схема позволяет процессам получать доступ на чтение ядерных структур прямо из пользовательского режима, без необходимости прибегать к системному вызову
Структура ядра SHAREDINFO довольно творческая единица, и в ней зарыт огромный объём информации. Отложим пока на полку таблицу дескрипторов USER, и посмотрим на содержимое шары. Для этого в отладчике нужно присоединиться к любому процессу с оконным интерфейсом GUI, например к тому-же диспетчеру задач Win taskmgr.exe. Далее запрашиваем значение переменной win32k!gSharedInfo, и дампим структуру ядра:
Значит в поле «aheList» лежит указатель на таблицу дескрипторов, которая содержит в себе массив структур HANDLEENTRY с описанием каждого из объектов USER, а в сл.поле «HeEntrySize» размер одной структуры 18h байт. Мы вернёмся к этому массиву чуть позже, а сейчас запросим инфу о сервере, в качестве которого выступает ядро. Поскольку структура большая, я оставил в ней только наиболее интересные поля:
Обратите внимание, что объект «BRUSH» (кисть) принадлежит к GDI и находится в ядре, а объект иконки окна «ICON» уже в пространстве юзера. Это говорит о том, что не все объекты создаются в ядре – некоторые из них порождает сама либа User32.dll. В поле «apfnClientA» хранится массив указателей на внутренние функции оконного менеджера, которые использует данный процесс. Поскольку я под отладчиком режима ядра, он не может распознать имена пользовательских функций, и предоставляет только их адреса:
Ну и теперь поговорим о таблице дескрипторов, на которую указывает поле
Общее кол-во записей в таблице лежит в поле «HandleEntries» структуры SERVERINFO, и в данном случае видим в нём значение
Учитывая размер одной HANDLEENTRY = 18h байт, добавляя его к базе можно сдампить например первые три записи из таблицы USER. Как видим первая пустая и содержит нули, а вот следущие уже хотят о чём-то нам поведать:
В записях объектов GDI имелось поле Pid, по значению которого можно было искать в таблице объекты только своего процесса. В дескрипторах HANDLEENTRY уже такого флага нет – здесь свои объекты мы можем обнаружить только прочитав указатель в поле «Owner» (владелец). Не беда, что из пользовательского процесса у нас нет доступа к ядерной win32k!_W32THREAD – значение «Owner» система специально для юзера дублирует в поле «Win32ThreadInfo» структуры TEB потока. Если команде
Теперь прочитав поле «Owner» в записи HANDLEENTRY и сравнив его со-своим, мы сможем отфильтровать принадлежащие нашему процессу дескрипторы:
В данном случае в третьей записи таблицы видим
Вернёмся к структуре HANDLEENTRY и разберём её поля, чтобы позже сдампить их массив программно.
По сути названия всех полей говорят сами за себя, и к этому трудно что-то добавить. Как и в случае с GDI, 32-битный дескриптор Handle собирается из двух 16-битных частей – в старшем слово кладём значение «Uniq», а в младшее порядковый номер (индекс) записи в таблице, т.е
Значит чтобы подобраться к таблице под юзером, нужно запросить у либы user32.dll адрес её функции gSharedInfo(), через вызов двойки LoadLibrary() + GetProcAddress(). Так мы получим указатель не на ядерный оригинал таблицы дескрипторов, а на доступную только для чтения её копию в пользовательском пространстве (см.рисунок с отображением памяти выше). Поскольку вызов GetProcAddress() всегда возвращает адрес из указанного нами модуля, выходит, что win32k.sys мапит таблицу в юзер-мод именно в пространство библиотеки user32.dll.
Вот собственно и код сбора инфы об объектах USER. На старте он запрашивает тип объекта для вывода в диапазоне 1-21, после чего проходит по всей таблице поиском. Кстати работать будет только на системах Win7+, т.к. либа user32.dll на WinХР и ниже не экспортирует функцию gSharedInfo():
4. Kernel Callback Table
Под занавес рассмотрим ещё один механизм, который напрямую связан с объектами USER и GDI.
В силу того, что поддержка графических окон размазана чуть-ли не по всей системе Windows, половина служебных структур находится непосредственно в ядерной win32.sys, а половина заброшена на «вражескую» территорию юзера. Как результат, не только код пользовательской подсистемы Win32 должен обращаться к сервисам ядра, но и наоборот кернелу требуется звать на помощь функции из user32.dll. Звучит необычно, но это факт, которым злоупотребляет достаточное кол-во малвари.
Список нужных оконному менеджеру пользовательских функций приличный (порядка 100 штук на Win7), и чтобы постоянно не вычислять их адреса, драйвер win32k.sys создаёт специальный их лист, указатель на который прописывает в поле «KernelCallbackTable» структуры РЕВ. Таблица создаётся динамически при каждой загрузке user32.dll в пространство нашего приложения. Функции из этой таблицы колбэков являются внутренними, и либа user32.dll не экспортирует их из своей тушки напрямую.
В отладчике WinDbg можно собрать всю информацию о данном механизме обратных вызовов, включая полный список самих функций. Для этого нужно переключиться в режим юзера, и загрузить какого-нибудь клиента с графическим интерфейсом GUI. Далее ставим брейк на точку-входа, сразу даём команду
Так мы получили указатель на таблицу обратных вызовов ядра, и теперь командой
Функции из библиотек подсистемы Win32 переходят в ядро инструкцией
Далее вызывается nt!ProbeForWrite() для проверки, достаточно-ли места в стеке для передаваемых данных, после чего содержимое InputBuffer копируется в стек юзера. Внутренняя функция nt!KiCallUserMode() подготавливает переход в пользовательский режим, сохраняя важную информацию в стеке ядра. Непосредственно вызов функции из таблицы осуществляет nt!KeUserCallbackDispatcher(), а nt!NtCallbackReturn() возвращается обратно в ядро. Команда
Так чем-же примечательны эти обратные вызовы ядра?
Во-первых, поскольку таблица лежит в пространстве нашего приложения, то снять запрет на запись в неё можно элементарно через VirtualProtect(). Теперь если подменить в ней указатели на свои процедуры, то ничего не подозревающее ядро будет передавать на них управление. В списке есть довольно много функций, которые можно перехватывать таким образом, например почти все с префиксом user32!_ClientXXX. В частности user32!_ClientLoadLibrary() позволит прямо из ядра незаметно загружать в процессы юзера сторонние DLL. В общем возможности здесь ограничивает только фантазия.
Заключение
Придумать идеальную архитектуру операционной системы не так просто – если заткнёшь дыру в одном месте, она открывается в другом. Организация поддержки объектов USER и GDI прямое этому доказательство. Жили ведь до Win-NT спокойно, когда диспетчер форточек полностью находился в пространстве юзера. Но потом лёгким движением руки инженеры отправили диспетчера в ядро, оставив всех его подопечных на чужбине пользовательского пространства. Такая организация несёт потенциальные угрозы, исключить которые полностью в принципе не возможно. По ссылкам ниже лежат пдфки, где описываются некоторые уязвимости модуля win32k.sys. Всем удачи, до скорого.
1. Пулы памяти Windows
Прежде, чем начать разговор о пользовательских объектах, определимся с расположением их в памяти.
Известно, что всей физической ОЗУ управляет системный диспетчер. Его аллокатор может распределять память как фиксированными страницами по 4КБ, так и произвольными блоками внутри этих страниц – это позволяет более экономно расходовать ресурсы. Для выделения больших объёмов юзеру доступна VirtualAlloc(), а запросы для мелких нужд обеспечивает HeapAlloc(). В конечном счёте, обе эти функции обращаются к ядерным NtAllocateVirtualMemory() и RtlAllocateHeap() соответственно. Таким образом Pool – это несколько областей распределённой на данный момент памяти.
В зависимости от назначения и свойств, пулы делятся на Paged (выгружаемый из физ.памяти на диск в pagefile.sys), и закреплённый в ОЗУ невыгружаемый NonPagedPool, к которому может приняться атрибут запрета на исполнение NХ (No eXecute). При старте системы диспетчер создаёт 5 базовых выгружаемых пула 0-4, которые позже могут расширяться динамически. Команда отладчика WinDbg
!vm
собирает информацию о текущем положении дел с вирт.памятью, и на своей машине я виже такой лог. Здесь «Usage» подразумевает используется, а «Сommit» – изначально выделено (значения за скобками представляют счётчик в 4К байтных пейджах):
Код:
0: kd> !vm 5
*** Virtual Memory Usage ***
Physical Memory: 521790 ( 2087160 Kb)
Page File: \??\H:\pagefile.sys
Current: 2087160 Kb Free Space: 1364748 Kb
Minimum: 2087160 Kb Maximum: 6261480 Kb
NonPagedPool Usage: 97295160 ( 389180640 Kb)
NonPagedPoolNx Usage: 6716 ( 26864 Kb)
NonPagedPool Maximum: 380442 ( 1521768 Kb)
********** Excessive NonPaged Pool Usage *****
PagedPool 0 Usage: 40011 ( 160044 Kb)
PagedPool 1 Usage: 2859 ( 11436 Kb)
PagedPool 2 Usage: 862 ( 3448 Kb)
PagedPool 3 Usage: 829 ( 3316 Kb)
PagedPool 4 Usage: 875 ( 3500 Kb)
PagedPool Usage: 45436 ( 181744 Kb)
PagedPool Commit: 45647 ( 182588 Kb)
PagedPool Maximum: 33554432 ( 134217728 Kb)
Terminal Server Memory Usage By Session:
Session ID 0 @ fffff88002e34000:
Paged Pool Usage: 3784K
Commit Usage: 7496K
Session ID 1 @ fffff88002ef3000:
Paged Pool Usage: 24464K
Commit Usage: 28440K
Каждого пользователя система запирает в свою Session, внутри каждой сессии находится одна или несколько рабочих станций WorkStation, причём интерактивной (с клавиатурой и мышью) может быть только одна «WinSta0». И наконец в станцию уже помещается один или несколько рабочих столов Desktop. Этой матрёшке была когда-то посвящена статья «Сессии, станции, рабочие столы», поэтому повторяться не будем. Сейчас важно понять, что у каждой сессии свой личный пул памяти, о чём свидетельствуют 2 записи в хвосте лога выше. Начиная с Win7, ОС создаёт закрытую от всех сессию(0) для своих служб «Services», чтобы оградить смертных юзеров от доступа к ним. В данном случае на своём узле зареган только я, а если добавить ещё и юзера с гостем, то сессий в логе было-бы уже 4. Такой расклад приводит к тому, что имеем три основных пула памяти, каждый из которых числится в системе под своим ID – при выделении памяти, драйвер передаёт тип пула в функцию ExAllocatePoolWithTag():
Код:
PagedPool = 1
NonPagedPool = 0
NonPagedPoolNx = 200h
PagedPoolSession = 21h
NonPagedPoolSession = 20h
NonPagedPoolSessionNx = 220h
Обычно пул содержит в себе сотни мелких блоков памяти, общий объём которых может переполнить не одну 4КБ страницу. Поэтому диспетчер ведёт учёт не только каждого из пулов, но и каждой из его страниц, сохраняя инфу в своей структуре POOL_HEADER. Здесь видим индекс блока внутри страницы, его размер, перечисленный выше тип пула, 4-байтный тег для быстрого поиска, а так-же указатель на структуру EPROCESS хозяина:
Код:
0: kd> dt _pool_header
nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 8 Bits
+0x000 PoolIndex : Pos 8, 8 Bits
+0x000 BlockSize : Pos 16, 8 Bits
+0x000 PoolType : Pos 24, 8 Bits
+0x004 PoolTag : Uint4B
+0x008 ProcessBilled : Ptr64 _EPROCESS
+0x00a PoolTagHash : Uint2B
Собрать информацию о пуле любой сессии можно командой
!sprocess
отладчика, чтобы получить от него линк на структуру MM_SESSION_SPACE. В ней указывается размер, а в дескрипторе более детальные сведения:
Код:
0: kd> !sprocess
Dumping Session 1
_MM_SESSION_SPACE fffff88002ef3000 <----------//
_MMSESSION fffff88002ef3b40
......
0: kd> dt _MM_SESSION_SPACE fffff88002ef3000 Paged* Session* Non*
nt!_MM_SESSION_SPACE
+0x008 SessionId : 1
+0x028 SessionPageDirIndex : 0x16f7f
+0x030 NonPagablePages : 0x79
+0x040 PagedPoolStart : 0xfffff900`c0000000 Void
+0x048 PagedPoolEnd : 0xfffff920`bfffffff Void
+0x050 SessionObject : 0xfffffa80`0189d590 Void
+0x058 SessionObjectHandle : 0xffffffff`80000298 Void
+0xb40 Session : _MMSESSION
+0xb98 PagedPoolInfo : _MM_PAGED_POOL_INFO
+0xcc0 PagedPool : _POOL_DESCRIPTOR <------------//
0: kd> dt _MM_SESSION_SPACE fffff88002ef3000 PagedPool.
nt!_MM_SESSION_SPACE
+0xcc0 PagedPool :
+0x000 PoolType : 21 ( PagedPoolSession )
+0x008 PagedLock : _KGUARDED_MUTEX
+0x008 NonPagedLock : 1
+0x040 RunningAllocs : 0n3327649
+0x044 RunningDeAllocs : 0n3313992
+0x048 TotalBigPages : 0n4508
+0x04c ThreadsDeferrals : 0n0
+0x050 TotalBytes : 0x1ff2180
+0x080 PoolIndex : 0
+0x0c0 TotalPages : 0n1608
+0x100 PendingFrees : 0xfffff900`c2531680 -> 0xfffff900`c273d980 Void
+0x108 PendingFreeDepth : 0n9
+0x140 ListHeads : [256] _LIST_ENTRY [ 0xfffff880`02ef3e00 - 0xfffff880`02ef3e00 ]
Учитывая вышеописанную иерархию Session-->WorkStation-->Desktop можно утверждать, что пользовательские объекты USER находятся в выделенной для рабочего стола кучи Heap, а для объектов GDI резервируется память из невыгружаемого пула NonPagedSession. Передаваемые в функцию ExAllocatePoolWithTag() теги (4 символа) способствуют быстрому поиску блоков внутри огромных пулов, и для объектов GDI они начинаются с буквы(G). Во-второй аргумент команды отладчика
!poolused
можно передать маску для поиска (опционально), а первый аргумент позволяет сортировать вывод:
Код:
0: kd> !poolused 2 G* <-----// Сортировка: 2 = NonPaged, 4 = Paged, G* = маска тега.
NonPaged Paged
Tag Allocs Used Allocs Used
---- ------ ----- ------ ---- --------------
Gsem 331 52832 0 0 Gdi Semaphores
Gfsm 6 480 0 0 Gdi Fast mutex
GOPM 4 320 2 96 GDITAG_OPM, Binary: win32k.sys
Gla4 2 288 0 0 GDITAG_HMGR_LOOKASIDE_RGN_TYPE, Binary: win32k.sys
Gla; 2 288 0 0 GDITAG_HMGR_LOOKASIDE_RFONT_TYPE, Binary: win32k.sys
Gla: 2 288 0 0 GDITAG_HMGR_LOOKASIDE_LFONT_TYPE, Binary: win32k.sys
Gla1 2 288 0 0 GDITAG_HMGR_LOOKASIDE_DC_TYPE, Binary: win32k.sys
Gla5 2 288 0 0 GDITAG_HMGR_LOOKASIDE_SURF_TYPE, Binary: win32k.sys
Gla8 2 288 0 0 GDITAG_HMGR_LOOKASIDE_PAL_TYPE, Binary: win32k.sys
Gla@ 2 288 0 0 GDITAG_HMGR_LOOKASIDE_BRUSH_TYPE, Binary: win32k.sys
Gi2c 0 0 2 96 GDITAG_DDCCI, Binary: win32k.sys
TOTAL 355 55648 4 192
Обратите внимание, что все эти блоки внутри пула сессии создал диспетчер окон win32k.sys, причём отчётливо видны объекты GDI типа: регион, шрифт, контекст устройства вывода DC (монитор или принтер), палитра и кисть. Аналогичную картину можно наблюдать и в окне софта П.Йосифовича «Kernel-Pool-Monitor». Память из невыгружаемого пула обычно выделяется драйверам режима ядра, но для объектов USER и GDI система делает исключение:
2. Объекты графики GDI
GDI это интерфейс подсистемы Win32, который позволяет рисовать в приложениях двумерные графические примитивы. К объектам GDI относятся: DC, Pen, Brush, Font, Bitmap, Region, Palette и Metafile, а библиотека Gdi32.dll предоставляет массу функций для работы с ними. В большинстве случаев предполагается, что эти объекты не живут долго – их следует создавать, использовать и немедленно освобождать. Причина в строгом ограничении доступных для каждого процесса объектов, о чём пойдёт речь далее.
В Win7+ объекты GDI хранятся в клиентской части модуля Gdi32.dll. Это означает, что любой объект действителен только в контексте приложения, которое его создало, т.к. у каждого процесса свой экземпляр системных библиотек. Например если создать новую кисть Brush и передать его дескриптор другому приложению, то выбор им нашего объекта через SelectObject() может вернуть OK, однако с вероятностью 99% вместо кисти в контекст устройства попадёт например карандаш Pen. Это потому, что дескрипторы GDI внутренне реализуются как смещения в «глобальной таблице дескрипторов», которая проецируется в пространство каждого процесса.
Указатель на таблицу система прописывает в поле «GdiSharedHandleTable» структуры PEB, а драйвер оконного менеджера на стороне ядра win32k.sys потом оперирует записями в её ячейках cell. Следующей последовательность команд отладчика можно вывести дамп этой таблицы, только подопытным обязательно должно быть приложение с графическим интерфейсом, импортируещее в свою тушку либу Gdi32.dll:
Код:
0: kd> !process 0 0 taskmgr.exe
PROCESS fffffa8001beb060
SessionId: 1 Cid: 0d5c Peb: 7fffffdf000 ParentCid: 06a4
Image: taskmgr.exe
0: kd> .process /p fffffa8001beb060 <-------// Подключиться к процессу
Implicit process is now fffffa80`01beb060
0: kd> dt _peb 7fffffdf000 Gdi* <-------// Запросить из РЕВ поля по маске
ntdll!_PEB
+0x0f8 GdiSharedHandleTable : 0x00000000`00670000 <----// линк на таблицу GDI
+0x108 GdiDCAttributeList : 0x14
+0x140 GdiHandleBuffer : [60] 0
0: kd> db 0x00000000`006700f0 L60
00000000`006700f0 e0 0e 00 c0 00 f9 ff ff-00 00 00 00 04 01 04 40 ...............@
00000000`00670100 00 00 00 00 00 00 00 00-e0 0d 00 c0 00 f9 ff ff ................
00000000`00670110 00 00 00 00 88 01 08 40-00 00 00 00 00 00 00 00 .......@........
00000000`00670120 30 09 00 c0 00 f9 ff ff-00 00 00 00 08 01 08 40 0..............@
00000000`00670130 00 00 00 00 00 00 00 00-40 08 00 c0 00 f9 ff ff ........@.......
00000000`00670140 00 00 00 00 08 01 08 40-00 00 00 00 00 00 00 00 .......@........
......
Перед нами фрагмент таблицы дескрипторов GDI текущего процесса. Каждый дескриптор описывает в нём оформленная в структуру запись GDI_TABLE_ENTRY размером 18h байт (полтора строки в дампе), а сама таблица состоит из массива таких записей. Важно отметить, что данная таблица имеет строго фиксированный размер, а не расширяется динамически как большинство системных таблиц. Именно поэтому использовав объект нужно сразу освобождать его. В глобальной таблице сессии зарезервировано место всего для 65.536 дескрипторов, хотя лимит для одного процесса итого меньше 16.384. В ветке реестра
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
имеется ключ «GDIProcessHandleQuota», где по умолчанию выставлена квота в 10.000 хэндлов на процесс, хотя при желании можно подкрутить её в диапазоне 256-16384. Возьмём из дампа выше первые 18h байт, и заполним ими одну запись таблицы:
Код:
struct GDI_TABLE_ENTRY
pKernelAddress dq fffff900`c0000ee0
wProcessId dw 0000
wCount dw 0000
wUniq dw 0104
wType dw 4004
pUserAddress dq 00000000`00000000
ends
Значит каждая такая структура описывает один объект, тип которого указывается в поле «Type». Информативны в этом поле только 5 мл.бит, поэтому нужно сбрасывать всё остальное маской
1Fh=00011111
. В данном случае получаем Type=4, что соответствует объекту типа «Region». В поле «Uniq» лежит счётчик уникальности – диспетчер объектов меняет его, когда данная запись используется повторно после освобождения уже отработанного объекта. Примечательно, что младший байт поля «Uniq» всегда будет совпадать со-значением поля «Type».32-битное значение Handle, которое возвращает например функция CreateBitmap() состоит из двух 16-битных частей: старшее слово это как раз значение поля «Uniq», а младшее – это индекс (порядковый номер) записи в таблице. Таким образом, данная запись с индексом(1) принадлежит дескриптору Handle со значением
0x01040001
, и описывает она графический объект типа «Region». Кстати лимит в 65536 дескрипторов для одной сессии связан именно с 16-битным значением индекса в дескрипторе – этот хвост тянется ещё с 16-битных версий Windows 3.1, обрезать который у инженеров никак не хватает духа.В поле «Count» лежит счётчик-ссылок на объект – при выборе объекта он взводится в единицу, а при освобождении сбрасывается в нуль, что означает свободную запись Entry. Поле «ProcessId» хранит идентификатор процесса-создателя. Использование чужих объектов GDI как-правило приводит к сбою, за исключением стандартных объектов. Такие объекты как: чёрное перо, белая кисть и системный шрифт являются (в терминологии gdi) стоковыми объектами, и выбираются специальной функцией gdi32!GetStockObject(). Они создаются один раз с предопределёнными свойствами, и могут использоваться во всех процессах – их Pid будет равен нуль.
И наконец в поле «KernelAddress» диспетчер прописывает указатель на объект, структура которого напрямую зависит от его типа. Драйвер win32k.sys держит все объекты у себя в ядерном пространстве, а в юзер-спэйс таблица лишь проецируется (мапится) с атрибутом «Только для чтения». После того-как объект создан в ядре, благодаря отображённой таблице код пользователя получает к нему мгновенный доступ, без дорогостоящих переходов в кернел. Раньше и сами структуры объектов мапились в память юзера, и поле «UserAddress» хранило как-раз линк на них. Но сейчас лавочку прикрыли, а потому адрес юзера будет всегда сброшен в нуль.
Программно обойти таблицу дескрипторов GDI можно следующим образом..
Значит берём из РЕВ поле «GdiSharedHandleTable», и с шагом 18h парсим структуры таблицы.
Если в первом аргументе функции user32!GetGuiResources() передать константу
GR_GLOBAL = -2
, то получим общее кол-во записей в глобальной таблице на текущий момент. Если-же хотим отфильтровать вывод только для своего процесса, то передаём его Pid предварительным вызовом kernel32!GetCurrentProcessId(). Функция может считать как объекты GDI так и USER, для чего предусмотрен второй аргумент. А в остальном всё элементарно:
C-подобный:
format pe64 console 5.0
include 'win64ax.inc'
entry start
;//-----------
section '.data' data readable writable
struct GDI_TABLE_ENTRY ;//<---- массив структур
pKernelAddress dq 0 ;
wProcessId dw 0 ;
wCount dw 0 ;
wUniq dw 0 ;
wType dw 0 ;
pUserAddress dq 0 ;
ends
typeTable dd 0x01,@00,0x04,@01,0x05,@02,0x06,@03
dd 0x08,@04,0x0A,@05,0x10,@06
@00 db '0x01 = DC',0 ;// список типов объектов GDI
@01 db '0x04 = Region',0
@02 db '0x05 = Bitmap',0
@03 db '0x06 = Metafile',0
@04 db '0x08 = Pallete',0
@05 db '0x0A = Font',0
@06 db '0x10 = Brush\Pen',0
@unk db 'Unknown',0
align 16
GR_GDIOBJ = 0 ;// запрос объектов GDI
GR_USEROBJ = 1 ;// запрос объектов USER
GR_GLOBAL = -2 ;// по всей системе, иначе PID
gdiTable dq 0
pid dq 0
index dq 0
totalObj dq 0
;//-----------
section '.text' code readable executable
start: sub rsp,8
invoke GetCurrentProcessId
mov [pid],rax
invoke GetGuiResources, GR_GLOBAL, GR_GDIOBJ
mov [totalObj],rax
push rax
mov rsi,[gs:0x60] ;// PEB address
mov rax,[rsi+0xF8] ;// GdiSharedHandleTable
mov [gdiTable],rax
pop rbx
cinvoke printf,<10,' Current process ID....: 0x%04x',\
10,' GdiSharedHandleTable..: 0x%016llx',\
10,' Total GDI objects.....: %d',\
10,\
10,' Index Pid Kernel address Handle Type',\
10,' ----- ---- ---------------- -------- --------------',0>,\
[pid],rax,rbx
mov rdi,[gdiTable]
@@: push rdi
movzx eax,word[rdi+GDI_TABLE_ENTRY.wProcessId]
;// cmp rax,[pid] ;// для фильтра своего PID
;// jnz @next
cmp eax,4 ;// валидный PID начинается с 4
jb @next ;// пропустить, если меньше
movzx ebx,word[rdi+GDI_TABLE_ENTRY.wUniq]
shl ebx,16 ;// собрать Handle из [Uniq << 16 + Index]
add rbx,[index]
movzx ebp,word[rdi+GDI_TABLE_ENTRY.wType]
and ebp,0x1F
push rax ;// поиск строки по типу
mov esi,typeTable
mov ecx,7
@type: lodsd
cmp eax,ebp
je @found
add esi,4
loop @type
mov ebp,@unk ;// ошибка!
jmp @fuck
@found: mov ebp,[esi]
@fuck: pop rax
cinvoke printf,<10,' %04x %04x %016llx %08x %s',0>,\
[index],rax,qword[rdi],rbx,rbp
@next: pop rdi
add rdi,18h ;// sizeof GDI_TABLE_ENTRY
inc [index]
dec [totalObj] ;// всю таблицу проверили?
jnz @b
@exit: invoke GetDC,0 ;// подгрузить либу User32.dll
invoke ReleaseDC,0,eax
cinvoke _getch
cinvoke exit, 0
;//-----------
section '.idata' import data readable
library kernel32,'kernel32.dll', user32,'user32.dll', msvcrt,'msvcrt.dll'
include 'api\kernel32.inc'
include 'api\user32.inc'
include 'api\msvcrt.inc'
3. Пользовательские объекты USER
C объектами USER дела обстоят аналогично, только есть несколько принципиальных отличий.
Все пользовательские объекты индексируются в таблице дескрипторов для каждого сеанса. Сама таблица хранится в ядре, и проецируется в каждый процесс с графическим интерфейсом GUI. Как и в случае с объектами GDI, такая схема позволяет процессам получать доступ на чтение ядерных структур прямо из пользовательского режима, без необходимости прибегать к системному вызову
syscall
. Указатель на таблицу хранится в структуре общей информации ядра win32k!tagSHAREDINFO, а указатель на саму SHAREDINFO возвращает как переменная ядра win32k!gSharedInfo, так и пользовательская функция user32!gSharedInfo().Структура ядра SHAREDINFO довольно творческая единица, и в ней зарыт огромный объём информации. Отложим пока на полку таблицу дескрипторов USER, и посмотрим на содержимое шары. Для этого в отладчике нужно присоединиться к любому процессу с оконным интерфейсом GUI, например к тому-же диспетчеру задач Win taskmgr.exe. Далее запрашиваем значение переменной win32k!gSharedInfo, и дампим структуру ядра:
Код:
0: kd> !process 0 0 taskmgr.exe
PROCESS fffffa800210b1f0
SessionId: 1 Cid: 0df8 Peb: 7fffffde000 ParentCid: 069c
Image: taskmgr.exe
0: kd> .process /p fffffa800210b1f0 <-----// подключились к процессу
Implicit process is now fffffa80`0210b1f0
0: kd> x win32k!gSharedInfo
fffff960`003ce520 win32k!gSharedInfo <-----// переменная ядра
0: kd> dt win32k!tagSHAREDINFO fffff960`003ce520
+0x000 psi : 0xfffff900`c0580a70 tagSERVERINFO
+0x008 aheList : 0xfffff900`c0400000 _HANDLEENTRY
+0x010 HeEntrySize : 0x18
+0x018 pDispInfo : 0xfffff900`c0581e50 tagDISPLAYINFO
+0x020 ulSharedDelta : 0
+0x028 awmControl : [31] _WNDMSG
+0x218 DefWindowMsgs : _WNDMSG
+0x228 DefWindowSpecMsgs : _WNDMSG
0: kd>
Значит в поле «aheList» лежит указатель на таблицу дескрипторов, которая содержит в себе массив структур HANDLEENTRY с описанием каждого из объектов USER, а в сл.поле «HeEntrySize» размер одной структуры 18h байт. Мы вернёмся к этому массиву чуть позже, а сейчас запросим инфу о сервере, в качестве которого выступает ядро. Поскольку структура большая, я оставил в ней только наиболее интересные поля:
Код:
0: kd> dt win32k!tagSERVERINFO 0xfffff900`c0580a70
+0x008 cHandleEntries : 0x2c00 <-------// Всего дескрипторов в таблице USER
+0x188 apfnClientA : _PFNCLIENT
+0x38c dwDefaultHeapSize : 0x01400000 <-------// Размер хипа в дефолте
+0x754 dwInstalledEventHooks : 0x8001 <-------// Кол-во установленных в системе хуков
+0xad0 hbrGray : 0x00ff9933`00f2e4d7 HBRUSH__
+0xad8 ptCursor : tagPOINT
+0xb14 tmSysFont : tagTEXTMETRICW
+0xb80 hIcoWindows : 0x00000000`0110007a HICON__
Обратите внимание, что объект «BRUSH» (кисть) принадлежит к GDI и находится в ядре, а объект иконки окна «ICON» уже в пространстве юзера. Это говорит о том, что не все объекты создаются в ядре – некоторые из них порождает сама либа User32.dll. В поле «apfnClientA» хранится массив указателей на внутренние функции оконного менеджера, которые использует данный процесс. Поскольку я под отладчиком режима ядра, он не может распознать имена пользовательских функций, и предоставляет только их адреса:
Код:
0: kd> dt win32k!tagSERVERINFO 0xfffff900`c0580a70 apfnClientA.
+0x188 apfnClientA :
+0x000 pfnScrollBarWndProc : 0x00000000`77572ff4
+0x008 pfnTitleWndProc : 0x00000000`7751e5a4
+0x010 pfnMenuWndProc : 0x00000000`77573014
+0x018 pfnDesktopWndProc : 0x00000000`77573034
+0x020 pfnDefWindowProc : 0x00000000`7751e5a4
+0x028 pfnMessageWindowProc : 0x00000000`7751e5a4
+0x030 pfnSwitchWindowProc : 0x00000000`77573054
+0x038 pfnButtonWndProc : 0x00000000`77573074
+0x040 pfnComboBoxWndProc : 0x00000000`77573094
+0x048 pfnComboListBoxProc : 0x00000000`775730b4
+0x050 pfnDialogWndProc : 0x00000000`775730d4
+0x058 pfnEditWndProc : 0x00000000`775730e4
+0x060 pfnListBoxWndProc : 0x00000000`77573104
+0x068 pfnMDIClientWndProc : 0x00000000`77573124
+0x070 pfnStaticWndProc : 0x00000000`77573144
+0x078 pfnImeWndProc : 0x00000000`77573164
+0x080 pfnGhostWndProc : 0x00000000`77573174
+0x088 pfnHkINLPCWPSTRUCT : 0x00000000`77573194
+0x090 pfnHkINLPCWPRETSTRUCT : 0x00000000`775731a4
+0x098 pfnDispatchHook : 0x00000000`775731c4
+0x0a0 pfnDispatchDefWindowProc : 0x00000000`775731e4
+0x0a8 pfnDispatchMessage : 0x00000000`77573204
+0x0b0 pfnMDIActivateDlgProc : 0x00000000`77573214
Ну и теперь поговорим о таблице дескрипторов, на которую указывает поле
aheList = 0xfffff900`c0400000
.Общее кол-во записей в таблице лежит в поле «HandleEntries» структуры SERVERINFO, и в данном случае видим в нём значение
2c00h = 11.264
, некоторые из которых могут быть свободные. Поскольку таблица объектов USER поддерживается на стороне сервера win32k.sys, ограничение на кол-во объектов в таблице является глобальным – начиная с Win7 оно составляет 64К записей Entry. Как и в случае с GDI, лимит в диапазоне 256...65536 можно выставить в указанной выше ветке реестра.Учитывая размер одной HANDLEENTRY = 18h байт, добавляя его к базе можно сдампить например первые три записи из таблицы USER. Как видим первая пустая и содержит нули, а вот следущие уже хотят о чём-то нам поведать:
Код:
0: kd> dt win32k!_HANDLEENTRY 0xfffff900`c0400000
+0x000 phead : (null) <---- указатель на заголовок объекта в ядре
+0x008 pOwner : (null) <---- линк на структуру W32THREAD владельца
+0x010 bType : 0 <---- тип объекта (см.табл.ниже)
+0x011 bFlags : 0 <---- флаги
+0x012 wUniq : 1 <---- счётчик уникальности объекта
0: kd> dt win32k!_HANDLEENTRY 0xfffff900`c0400000 + 0x18
+0x000 phead : 0xfffff900`c05824b0 _HEAD
+0x008 pOwner : (null)
+0x010 bType : 0xc <---- тип «Montor»
+0x011 bFlags : 0
+0x012 wUniq : 1
0: kd> dt win32k!_HANDLEENTRY 0xfffff900`c0400000 + 0x18 + 0x18
+0x000 phead : 0xfffff900`c08ddd20 _HEAD
+0x008 pOwner : 0xfffff900`c0849870 Void
+0x010 bType : 0x1 <---- тип «Window»
+0x011 bFlags : 0x40
+0x012 wUniq : 1
0: kd> dt win32k!_W32THREAD 0xfffff900`c0849870
+0x000 pEThread : 0xfffffa80`03306060 _ETHREAD
+0x008 RefCount : 1
+0x010 ptlW32 : (null)
+0x018 pgdiDcattr : 0x00000000`00110680 Void
........
В записях объектов GDI имелось поле Pid, по значению которого можно было искать в таблице объекты только своего процесса. В дескрипторах HANDLEENTRY уже такого флага нет – здесь свои объекты мы можем обнаружить только прочитав указатель в поле «Owner» (владелец). Не беда, что из пользовательского процесса у нас нет доступа к ядерной win32k!_W32THREAD – значение «Owner» система специально для юзера дублирует в поле «Win32ThreadInfo» структуры TEB потока. Если команде
!process
отладчика передать аргумент(4), она выводит инфу не только о процессах, но и всех его потоках (см.первую строку в блоке):
Код:
0: kd> !process 0 4 taskmgr.exe
PROCESS fffffa800215bb10
SessionId: 1 Cid: 0bac Peb: 7fffffdf000 ParentCid: 06a8
Image: taskmgr.exe
THREAD fffffa8001b98b50 Cid 0bac.0b24 Teb: 000007fffffdd000 Win32Thread: fffff900c1ecb5e0 WAIT <----//
THREAD fffffa8001ac0b50 Cid 0bac.0794 Teb: 000007fffffdb000 Win32Thread: fffff900c204f010 WAIT
THREAD fffffa8002097b50 Cid 0bac.060c Teb: 000007fffffd7000 Win32Thread: fffff900c2e1dc30 WAIT
THREAD fffffa8001a9e640 Cid 0bac.08e8 Teb: 000007fffffd5000 Win32Thread: fffff900c2b5c9a0 WAIT
THREAD fffffa800379eaf0 Cid 0bac.0510 Teb: 000007fffffd3000 Win32Thread: 0000000000000000 WAIT
THREAD fffffa80020f8b50 Cid 0bac.06e0 Teb: 000007fffffae000 Win32Thread: 0000000000000000 WAIT
Теперь прочитав поле «Owner» в записи HANDLEENTRY и сравнив его со-своим, мы сможем отфильтровать принадлежащие нашему процессу дескрипторы:
Код:
0: kd> dt _teb 000007fffffdd000 Win32*
ntdll!_TEB
+0x078 Win32ThreadInfo : 0xfffff900`c1ecb5e0 <-----//
+0x800 Win32ClientInfo : [62] 0x188
0: kd>
В данном случае в третьей записи таблицы видим
Owner = 0xfffff900`c0849870
, а у себя получили Win32ThreadInfo = 0xfffff900`c1ecb5e0
. От сюда следует, что дескриптор(3) чужой и не принадлежит USER-объекту нашего процесса. Командой отладчика !pool
можно узнать, в каком из пулов система выделила ему память – это PagedSessionPool:
Код:
0: kd> !pool 0xfffff900`c1ecb5e0
Pool page fffff900c1ecb5e0 region is Paged session pool <--------//
fffff900c1ecb000 size: 330 previous size: 0 (Allocated) Uspi Process: fffffa80032dbb10
fffff900c1ecb330 size: 30 previous size: 330 (Free) ....
fffff900c1ecb360 size: d0 previous size: 30 (Allocated) Gla@ win32k.sys
fffff900c1ecb430 size: 10 previous size: d0 (Free) GTmp win32k.sys
fffff900c1ecb440 size: a0 previous size: 10 (Allocated) Gh1? win32k.sys
fffff900c1ecb4e0 size: f0 previous size: a0 (Allocated) Gla8 win32k.sys
*fffff900c1ecb5d0 size: 3e0 previous size: f0 (Allocated) *Usti Process: fffffa800215bb10
Pooltag Usti: USERTAG_THREADINFO, Binary : win32k!AllocateW32Thread
0: kd>
Вернёмся к структуре HANDLEENTRY и разберём её поля, чтобы позже сдампить их массив программно.
По сути названия всех полей говорят сами за себя, и к этому трудно что-то добавить. Как и в случае с GDI, 32-битный дескриптор Handle собирается из двух 16-битных частей – в старшем слово кладём значение «Uniq», а в младшее порядковый номер (индекс) записи в таблице, т.е
Uniq << 16 + Index
. Всего в системе числится 21-тип пользовательских объектов, при этом флаги нам не интересны:
C-подобный:
struct HANDLEENTRY ;//<----- размер 18h байт
phead dq 0 ;// линк на заголовок объекта
pOwner dq 0 ;// линк на структуру W32THREAD владельца
bType db 0 ;// тип объекта из таблицы ниже
bFlags db 0 ;// флаги
wUniq dw 0 ;// счётчик уникальности объекта
ends
Значит чтобы подобраться к таблице под юзером, нужно запросить у либы user32.dll адрес её функции gSharedInfo(), через вызов двойки LoadLibrary() + GetProcAddress(). Так мы получим указатель не на ядерный оригинал таблицы дескрипторов, а на доступную только для чтения её копию в пользовательском пространстве (см.рисунок с отображением памяти выше). Поскольку вызов GetProcAddress() всегда возвращает адрес из указанного нами модуля, выходит, что win32k.sys мапит таблицу в юзер-мод именно в пространство библиотеки user32.dll.
Вот собственно и код сбора инфы об объектах USER. На старте он запрашивает тип объекта для вывода в диапазоне 1-21, после чего проходит по всей таблице поиском. Кстати работать будет только на системах Win7+, т.к. либа user32.dll на WinХР и ниже не экспортирует функцию gSharedInfo():
C-подобный:
format pe64 console 5.0
include 'win64ax.inc'
entry start
;//-----------
section '.data' data readable writable
struct HANDLEENTRY
phead dq 0
pOwner dq 0
bType db 0
bFlags db 0
wUniq dw 0
ends
struct SERVERINFO
dwSRVIFlags dq 0
cHandleEntries dq 0
ends
struct SHAREDINFO
psi dq 0 ;// tagSERVERINFO
aheList dq 0 ;// HANDLEENTRY
HeEntrySize dq 0 ;// sizeof HANDLEENTRY
ends
align 16
typeTable dd @00,@01,@02,@03,@04,@05,@06,@07,@08,@09,@10
dd @11,@12,@13,@14,@15,@16,@17,@18,@19,@20,@21
@00 db '00 = Free',0
@01 db '01 = Window',0
@02 db '02 = Icon\Menu',0
@03 db '03 = Cursor',0
@04 db '04 = SetWindowPos',0
@05 db '05 = Hook',0
@06 db '06 = ClipData',0
@07 db '07 = CallProc',0
@08 db '08 = Acceltable',0
@09 db '09 = DDE.Access',0
@10 db '10 = DDE.Conv',0
@11 db '11 = DDE.Xact',0
@12 db '12 = Monitor',0
@13 db '13 = KBD.Layout',0
@14 db '14 = KBD.File',0
@15 db '15 = EventHook',0
@16 db '16 = Timer',0
@17 db '17 = InputContext',0
@18 db '18 = HID.Data',0
@19 db '19 = DeviceInfo',0
@20 db '20 = TouchInput',0
@21 db '21 = GestureInfo',0
pid dq 0
index dq 0
totalObj dd 0
found dd 0
bType dd 0
;//-----------
section '.text' code readable executable
start: sub rsp,8
invoke SetConsoleTitle,<' *** User object list from SharedInfo ***',0>
;// Принимаем у юзера тип объекта
@@: cinvoke printf,<10,' Input object type (1-21): ',0>
cinvoke scanf,<'%d',0>,bType
cmp [bType],21
jbe @f
cinvoke printf,<' Error - Invalid type!',10,0>
jmp @b
@@: invoke GetCurrentProcessId
mov [pid],rax
;// Берём указатель на таблицу дескрипторов
invoke LoadLibrary,'user32.dll'
invoke GetProcAddress,rax,'gSharedInfo'
push rax
;// Узнаем общее кол-во записей в таблице
mov rbx,[rax]
mov rbx,[rbx+SERVERINFO.cHandleEntries]
mov [totalObj],ebx
cinvoke printf,<10,' Current process ID: 0x%04x',\
10,' SharedInfo address: 0x%016llx',\
10,' Total objects.....: %d',\
10,\
10,' Index Object Head Owner ThreadInfo Handle Type',\
10,' ----- ---------------- ---------------- ---------- ----------',0>,\
[pid],rax,rbx
;// Парсим таблицу дескрипторов!
pop rsi
mov rsi,[rsi+SHAREDINFO.aheList]
@@: push rsi
movzx eax,byte[rsi+HANDLEENTRY.bType]
cmp eax,[bType]
jne @next
inc [found]
mov edi,typeTable ;// переводим тип в строку
shl eax,2
add edi,eax
mov eax,[edi]
movzx ebx,word[rsi+HANDLEENTRY.wUniq]
shl ebx,16 ;// соберём хэндл из двух составляющих
add rbx,[index]
cinvoke printf,<10,' %04x %016llx %016llx 0x%08x %s',0>,\
[index],[rsi+HANDLEENTRY.phead],[rsi+HANDLEENTRY.pOwner],ebx,eax
@next: pop rsi
add rsi,18h ;// sizeof HANDLEENTRY
inc [index]
dec [totalObj] ;// конец таблицы?
jnz @b ;// jump if non zero
cinvoke printf,<10,10,' -----------------------',\
10,' Found: %d entries',0>,[found]
@exit: invoke GetDC
cinvoke _getch
cinvoke exit, 0
;//-----------
section '.idata' import data readable
library kernel32,'kernel32.dll', user32,'user32.dll',msvcrt,'msvcrt.dll'
include 'api\kernel32.inc'
include 'api\user32.inc'
include 'api\msvcrt.inc'
4. Kernel Callback Table
Под занавес рассмотрим ещё один механизм, который напрямую связан с объектами USER и GDI.
В силу того, что поддержка графических окон размазана чуть-ли не по всей системе Windows, половина служебных структур находится непосредственно в ядерной win32.sys, а половина заброшена на «вражескую» территорию юзера. Как результат, не только код пользовательской подсистемы Win32 должен обращаться к сервисам ядра, но и наоборот кернелу требуется звать на помощь функции из user32.dll. Звучит необычно, но это факт, которым злоупотребляет достаточное кол-во малвари.
Список нужных оконному менеджеру пользовательских функций приличный (порядка 100 штук на Win7), и чтобы постоянно не вычислять их адреса, драйвер win32k.sys создаёт специальный их лист, указатель на который прописывает в поле «KernelCallbackTable» структуры РЕВ. Таблица создаётся динамически при каждой загрузке user32.dll в пространство нашего приложения. Функции из этой таблицы колбэков являются внутренними, и либа user32.dll не экспортирует их из своей тушки напрямую.
В отладчике WinDbg можно собрать всю информацию о данном механизме обратных вызовов, включая полный список самих функций. Для этого нужно переключиться в режим юзера, и загрузить какого-нибудь клиента с графическим интерфейсом GUI. Далее ставим брейк на точку-входа, сразу даём команду
go
и делаем один шаг p
, чтобы все либы импорта проинициализировались. Команды можно передавать прямо в одной строке, разделяя их символом ;
. Получив из РЕВ указатель на «KernelCallbackTable», командой !address
соберём о нём доп.сведения – угу, область модуля user32.dll с атрибутом ReadOnly. Кстати сам win32k.sys запрашивает адрес таблицы у юзера в виде переменной user32!apfnDispatch, после чего сохраняет этот адрес в РЕВ – команда ln
подтверждает сей факт:
Код:
0:000> bp @$exentry; g; p; dt _peb @$peb KernelCallbackTable
Breakpoint 0 hit
ntdll!_PEB
+0x058 KernelCallbackTable : 0x00000000`775f9500 Void
0:000> ln 0x00000000`775f9500
(00000000`775f9500) user32!apfnDispatch | (00000000`775fa730) user32!szIndicDLL
0:000> !address 0x00000000`775f9500
Usage: Image
Allocation Base: 00000000`77570000
Base Address: 00000000`775f2000
End Address: 00000000`77602000
Region Size: 00000000`00010000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY
More info: lmv m user32
More info: !lmi user32
More info: ln 0x775f9500
Так мы получили указатель на таблицу обратных вызовов ядра, и теперь командой
dqs
можем просмотреть её содержимое.
Код:
0:000> dqs 0x00000000`775f9500 L100
00000000`771a9500 00000000`77126f74 user32!_fnCOPYDATA
00000000`771a9508 00000000`7716f760 user32!_fnCOPYGLOBALDATA
00000000`771a9510 00000000`771367fc user32!_fnDWORD
00000000`771a9518 00000000`7712cb7c user32!_fnNCDESTROY
00000000`771a9520 00000000`7713f470 user32!_fnDWORDOPTINLPMSG
00000000`771a9528 00000000`7716f878 user32!_fnINOUTDRAG
00000000`771a9530 00000000`771485a0 user32!_fnGETTEXTLENGTHS
00000000`771a9538 00000000`7716fb9c user32!_fnINCNTOUTSTRING
00000000`771a9540 00000000`7712b790 user32!_fnPOUTLPINT
00000000`771a9548 00000000`7716fa60 user32!_fnINLPCOMPAREITEMSTRUCT
00000000`771a9550 00000000`77130710 user32!_fnINLPCREATESTRUCT
00000000`771a9558 00000000`7713cd30 user32!_fnINLPDELETEITEMSTRUCT
00000000`771a9560 00000000`771480d4 user32!_fnINLPDRAWITEMSTRUCT
00000000`771a9568 00000000`7716fab0 user32!_fnINLPHLPSTRUCT
00000000`771a9570 00000000`7716fab0 user32!_fnINLPHLPSTRUCT
00000000`771a9578 00000000`7716f8cc user32!_fnINLPMDICREATESTRUCT
00000000`771a9580 00000000`7713edb4 user32!_fnINOUTLPMEASUREITEMSTRUCT
00000000`771a9588 00000000`77132624 user32!_fnINLPWINDOWPOS
00000000`771a9590 00000000`77130a3c user32!_fnINOUTLPWINDOWPOS
00000000`771a9598 00000000`771455fc user32!_fnINOUTLPSCROLLINFO
00000000`771a95a0 00000000`7712bbf0 user32!_fnINOUTLPRECT
00000000`771a95a8 00000000`771306ac user32!_fnINOUTNCCALCSIZE
00000000`771a95b0 00000000`77130a3c user32!_fnINOUTLPWINDOWPOS
00000000`771a95b8 00000000`7716f930 user32!_fnINPAINTCLIPBRD
00000000`771a95c0 00000000`7716f9c4 user32!_fnINSIZECLIPBRD
00000000`771a95c8 00000000`7713e910 user32!_fnINDESTROYCLIPBRD
00000000`771a95d0 00000000`77129de0 user32!_fnINSTRING
00000000`771a95d8 00000000`77129de0 user32!_fnINSTRING
00000000`771a95e0 00000000`77125a20 user32!_fnINDEVICECHANGE
00000000`771a95e8 00000000`7712ab90 user32!_fnPOWERBROADCAST
00000000`771a95f0 00000000`77145934 user32!_fnINOUTNEXTMENU
00000000`771a95f8 00000000`7713ee08 user32!_fnOPTOUTLPDWORDOPTOUTLPDWORD
00000000`771a9600 00000000`7713ee08 user32!_fnOPTOUTLPDWORDOPTOUTLPDWORD
00000000`771a9608 00000000`7716f700 user32!_fnOUTDWORDINDWORD
00000000`771a9610 00000000`7713f394 user32!_fnOUTLPRECT
00000000`771a9618 00000000`7712b790 user32!_fnPOUTLPINT
00000000`771a9620 00000000`7716fab0 user32!_fnINLPHLPSTRUCT
00000000`771a9628 00000000`7712b790 user32!_fnPOUTLPINT
00000000`771a9630 00000000`7716f7ec user32!_fnSENTDDEMSG
00000000`771a9638 00000000`77137f50 user32!_fnINOUTSTYLECHANGE
00000000`771a9640 00000000`7712f63c user32!_fnHkINDWORD
00000000`771a9648 00000000`771401bc user32!_fnHkINLPCBTACTIVATESTRUCT
00000000`771a9650 00000000`7713f918 user32!_fnHkINLPCBTCREATESTRUCT
00000000`771a9658 00000000`7716fe00 user32!_fnHkINLPDEBUGHOOKSTRUCT
00000000`771a9660 00000000`771485f0 user32!_fnHkINLPMOUSEHOOKSTRUCTEX
00000000`771a9668 00000000`7716fc98 user32!_fnHkINLPKBDLLHOOKSTRUCT
00000000`771a9670 00000000`7716fcf0 user32!_fnHkINLPMSLLHOOKSTRUCT
00000000`771a9678 00000000`7713483c user32!_fnHkINLPMSG
00000000`771a9680 00000000`7716fc4c user32!_fnHkINLPRECT
00000000`771a9688 00000000`7716fd3c user32!_fnHkOPTINLPEVENTMSG
00000000`771a9690 00000000`77143070 user32!_ClientCopyDDEIn1
00000000`771a9698 00000000`77142ea0 user32!_ClientCopyDDEIn2
00000000`771a96a0 00000000`77142f00 user32!_ClientCopyDDEOut1
00000000`771a96a8 00000000`7716ffdc user32!_ClientCopyDDEOut2
00000000`771a96b0 00000000`77131ee4 user32!_ClientCopyImage
00000000`771a96b8 00000000`7713d4ac user32!_ClientEventCallback
00000000`771a96c0 00000000`77170244 user32!_ClientFindMnemChar
00000000`771a96c8 00000000`7716fe58 user32!_ClientFreeDDEHandle
00000000`771a96d0 00000000`77148644 user32!_ClientFreeLibrary
00000000`771a96d8 00000000`77121774 user32!_ClientGetCharsetInfo
00000000`771a96e0 00000000`7716ff68 user32!_ClientGetDDEFlags
00000000`771a96e8 00000000`771700c8 user32!_ClientGetDDEHookData
00000000`771a96f0 00000000`7713ea0c user32!_ClientGetListboxString
00000000`771a96f8 00000000`77139144 user32!_ClientGetMessageMPH
00000000`771a9700 00000000`77125fc0 user32!_ClientLoadImage
00000000`771a9708 00000000`77130bec user32!_ClientLoadLibrary
00000000`771a9710 00000000`771318c0 user32!_ClientLoadMenu
00000000`771a9718 00000000`77123e50 user32!_ClientLoadLocalT1Fonts
00000000`771a9720 00000000`771702a0 user32!_ClientPSMTextOut
00000000`771a9728 00000000`77170308 user32!_ClientLpkDrawTextEx
00000000`771a9730 00000000`771484f0 user32!_ClientExtTextOutW
00000000`771a9738 00000000`7712b728 user32!_ClientGetTextExtentPointW
00000000`771a9740 00000000`771701c4 user32!_ClientCharToWchar
00000000`771a9748 00000000`77123f14 user32!_ClientAddFontResourceW
00000000`771a9750 00000000`7713a824 user32!_ClientThreadSetup
00000000`771a9758 00000000`77170424 user32!_ClientDeliverUserApc
00000000`771a9760 00000000`7717039c user32!_ClientNoMemoryPopup
00000000`771a9768 00000000`77135be0 user32!_ClientMonitorEnumProc
00000000`771a9770 00000000`7712ddd4 user32!_ClientCallWinEventProc
00000000`771a9778 00000000`77139344 user32!_ClientWaitMessageExMPH
00000000`771a9780 00000000`7712ba08 user32!_ClientWOWGetProcModule
00000000`771a9788 00000000`77170568 user32!_ClientWOWTask16SchedNotify
00000000`771a9790 00000000`771217c0 user32!_ClientImmLoadLayout
00000000`771a9798 00000000`771489a8 user32!_ClientImmProcessKey
00000000`771a97a0 00000000`77170438 user32!_fnIMECONTROL
00000000`771a97a8 00000000`7713f608 user32!_fnINWPARAMDBCSCHAR
00000000`771a97b0 00000000`771485a0 user32!_fnGETTEXTLENGTHS
00000000`771a97b8 00000000`7716fb14 user32!_fnINLPKDRAWSWITCHWND
00000000`771a97c0 00000000`771229ec user32!_ClientLoadStringW
00000000`771a97c8 00000000`77190b94 user32!_ClientLoadOLE
00000000`771a97d0 00000000`771909f8 user32!_ClientRegisterDragDrop
00000000`771a97d8 00000000`77190a54 user32!_ClientRevokeDragDrop
00000000`771a97e0 00000000`77170510 user32!_fnINOUTMENUGETOBJECT
00000000`771a97e8 00000000`7713f404 user32!_ClientPrinterThunk
00000000`771a97f0 00000000`77144924 user32!_fnOUTLPCOMBOBOXINFO
00000000`771a97f8 00000000`77145660 user32!_fnOUTLPSCROLLBARINFO
00000000`771a9800 00000000`77145934 user32!_fnINOUTNEXTMENU
00000000`771a9808 00000000`771459e0 user32!_fnINLPUAHDRAWMENUITEM
00000000`771a9810 00000000`77145934 user32!_fnINOUTNEXTMENU
00000000`771a9818 00000000`77145988 user32!_fnINOUTLPUAHMEASUREMENUITEM
00000000`771a9820 00000000`77145934 user32!_fnINOUTNEXTMENU
00000000`771a9828 00000000`77145730 user32!_fnOUTLPTITLEBARINFOEX
00000000`771a9830 00000000`771705b8 user32!_fnTOUCH
00000000`771a9838 00000000`77170624 user32!_fnGESTURE
00000000`771a9840 00000000`7716fab0 user32!_fnINLPHLPSTRUCT
00000000`771a9848 00000000`00000000
00000000`771a9850 00000000`771429f8 user32!ButtonWndProcWorker
00000000`771a9858 00000000`77163c6c user32!ComboBoxWndProcWorker
00000000`771a9860 00000000`771865b0 user32!ListBoxWndProcWorker
00000000`771a9868 00000000`771476d8 user32!DefDlgProcWorker
00000000`771a9870 00000000`77178ba4 user32!EditWndProcWorker
00000000`771a9878 00000000`771865b0 user32!ListBoxWndProcWorker
00000000`771a9880 00000000`771412a0 user32!MDIClientWndProcWorker
00000000`771a9888 00000000`771974cc user32!StaticWndProcWorker
00000000`771a9890 00000000`77130ed0 user32!ImeWndProcWorker
00000000`771a9898 00000000`77148ce4 user32!DefWindowProcWorker
00000000`771a98a0 00000000`7712f610 user32!CtfHookProcWorker
00000000`771a98a8 00000000`00000000
00000000`771a98c0 00000000`77162884 user32!ScrollBarWndProcW
00000000`771a98c8 00000000`771374d4 user32!DefWindowProcW
00000000`771a98d0 00000000`771626d8 user32!MenuWndProcW
00000000`771a98d8 00000000`771625ac user32!DesktopWndProcW
00000000`771a98e0 00000000`771374d4 user32!DefWindowProcW
00000000`771a98e8 00000000`771374d4 user32!DefWindowProcW
00000000`771a98f0 00000000`771374d4 user32!DefWindowProcW
00000000`771a98f8 00000000`77142940 user32!ButtonWndProcW
00000000`771a9900 00000000`77165e80 user32!ComboBoxWndProcW
00000000`771a9908 00000000`77187804 user32!ComboListBoxWndProcW
00000000`771a9910 00000000`7714768c user32!DefDlgProcW
00000000`771a9918 00000000`77178af8 user32!EditWndProcW
00000000`771a9920 00000000`77187804 user32!ComboListBoxWndProcW
00000000`771a9928 00000000`77141250 user32!MDIClientWndProcW
00000000`771a9930 00000000`77197ca8 user32!StaticWndProcW
00000000`771a9938 00000000`77131160 user32!ImeWndProcW
00000000`771a9940 00000000`771374d4 user32!DefWindowProcW
00000000`771a9948 00000000`77125388 user32!fnHkINLPCWPSTRUCTW
00000000`771a9950 00000000`771621b0 user32!fnHkINLPCWPRETSTRUCTW
00000000`771a9958 00000000`77127950 user32!DispatchHookW
00000000`771a9960 00000000`7712ccac user32!DispatchDefWindowProcW
00000000`771a9968 00000000`7713726c user32!DispatchClientMessage
00000000`771a9970 00000000`7718ed04 user32!MDIActivateDlgProcW
00000000`771a9978 00000000`00000000
00000000`771a9980 00000000`77162868 user32!ScrollBarWndProcA
00000000`771a9988 00000000`7712b830 user32!DefWindowProcA
00000000`771a9990 00000000`771626bc user32!MenuWndProcA
00000000`771a9998 00000000`77162590 user32!DesktopWndProcA
00000000`771a99a0 00000000`7712b830 user32!DefWindowProcA
00000000`771a99a8 00000000`7712b830 user32!DefWindowProcA
00000000`771a99b0 00000000`7712b830 user32!DefWindowProcA
00000000`771a99b8 00000000`7715f1f8 user32!ButtonWndProcA
00000000`771a99c0 00000000`77165db4 user32!ComboBoxWndProcA
00000000`771a99c8 00000000`77187758 user32!ComboListBoxWndProcA
00000000`771a99d0 00000000`77173218 user32!DefDlgProcA
00000000`771a99d8 00000000`77178a4c user32!EditWndProcA
00000000`771a99e0 00000000`77187758 user32!ComboListBoxWndProcA
00000000`771a99e8 00000000`77190480 user32!MDIClientWndProcA
00000000`771a99f0 00000000`77197c58 user32!StaticWndProcA
00000000`771a99f8 00000000`7719e520 user32!ImeWndProcA
00000000`771a9a00 00000000`7712b830 user32!DefWindowProcA
00000000`771a9a08 00000000`771486f8 user32!fnHkINLPCWPSTRUCTA
00000000`771a9a10 00000000`7719cab0 user32!fnHkINLPCWPRETSTRUCTA
00000000`771a9a18 00000000`7714878c user32!DispatchHookA
00000000`771a9a20 00000000`7719cbec user32!DispatchDefWindowProcA
00000000`771a9a28 00000000`7713726c user32!DispatchClientMessage
00000000`771a9a30 00000000`7718ed04 user32!MDIActivateDlgProcW
00000000`771a9a38 00000074`736f6847
00000000`771a9a40 0073006f`00680047
00000000`771a9a48 00000000`00000074
Функции из библиотек подсистемы Win32 переходят в ядро инструкцией
syscall
, но она не предусмотрена для обратных переходов из ядра к юзеру. Поэтому здесь применяется механизм трамплинов на основе стековых кадров, которые описывает ядерная структура KTRAP_FRAME. По сути это дальний переход инструкцией ret far
. Операцию начинает nt!KeUserModeCallback() с таким прототипом:
C-подобный:
NTSTATUS
KeUserModeCallback (
IN ULONG ApiNumber, ;// индекс функции в таблице
IN PVOID InputBuffer, ;// данные для передачи (копируются в стек юзера)
IN ULONG InputLength, ;// их размер
OUT PVOID *OutputBuffer, ;// адрес приёмного буфа
IN PULONG OutputLength ;// его размер
)
Далее вызывается nt!ProbeForWrite() для проверки, достаточно-ли места в стеке для передаваемых данных, после чего содержимое InputBuffer копируется в стек юзера. Внутренняя функция nt!KiCallUserMode() подготавливает переход в пользовательский режим, сохраняя важную информацию в стеке ядра. Непосредственно вызов функции из таблицы осуществляет nt!KeUserCallbackDispatcher(), а nt!NtCallbackReturn() возвращается обратно в ядро. Команда
uf
отладчика умеет дизасмить функции, а если передать ей аргументы /c /i
, то получим только цепочку вызовов с общим кол-во инструкций – удобно для анализа взаимосвязей:
Код:
0: kd> uf /c /i nt!KeUserModeCallback
nt!KeUserModeCallback (fffff800`02f64f1c), 126 instructions
call to nt!ProbeForWrite (fffff800`02f93f80)
call to nt!RtlpCopyExtendedContext (fffff800`02c691c4)
call to nt!memmove (fffff800`02c6cb50)
call to nt!KiCallUserMode (fffff800`02c6d7d0)
call to win32k!NtGdiFlushUserBatch (fffff960`0016cb3c)
call to nt!KeBugCheckEx (fffff800`02c76180)
0: kd>
Так чем-же примечательны эти обратные вызовы ядра?
Во-первых, поскольку таблица лежит в пространстве нашего приложения, то снять запрет на запись в неё можно элементарно через VirtualProtect(). Теперь если подменить в ней указатели на свои процедуры, то ничего не подозревающее ядро будет передавать на них управление. В списке есть довольно много функций, которые можно перехватывать таким образом, например почти все с префиксом user32!_ClientXXX. В частности user32!_ClientLoadLibrary() позволит прямо из ядра незаметно загружать в процессы юзера сторонние DLL. В общем возможности здесь ограничивает только фантазия.
Заключение
Придумать идеальную архитектуру операционной системы не так просто – если заткнёшь дыру в одном месте, она открывается в другом. Организация поддержки объектов USER и GDI прямое этому доказательство. Жили ведь до Win-NT спокойно, когда диспетчер форточек полностью находился в пространстве юзера. Но потом лёгким движением руки инженеры отправили диспетчера в ядро, оставив всех его подопечных на чужбине пользовательского пространства. Такая организация несёт потенциальные угрозы, исключить которые полностью в принципе не возможно. По ссылкам ниже лежат пдфки, где описываются некоторые уязвимости модуля win32k.sys. Всем удачи, до скорого.
Ссылка скрыта от гостей
Ссылка скрыта от гостей
Ссылка скрыта от гостей