- PVSM.RU - https://www.pvsm.ru -
Если вы возитесь с уязвимостями режима ядра в Windows, то рано или поздно приходится иметь дело с такой техникой, как kernel pool spraying (только не называйте ее «распыление ядерной кучи»). Думаю, умение держать под контролем поведение пула памяти ядра будет полезным для разработчика эксплойтов.
Чтобы осилить данную технику, необходимо иметь хотя бы примерное представление об устройстве пула ядра. В этой статье я постараюсь привести описание только значимых в контексте техники pool spraying деталей его реализации. Устройство пула ядра хорошо изучено, поэтому если вам все-таки нужны более глубокие познания, можете обратиться в любую поисковую службу или к ссылкам в конце статьи.
Пул памяти ядра – единое место в ядре операционной системы, куда можно обратиться с запросом на выделение памяти. Стеки в режиме ядра имеют небольшой размер и пригодны только для хранения нескольких переменных, причем не являющихся массивами. Когда драйверу требуется создать большую структуру данных или строку, он может использовать разные интерфейсы для выделения памяти, но в конце концов они приведут к памяти из пула.»
Существует несколько типов пулов, но все они имеют одинаковое строение (кроме особого пула (special pool), который используется утилитой проверки драйверов (driver verifier)). Каждый пул имеет управляющую структуру, называемую дескриптором пула. Помимо прочего, она хранит списки свободных блоков (chunk) пула, образующих свободное пространство пула. Сам пул состоит из страниц памяти. Они могут быть стандартными 4х-килобайтными или большими 2х-мегабайтными. Количество используемых страниц динамически регулируется.
Страницы пула ядра разделены на фрагменты разного размера – блоки (chunk). Именно блоки выделяются модулям ядра при запросе на выделение памяти из пула.

Блоки содержат в себе следующие метаданные:
Строение блока имеет пару отличий на 64-битных системах. Во-первых, поля заголовков имеют больший размер, а во-вторых, имеется 8-байтовое поле с указателем на процесс, использующий данный блок.

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

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

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

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


Однако, есть значимое в контексте эксплуатации обстоятельство: когда не остается регионов черного цвета при «закраске», мы получим новенький, без лишних пятен. И с этого момента «кисточка-распылитель» превращается в обычную, со сплошной заливкой. Этот факт дает нам значительный уровень контроля над поведением пула и над его «картинкой». Значительный не является полным контролем, поскольку даже в этом случае нет никаких гарантий того, что мы всецело владеем «картинкой», ведь нас всегда может прервать «брызгами» другого цвета кто-то еще.

В зависимости от типа объекта, используемого для pool spraying, у нас есть возможность создавать окна заданного размера из свободных блоков путем удаления необходимого количества созданных ранее объектов. Но самым важным фактом, позволяющим нам контролировать выделение памяти из пула, является то, что распределитель стремится к максимальной производительности. Для максимально эффективного использования кэша процессора последний освобожденный блок памяти будет первым выделенным. В этом вся суть контролируемого выделения, потому что есть возможность угадать адрес выделяемого блока.
Конечно, размер блока имеет значение. Поэтому необходимо предварительно расcчитать размер окна из освобожденных блоков. Если мы хотим контролируемо выделить блок размером в 0x315 байт при размере объектов для pool spraying в 0x20 байт, необходимо освободить 0x315 / 0x20 = (0x18 + 1) блоков. Думаю, это понятно.
Несколько заметок о том, как успешно использовать технику kernel pool spraying:
Для того чтобы освободить память, занимаемую данными объектами, достаточно закрыть соответствующие им дескрипторы.
В начале февраля были выпущены интересные рекомендации для обновления продуктов VMware. Судя по ним, в не обновленных компонентах присутствовала уязвимость, приводящая к локальному повышению привилегий как на основной, так и на гостевой ОС. Обходить стороной такие «вкусные» уязвимости нельзя.
Уязвимым компонентом был vmci.sys. VMCI расшифровывается как Virtual Machine Communication Interface. Этот интерфейс используется для взаимодействия между виртуальными машинами и основной ОС. VMCI предоставляет проприетарный тип сокетов, реализованных в виде провайдера Windows Socket Service Provider в библиотеке vsocklib.dll. Драйвер vmci.sys создает виртуальное устройство, реализующее необходимые функциональные возможности. Он всегда запущен на основной ОС. Что касается гостевых систем, для работоспособности VMCI необходимо установить VMware tools.
При написании любого обзора приятно объяснить высокоуровневую логику уязвимости, чтобы обзор превратился в детектив. К сожалению, в данном случае сделать это не удастся, потому что открытой информации о реализации VMCI весьма немного. Однако я думаю, что разработчики эксплойтов не переживают по этому поводу. По крайней мере выгоднее скорее получить рабочий эксплойт, чем потратить кучу времени на разбор того, как работает вся система целиком.
PatchDiff выявил три запатченные функции. Все они относились к обработке одного и того же управляющего кода IOCTL 0x8103208C. Видимо, все конкретно пошло не так с его обработкой…

