Pull to refresh

Руководство по выстраиванию звезд: kernel pool spraying и VMware CVE-2013-2406

Reading time 13 min
Views 8.4K
Если вы возитесь с уязвимостями режима ядра в Windows, то рано или поздно приходится иметь дело с такой техникой, как kernel pool spraying (только не называйте ее «распыление ядерной кучи»). Думаю, умение держать под контролем поведение пула памяти ядра будет полезным для разработчика эксплойтов.

Чтобы осилить данную технику, необходимо иметь хотя бы примерное представление об устройстве пула ядра. В этой статье я постараюсь привести описание только значимых в контексте техники pool spraying деталей его реализации. Устройство пула ядра хорошо изучено, поэтому если вам все-таки нужны более глубокие познания, можете обратиться в любую поисковую службу или к ссылкам в конце статьи.

Обзор структуры пула ядра


Пул памяти ядра – единое место в ядре операционной системы, куда можно обратиться с запросом на выделение памяти. Стеки в режиме ядра имеют небольшой размер и пригодны только для хранения нескольких переменных, причем не являющихся массивами. Когда драйверу требуется создать большую структуру данных или строку, он может использовать разные интерфейсы для выделения памяти, но в конце концов они приведут к памяти из пула.»

Существует несколько типов пулов, но все они имеют одинаковое строение (кроме особого пула (special pool), который используется утилитой проверки драйверов (driver verifier)). Каждый пул имеет управляющую структуру, называемую дескриптором пула. Помимо прочего, она хранит списки свободных блоков (chunk) пула, образующих свободное пространство пула. Сам пул состоит из страниц памяти. Они могут быть стандартными 4х-килобайтными или большими 2х-мегабайтными. Количество используемых страниц динамически регулируется.

Страницы пула ядра разделены на фрагменты разного размера – блоки (chunk). Именно блоки выделяются модулям ядра при запросе на выделение памяти из пула.



Блоки содержат в себе следующие метаданные:
  • Previous size (предыдущий размер) — размер предыдущего блока.
  • Pool index (индекс пула) используется в ситуациях, когда существует несколько пулов одного типа. К примеру, подкачиваемых пулов в системе несколько. Данное поле используется для определения, какому именно пулу принадлежит блок.
  • Block size (размер блока) — размер текущего блока. Аналогично полю previous size, его размер кодируется как
    (размер данных блока + размер заголовка + опциональные 4 байта указателя на процесс, занявший блок) >> 3 (или >> 4 для x64 систем).
  • Pool type (тип пула) является набором битовых флагов, которые не документированы (!).
    • T (Tracked): блок отслеживается утилитой проверки драйверов. Данный флаг используется для отладки.
    • S (Session): блок принадлежит подкачиваемому пулу сессии, который используется для выделения памяти для специфичных пользовательской сессии данных.
    • Q (Quota): блок состоит на учете системы управления квотами. Этот флаг имеет отношение только к 32-битным системам. Если он выставлен, в конец блока записывается указатель на процесс, владеющий этим блоком.
    • U (In use): блок используется в настоящее время. В отличие от состояния «используется», блок может быть свободным, что значит, что из него можно выделять память. Данный флаг находится во втором бите, начиная с Windows Vista, до этого он находился в третьем бите.
    • B (Base pool): данное поле определяет, какому базовому пулу принадлежит блок. Есть два базовых пула — подкачиваемый и неподкачиваемый. Неподкачиваемый кодируется нулем, подкачиваемый — единицей. До Windows Vista этот флаг занимал два бита, поскольку кодировался как (тип базового пула + 1), т.е. 0x10 для подкачиваемого пула и 0x1 для неподкачиваемого.
  • Pool tag (тэг пула) используется для отладочных целей. Модули ядра указывают сигнатуру из четырех печатаемых символов, идентифицирующих подсистему или драйвер, которому принадлежит блок. К примеру, тэг “NtFs” значит, что блок принадлежит драйверу файловой системы NTFS ntfs.sys.

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



Обзор принципов выделения памяти в пуле


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



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



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



