• XSS.stack #1 – первый литературный журнал от юзеров форума

Techniques Эксплуатация пула ядра на Windows 7

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Резюме.

В Windows 7 Microsoft представила safe unlinking, чтобы ответить на растущее число бюллетеней по безопасности, влияющих на ядро Windows. Перед удалением записи из двусвязного списка safe unlinking направлено на обнаружение повреждения памяти путем проверки указателей на соседние записи списка. Следовательно, злоумышленник не может легко использовать общие методы для использования переполнения пула или других уязвимостей, связанных с повреждением пула. Способности. В этой статье мы показываем, что, несмотря на введенные меры безопасности, Windows 7 по-прежнему уязвима для общих атак на пул ядер. В частности, мы показываем, что распределитель пула может при определенных условиях не в состоянии безопасно разъединить свободные записи списка, что позволяет злоумышленнику повредить произвольную память. Чтобы предотвратить представленные атаки, мы предлагаем способы дальнейшего усиления и повышения безопасности пула ядер.

1. Введение

Поскольку программные ошибки трудно полностью устранить из-за сложности современных вычислений, поставщики делают все возможное, чтобы изолировать и предотвратить использование уязвимостей безопасности. Такие средства защиты, как DEP и ASLR, были введены в современные операционные системы для решения ряда широко используемых методов эксплуатации. Однако, поскольку средства защиты от эксплойтов не устраняют основную причину уязвимостей системы безопасности, всегда будут сценарии крайних случаев, в которых они не работают. Например, только DEP легко обойти с помощью возвратно-ориентированного программирования (ROP) [15]. Кроме того, новые методы, использующие возможности мощных встроенных в приложения движков сценариев может полностью обходить DEP и ASLR [4].

Дополнительным подходом к предотвращению эксплойтов является изоляция привилегий. Налагая ограничения на пользователей и процессы с помощью встроенных механизмов безопасности операционной системы, злоумышленник не может легко получить доступ и управлять системными файлами и информацией реестра в скомпрометированной системе. С момента введения в Vista управления учетными записями пользователей (UAC) пользователи больше не запускают обычные приложения с правами администратора по умолчанию. Кроме того, современные браузеры [2] и программы чтения документов [13] [12] используют "изолированные" процессы рендеринга, чтобы уменьшить влияние уязвимостей в библиотеках парсинга и механизмах слоев. В свою очередь, это побудило злоумышленников (а также исследователей) сосредоточить свои усилия на атаках с повышением привилегий. Выполнение произвольного кода в кольце с наивысшими привилегиями подрывает безопасность операционной системы.

Уязвимости повышения привилегий в большинстве случаев вызваны ошибками в ядре операционной системы или сторонних драйверах. Многие недостатки связаны с обработкой динамически выделяемой памяти пула ядра. Пул ядра аналогичен куче пользовательского режима и в течение многих лет был подвержен типичным атакам write-4, злоупотребляющим операцией разъединения двусвязных списков [8] [16]. В ответ на растущее число уязвимостей ядра Microsoft представила safe unlinking в Windows 7 [3]. Safe unlinking гарантирует, что указатели на соседние фрагменты пула в двусвязных свободных списках проверяются до разъединения фрагмента.

Цель злоумышленника в использовании уязвимостей повреждения пула - в конечном итоге выполнить произвольный код в нулевом кольце. Это часто начинается с произвольной записи в память или n-байтового повреждения в выбранном месте. В этой статье мы показываем, что, несмотря на введенные меры безопасности, пул ядер в Windows 7 по-прежнему подвержен атакам. В свою очередь, эти атаки могут позволить злоумышленнику полностью скомпрометировать ядро операционной системы. Мы также показываем, что safe unlinking, предназначенное для устранения атак write-4, может при определенных условиях не достичь поставленных целей и позволить злоумышленнику повредить произвольную память. Чтобы предотвратить представленные атаки, мы окончательно предлагаем способы дальнейшего усиления и повышения безопасности пула ядер.

Оставшаяся часть статьи организована следующим образом. В Разделе 2 мы подробно рассмотрим внутреннюю структуру и изменения, внесенные в ядро Windows 7 (и Vista). В разделах 3 и 4 мы обсуждаем и демонстрируем практические атаки на пул ядра, влияющие на Windows 7. В Разделе 5 мы обсуждаем контрмеры и предлагаем способы усиления защиты пула ядра. Наконец, в разделе 6 мы приводим заключение статьи.

2. Внутреннее устройство пула ядра

В этом разделе мы подробно рассмотрим структуры управления пулом ядра и алгоритмы, участвующие в распределении и освобождении памяти пула. Понимание поведения пула ядра имеет жизненно важное значение для правильной оценки его безопасности и надежности. Для краткости мы предполагаем архитектуру x86 (32-битную). Однако большинство структур применимо к AMD64/x64 (64-бит). Заметные различия в пуле ядер между архитектурами x86 и x64 обсуждаются в Разделе 2.9.

2.1. Неоднородная архитектура памяти

Для каждой новой версии Windows диспетчер памяти улучшается, чтобы лучше поддерживать неоднородную архитектуру памяти (NUMA), архитектуру проектирования памяти, используемую в современных многопроцессорных системах. NUMA выделяет разные банки памяти для разных процессоров, позволяя обращаться к локальной памяти быстрее, а к удаленной памяти — медленнее. Процессоры и память сгруппированы вместе в более мелкие блоки, называемые узлами, которые определяются структурой KNODE в исполнительном ядре.

C:
typedef struct _KNODE
{
/*0x000*/ union _SLIST_HEADER PagedPoolSListHead;
/*0x008*/ union _SLIST_HEADER NonPagedPoolSListHead[3];
/*0x020*/ struct _GROUP_AFFINITY Affinity;
/*0x02C*/ ULONG32 ProximityId;
/*0x030*/ UINT16 NodeNumber;
/*0x032*/ UINT16 PrimaryNodeNumber;
/*0x034*/ UINT8 MaximumProcessors;
/*0x035*/ UINT8 Color;
/*0x036*/ struct _flags Flags;
/*0x037*/ UINT8 NodePad0;
/*0x038*/ ULONG32 Seed;
/*0x03C*/ ULONG32 MmShiftedColor;
/*0x040*/ ULONG32 FreeCount[2];
/*0x048*/ struct _CACHED_KSTACK_LIST CachedKernelStacks;
/*0x060*/ LONG32 ParkLock;
/*0x064*/ ULONG32 NodePad1;
/*0x068*/ UINT8 _PADDING0_[0x18];
} KNODE, *PKNODE;

В многоузловых системах (nt!KeNumberNodes > 1) диспетчер памяти всегда будет пытаться выделить из идеального узла. Таким образом, KNODE предоставляет информацию о том, где находится локальная память в цветовом поле. Это значение представляет собой индекс массива, используемый алгоритмами распределения и освобождения для связывания узлов с его предпочтительным пулом. Кроме того, KNODE определяет четыре односвязных альтернативных списка для каждого узла для свободных страниц пула (обсуждается в Разделе 2.6).

2.2. Пулы системной памяти

При инициализации системы диспетчер памяти создает пулы памяти динамического размера в соответствии с количеством системных узлов. Каждый пул определяется дескриптором пула (обсуждается в разделе 2.3), структурой управления, которая отслеживает использование пула и определяет свойства пула, такие как тип памяти. Есть два различных типа пула памяти: выгружаемая и невыгружаемая.