Третья обновленная функция в конечном итоге вызывалась и из первой, и из второй. Она должна была выделять блок запрошенного размера, умноженного на 0x68, и инициализировать его, заполнив нулями. Этот блок содержит внутреннюю структуру данных для обработки запроса. Проблема была в том, что размер выделяемого блока указывался в режиме пользователя и толком не проверялся, в результате чего внутренняя структура не выделялась, что приводило к некоторым интересным последствиям.
Для управляющего кода 0x8103208C указывались входной и выходной буфер. Чтобы добраться до уязвимого места, необходимо, чтобы его размер был равен 0x624 байта. Чтобы обработать запрос, выделялась внутренняя структура размером в 0x20C байт. Первые ее 4 байта заполнялись значением, указанным по адресу [user_buffer + 0x10]. Именно эти байты использовались в дальнейшем для выделения второй структуры данных, адрес на которую указывался в конце первой. При всем этом, вне зависимости от результата выделения второй структуры вызывалась некая диспетчерская функция.
.text:0001B2B4 ; int __stdcall DispatchChunk(PVOID pChunk)
.text:0001B2B4 DispatchChunk proc near ; CODE XREF: PatchedOne+78
.text:0001B2B4 ; UnsafeCallToPatchedThree+121
.text:0001B2B4
.text:0001B2B4 pChunk = dword ptr 8
.text:0001B2B4
.text:0001B2B4 000 mov edi, edi
.text:0001B2B6 000 push ebp
.text:0001B2B7 004 mov ebp, esp
.text:0001B2B9 004 push ebx
.text:0001B2BA 008 push esi
.text:0001B2BB 00C mov esi, [ebp+pChunk]
.text:0001B2BE 00C mov eax, [esi+208h]
.text:0001B2C4 00C xor ebx, ebx
.text:0001B2C6 00C cmp eax, ebx
.text:0001B2C8 00C jz short CheckNullUserSize
.text:0001B2CA 00C push eax ; P
.text:0001B2CB 010 call ProcessParam ; We won’t get here
.text:0001B2D0
.text:0001B2D0 CheckNullUserSize: ; CODE XREF: DispatchChunk+14
.text:0001B2D0 00C cmp [esi], ebx
.text:0001B2D2 00C jbe short CleanupAndRet
.text:0001B2D4 00C push edi
.text:0001B2D5 010 lea edi, [esi+8]
.text:0001B2D8
.text:0001B2D8 ProcessUserBuff: ; CODE XREF: DispatchChunk+51
.text:0001B2D8 010 mov eax, [edi]
.text:0001B2DA 010 test eax, eax
.text:0001B2DC 010 jz short NextCycle
.text:0001B2DE 010 or ecx, 0FFFFFFFFh
.text:0001B2E1 010 lea edx, [eax+38h]
.text:0001B2E4 010 lock xadd [edx], ecx
.text:0001B2E8 010 cmp ecx, 1
.text:0001B2EB 010 jnz short DerefObj
.text:0001B2ED 010 push eax
.text:0001B2EE 014 call UnsafeFire ; BANG!!!!
.text:0001B2F3
.text:0001B2F3 DerefObj: ; CODE XREF: DispatchChunk+37
.text:0001B2F3 010 mov ecx, [edi+100h] ; Object
.text:0001B2F9 010 call ds:ObfDereferenceObject
.text:0001B2FF
.text:0001B2FF NextCycle: ; CODE XREF: DispatchChunk+28
.text:0001B2FF 010 inc ebx
.text:0001B300 010 add edi, 4
.text:0001B303 010 cmp ebx, [esi]
.text:0001B305 010 jb short ProcessUserBuff
.text:0001B307 010 pop edi
.text:0001B308
.text:0001B308 CleanupAndRet: ; CODE XREF: DispatchChunk+1E
.text:0001B308 00C push 20Ch ; size_t
.text:0001B30D 010 push esi ; void *
.text:0001B30E 014 call ZeroChunk
.text:0001B313 00C push 'gksv' ; Tag
.text:0001B318 010 push esi ; P
.text:0001B319 014 call ds:ExFreePoolWithTag
.text:0001B31F 00C pop esi
.text:0001B320 008 pop ebx
.text:0001B321 004 pop ebp
.text:0001B322 000 retn 4
.text:0001B322 DispatchChunk endp
Данная диспетчерская функция искала указатель для обработки. Обработка включала в себя разыменовывание некоторого объекта и вызова некоторой функции в зависимости от установленных в структуре флагов. Но поскольку при некорректных параметрах выделить структуру для обработки не удавалось, диспетчерская функция просто «проезжала» за границу первого блока. Такая обработка приводила к нарушению доступа и «синему экрану смерти».