Заметьте, что описанная ситуация является вымышленной и используется только для примера, поскольку на практике пулы заполняются страницами памяти задолго до того момента, когда пул будет готов для использования модулями ядра.

Контролируем выделение памяти из пулов


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

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



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



В зависимости от типа объекта, используемого для pool spraying, у нас есть возможность создавать окна заданного размера из свободных блоков путем удаления необходимого количества созданных ранее объектов. Но самым важным фактом, позволяющим нам контролировать выделение памяти из пула, является то, что распределитель стремится к максимальной производительности. Для максимально эффективного использования кэша процессора последний освобожденный блок памяти будет первым выделенным. В этом вся суть контролируемого выделения, потому что есть возможность угадать адрес выделяемого блока.

Конечно, размер блока имеет значение. Поэтому необходимо предварительно расcчитать размер окна из освобожденных блоков. Если мы хотим контролируемо выделить блок размером в 0x315 байт при размере объектов для pool spraying в 0x20 байт, необходимо освободить 0x315 / 0x20 = (0x18 + 1) блоков. Думаю, это понятно.

Несколько заметок о том, как успешно использовать технику kernel pool spraying:
  • Если возможность выделения памяти из пулов посредством эксплуатируемого драйвера отсутствует, всегда есть возможность использовать объекты операционной системы в качестве объектов для pool spraying. Поскольку объекты ОС, как ни странно, хранятся в ядре ОС, память для них выделяется из различных пулов.
    • В неподкачиваемом пуле хранятся процессы, потоки, семафоры, мьютексы и т.д.
    • В подкачиваемом пуле хранятся объекты каталогов (directory object), ключи реестра, секции (так называемые сопоставления файлов или file mapping) и т.д.
    • В пуле сессии хранятся объекты подсистем GDI и USER: палитры (palette), контексты устройств (DC), кисти (brush) и т.д.
    Для того чтобы освободить память, занимаемую данными объектами, достаточно закрыть соответствующие им дескрипторы.
  • К тому времени, когда мы начнем заполнение пула объектами, он будет содержать некоторое количество страниц памяти, из которых можно выделять блоки. Однако данные страницы будут фрагментированными. Т. к. нам необходимо получить пространство с непрерывным заполнением подконтрольными блоками, первым делом нужно «заспамить» пул таким образом, чтобы на текущих страницах не осталось свободного места. Только в таком случае нам будут доступны свежие страницы, которые можно последовательно заполнить подконтрольными блоками. Короче говоря, необходимо создавать много объектов.
  • При вычислении необходимого размера окна учитывайте также размер заголовка блока, а также тот факт, что итоговый размер округляется до 8 и 16 байт на 32-битных и 64-битных системах соответственно.
  • Несмотря на то, что мы можем контролировать выделение блоков, предугадать их относительное положение довольно сложно. Однако при использовании объектов ОС для pool spraying, имеется возможность узнать адрес объекта по его дескриптору при помощи функции NtQuerySystemInformation() с параметром SystemExtendedHandleInformation. Предоставляемая ей информация необходима для повышения точности pool spraying.
  • Соблюдайте баланс при pool spraying. Не жадничайте при выделении объектов. Очевидно, что контролировать выделение блоков невозможно, если память в системе попросту закончилась.
  • Одним из трюков для повышения надежности эксплойтов, использующих пул ядра, является повышение приоритета потока, осуществляющего pool spraying и инициирующего уязвимость. Поскольку потоки по сути находятся в постоянном состоянии гонки за памятью пула, полезно повысить приоритет использования кучи путем повышения шанса быть исполненным чаще других потоков в системе. Это поможет технике быть более целостной. Также принимайте во внимание задержку между pool spraying и инициацией уязвимости: чем она меньше, тем больше шанс того, что мы попадем в нужный нам блок.


VMware CVE 2013-1406


В начале февраля были выпущены интересные рекомендации для обновления продуктов 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, которые, наоборот, можно предугадать:
  • B = 0: блок из неподкачиваемого пула
  • U = 1: подразумевается, что блок используется
  • Q = 0/1: блок может быть квотированным
  • S = 0: пул не является сессионным
  • T = 0: блок не является отслеживаемым по умолчанию
  • Неиспользуемые биты равны нулю