Память выгружаемого пула может быть выделена и доступна из любого контекста процесса, но только на уровне IRQL < DPC / dispatch. Количество используемых выгружаемых пулов задается параметром nt!ExpNumberOfPagedPools. В однопроцессорных системах определены четыре (4) дескриптора выгружаемого пула, которые обозначены индексами с 1 по 4 в массиве nt!ExpPagedPoolDescriptor. В многопроцессорных системах для каждого узла определяется один (1) дескриптор выгружаемого пула. В обоих случаях дополнительный дескриптор выгружаемого пула определяется для пулов прототипов/полных распределений страниц, что обозначается индексом 0 в nt!ExpPagedPoolDescriptor. Следовательно, в большинстве настольных систем определены пять (5) дескрипторов выгружаемого пула.

Память невыгружаемого пула гарантированно постоянно находится в физической памяти. Это требуется потокам, выполняющимся на уровне IRQL> = DPC / dispatch (например, обработчикам прерываний), поскольку ошибки страниц не могут быть устранены вовремя. Число невыгружаемых пулов, используемых в настоящее время, задается параметром nt!ExpNumberOfNonPagedPools.

В однопроцессорных системах первый индекс массива nt!PoolVector указывает на дескриптор невыгружаемого пула. В многопроцессорных системах каждый узел имеет свой собственный дескриптор невыгружаемого пула, индексированный массивом nt!ExpNonPagedPoolDescriptor.

Кроме того, память пула сеансов (используемая win32k) используется для выделения пространства сеанса и уникальна для каждого сеанса пользователя. В то время как невыгружаемая память сеанса использует дескриптор(ы) глобального невыгружаемого пула, память выгружаемого пула сеанса имеет свой собственный дескриптор пула, определенный в nt!MM SESSION SPACE. Чтобы получить дескриптор пула сеансов, Windows 7 анализирует связанную структуру nt!EPROCESS (текущего выполняемого потока) на наличие структуры пространства сеанса, а затем находит встроенный дескриптор выгружаемого пула.

2.3. Дескриптор пула

Подобно куче пользовательского режима, каждому пулу ядра требуется структура управления. Дескриптор пула отвечает за отслеживание количества выполняемых выделений, используемых страниц и другой информации об использовании пула. Это также помогает системе отслеживать повторно используемые фрагменты пула. Дескриптор пула определяется следующей структурой (nt!POOL DESCRIPTOR).

C:
typedef struct _POOL_DESCRIPTOR
{
/*0x000*/ enum _POOL_TYPE PoolType;
union {
/*0x004*/ struct _KGUARDED_MUTEX PagedLock;
/*0x004*/ ULONG32 NonPagedLock;
};
/*0x040*/ LONG32 RunningAllocs;
/*0x044*/ LONG32 RunningDeAllocs;
/*0x048*/ LONG32 TotalBigPages;
/*0x04C*/ LONG32 ThreadsProcessingDeferrals;
/*0x050*/ ULONG32 TotalBytes;
/*0x054*/ UINT8 _PADDING0_[0x2C];
/*0x080*/ ULONG32 PoolIndex;
/*0x084*/ UINT8 _PADDING1_[0x3C];
/*0x0C0*/ LONG32 TotalPages;
/*0x0C4*/ UINT8 _PADDING2_[0x3C];
/*0x100*/ VOID** PendingFrees;
/*0x104*/ LONG32 PendingFreeDepth;
/*0x108*/ UINT8 _PADDING3_[0x38];
/*0x140*/ struct _LIST_ENTRY ListHeads[512];
} POOL_DESCRIPTOR, *PPOOL_DESCRIPTOR;

Дескриптор пула содержит несколько важных списков, используемых диспетчером памяти. Список отложенного освобождения, на который указывает PendingFrees, представляет собой односвязный список фрагментов пула, ожидающих освобождения. Это подробно объясняется в разделе 2.8. ListHeads - это массив двусвязных списков свободных фрагментов пула одинакового размера. В отличие от списка отложенного освобождения фрагменты в списках ListHeads были освобождены и могут быть выделены диспетчером памяти в любое время. Мы обсудим ListHeads в следующем разделе.

2.4. Списки ListHeads (свободные списки)

Списки ListHeads или свободные списки упорядочены по размеру с 8-байтовой гранулярностью и используются для выделения до 4080 байт. Свободные фрагменты индексируются в массиве ListHeads по размеру блока, вычисляемому как запрошенное количество байтов, округленное до кратного 8 и деленное на 8, или BlockSize = (NumberOfBytes + 0xF) >> 3. Округление выполняется, чтобы зарезервировать место для заголовка пула, структуры, предшествующей всем фрагментам пула. Заголовок пула в x86 Windows определяется следующим образом.

C:
typedef struct _POOL_HEADER
{
union {
struct {
/*0x000*/ UINT16 PreviousSize : 9;
/*0x000*/ UINT16 PoolIndex : 7;
/*0x002*/ UINT16 BlockSize : 9;
/*0x002*/ UINT16 PoolType : 7;
};
/*0x000*/ ULONG32 Ulong1;
};
union {
/*0x004*/ ULONG32 PoolTag;
struct {
/*0x004*/ UINT16 AllocatorBackTraceIndex;
/*0x006*/ UINT16 PoolTagHash;
};
};
} POOL_HEADER, *PPOOL_HEADER;

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

PoolIndex предоставляет индекс в связанный массив дескрипторов пула, например nt!ExpPagedPoolDescriptor. Он используется свободным алгоритмом, чтобы убедиться, что фрагмент пула освобожден для правильного дескриптора пула ListHeads. В Разделе 3.4 мы показываем, как злоумышленник может повредить это значение, чтобы расширить повреждение заголовка пула (например, переполнение пула) до произвольного повреждения памяти.

Как следует из названия, PoolType определяет тип пула чанка. Однако он также указывает, занят или свободен блок. Если кусок свободен, PoolType устанавливается равным нулю. С другой стороны, если блок занят, PoolType устанавливается равным типу пула его дескриптора (значение в перечислении POOL TYPE, показанное ниже) ИЛИ с битовой маской используемого пула. Для этой битовой маски установлено значение 2 в Vista и более поздних версиях, а в XP/2003 - 4. Например для загруженного фрагмента выгружаемого пула в Vista и Windows 7 PoolType = PagedPool | 2 = 3.

C:
typedef enum _POOL_TYPE
{
NonPagedPool = 0 /*0x0*/,
PagedPool = 1 /*0x1*/,
NonPagedPoolMustSucceed = 2 /*0x2*/,
DontUseThisType = 3 /*0x3*/,
NonPagedPoolCacheAligned = 4 /*0x4*/,
PagedPoolCacheAligned = 5 /*0x5*/,
NonPagedPoolCacheAlignedMustS = 6 /*0x6*/,
MaxPoolType = 7 /*0x7*/,
NonPagedPoolSession = 32 /*0x20*/,
PagedPoolSession = 33 /*0x21*/,
NonPagedPoolMustSucceedSession = 34 /*0x22*/,
DontUseThisTypeSession = 35 /*0x23*/,
NonPagedPoolCacheAlignedSession = 36 /*0x24*/,
PagedPoolCacheAlignedSession = 37 /*0x25*/,
NonPagedPoolCacheAlignedMustSSession = 38 /*0x26*/
} POOL_TYPE, *PPOOL_TYPE;