Таким образом мы имеем возможность исполнять произвольный код по контролируемому адресу:
.text:0001B946 UnsafeFire proc near
.text:0001B946
.text:0001B946
.text:0001B946 arg_0 = dword ptr 8
.text:0001B946
.text:0001B946 000 mov edi, edi
.text:0001B948 000 push ebp
.text:0001B949 004 mov ebp, esp
.text:0001B94B 004 mov eax, [ebp+arg_0]
.text:0001B94E 004 push eax
.text:0001B94F 008 call dword ptr [eax+0ACh] ; BANG!!!!
.text:0001B955 004 pop ebp
.text:0001B956 000 retn 4
.text:0001B956 UnsafeFire endp
Поскольку диспетчерская функция выходит за границу блока, она встречается либо с соседним блоком, либо с не спроецированной страницей. Если она выходит в не спроецированную память, случится необрабатываемое исключение, и, следовательно, отобразится «синий экран». Но при попадании на соседний блок, диспетчерская функция интерпретирует его заголовок как указатель на структуру для обработки.
Допустим, имеется x86-система. Четыре байта, которые диспетчерская функция пытается интерпретировать как указатель, на самом деле являются полями Previous Block Size, Pool Index, Current Block Size и флагами Pool Type. Поскольку нам известны размер и индекс пула для обрабатываемого блока, то нам известно значение младшего слова указателя:
0xXXXX0043 – 0x43 является размером блока, который становится полем Previous Size для соседнего. 0 – индекс пула, который гарантированно будет именно нулем, поскольку данные блоки находятся в неподкачиваемом пуле, а он только один в системе. Заметьте, что если соседние блоки разделяют одну и ту же страницу памяти, они принадлежат одному и тому же типу и индексу пула.
Старшее слово хранит в себе размер блока, который мы не можем предугадать, и флаги pool type, которые, наоборот, можно предугадать:
Таким образом, имеем следующие регионы памяти, действительные для Windows 7 и 8:
Основываясь на приведенной выше информации, вы можете самостоятельно вычислить регионы памяти для Windows XP и ей подобных.
Как видно, эти регионы принадлежат пространству пользователя, поэтому мы можем заставить диспетчерскую функцию исполнить любой код, включая подконтрольный нам. Для этого сначала необходимо спроецировать указанные регионы памяти в процессе, а затем для каждых 0x10000 байт удовлетворить требования диспетчерской функции:
.text:0001B2E1 010 lea edx, [eax+38h]
.text:0001B2E4 010 lock xadd [edx], ecx
.text:0001B2E8 010 cmp ecx, 1
Примите во внимание тот факт, что для разных продуктов VMware значения смещений могут быть разными.

Все правильно сделано!
Несмотря на то, что мы выработали хорошую стратегию для эксплуатации данной уязвимости, она все еще остается ненадежной. Например, диспетчерская функция может попасть на свободный блок, поля которого предугадать невозможно. Несмотря на то, что заголовок такого блока будет интерпретирован как указатель (потому что он не равен нулю), результатом его обработки будет ошибка «синемий экран». Подобное случится также, когда диспетчерская функция попадет в неспроецированный участок памяти.
В данном случае на помощь приходит техника kernel pool spraying. В качестве объекта pool spraying я выбрал семафоры, поскольку они являются наиболее подходящими по размеру. В результате применения данной техники, стабильность эксплойта повысилась в разы.
Напомню, что в системе Windows 8 появилась поддержка такого механизма защиты, как SMEP, поэтому лень разработчика несколько усложняет разработку эксплойта. Написание базонезависимого кода с обходом SMEP остается упражнением для читателя.
Что касается х64-систем, есть проблема с тем, что размер указателя стал равен 8 байтам. Это значит, что старшее двойное слово (DWORD) указателя будет попадать на поле Pool Tag. А поскольку большинство драйверов и подсистем ядра используют ASCII-символы для таких меток, указатель попадает в пространство неканонических адресов и не может использоваться для эксплуатации. На момент написания статьи я ничего дельного по этому поводу не придумал.
Надеюсь, приведенная информация была полезной. Прошу прощения за то, что не смог уместить все необходимое в пару абзацев. Желаю успехов в исследованиях и эксплуатации во имя всецелого повышения уровня безопасности.
P.S. Напоминаю, что для устранения уязвимости нужно обновить не только основную, но и все гостевые системы!
P.P.S Если вы ощущаете некоторый дискомфорт от перевода некоторых терминов, будьте готовы мириться с ним в будущем, поскольку данный перевод рекомендован на языковом портале Microsoft [1].
Demo!
Ссылки
[1] Tarjei Mandt. Kernel Pool Exploitation on Windows 7. Black Hat DC, 2011
[2] Nikita Tarakanov. Kernel Pool Overflow from Windows XP to Windows 8. ZeroNights, 2011
[3] Kostya Kortchinsky. Real world kernel pool exploitation. SyScan, 2008
[4] SoBeIt. How to exploit Windows kernel memory pool. X’con, 2005
Автор: HonoraryBoT
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/vulnerability/29384
Ссылки в тексте:
[1] языковом портале Microsoft: http://www.microsoft.com/Language/
[2] Источник: http://habrahabr.ru/post/172719/
Нажмите здесь для печати.