Таким образом, имеем следующие регионы памяти, действительные для Windows 7 и 8:
  • 0x04000000 – 0x06000000 для обычных блоков
  • 0x14000000 – 0x16000000 для квотированных блоков

Основываясь на приведенной выше информации, вы можете самостоятельно вычислить регионы памяти для Windows XP и ей подобных.

Как видно, эти регионы принадлежат пространству пользователя, поэтому мы можем заставить диспетчерскую функцию исполнить любой код, включая подконтрольный нам. Для этого сначала необходимо спроецировать указанные регионы памяти в процессе, а затем для каждых 0x10000 байт удовлетворить требования диспетчерской функции:
  1. По адресу [0x43 + 0x38] необходимо поместить DWORD = 0x00000001 для удовлетворения следующего условия:
    .text:0001B2E1 010                 lea     edx, [eax+38h]
    .text:0001B2E4 010                 lock xadd [edx], ecx
    .text:0001B2E8 010                 cmp     ecx, 1
    
  2. По адресу [0x43 + 0xAC] необходимо поместить указатель на шелл-код.
  3. По адресу [0x43 + 0x100] нужно поместить указатель на подставной объект, который будет разыменован функцией ObfDereferenceObject(). Учтите, что счетчик ссылок хранится в заголовке с отрицательным смещением по отношению к объекту, поэтому убедитесь в том, что код в функции ObfDereferenceObject() не попадет на неспроецированный регион. Также укажите подходящее значение счетчика ссылок, поскольку, например, при достижении счетчиком ссылок нуля, ObfDereferenceObject() попытается освободить память функциями, совершенно не пригодными для памяти режима пользователя.

Примите во внимание тот факт, что для разных продуктов VMware значения смещений могут быть разными.

Все правильно сделано!

Повышение стабильности эксплойта


Несмотря на то, что мы выработали хорошую стратегию для эксплуатации данной уязвимости, она все еще остается ненадежной. Например, диспетчерская функция может попасть на свободный блок, поля которого предугадать невозможно. Несмотря на то, что заголовок такого блока будет интерпретирован как указатель (потому что он не равен нулю), результатом его обработки будет ошибка «синемий экран». Подобное случится также, когда диспетчерская функция попадет в неспроецированный участок памяти.

В данном случае на помощь приходит техника kernel pool spraying. В качестве объекта pool spraying я выбрал семафоры, поскольку они являются наиболее подходящими по размеру. В результате применения данной техники, стабильность эксплойта повысилась в разы.

Напомню, что в системе Windows 8 появилась поддержка такого механизма защиты, как SMEP, поэтому лень разработчика несколько усложняет разработку эксплойта. Написание базонезависимого кода с обходом SMEP остается упражнением для читателя.

Что касается х64-систем, есть проблема с тем, что размер указателя стал равен 8 байтам. Это значит, что старшее двойное слово (DWORD) указателя будет попадать на поле Pool Tag. А поскольку большинство драйверов и подсистем ядра используют ASCII-символы для таких меток, указатель попадает в пространство неканонических адресов и не может использоваться для эксплуатации. На момент написания статьи я ничего дельного по этому поводу не придумал.

Итог


Надеюсь, приведенная информация была полезной. Прошу прощения за то, что не смог уместить все необходимое в пару абзацев. Желаю успехов в исследованиях и эксплуатации во имя всецелого повышения уровня безопасности.

P.S. Напоминаю, что для устранения уязвимости нужно обновить не только основную, но и все гостевые системы!
P.P.S Если вы ощущаете некоторый дискомфорт от перевода некоторых терминов, будьте готовы мириться с ним в будущем, поскольку данный перевод рекомендован на языковом портале Microsoft.

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
Tags:
Hubs:
+38
Comments 4
Comments Comments 4

Articles

Information

Website
www.ptsecurity.com
Registered
Founded
2002
Employees
1,001–5,000 employees
Location
Россия