Если фрагмент пула свободен и находится в списке ListHeads, за его заголовком сразу следует структура LIST ENTRY. По этой причине куски одного размера блока (8 байтов) не поддерживаются ListHeads, поскольку они достаточно невелики , чтобы хранить структуру.

C:
typedef struct _LIST_ENTRY
{
/*0x000*/ struct _LIST_ENTRY* Flink;
/*0x004*/ struct _LIST_ENTRY* Blink;
} LIST_ENTRY, *PLIST_ENTRY;

Структура LIST ENTRY используется для объединения фрагментов пула в двусвязных списках. Исторически он был целью использования уязвимостей, связанных с повреждением памяти как в куче пользовательского режима [5], так и в пуле ядра [8] [16], в первую очередь из-за хорошо известных методов эксплуатации write-4.

2.5. Списки Lookaside

Ядро использует односвязные альтернативные списки (LIFO) для более быстрого выделения и освобождения небольших фрагментов пула. Они предназначены для работы в коде с высокой степенью параллелизма и использования атомарных инструкций сравнения и обмена при добавлении и удалении записей. Чтобы лучше использовать кэширование ЦП, lookaside списки определяются для каждого процессора в блоке управления процессором (KPRCB). Структура KPRCB содержит резервные списки как для страничного (PPPagedLookasideList), так и для невыгружаемые (PPNPagedLookasideList) выделения, а также специальные специальные списки дополнительных (PPLookasideList) для часто запрашиваемых фиксированных распределений (например, для пакетов запросов ввода-вывода и списков дескрипторов памяти).


C:
typedef struct _KPRCB
{
...
/*0x5A0*/ struct _PP_LOOKASIDE_LIST PPLookasideList[16];
/*0x620*/ struct _GENERAL_LOOKASIDE_POOL PPNPagedLookasideList[32];
/*0xF20*/ struct _GENERAL_LOOKASIDE_POOL PPPagedLookasideList[32];
...
} KPRCB, *PKPRCB;

Для страничных и невыгружаемых списков максимальный размер блока составляет 0x20. Следовательно, существует 32 уникальных lookaside списка для каждого типа. Каждый альтернативный список определяется структурой GENERAL LOOKASIDE POO, показанной ниже.

C:
typedef struct _GENERAL_LOOKASIDE_POOL
{
union
{
/*0x000*/ union _SLIST_HEADER ListHead;
/*0x000*/ struct _SINGLE_LIST_ENTRY SingleListHead;
};
/*0x008*/ UINT16 Depth;
/*0x00A*/ UINT16 MaximumDepth;
/*0x00C*/ ULONG32 TotalAllocates;
union
{
/*0x010*/ ULONG32 AllocateMisses;
/*0x010*/ ULONG32 AllocateHits;
};
/*0x014*/ ULONG32 TotalFrees;
union
{
/*0x018*/ ULONG32 FreeMisses;
/*0x018*/ ULONG32 FreeHits;
};
/*0x01C*/ enum _POOL_TYPE Type;
/*0x020*/ ULONG32 Tag;
/*0x024*/ ULONG32 Size;
union
{
/*0x028*/ PVOID AllocateEx;
/*0x028*/ PVOID Allocate;
};
union

/*0x02C*/ PVOID FreeEx;
/*0x02C*/ PVOID Free;
};
/*0x030*/ struct _LIST_ENTRY ListEntry;
/*0x038*/ ULONG32 LastTotalAllocates;
union
{
/*0x03C*/ ULONG32 LastAllocateMisses;
/*0x03C*/ ULONG32 LastAllocateHits;
};
/*0x040*/ ULONG32 Future[2];
} GENERAL_LOOKASIDE_POOL, *PGENERAL_LOOKASIDE_POOL;

В этой структуре SingleListHead.Next указывает на первый свободный фрагмент пула в односвязном lookaside списке. Размер lookaside списка ограничен значением глубины, которое периодически корректируется менеджером набора балансов в соответствии с количеством совпадений и промахов в lookaside списке. Следовательно, часто используемый lookaside список будет иметь большее значение глубины, чем редко используемый список. Начальная глубина равна 4 (nt!ExMinimumLookasideDepth), максимальная - MaximumDepth (256). Если lookaside список заполнен, фрагмент пула освобождается в соответствующий список ListHeads.

Lookaside списки также определены для пула сеансов. Выделяемые пулы сеансов с разбивкой по страницам используют отдельные альтернативные списки (nt! ExpSessionPoolLookaside), определенные в пространстве сеанса. Максимальный размер блока для вспомогательных списков на сеанс составляет 0x19, как установлено параметром nt! ExpSessionPoolSmallLists. Lookaside списки пула сеансов используют структуру GENERAL LOOKASIDE, идентичную GENERAL LOOKASIDE POOL, но с дополнительным заполнением. Для выделения невыгружаемого пула сеансов используются ранее обсуждавшиеся невыгружаемые lookaside списки для каждого процессора.

Lookaside списки для фрагментов пула отключены, если установлен флаг разделения горячих/ холодных страниц (nt! ExpPoolFlags & 0x100). Флаг устанавливается во время загрузки системы, чтобы увеличить скорость и уменьшить объем памяти. Таймер (установлен в nt!ExpBootFinishedTimer) отключает разделение горячих/ холодных страниц через 2 минуты после загрузки.

2.6. Распределение большого пула

Дескриптор пула ListHeads поддерживает фрагменты меньше страницы. Выделение пула размером более 4080 байт (требуется страница или больше) обрабатывается nt!ExpAllocateBigPool. В свою очередь, эта функция вызывает nt!MiAllocatePoolPages, распределитель страниц пула, который округляет запрошенный размер до ближайшего размера страницы. Фрагмент блока размером 1 и предыдущим размером 0 помещается сразу после выделения большого пула, так что распределитель пула может использовать оставшийся фрагмент страницы. Затем лишние байты возвращаются в конец соответствующего списка дескрипторов пула ListHeads.

Вспомните из Раздела 2.1, что с каждым узлом (определенным KNODE) связано 4 односвязных списка альтернативных ссылок. Эти списки используются распределителем страниц пула для быстрого обслуживания запросов на небольшое количество страниц.Для выгружаемой памяти KNODE определяет один резервный список (PagedPoolSListHead) для выделения отдельных страниц. Для невыгружаемых выделений определены альтернативные списки (NonPagedPoolSListHead [3]) для количества страниц 1, 2 и 3. Размер резервных списков страниц пула определяется количеством физических страниц, присутствующих в системе.

Если lookaside списки использовать нельзя, для получения запрошенных страниц пула используется битовая карта распределения. Битовая карта (определенная в RTL BITMAP) представляет собой массив битов, который указывает, какие страницы памяти используются, и создается для каждого основного типа пула. Ищется первый индекс, содержащий запрошенное количество неиспользуемых страниц. Для выгружаемого пула битовая карта определяется в структуре MM PAGED POOL INFO, на которую указывает nt! MmPagedPoolInfo. Для невыгружаемого пула на битовую карту указывает nt!MiNonPagedPoolBitMap. Для пула сеансов битовая карта определяется в структуре MM SESSION SPACE.

Для большинства больших распределений пула nt!ExAllocatePoolWithTag запросит дополнительные 4 байта (8 на x64) для хранения размера выделения в конце тела пула. Это значение впоследствии проверяется при освобождении выделения (в ExFreePoolWithTag) для выявления возможных переполнений пула.

2.7 Алгоритм распределения

Для выделения памяти пула модули ядра и сторонние драйверы вызывают ExAllocatePoolWithTag (или любую из его функций-оберток), экспортируемую исполнительным ядром. Эта функция сначала попытается использовать lookaside списки, а затем списки ListHeads, и, если не удалось вернуть фрагмент пула, запросит страницу у распределителя страниц пула. Следующий псевдокод в общих чертах описывает его реализацию.

C:
PVOID
ExAllocatePoolWithTag( POOL_TYPE PoolType,
SIZE_T NumberOfBytes,
ULONG Tag)
// call pool page allocator if size is above 4080 bytes
if (NumberOfBytes > 0xff0) {
// call nt!ExpAllocateBigPool
}
// attempt to use lookaside lists
if (PoolType & PagedPool) {
if (PoolType & SessionPool && BlockSize <= 0x19) {
// try the session paged lookaside list
// return on success
}
else if (BlockSize <= 0x20) {
// try the per-processor paged lookaside list

}
// lock paged pool descriptor (round robin or local node)
}
else { // NonPagedPool
if (BlockSize <= 0x20) {
// try the per-processor non-paged lookaside list
// return on success
}
// lock non-paged pool descriptor (local node)
}
// attempt to use listheads lists
for (n = BlockSize-1; n < 512; n++) {
if (ListHeads[n].Flink == &ListHeads[n]) { // empty
continue; // try next block size
}
// safe unlink ListHeads[n].Flink
// split if larger than needed
// return chunk
}
// no chunk found, call nt!MiAllocatePoolPages
// split page and return chunk

Если из списка ListHeads[n] возвращается блок, превышающий запрошенный размер, блок разделяется. Чтобы уменьшить фрагментацию, часть слишком большого фрагмента, возвращаемого распределителем, зависит от его относительной позиции на странице. Если фрагмент выровнен по странице, запрошенный размер выделяется с начала фрагмента. Если блок не выровнен по странице, запрошенный размер выделяется с обратной стороны блока. В любом случае оставшийся (неиспользованный) фрагмент разбитого блока помещается в конец соответствующего списка ListHeads.

2.8. Алгоритм освобождения

Алгоритм освобождения, реализованный ExFreePoolWithTag, проверяет заголовок пула блока, который нужно освободить, и помещает его в соответствующий список. Чтобы уменьшить фрагментацию, он также пытается объединить смежные свободные фрагменты. Следующий псевдокод показывает, как работает алгоритм.

C:
VOID
ExFreePoolWithTag( PVOID Entry,
ULONG Tag)
if (PAGE_ALIGNED(Entry)) {
// call nt!MiFreePoolPages
// return on success
}
if (Entry->BlockSize != NextEntry->PreviousSize)
BugCheckEx(BAD_POOL_HEADER);
if (Entry->PoolType & SessionPagedPool && Entry->BlockSize <= 0x19) {
// put in session pool lookaside list
// return on success
}
else if (Entry->BlockSize <= 0x20) {
if (Entry->PoolType & PagedPool) {
// put in per-processor paged lookaside list
// return on success
}
else { // NonPagedPool
// put in per-processor non-paged lookaside list
// return on success
}
}
if (ExpPoolFlags & DELAY_FREE) { // 0x200
if (PendingFreeDepth >= 0x20) {
// call nt!ExDeferredFreePool
}
// add Entry to PendingFrees list
}
else {
if (IS_FREE(NextEntry) && !PAGE_ALIGNED(NextEntry)) {
// safe unlink next entry
// merge next with current chunk
}
if (IS_FREE(PreviousEntry)) {
// safe unlink previous entry
// merge previous with current chunk
}
if (IS_FULL_PAGE(Entry))
// call nt!MiFreePoolPages
else {
// insert Entry to ListHeads[BlockSize - 1]
}
}

Флаг пула DELAY FREE (nt!ExpPoolFlags & 0x200) обеспечивает оптимизацию производительности, которая освобождает несколько выделений пула одновременно, чтобы амортизировать получение и освобождение пула. Этот механизм был кратко упомянут в [11] и включен в Windows XP SP2 или выше, если количество доступных физических страниц (nt!MmNumberOfPhysicalPages) больше или равно 0x1fc00. При использовании каждый новый вызов ExFreePoolWithTag добавляет фрагмент, который нужно освободить, в список PendingFrees, специфичный для каждого дескриптора пула. Если список содержит 32 или более фрагментов (определяется PendingFreeDepth), он обрабатывается при вызове ExDeferredFreePool. Эта функция выполняет итерацию по каждой записи и освобождает ее до соответствующего списка ListHeads, как показано в следующем псевдокоде

C:
VOID
ExDeferredFreePool( PPOOL_DESCRIPTOR PoolDesc,
BOOLEAN bMultipleThreads)
for each (Entry in PendingFrees) {
if (IS_FREE(NextEntry) && !PAGE_ALIGNED(NextEntry)) {
// safe unlink next entry
// merge next with current chunk
}
if (IS_FREE(PreviousEntry)) {
// safe unlink previous entry
// merge previous with current chunk
}
if (IS_FULL_PAGE(Entry))
// add to full page list
else {
// insert Entry to ListHeads[BlockSize - 1]
}
}
for each (page in full page list) {
// call nt!MiFreePoolPages
}

Свободные для lookaside и дескриптора пула ListHeads всегда помещается в начало соответствующего списка. Исключением из этого правила являются оставшиеся фрагменты разделенных блоков, которые помещаются в конец списка. Блоки разделяются, когда диспетчер памяти возвращает фрагменты, размер которых превышает запрошенный (как объяснено в разделе 2.7), например, полные страницы, разделенные в ExpBigPoolAllocation, и записи ListHeads, разделенные в ExAllocatePoolWithTag. Чтобы использовать кэш ЦП как можно чаще, выделения всегда делаются из самых последних использованных фрагментов, начиная с начала соответствующего списка.

2.9. Изменения пула ядра AMD64/x64

Несмотря на поддержку большего физического адресного пространства, 64-разрядная Windows не вносит существенных изменений в пул ядра. Однако, чтобы приспособиться к изменению ширины указателя, степень детализации размера блока увеличена до 16 байтов, вычисляемая как BlockSize = (NumberOfBytes + 0x1F) >> 4. Чтобы отразить это изменение, заголовок пула обновляется соответствующим образом.

C:
typedef struct _POOL_HEADER
{
union
{
struct
{
/*0x000*/ ULONG32 PreviousSize : 8;
/*0x000*/ ULONG32 PoolIndex : 8;
/*0x000*/ ULONG32 BlockSize : 8;
/*0x000*/ ULONG32 PoolType : 8;
};
/*0x000*/ ULONG32 Ulong1;
};
/*0x004*/ ULONG32 PoolTag;
union
{
/*0x008*/ struct _EPROCESS* ProcessBilled;
struct
{
/*0x008*/ UINT16 AllocatorBackTraceIndex;
/*0x00A*/ UINT16 PoolTagHash;
/*0x00C*/ UINT8 _PADDING0_[0x4];
};
};
} POOL_HEADER, *PPOOL_HEADER;

Из-за изменения гранулярности размера блока значения PreviousSize и BlockSize уменьшаются до восьми бит. Таким образом, дескриптор пула ListHeads содержит 256 двусвязных списков, а не 512, как на x86. Это также позволяет назначить дополнительный бит для PoolIndex, следовательно, 256 узлов (дескрипторы пула) могут поддерживаться на x64, более 128 на x86. Кроме того, заголовок пула расширен до 16 байтов и включает указатель ProcessBilled, используемый в управлении квотами. для определения процесса, взимаемого за выделение. На x86 этот указатель хранится в последних четырех байтах тела пула. Мы обсуждаем атаки с использованием указателя процесса квоты в разделе 3.5.

3. Атаки на пул ядра

В этом разделе мы обсудим несколько практических атак на пул ядер Windows 7. Во-первых, в разделе 3.1 мы показываем атаку на структуру LIST ENTRY при (не) безопасном разыменовании блоков пула ListHeads. В разделах 3.2 и 3.3 мы покажем атаки на односвязные списки и отложенные свободные списки соответственно. В разделе 3.4 мы представляем атаку на заголовок пула освобождаемых выделенных фрагментов, и, наконец, в разделе 3.5 мы покажем атаку на выделение пула с начислением квот.

3.1. Перезапись ListEntry Flink

Чтобы предотвратить обычную эксплуатацию переполнений пула ядра, Windows 7 выполняет безопасное отключение для проверки указателей LIST ENTRY блоков пула в списках ListHeads. Однако при выделении фрагмента пула из ListHeads [n] (для данного размера блока) алгоритм проверяет структуру LIST ENTRY для ListHeads[n], а не структуру фактического фрагмента, который не связывается. Следовательно, перезапись прямой ссылки в свободном фрагменте может привести к тому, что адрес ListHeads [n] для записи на адрес, контролируется злоумышленником (рисунок 1).


Screenshot_8.png

Эта атака требует, чтобы в целевом списке ListHeads[n] присутствовали как минимум два свободных фрагмента. В противном случае ListHeads[n] .Blink проверит прямую ссылку unlinked фрагмента. В примере 1 прямая ссылка фрагмента пула в списке ListHeads была повреждена адресом, выбранным злоумышленником. В свою очередь, когда этот фрагмент выделяется в ExAllocatePoolWithTag, алгоритм пытается записать адрес ListHeads[n] (esi) по обратной ссылке структуры LIST ENTRY по адресу, контролируемому злоумышленником (eax).


Screenshot_8.png

Хотя значение esi не может быть легко определено из контекста пользовательского режима, иногда можно вывести его значение. Например, если определен только один невыгружаемый пул (как обсуждается в 2.2), esi будет указывать на фиксированное местоположение (nt! NonPagedPoolDescriptor) в сегменте данных ntoskrnl. Если дескриптор пула был выделен из памяти, предположение о его местонахождении можно сделать из определенного диапазона памяти пула. Таким образом, злоумышленник может перезаписать важные глобальные переменные [14] или указатели объектов ядра [6] (например, посредством частичной перезаписи указателя), чтобы добиться выполнения произвольного кода.

Злоумышленник также может расширить произвольную запись до полностью контролируемого распределения ядра, используя указатель пользовательского режима в перезаписи. Это следует из того факта, что ListHeads[n].Flink обновляется и указывает на следующий свободный фрагмент ( указатель, управляемый атакующим) после отключения поврежденного фрагмента. Поскольку обратная ссылка на адрес, предоставленный злоумышленником, была обновлена, чтобы указывать на ListHeads[n], у распределителя пула нет проблем с безопасным отключением указателя пользовательского режима от свободных списков.

3.2. Перезапись Lookaside Next Pointer

Lookaside списки разработаны так, чтобы быть быстрыми и легкими, поэтому в них не используется такая же проверка согласованности, как в двусвязных списках ListHeads. Будучи одинарной связью, каждая запись в дополнительном списке содержит указатель на следующую запись. Поскольку нет никаких проверок, подтверждающих действительность этих указателей, злоумышленник может, используя уязвимость повреждения пула, принудить распределитель пула вернуть произвольный адрес при получении следующего свободного lookaside фрагмента. В свою очередь, это может позволить злоумышленнику повредить произвольную память ядра.

Screenshot_10.png

Как обсуждалось в разделе 2.5, диспетчер памяти использует lookaside списки как для блоков пула, так и для страниц пула. Для lookaside фрагментов пула указатель Next следует непосредственно за 8-байтовым заголовком пула (POOL HEADER). Таким образом, для перезаписи указателя Next требуется не более 12 байтов на x86. Чтобы фрагмент пула был освобожден для lookaside списка, должно выполняться следующее:

- Размер блока <= 0x20 для (выгружаемых/невыгружаемых) блоков пула
- BlockSize <= 0x19 для фрагментов выгружаемого пула сеансов
- Lookaside список для целевого BlockSize не полон
- Разделение горячих/холодных страниц не используется (ExpPoolFlags & 0x100)

Для того, чтобы преобразовать lookaside Next указатель Next в n-байтовую произвольную перезапись памяти, необходимо выделить размер целевого блока до тех пор, пока не будет возвращен поврежденный указатель (рисунок 2). Кроме того, содержимое выделенного фрагмента необходимо до некоторой степени контролировать, чтобы повлиять на данные, используемые для перезаписи. Для выделения выгружаемого пула собственные API-интерфейсы, которые выделяют строки Unicode, такие как NtCreateSymbolicLinkObject, предоставляют удобный способ заполнения блока любого размера практически любой комбинацией байтов. Такие API-интерфейсы также можно использовать для дефрагментации и управления компоновкой памяти пула для управления эксплуатируемыми примитивами, такими как неинициализированные указатели и двойные освобождения.

Screenshot_11.png

В отличие от фрагментов lookaside пула, на страницах lookaside пула (рисунок 3) указатель Next хранится с нулевым значением, так как с ними не связаны заголовки пула. Выделенная страница пула освобождается для lookaside списка, если выполняется следующее:

- NumberOfPages = 1 для страниц выгружаемого пула
- NumberOfPages <= 3 для страниц невыгружаемого пула
- Дополнительный список для количества целевых страниц не полон

Страницы пула возвращаются nt!MiAllocatePoolPages всякий раз, когда диспетчер памяти должен запросить дополнительную память пула, недоступную из ListHeads или резервных списков. Поскольку это обычно выполняется многими параллельными системными потоками, очевидно, что легче сказать, чем сделать, манипулировать макетом пула ядра, чтобы разместить переполнение рядом со страницей свободного пула в дополнительном списке. С другой стороны, при работе с фрагментами lookaside пула можно использовать редко запрашиваемые значения размера блока, чтобы получить более детальный контроль над структурой памяти. Это можно сделать, изучив значение TotalAllocates в дополнительных структурах управления.

3.3 Перезапись PendingFrees Next Pointer

Вспомните из Раздела 2.8, что записи пула, ожидающие освобождения, хранятся в односвязных списках PendingFrees. Поскольку при обходе этих списков проверки не выполняются, злоумышленник может воспользоваться уязвимостью, связанной с повреждением пула, чтобы повредить указатель Next записи списка PendingFrees. В свою очередь, это позволит злоумышленнику освободить произвольный адрес для выбранного списка дескрипторов пула ListHeads и, возможно, управлять памятью для последующего выделения пула (рисунок 4).

Screenshot_12.png

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

3.4 Перезапись PoolIndex

Если для данного типа пула определено более одного дескриптора пула, PoolIndex блока пула обозначает индекс в связанном массиве дескрипторов пула. Следовательно, при работе с записями ListHeads фрагмент пула всегда освобождается в соответствующий дескриптор пула. Однако из-за недостаточной проверки неверно сформированный индекс PoolIndex может вызвать разыменование массива вне границ и впоследствии позволить злоумышленнику перезаписать произвольную память ядра.


Screenshot_13.png


Для выгружаемых пулов PoolIndex всегда обозначает индекс в массиве дескрипторов выгружаемого пула (nt! ExpPagedPoolDescriptor). В проверенных сборках значение индекса проверяется при сравнении с nt!ExpNumberOfPagedPools, чтобы предотвратить любой доступ к массиву за пределы. Однако в чистых (retail) сборках индекс не проверяется. Для невыгружаемых пулов PoolIndex обозначает индекс в nt!ExpNonPagedPoolDescriptor, только если в системе с поддержкой NUMA присутствует несколько узлов. Опять же, в чистых сборках PoolIndex не проверяется.

Неправильно сформированный PoolIndex (требующий только 2-байтового переполнения пула) может привести к тому, что выделенный фрагмент пула будет освобожден для дескриптора пула с нулевым указателем (рисунок 5). Сопоставляя виртуальную нулевую страницу, злоумышленник может полностью контролировать дескриптор пула и его записи ListHeads. В свою очередь, это может позволить злоумышленнику записать адрес блока пула на произвольный адрес при подключении к списку. Это связано с тем, что Blink фрагмента, находящегося в данный момент впереди, обновляется адресом освобожденного фрагмента, например ListHeads[n].Flink-> Blink = FreedChunk. Следует отметить, что, поскольку освобожденный фрагмент не возвращается ни в какой реальный дескриптор пула, нет необходимости очищать (удалять устаревшие записи и так далее) пул ядра.

Screenshot_14.png


Если включена функция отложенного освобождения пула (как описано в разделе 2.8), аналогичный эффект может быть достигнут путем создания поддельного списка PendingFrees (рисунок 6). В этом случае первая запись в списке будет указывать на адрес, контролируемый злоумышленником. Кроме того, значение PendingFreeDepth в дескрипторе пула будет больше или равно 0x20, чтобы запустить обработку списка PendingFrees.

Пример 2 демонстрирует, как перезапись PoolIndex может потенциально привести к записи управляемого пользователем адреса страницы (eax) на произвольный адрес назначения (esi). Чтобы выполнить произвольный код, злоумышленник может использовать этот метод для перезаписи редко используемого указателя функции ядра адресом страницы пользовательского режима и инициирования его выполнения из того же контекста процесса.

Screenshot_15.png

Атака перезаписи PoolIndex может быть применена к любому типу пула, если также перезаписан PoolType блока (например, установив его в PagedPool). Поскольку для этого требуется перезапись размера блока, злоумышленник должен либо знать размер более собственного фрагмента или создайте поддельный граничный фрагмент, встроенный в него. Это необходимо, так как FreedBlock->BlockSize = NextBlock->PreviousSize должен удерживаться, что проверяется свободным алгоритмом. Кроме того, размер блока должен быть больше 0x20, чтобы избежать lookaside списков (которые игнорируют PoolIndex). Однако обратите внимание, что фрагменты встроенного пула могут потенциально повредить важные поля или указатели в данных фрагментов.

3.5. Перезапись указателя процесса квоты

Поскольку для процессов может взиматься плата за выделенную память пула, выделения пула должны предоставлять достаточную информацию для алгоритмов пула, чтобы вернуть начисленную квоту нужному процессу. По этой причине фрагменты пула могут дополнительно хранить указатель на связанный объект процесса. В x64 указатель объекта процесса хранится в последних восьми байтах заголовка пула, как описано в разделе 2.9, тогда как на x86 указатель добавляется к телу пула. Перезапись этого указателя (рис. 7) в уязвимости с повреждением пула может позволить злоумышленнику освободить используемый объект процесса или повредить произвольную память при возврате начисленной квоты.

Screenshot_16.png



Каждый раз, когда выделение пула освобождается, алгоритм освобождения проверяет тип пула на наличие бита квоты (0x8) перед фактическим возвратом памяти в соответствующий список свободных или резервных копий. Если бит установлен, он попытается вернуть начисленную квоту, вызвав nt!PspReturnQuota, а затем разыменует связанный объект процесса. Таким образом, перезапись указателя объекта процесса может позволить злоумышленнику уменьшить счетчик ссылок (указателя) произвольного объекта процесса. Несогласованность счетчика ссылок может впоследствии привести к UAF, если соблюдаются правильные условия (например, счетчик дескрипторов равен нулю, когда счетчик ссылок понижается до нуля).

Screenshot_17.png



Если указатель объекта процесса заменен указателем на память пользовательского режима, злоумышленник может создать поддельный объект EPROCESS для управления указателем на структуру EPROCESS QUOTA BLOCK (рисунок 8), в которой содержится информация о квотах. В свободном состоянии значение, указывающее квоту, используемую в этой структуре, обновляется путем вычитания размера выделения. Таким образом, злоумышленник может уменьшить значение произвольного адреса после возврата заполненной квоты. Злоумышленник может провести обе атаки на любое выделение пула, если установлены бит квоты и указатель объекта процесса квоты.

4. Пример: CVE-2010-1893

В этом разделе мы применяем метод перезаписи PoolIndex, описанный в разделе 3.4, для использования переполнения пула в модуле ядра Windows TCP/IP (CVE-2010-1893), описанном в MS10-058 [10]. Описанная атака действует исключительно на структуры управления пулом, следовательно, не полагается на данные, хранящиеся в любом из задействованных блоков пула.

4.1. Об уязвимости

Модуль ядра Windows TCP/IP, или tcpip.sys, реализует несколько функций для управления режимом сокета. Эти функции по большей части доступны из пользовательского режима путем вызова WSAIoctl и предоставления управляющего кода ввода-вывода для желаемой операции. При указании SIO ADDRESS LIST SORT ioctl, tcpip.sys IppSortDestinationAddresses() для сортировки списка адресов назначения IPv6 и IPv4 для определения наилучшего доступного адреса для установления соединения. Эта функция была признана уязвимой [17] к переполнению целых чисел в Windows 7/Windows 2008 R2 и Windows Vista/ Windows 2008, поскольку она не использовала последовательно безопасные целочисленные функции. Следовательно, указание большого количества адресов для списка адресов может привести к недостаточному распределению буфера, что приведет к переполнению пула в IppFlattenAddressList().

Уязвимость позволяет злоумышленнику повредить память соседнего пула, используя любую комбинацию байтов в записях размера SOCKADDR IN6 (0x1c байтов). Копирование в память останавливается в точке, где член семейства sin6 структуры больше не равен 0x17 (AF INET6). Однако, поскольку эта проверка выполняется после того, как было выполнено копирование, злоумышленнику не требуется устанавливать это поле при переполнении только одной записи адреса.

4.2. Подготовка памяти пула

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

Screenshot_18.png


В примере 3 пул ядра был заполнен объектами IoCompletionReserve (с помощью NtAllocateReserveObject [7]), для которых впоследствии было освобождено каждое второе выделение. Таким образом, когда в IppSortDestinationAddresses() выделяется адресный буфер сортировки, соответствующий размеру (три записи SOCKADDR IN6) освобожденных фрагментов, он попадет в одну из созданных дыр.

4.3. Использование перезаписи PoolIndex

Чтобы использовать атаку PoolIndex, злоумышленник должен перекрыть заголовок пула следующего фрагмента пула и установить для его PoolType значение PagedPool | InUse (3), а для его PoolIndex значение индекса за пределами диапазона (например 5 в большинстве однопроцессорных систем), как показано в Примере 4. Это приведет к обращению к дескриптору пула с нулевым указателем после освобождения поврежденного фрагмента пула.

Screenshot_19.png


В функции листинга 1 мы инициализируем необходимые значения дескриптора пула для проведения атаки. В этой функции PoolAddress указывает на управляемый пользователем фрагмент пула (например, выделенный на странице пользовательского режима), а WriteAddress устанавливает адрес, по которому записан указатель PoolAddress.

C:
VOID
InitPoolDescriptor( PPOOL_DESCRIPTOR PoolDescriptor ,
PPOOL_HEADER PoolAddress ,
PVOID WriteAddress )
{
ULONG i;
RtlZeroMemory(PoolDescriptor ,sizeof(POOL_DESCRIPTOR));
PoolDescriptor ->PoolType = PagedPool;
PoolDescriptor ->PagedLock.Count = 1;

// create pending frees list
PoolDescriptor ->PendingFreeDepth = 0x20;
PoolDescriptor ->PendingFrees = (VOID **)(PoolAddress +1);
// create ListHeads entries with target address
for (i=0; i<512; i++) {
PoolDescriptor ->ListHeads[i].Flink = (PCHAR)
WriteAddress - sizeof(PVOID);
PoolDescriptor ->ListHeads[i].Blink = (PCHAR)
WriteAddress - sizeof(PVOID);
}
}

Мы предполагаем, что будет использоваться список ожидающих освобождения, поскольку в большинстве систем ОЗУ 512 МБ или больше. Таким образом, адрес блока пула, управляемого пользователем, в конечном итоге будет записан по адресу, указанному WriteAddress в процессе связывания. Это может быть использовано для перезаписи указателя функции ядра, что упрощает эксплуатацию. Если список ожидающих освобождений не использовался, адрес освобожденного фрагмента пула ядра (адрес ядра) в конечном итоге был бы записан по указанному адресу, и в этом случае для выполнения произвольного кода потребовались бы другие средства, такие как частичная перезапись указателя.

Последней задачей перед запуском переполнения является инициализация памяти, на которую указывает PoolAddress, чтобы фрагмент фальшивого пула (в ожидающем списке освобождений) должным образом возвращался в созданные списки ListHeads
В функции листинга 2 мы создаем макет из двух граничащих блоков пула, для которых PoolIndex снова ссылается на индекс вне границ в связанный массив дескрипторов пула. Кроме того, BlockSize должен быть достаточно большим, чтобы избежать использования дополнительных списков.

C:
#define BASE_POOL_TYPE_MASK 1
#define POOL_IN_USE_MASK 2
#define BLOCK_SHIFT 3 // 4 on x64
VOID
InitPoolChunks(PVOID PoolAddress , USHORT BlockSize)
{
POOL_HEADER * pool;
SLIST_ENTRY * entry;
// chunk to be freed
pool = (POOL_HEADER *) PoolAddress;
pool ->PreviousSize = 0;
pool ->PoolIndex = 5; // out -of-bounds pool index
pool ->BlockSize = BlockSize;
pool ->PoolType = POOL_IN_USE_MASK | (PagedPool &
BASE_POOL_TYPE_MASK);
// last chunk on the pending frees list
entry = (SLIST_ENTRY *) ((PCHAR)PoolAddress + sizeof(
POOL_HEADER)));
entry ->Next = NULL;
// bordering chunk (busy to avoid coalescing)
pool = (POOL_HEADER *) ((PCHAR)PoolAddress + (BlockSize
<< BLOCK_SHIFT));
pool ->PreviousSize = BlockSize;
pool ->PoolIndex = 0;
pool ->BlockSize = BlockSize;
pool ->PoolType = POOL_IN_USE_MASK | (PagedPool &
BASE_POOL_TYPE_MASK);
}

5. Укрепление пула ядра

Хотя введение safe unlinking является шагом в правильном направлении, предотвращению эксплуатации пула ядра еще предстоит пройти долгий путь с точки зрения соответствия устойчивости кучи пользовательского пространства. В этом разделе мы предлагаем способы противодействия атакам, обсуждаемым в разделе 3, а также предложения по дальнейшему улучшению пула ядра.

5.1. Перезапись ListEntry Flink

В пуле ядра было введено safe unlinking для предотвращения общего использования переполнения пула. Однако, как показано в Разделе 3.1, недостаточная проверка может позволить злоумышленнику повредить произвольную память при выделении записи из свободного списка (ListHeads). Как указывалось ранее, это вызвано тем, что safe unlinking выполняется не для фактического отключаемого фрагмента, а для структуры LIST ENTRY целевой записи массива ListHeads. Таким образом, простым исправлением будет правильная проверка прямой и обратной связи фрагмента, который не связывается.

Основная проблема при введении дополнительных мер по снижению рисков в уже оптимизированные алгоритмы управления пулом заключается в том, могут ли эти изменения существенно повлиять на производительность [3]. Наибольшую озабоченность вызывает не количество введенных дополнительных инструкций, а то, что изменение требует дополнительных операций подкачки страниц, которые очень дороги с точки зрения производительности. Рассмотрение атаки в разделе 3.1 может повлиять на производительность, поскольку адрес прямой ссылки несвязанного фрагмента не гарантированно будет выгружен в память, следовательно, может вызвать сбой страницы при safe unlinking.

5.2. Перезапись Lookaside Next Pointer

Поскольку lookaside списки по своей сути небезопасны, устранение их недостатков без внесения значительных изменений в пул ядра, несомненно, является сложной задачей. В куче Vista и Windows 7 lookaside списки были удалены в пользу кучи с низкой фрагментацией [9]. LFH избегает использования встроенных указателей и значительно снижает способность злоумышленника точно управлять кучей. Таким образом, аналогичный подход можно использовать в ядре. Однако удаление высокооптимизированных lookaside списков, вероятно, в некоторой степени повлияет на производительность.

Screenshot_20.png


В качестве альтернативы можно добавить проверки целостности фрагментов пула, чтобы предотвратить использование указателей lookaside списка. Поскольку все фрагменты пула должны зарезервировать пространство для структуры LIST ENTRY, а для lookaside указателей требуется только половина размера (SLIST ENTRY), фрагменты пула в ). списках могут хранить 4-байтовый (или 8 в x64) cookie перед указателем Next (рисунок 9). Этот файл cookie должен быть нетривиальным для определения в пользовательском режиме и может быть случайным значением (например, определенным структурой lookaside списка или блоком управления процессором), обработанным XOR с адресом фрагмента. Однако обратите внимание, что это не обязательно предотвратит эксплуатацию в ситуациях, когда злоумышленник может писать в выбранный набор настроек из выделенного фрагмента (уязвимости индексации массива).

5.3 Перезапись PendingFrees Next Pointer

Поскольку списки PendingFrees односвязны, очевидно, что у них те же проблемы, что и у вышеупомянутых дополнительных списков. Таким образом, списки PendingFrees также могут получить выгоду от встроенного cookie фрагмента пула, чтобы предотвратить использование переполенение пула. Хотя вместо этого можно было бы использовать двусвязный список, это потребовало бы дополнительной блокировки в ExFreePoolWithTag (при вставке записей в список), что было бы дорогостоящим в вычислительном отношении и нарушило бы цель отложенного свободного списка.

5.4 Перезапись PoolIndex

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

Обратите внимание, что этот метод был еще одним явным случаем злоупотребления нулевым указателем. Таким образом, отказ от сопоставления виртуального адреса null (0) в несистемных процессах может быть решением не только для борьбы с этой конкретной атакой, но и для многих других уязвимостей ядра, использующих нулевой указатель. В настоящее время нулевая страница в основном используется для обратной совместимости, например, виртуальной машиной Dos (VDM) для адресации 16-разрядной памяти в приложениях WOW. Следовательно, злоумышленник может обойти ограничение отображения нулевой страницы, внедрив его в процесс WOW.

5.5 Перезапись Quota Process Pointer

В Разделе 3.5 мы показали, как злоумышленник может использовать уязвимость, связанную с повреждением пула, для разыменования произвольного указателя объекта процесса. Это было особенно легко выполнить в системах x64, поскольку указатель хранился в заголовке пула, а не в конце фрагмента пула, как в случае с системами x86. Чтобы предотвратить эксплуатацию, связанную с использованием этого указателя, можно использовать простое кодирование (с использованием константы, неизвестной злоумышленнику), чтобы скрыть его фактическое значение. Однако очевидная проблема с этим подходом состоит в том, что повреждение пула может быть значительно труднее отлаживать, поскольку неправильно декодированные указатели, скорее всего, будут ссылаться на данные, не связанные с аварийным отказом. Тем не менее, есть определенные проверки, которые могут быть выполнены для проверки декодированного указателя, например, обеспечение его правильного выравнивания и в пределах ожидаемых границ.

6. Заключение

В этой статье мы показали, что, несмотря на safe unlinking, пул ядра Windows 7 по-прежнему уязвим для общих атак. Тем не менее, большинство идентифицированных векторов атак можно устранить путем добавления простых проверок или использования функций предотвращения эксплойтов из кучи пользовательского пространства. Таким образом, в будущих выпусках Windows и пакетах обновления мы, вероятно, увидим дополнительную защиту пула ядра. В частности, пул ядра получит большую выгоду от контрольной суммы заголовка пула или cookie, чтобы предотвратить использование, включающее повреждение заголовка пула или создание вредоносного пула.

Ссылки

[1] Alexander Anisimov: Defeating Microsoft Windows XP SP2 Heap Protection and DEP Bypass. http://www.ptsecurity.com/download/defeating-xpsp2-heap-protection.pdf
[2] Adam Barth, Collin Jackson, Charles Reis: The Security Architecture of the Chromium Browser. http://crypto.stanford.edu/websec/chromium/chromium-security-architecture.pdf
[3] Pete Beck: Safe Unlinking in the Kernel Pool. Microsoft Security Research and Defense. http://blogs.technet.com/srd/archive/2009/05/26/safe-unlinking-in-the-kernel-pool.aspx
[4] Dion Blazakis: Interpreter Exploitation: Pointer Inference and JIT Spraying. Black Hat DC 2010. http://www.semantiscope.com/research/BHDC2010
[5] Matt Conover & Oded Horovitz: Windows Heap Exploitation. CanSecWest 2004.
[6] Matthew Jurczyk: Windows Objects in Kernel Vulnerability Exploitation. Hack-in-the-Box Magazine 002. http://www.hackinthebox.org/misc/HITB-Ezine-Issue-002.pdf
[7] Matthew Jurczyk: Reserve Objects in Windows 7. Hack-in-the-Box Magazine 003.
[8] Kostya Kortchinsky: Real World Kernel Pool Exploitation. SyScan 2008. http://www.immunitysec.com/downloads/KernelPool.odp
[9] Adrian Marinescu: Windows Vista Heap Management Enhancements. Black Hat USA 2006. http://www.blackhat.com/presentations/bh-usa-06/BH-US-06-Marinescu.pdf
[10] Microsoft Security Bulletin MS10-058: Vulnerabilities in TCP/IP Could Allow Elevation of Privilege. http://www.microsoft.com/technet/security/Bulletin/MS10-058.mspx
[11] mxatone: Analyzing Local Privilege Escalation in win32k. Uninformed Journal, vol. 10, article 2. http://www.uninformed.org/?v=10&a=2
[12] Office Team: Protected View in Office 2010. Microsoft Office 2010 Engineering. http://blogs.technet.com/b/office2010/archive/2009/08/13/protected-view-in-office-2010.aspx
[13] Kyle Randolph: Inside Adobe Reader Protected Mode - Part 1 - Design. Adobe Secure Software Engineering Team (ASSET) Blog. http://blogs.adobe.com/asset/2010/10/inside-adobe-reader-protected-mode-part-1-design.html
[14] Ruben Santamarta: Exploiting Common Flaws in Drivers. http://reversemode.com/index.php?option=com_remository&Itemid=2&func=fileinfo&id=51
[15] Hovav Shacham: The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86). In Proceedings of CCS 2007, pages 552561.ACM Press, Oct. 2007.
[16] SoBeIt: How To Exploit Windows Kernel Memory Pool. Xcon 2005. http://packetstormsecurity.nl/Xcon2005/Xcon2005_SoBeIt.pdf
[17] Matthieu Suiche: Microsoft Security Bulletin (August). http://moonsols.com/blog/14-august-security-bulletin

Источник: BlackHat.com
Автор перевода: yashechka
Переведено специально для https://xss.pro
 
Большое спасибо за перевод! Давно хотел разобраться с устройством пулов ядра, да всё никак не мог собраться.
Если будет время, если ещё материал по пулам в Windows 10: https://www.sstic.org/media/SSTIC20...tion_since_windows_10_19h1-bayet_fariello.pdf
Его тоже было бы приятно видеть на форуме.
 
Большое спасибо за перевод! Давно хотел разобраться с устройством пулов ядра, да всё никак не мог собраться.
Если будет время, если ещё материал по пулам в Windows 10: https://www.sstic.org/media/SSTIC20...tion_since_windows_10_19h1-bayet_fariello.pdf
Его тоже было бы приятно видеть на форуме.
Да, точно, хотел тот же линк сбросить )
 
В
Большое спасибо за перевод! Давно хотел разобраться с устройством пулов ядра, да всё никак не мог собраться.
Если будет время, если ещё материал по пулам в Windows 10: https://www.sstic.org/media/SSTIC20...tion_since_windows_10_19h1-bayet_fariello.pdf
Его тоже было бы приятно видеть на форуме.
В работе
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх