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

Techniques Scoop the Windows 10 pool!

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Heap Overflow - довольно распространенная уязвимость в приложениях. Эксплуатация таких уязвимостей часто зависит от глубокого понимания основных механизмов, используемых для управления кучей. Windows 10 недавно изменила способ управления своей кучей в области ядра. Цель этой статьи - представить недавнюю эволюцию механизмов кучи в ядре Windows NT и представить новые методы эксплуатации, характерные для пула ядра.

1. Введение

Пул - это куча, зарезервированная для ядра в системах Windows. В течение многих лет распределитель пула был очень конкретным и отличался от распределителя в пользовательсокм режиме. Все изменилось после обновления Windows 10 19H1 в марте 2019 года. Хорошо известная и документированная Segment Heap [7], используемая в пользовательской среде, была перенесена в ядро.

Однако остаются некоторые различия между распределителем, реализованным в ядре, и в пользовательской области, поскольку в области ядра все еще требуются некоторые специфические материалы. В этой статье основное внимание уделяется внутреннему устройству, которое настраивается для Segment Heap ядра с точки зрения эксплуатации.

Исследование, представленное в этой статье, адаптировано к архитектуре x64. Настройка, необходимая для различных архитектур, не изучена.

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

1.1 Внутреннее устройство пула.

Эта статья не будет слишком углубляться в внутренности распределителя пула, поскольку эта тема уже широко освещена [5], но для полного понимания статьи, тем не менее, необходимо краткое напоминание о некоторых внутренних компонентах. В этом разделе будут представлены некоторые внутренние компоненты пула, какие они были в Windows 7, а также различные меры по безопасности и изменения, внесенные в пул за последние несколько лет.
Описание внутреннего устройства будет сосредоточено на чанках, которые умещаются на одной странице, которые являются наиболее распространенным распределением в ядре. Выделения размером больше 0xFE0 ведут себя по-другому и здесь не рассматриваются.

Выделение памяти в пуле.
Основными функциями выделения и освобождения памяти в ядре Windows являются соответственно ExAllocatePoolWithTag и ExFreePoolWithTag.
Screenshot_60.png



Screenshot_61.png


PoolType - это битовое поле с этим связанным перечислением:
NonPagedPool = 0
PagedPool = 1
NonPagedPoolMustSucceed = 2
DontUseThisType = 3
NonPagedPoolCacheAligned = 4
PagedPoolCacheAligned = 5
NonPagedPoolCacheAlignedMustSucceed = 6
MaxPoolType = 7
PoolQuota = 8
NonPagedPoolSession = 20h
PagedPoolSession = 21h
NonPagedPoolMustSucceedSession = 22h
DontUseThisTypeSession = 23h
NonPagedPoolCacheAlignedSession = 24h
PagedPoolCacheAlignedSession = 25h
NonPagedPoolCacheAlignedMustSSession = 26h
NonPagedPoolNx = 200h
NonPagedPoolNxCacheAligned = 204h


В PoolType может храниться некоторая информация:

- тип используемой памяти, который может быть NonPagedPool, PagedPool, SessionPool или NonPagedPoolNx;
- если выделение критическое (бит 1) и должно быть успешным. Если выделение не удается, запускается BugCheck;
- если распределение выровнено по размеру кеша (бит 2);
- если выделение использует механизм PoolQuota (бит 3);
- другие недокументированные механизмы.


Тип используемой памяти важен, поскольку он изолирует распределения в разных диапазонах памяти. Два основных типа используемой памяти - это PagedPool и NonPagedPool. В документации MSDN это описывается следующим образом:

"Nonpaged pool - это невыгружаемая системная память. К нему можно получить доступ из любого IRQL, но это ограниченный ресурс, и драйверы должны выделять его только при необходимости. Выгружаемый пул - это выгружаемая системная память, и ее можно выделить и получить доступ только на IRQL <DIS-PATCH_LEVEL."

Как объяснялось в разделе 1.2, NonPagedPoolNx был введен в Windows 8 и должен использоваться вместо NonPagedPool.

SessionPool используется для выделения пространства сеанса и уникален для каждого сеанса пользователя. В основном он используется win32k.

Наконец, тег представляет собой ненулевой символьный литерал от одного до четырех символов (например, ’Tag1’). Разработчикам ядра рекомендуется использовать уникальный тег пула по пути кода, чтобы помочь отладчикам и верификаторам идентифицировать путь кода.

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

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

C:
struct POOL_HEADER
{
char PreviousSize;
char PoolIndex;
char BlockSize;
char PoolType;
int PoolTag;
Ptr64 ProcessBilled;
};

Структура POOL_HEADER, представленная на рисунке 3, немного изменилась с течением времени, но всегда сохраняет те же основные поля. В Windows 1809, до Windows 19H1, использовались все поля:

PreviousSize - это размер предыдущего блока, деленный на 16;
PoolIndex - это индекс в массиве PoolDescriptor;
BlockSize - это размер текущего распределения, деленный на 16;
PoolType - это битовое поле, содержащее информацию о типе распределения;
ProcessBilled - это указатель на KPROCESS, который произвел выделение.

Он устанавливается, только если в PoolType установлен флаг PoolQuota.

1.2 Атаки и защита, начиная с Windows 7

Tarjei Mandt и его статья "Использование пула ядра в Windows 7" [5] - это справочник об атаках, нацеленных на пул ядра. В нем представлены все внутренние компоненты пула и многочисленные атаки, а некоторые нацелены на POOL_HEADER.

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

Распределение может взимать квоту с определенного процесса. Для этого ExAllocatePoolWithQuotaTag будет использовать поле ProcessBilled в POOL_HEADER для хранения указателя на _KPROCESS, которому назначено выделение.

Атака, описанная в документе - это перезапись указателя процесса квоты. Эта атака использует переполнение кучи для перезаписи указателя ProcessBilled выделенного фрагмента. Когда блок освобождается, если PoolType блока содержит флаг PoolQuota (0x8), указатель используется для разыменования значения. Управление этим указателем обеспечивает произвольный примитив разыменования, которого достаточно для повышения привилегий со стороны пользователя. Рисунок 4 представляет эту атаку.

Screenshot_62.png


Эта атака сошла на нет, начиная с Windows 8, с появлением ExpPoolQuotaCookie. Этот cookie генерируется случайным образом при загрузке и используется для защиты указателей от перезаписи злоумышленником. Например, он используется для XOR поля ProcessBilled:

ProcessBilled = KPROCESS_PTR ^ ExpPoolQuotaCookie ^ CHUNK_ADDR

Когда чанк освобождается, ядро проверяет, является ли закодированный указатель допустимым указателем KPROCESS:

C:
process_ptr = (struct _KPROCESS *)(chunk_addr ^ ExpPoolQuotaCookie ^
chunk_addr ->process_billed);
if ( process_ptr )
{
if (process_ptr < 0xFFFF800000000000 || (process_ptr ->Header.
Type & 0x7F) != 3 )
KeBugCheckEx ([...])
[...]
}

Не зная ни адреса блока, ни значения ExpPoolQuotaCookie невозможно предоставить действительный указатель и получить произвольное разыменование. Однако все еще возможно правильно переписать POOL_HEADER и провести полную атаку на данные, не задав флаг PoolQuota в PoolType. Для получения дополнительной информации об атаке Quota Process Pointer Overwrite, она была освещена на конференции Nuit du Hack XV [1].

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

Выделения, которые ранее выполнялись в NonPagedPool, теперь используют NonPagedPoolNx, но тип NonPagedPool был сохранен из соображений совместимости со сторонними драйверами. Даже сегодня в Windows 10 многие сторонние драйверы все еще используют исполняемый файл NonPagedPool.

Различные меры по защите, введенные сверхурочно, сделали POOL_HEADER неинтересным для атак с использованием переполнения кучи. В настоящее время проще правильно переписать POOL_HEADER и атаковать данные следующего блока. Однако введение Segment Heap в пул изменило способ использования POOL_HEADER, и в этом документе показано, как его можно атаковать снова, чтобы эксплуатировать переполнение кучи в пуле ядра.

2 Распределитель пула с сегментом кучи

2.1 Внутреннее устройство Segment Heap


Segment Heap используется в области ядра, начиная с Windows 10 19H1, и очень похожа на сегментную кучу, используемую в пользовательской среде. Этот раздел направлен на представление основных функций Segment Heap и сосредоточение внимания на отличиях от той, которая используется в пользовательской области. Очень подробное объяснение внутреннего устройства Segment Heap пользовательского режима доступно в [7].

Как и та, которая используется в пользовательской области, Segment Heap направлена на предоставление различных функций в зависимости от размера выделения. Для этого определены четыре так называемых бэкэнда.

- Куча с низкой фрагментацией (сокращенно LFH): RtlHpLfhContextAllocate
- Переменный размер (сокращенно VS): RtlHpVsContextAllocateInternal
- Сегментый аллокатор (сокращенно. Seg): RtlHpSegAlloc
- Большой аллокатор: RtlHpLargeAlloc


Соответствие между запрошенным размером выделения и выбранным сервером показано на рисунке 5.

Три первых бэкэнда, Seg, VS и LFH, связаны с контекстом соответственно: _HEAP_SEG_CONTEXT, _HEAP_VS_CONTEXT и _HEAP_LFH_CONTEXT. Контексты бэкэнда хранятся в структуре _SEGMENT_HEAP.

Screenshot_63.png

1: kd> dt nt!_SEGMENT_HEAP
+0x000 EnvHandle : RTL_HP_ENV_HANDLE
+0x010 Signature : Uint4B
+0x014 GlobalFlags : Uint4B
+0x018 Interceptor : Uint4B
+0x01c ProcessHeapListIndex : Uint2B
+0x01e AllocatedFromMetadata : Pos 0, 1 Bit
+0x020 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x020 ReservedMustBeZero1 : Uint8B
+0x028 UserContext : Ptr64 Void
+0x030 ReservedMustBeZero2 : Uint8B
+0x038 Spare : Ptr64 Void
+0x040 LargeMetadataLock : Uint8B
+0x048 LargeAllocMetadata : _RTL_RB_TREE
+0x058 LargeReservedPages : Uint8B
+0x060 LargeCommittedPages : Uint8B
+0x068 StackTraceInitVar : _RTL_RUN_ONCE
+0x080 MemStats : _HEAP_RUNTIME_MEMORY_STATS
+0x0d8 GlobalLockCount : Uint2B
+0x0dc GlobalLockOwner : Uint4B
+0x0e0 ContextExtendLock : Uint8B
+0x0e8 AllocatedBase : Ptr64 UChar
+0x0f0 UncommittedBase : Ptr64 UChar
+0x0f8 ReservedLimit : Ptr64 UChar
+0x100 SegContexts : [2] _HEAP_SEG_CONTEXT
+0x280 VsContext : _HEAP_VS_CONTEXT
+0x340 LfhContext : _HEAP_LFH_CONTEXT

Существует 5 таких структур, соответствующих различным значениям _POOL_TYPE:

- Невыгружаемые пулы (бит 0 не установлен)
- Пул NonPagedNx (бит 0 не установлен, бит 9 установлен)
- Выгружаемые пулы (установлен бит 0)
- Пул PagedSession (бит 5 и 1 установлен)


Пятый _SEGMENT_HEAP выделен, но авторы не смогли найти ее предназачение. 3 первых _SEGMENT_HEAP, соответствующие пулам NonPaged, NonPagedNx и Paged, хранятся в HEAP_POOL_NODES. Что касается PagedPoolSession, соответствующий _SEGMENT_HEAP хранится в текущем потоке. На рисунке 6 показаны пять _SEGMENT_HEAP.

Screenshot_64.png

Хотя пользовательская Segment Heap использует только один контекст распределения сегментов для выделения от 128 до 508 килобайт, в области ядра Segment Heap использует 2 контекста распределения сегментов. Второй используется для выделения от 508К до 7Г.

Бэкэнд сегмента

Бэкэнд сегмента используется для выделения фрагментов памяти размером от 128 КиБ до 7 ГиБ. Он также используется "за сценой" для выделения памяти для бэкэндов VS и LFH.

Контекст бэкэнд сегмента хранится в структуре под названием _HEAP_SEG_CONTEXT.

1: kd> dt nt!_HEAP_SEG_CONTEXT
+0x000 SegmentMask : Uint8B
+0x008 UnitShift : UChar
+0x009 PagesPerUnitShift : UChar
+0x00a FirstDescriptorIndex : UChar
+0x00b CachedCommitSoftShift : UChar
+0x00c CachedCommitHighShift : UChar
+0x00d Flags : <anonymous -tag >
+0x010 MaxAllocationSize : Uint4B
+0x014 OlpStatsOffset : Int2B
+0x016 MemStatsOffset : Int2B
+0x018 LfhContext : Ptr64 Void

Screenshot_65.png


+0x020 VsContext : Ptr64 Void
+0x028 EnvHandle : RTL_HP_ENV_HANDLE
+0x038 Heap : Ptr64 Void
+0x040 SegmentLock : Uint8B
+0x048 SegmentListHead : _LIST_ENTRY
+0x058 SegmentCount : Uint8B
+0x060 FreePageRanges : _RTL_RB_TREE
+0x070 FreeSegmentListLock : Uint8B
+0x078 FreeSegmentList : [2] _SINGLE_LIST_ENTRY

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

Сегменты хранятся в связанном списке, хранящемся в SegmentListHead. Сегменты начинаются с _HEAP_PAGE_SEGMENT, за которым следуют 256 структур _HEAP_PAGE_RANGE_DESCRIPTOR.

1: kd> dt nt!_HEAP_PAGE_SEGMENT
+0x000 ListEntry : _LIST_ENTRY
+0x010 Signature : Uint8B
+0x018 SegmentCommitState : Ptr64 _HEAP_SEGMENT_MGR_COMMIT_STATE
+0x020 UnusedWatermark : UChar
+0x000 DescArray : [256] _HEAP_PAGE_RANGE_DESCRIPTOR

1: kd> dt nt!_HEAP_PAGE_RANGE_DESCRIPTOR
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 TreeSignature : Uint4B
+0x004 UnusedBytes : Uint4B
+0x008 ExtraPresent : Pos 0, 1 Bit
+0x008 Spare0 : Pos 1, 15 Bits
+0x018 RangeFlags : UChar
+0x019 CommittedPageCount : UChar
+0x01a Spare : Uint2B
+0x01c Key : _HEAP_DESCRIPTOR_KEY
+0x01c Align : [3] UChar
+0x01f UnitOffset : UChar
+0x01f UnitSize : UChar

Чтобы обеспечить быстрый поиск свободных диапазонов страниц, в _HEAP_SEG_CONTEXT также поддерживается красно-черное дерево.

Каждый _HEAP_PAGE_SEGMENT имеет подпись, вычисляемую следующим образом:

Signature = Segment ^ SegContext ^ RtlpHpHeapGlobals ^ 0 xA2E64EADA2E64EAD;

Эта подпись используется для получения принадлежащего _HEAP_SEG_CONTEXT и соответствующего _SEGMENT_HEAP из любого выделенного фрагмента памяти.

На рисунке 7 показаны внутренние структуры, используемые в серверной части сегмента.

Исходный сегмент можно легко вычислить по любому адресу, замаскировав его с помощью SegmentMask, хранящегося в _HEAP_SEG_CONTEXT. SegmentMask имеет значение 0xfffffffffff00000.

Segment = Addr & SegContext ->SegmentMask;

Соответствующий PageRange можно легко вычислить из любого адреса с помощью UnitShift из _HEAP_SEG_CONTEXT. UnitShift установлен на 12.

PageRange = Segment + sizeof(_HEAP_PAGE_RANGE_DESCRIPTOR) * (Addr- Segment) >> SegContext ->UnitShift;

Когда бэкэнд сегмента используется одним из других бэкэнд, поля RangeFlags _HEAP_PAGE_RANGE_DESCRIPTOR используются для хранения того, какой бэкэнд запросил выделение.

Бэкэнд переменного размера

Бэкэнд переменного размера выделяет фрагмент размером от 512 до 128 КБ. Его цель - обеспечить простое повторное использование свободного фрагмента.

Контекст бэкэнда переменного размера хранится в структуре под названием _HEAP_VS_CONTEXT.

0: kd> dt nt!_HEAP_VS_CONTEXT
+0x000 Lock : Uint8B
+0x008 LockType : _RTLP_HP_LOCK_TYPE
+0x010 FreeChunkTree : _RTL_RB_TREE

Screenshot_66.png


+0x020 SubsegmentList : _LIST_ENTRY
+0x030 TotalCommittedUnits : Uint8B
+0x038 FreeCommittedUnits : Uint8B
+0x040 DelayFreeContext : _HEAP_VS_DELAY_FREE_CONTEXT
+0x080 BackendCtx : Ptr64 Void
+0x088 Callbacks : _HEAP_SUBALLOCATOR_CALLBACKS
+0x0b0 Config : _RTL_HP_VS_CONFIG
+0x0b4 Flags : Uint4B
Свободные фрагменты хранятся в Красно-Черном дереве под названием FreeChunkTree. Когда запрашивается выделение, используется Красно-Черное дерево для нахождения любого свободного фрагмента точного размера или первого свободного фрагмента, превышающего запрошенный размер.

Освобожденный фрагмент возглавляется специальной структурой под названием _HEAP_VS_CHUNK_FREE_HEADER.

0: kd> dt nt!_HEAP_VS_CHUNK_FREE_HEADER
+0x000 Header : _HEAP_VS_CHUNK_HEADER
+0x000 OverlapsHeader : Uint8B
+0x008 Node : _RTL_BALANCED_NODE

После нахождения свободного фрагмента он разделяется до нужного размера с помощью вызова RtlpHpVsChunkSplit.

Все выделенные фрагменты возглавляются специальной структурой под названием _HEAP_VS_CHUNK_HEADER.

0: kd> dt nt!_HEAP_VS_CHUNK_HEADER
+0x000 Sizes : _HEAP_VS_CHUNK_HEADER_SIZE
+0x008 EncodedSegmentPageOffset : Pos 0, 8 Bits
+0x008 UnusedBytes : Pos 8, 1 Bit
+0x008 SkipDuringWalk : Pos 9, 1 Bit
+0x008 Spare : Pos 10, 22 Bits
+0x008 AllocatedChunkBits : Uint4B
0: kd> dt nt!_HEAP_VS_CHUNK_HEADER_SIZE
+0x000 MemoryCost : Pos 0, 16 Bits
+0x000 UnsafeSize : Pos 16, 16 Bits
+0x004 UnsafePrevSize : Pos 0, 16 Bits
+0x004 Allocated : Pos 16, 8 Bits
+0x000 KeyUShort : Uint2B
+0x000 KeyULong : Uint4B
+0x000 HeaderBits : Uint8B

Все поля внутри этого заголовка имеют значение RtlpHpHeapGlobals и адрес блока.

Chunk ->Sizes = Chunk ->Sizes ^ Chunk ^ RtlpHpHeapGlobals;

Внутренне распределитель VS использует распределитель сегментов. Он используется в RtlpHpVsSubsegmentCreate через поле _HEAP_SUBALLOCATOR_CALLBACKS _HEAP_VS_CONTEXT. Все обратные вызовы субраспределителя проxorены к адресам контекста VS и RtlpHpHeapGlobals.

callbacks.Allocate = RtlpHpSegVsAllocate;
callbacks.Free = RtlpHpSegLfhVsFree;
callbacks.Commit = RtlpHpSegLfhVsCommit;
callbacks.Decommit = RtlpHpSegLfhVsDecommit;
callbacks.ExtendContext = NULL;

Если в FreeChunkTree нет достаточно большого фрагмента, выделяется новый Subsegment, размер которого находится в диапазоне от 64 КиБ до 256 КиБ, и вставляется в SubsegmentList. Его возглавляет структура _HEAP_VS_SUBSEGMENT. Все оставшееся пространство используется как свободный кусок и вставляется в FreeChunkTree.

0: kd> dt nt!_HEAP_VS_SUBSEGMENT
+0x000 ListEntry : _LIST_ENTRY
+0x010 CommitBitmap : Uint8B
+0x018 CommitLock : Uint8B
+0x020 Size : Uint2B
+0x022 Signature : Pos 0, 15 Bits
+0x022 FullCommit : Pos 15, 1 Bit
На рисунке 8 показана организация памяти VS Backend.

Когда блок VS освобожден, если он меньше 1 КиБ и сервер VS настроен правильно (бит 4 Config.Flags установлен в 1), он временно сохраняется в списке внутри DelayFreeContext. Как только DelayFreeContext заполняется 32 блоками, все они действительно освобождаются сразу. DelayFreeContext никогда не используется для прямого выделения.

Когда блок VS действительно освобожден, если он смежен с двумя другими освобожденными блоками, все 3 будут объединены вместе с вызовом RtlpHpVsChunkCoalesce. Затем он будет вставлен в FreeChunkTree.

Бэкэнд с низкой фрагментацией кучи

Куча с низкой фрагментацией - это бэкэнд, предназначенная для небольших распределений от 1 до 512 байт.

Контекст LFH Backend хранится в структуре под названием _HEAP_LFH_CONTEXT.

0: kd> dt nt!_HEAP_LFH_CONTEXT
+0x000 BackendCtx : Ptr64 Void
+0x008 Callbacks : _HEAP_SUBALLOCATOR_CALLBACKS
+0x030 AffinityModArray : Ptr64 UChar
+0x038 MaxAffinity : UChar
+0x039 LockType : UChar
+0x03a MemStatsOffset : Int2B
+0x03c Config : _RTL_HP_LFH_CONFIG
+0x040 BucketStats : _HEAP_LFH_SUBSEGMENT_STATS
+0x048 SubsegmentCreationLock : Uint8B
+0x080 Buckets : [129] Ptr64 _HEAP_LFH_BUCKET

Основная особенность бэкэнда LFH - использовать корзины разных размеров, чтобы избежать фрагментации.

Screenshot_67.png


Каждый сегмент состоит из SubSegments, выделенных распределителем сегментов. Распределитель сегментов используется через поле _HEAP_SUBALLOCATOR_CALLBACKS _HEAP_LFH_CONTEXT. Все обратные вызовы субраспределителя xorятся к адресам контекста LFH и RtlpHpHeapGlobals.

callbacks.Allocate = RtlpHpSegLfhAllocate;
callbacks.Free = RtlpHpSegLfhVsFree;
callbacks.Commit = RtlpHpSegLfhVsCommit;
callbacks.Decommit = RtlpHpSegLfhVsDecommit;
callbacks.ExtendContext = RtlpHpSegLfhExtendContext;


Подсегмент LFH возглавляется структурой _HEAP_LFH_SUBSEGMENT.

0: kd> dt nt!_HEAP_LFH_SUBSEGMENT
+0x000 ListEntry : _LIST_ENTRY
+0x010 Owner : Ptr64 _HEAP_LFH_SUBSEGMENT_OWNER
+0x010 DelayFree : _HEAP_LFH_SUBSEGMENT_DELAY_FREE
+0x018 CommitLock : Uint8B
+0x020 FreeCount : Uint2B
+0x022 BlockCount : Uint2B
+0x020 InterlockedShort : Int2B
+0x020 InterlockedLong : Int4B
+0x024 FreeHint : Uint2B
+0x026 Location : UChar
+0x027 WitheldBlockCount : UChar
+0x028 BlockOffsets : _HEAP_LFH_SUBSEGMENT_ENCODED_OFFSETS
+0x02c CommitUnitShift : UChar
+0x02d CommitUnitCount : UChar
+0x02e CommitStateOffset : Uint2B
+0x030 BlockBitmap : [1] Uint8B

Затем каждый подсегмент разбивается на разные блоки LFH с соответствующим размером сегмента.

Чтобы узнать, какой сегмент используется, в каждом заголовке подсегмента сохраняется битовая карта.

Screenshot_68.png




Когда запрашивается выделение, распределитель LFH сначала ищет поле FreeHint структуры _HEAP_LFH_SUBSEGMENT, чтобы найти смещение последнего освобожденного блока в подсегменте. Затем он просканирует BlockBitmap по группе из 32 блоков в поисках свободного блока. Это сканирование рандомизировано благодаря таблице RtlpLowFragHeapRandomData.

В зависимости от конкуренции в данном сегменте может быть задействован механизм для упрощения выделения путем выделения SubSegment каждому ЦП. Этот механизм называется Affinity Slot.

На рисунке 9 представлена основная архитектура бэкэнд части LFH.

Динамические списки

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

Список представлен структурой _RTL_DYNAMIC_LOOKASIDE и хранится в поле UserContext _SEGMENT_HEAP.

0: kd> dt nt!_RTL_DYNAMIC_LOOKASIDE
+0x000 EnabledBucketBitmap : Uint8B
+0x008 BucketCount : Uint4B
+0x00c ActiveBucketCount : Uint4B
+0x040 Buckets : [64] _RTL_LOOKASIDE

Каждый освобожденный блок сохраняется в _RTL_LOOKASIDE, соответствующем его размеру (как выражено в POOL_HEADER). Соответствие размеров происходит по той же схеме, что и для Bucket в LFH.

0: kd> dt nt!_RTL_LOOKASIDE
+0x000 ListHead : _SLIST_HEADER
+0x010 Depth : Uint2B
+0x012 MaximumDepth : Uint2B
+0x014 TotalAllocates : Uint4B
+0x018 AllocateMisses : Uint4B
+0x01c TotalFrees : Uint4B
+0x020 FreeMisses : Uint4B
+0x024 LastTotalAllocates : Uint4B
+0x028 LastAllocateMisses : Uint4B
+0x02c LastTotalFrees : Uint4B

Screenshot_69.png

Одновременно включается только подмножество доступных сегментов (поле ActiveBucketCount _RTL_DYNAMIC_LOOKASIDE). Каждый раз, когда запрашивается выделение, обновляются метрики соответствующего списка.

Каждые 3 сканирования Диспетчера Набора Баланса ребалансирует динамический список. Включены наиболее часто используемые с момента последней перебалансировки. Размер каждого списка зависит от его использования, но не может быть больше MaximumDepth или меньше 4. Пока количество новых выделений меньше 25, глубина уменьшается на 10. В то же время глубина уменьшается на 1, если коэффициент промахов ниже 0,5%, в противном случае она увеличивается по следующей формуле.

Screenshot_70.png


2.2 POOL_HEADER

Как показано в разделе 1.1, структура POOL_HEADER возглавляла все выделенные фрагменты в распределителе кучи ядра до Windows 10 19H1. Тогда использовались все поля. С обновлением распределителя кучи ядра большинство полей POOL_HEADER стали бесполезными, но небольшая выделенная память все еще занята им.

Определение POOL_HEADER показано на рисунке 10.

C:
struct POOL_HEADER
{
char PreviousSize;
char PoolIndex;
char BlockSize;
char PoolType;
int PoolTag;
Ptr64 ProcessBilled;
};


Распределитель устанавливает следующие поля:

PoolHeader ->PoolTag = PoolTag;
PoolHeader ->BlockSize = BucketBlockSize >> 4;
PoolHeader ->PreviousSize = 0;
PoolHeader ->PoolType = changedPoolType & 0x6D | 2;


Вот краткое описание назначения каждого из полей POOL_HEADER, начиная с Windows 19H1.

PreviousSize - не используется и сохранен равным 0.
PoolIndex - Не используется.
BlockSize - Размер чанка. Используется только для последующего сохранения фрагмента в списке Dynamic Lookaside (см. 2.1).
PoolType - Использование не изменилось; используется для сохранения запрошенного POOL_TYPE.
PoolTag - Использование не изменилось; используется для сохранения PoolTag.
- Использование не изменилось; используется для отслеживания процесса, требующего выделения, если PoolType имеет значение PoolQuota (бит 3). Значение рассчитывается следующим образом:

ProcessBilled = chunk_addr ^ ExpPoolQuotaCookie ^KPROCESS;

CacheAligned


При вызове ExAllocatePoolWithTag, если для PoolType установлен бит CacheAligned (бит 2), возвращаемая память выравнивается по размеру строки кэша. Значение размера строки кэша зависит от ЦП, но обычно составляет 0x40.

Сначала распределитель увеличит размер выделения ExpCacheLineSize:

C:
if ( PoolType & 4 )
{
request_alloc_size += ExpCacheLineSize;
if ( request_alloc_size > 0xFE0 )
{
request_alloc_size -= ExpCacheLineSize;
PoolType = PoolType & 0xFB;
}
}

Если новый размер выделения не может уместиться на одной странице, то бит CacheAligned будет проигнорирован.

Затем выделенный фрагмент должен соответствовать трем условиям:

- окончательный адрес распределения должен быть выровнен по ExpCacheLineSize;
- чанк должен иметь POOL_HEADER в самом начале чанка;
- блок должен иметь POOL_HEADER по адресу выделения минус размер (POOL_HEADER).


Поэтому, если адрес распределения не выровнен должным образом, у блока может быть два заголовка.

Screenshot_71.png


Первый POOL_HEADER будет, как обычно, в начале чанка, а второй будет выровнен по ExpCacheLineSize - sizeof (POOL_HEADER), в результате чего окончательный адрес выделения будет выровнен по ExpCacheLineSize. Бит CacheAligned удаляется из первого POOL_HEADER, а второй POOL_HEADER заполняется следующими значениями:

PreviousSize - Используется для хранения разрыва, установленного между двумя заголовками.
PoolIndex - Не используется.
BlockSize - Размер выделенного сегмента в первом POOL_HEADER, уменьшенный размер во втором.
PoolType - Как обычно, но бит CacheAligned установлен.
PoolTag - Как обычно, то же самое для POOL_HEADER.
ProcessBilled - Не используется.

Кроме того, указатель, который мы назвали AlignedPoolHeader, может быть сохранен после первого POOL_HEADER, если в выравнивающем заполнении достаточно места. Он указывает на второй POOL_HEADER и xorится с помощью ExpPoolQuotaCookie.

На рисунке 11 показано расположение двух POOL_HEADER, используемых в случае выравнивания кеша.

2.3 Резюме

Начиная с Windows 19H1 и введение Segment Heap, некоторая информация, которая хранилась в POOL_HEADER каждого фрагмента, больше не требуется. Однако другие, такие как Pooltype, Pooltag или возможность использовать механизмы CacheAligned и PoolQuota, по-прежнему необходимы.

Вот почему каждому выделению под 0xFE0 по-прежнему предшествует хотя бы один POOL_HEADER. Использование полей POOL_HEADER начиная с Windows 19H1 описано в разделе 2.2. На рисунке 12 представлен фрагмент, выделенный с помощью бэкэнда LFH, поэтому перед ним стоит только POOL_HEADER.

Screenshot_72.png

Как объяснено в п. 2.1, в зависимости от бэкэнда память может возглавляться каким-то определенным заголовком. Например, блок размером 0x280 будет использовать бэкэнда VS, поэтому ему будет предшествовать _HEAP_VS_CHUNK_HEADER размером 0x10. На рисунке 13 представлен фрагмент, выделенный с использованием сегмента VS, которому, таким образом, предшествуют заголовок VS и POOL_HEADER.

Screenshot_73.png



Наконец, если требуется выровнять выделение в строке кэша, блок может содержать два POOL_HEADER. У второго будет установлен бит CacheAligned, и он будет использоваться для извлечения первого и адреса фактического выделения. На рисунке 14 представлен фрагмент, выделенный с помощью LFH и запрошенный для выравнивания по размеру кеша, таким образом, предшествуют два POOL_HEADER.

На рисунке 15 показано дерево решений, используемое при распределении.

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

Screenshot_74.png


3 Атака POOL_HEADER

Если уязвимость, связанная с переполнением кучи, позволяет действительно хорошо контролировать записанные данные и их размер, самым простым решением, вероятно, является перезапись POOL_HEADER и прямая атака на данные следующего фрагмента. Единственное, что нужно сделать, это убедиться, что бит PoolQuota не установлен в PoolType, чтобы избежать проверки целостности поля ProcessBilled при освобождении поврежденного фрагмента.

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

3.1 Нацеливание на BlockSize

От переполнения кучи к большему переполнению кучи


Как объяснялось в разделе 2.1, поле BlockSize используется в механизме освобождения для хранения некоторых фрагментов в Динамическом Списке.

Злоумышленник может использовать переполнение кучи, чтобы изменить значение поля BlockSize на большее, чем 0x200. Если поврежденный фрагмент будет освобожден, контролируемый BlockSize будет использоваться для хранения фрагмента в списке неправильного размера. Следующее выделение такого размера может использовать слишком маленькое выделение для хранения всех требуемых данных, что приведет к другому переполнению кучи.

Используя методы распыления и определенные объекты, злоумышленник может превратить переполнение 3-байтовой кучи в переполнение кучи до 0xFD0 байтов, в зависимости от размера уязвимого блока. Это также позволяет злоумышленнику выбрать объект, который переполнится, и, возможно, лучше контролировать условия переполнения.

Screenshot_75.png




3.2 Нацеливание на PoolType

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

Например, изменение типа памяти, хранящейся в PoolType, на самом деле не изменит тип памяти, используемый для выделения. Невозможно превратить память NonPagedPoolNx в NonPagedPool, просто изменив этот бит.

Но это неверно для битов PoolQuota и CacheAligned. Установка бита PoolQuota вызовет использование указателя ProcessBilled в POOL_HEADER для разыменования квоты после освобождения. Как показано в 1.2, атаки на указатель ProcessBilled были защищены.

Итак, единственный оставшийся бит - это бит CacheAligned.

Путаница выравнивания фрагментов

Как видно в разделе 2.2, если выделение запрашивается с битом CacheAligned, установленным в PoolType, макет блока будет другим.

Когда распределитель освобождает такое выделение, он пытается найти исходный адрес фрагмента, чтобы освободить фрагмент по правильному адресу. Он будет использовать поле PreviousSize выровненного заголовка POOL_HEADER. Распределитель выполняет простое вычитание для вычисления исходного адреса блока:

C:
if ( AlignedHeader ->PoolType & 4 )
{
OriginalHeader = (QWORD)AlignedHeader - AlignedHeader ->
PreviousSize * 0x10;
OriginalHeader ->PoolType |= 4;
}

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

- Распределитель проверял, был ли у исходного фрагмента установлен бит MustSucceed в PoolType.
- Смещение между двумя заголовками было пересчитано с использованием ExpCacheLineSize и подтверждено как то же самое, что ифактическое расстояние между двумя заголовками.
- Распределитель проверил, был ли размер блока выровненного заголовка равен размеру блока исходного заголовка плюс размер блока PreviousSize выровненного заголовка.
- Распределитель проверял, равен ли указатель, хранящийся в OriginalHeader + sizeof (POOL_HEADER), адресу выровненного заголовка, проxorенным вместе с ExpPoolQuotaCookie.

Начиная с Windows 10 19H1, когда распределитель пула использует кучу сегментов, все эти проверки пропали. Указатель по-прежнему присутствует после исходного заголовка, но никогда не проверяется механизмом освобождения. Авторы предполагают, что часть проверок удалена по ошибке. Вероятно, что некоторые проверки будут повторно включены в будущих выпусках, но в предварительной сборке Windows 10 20H1 такого патча нет.

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

Поскольку поле PreviousSize хранится в одном байте, злоумышленник может освободить любой адрес, выровненный по 0x10, до 0xFF * 0x10 = 0xFF0 перед исходным адресом блока.

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

4 Общая эксплуатация

4.1 Обязательные условия


В этом разделе представлены методы использования уязвимости для повышения привилегий в системе Windows. Предполагается, что злоумышленник находится на уровне Low Integrity.

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

- При нацеливании на BlockSize уязвимость должна обеспечивать возможность перезаписать 3-й байт POOL_HEADER следующего блока с контролируемым значением.
- При нацеливании на PoolType уязвимость должна обеспечивать возможность перезаписать 1-й и 4-й байты POOL_HEADER следующего фрагмента с контролируемыми значениями.
- Во всех случаях требуется контролировать выделение и освобождение уязвимого объекта, чтобы добиться максимального успеха распыления.

4.2 Стратегии эксплуатации

Выбранная стратегия эксплуатации использует возможность атаковать поля PoolType и PreviousSize следующего блока POOL_HEADER. Чанк, уязвимый для переполнения кучи, будет называться "уязвимым чанком", а чанк, помещенный после него, будет называться "перезаписанным чанком".

Как описано в разделе 3.2, управляя полями PoolType и PreviousSize следующего фрагмента POOL_HEADER, злоумышленник может изменить место фактического освобождения перезаписанного фрагмента. Этот примитив можно использовать по-разному.

Это может позволить переключить поток в пуле в ситуации Use-After-Free, когда злоумышленник устанавливает поле PreviousSize точно равным размеру уязвимого фрагмента. Таким образом, при запросе освобождения перезаписанного фрагмента уязвимый фрагмент будет освобожден и помещен в ситуацию Use-After-Free. Рисунок 16 представляет эту технику.

Screenshot_76.png


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

Чанк-привидение перекрывает как минимум два фрагмента: уязвимый и перезаписанный.

Рисунок 17 представляет эту технику.

Screenshot_77.png


Этот последний метод кажется более пригодным для эксплуатации, чем Use-After-Free, потому что он позволяет злоумышленнику лучше контролировать содержимое произвольного объекта.

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

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

- обеспечивает произвольный примитив чтения/записи, если он полностью или частично контролируется;
- возможность контролировать его распределение и освобождение;
- иметь переменный размер не менее 0x210, чтобы быть выделенным в чанке привидении из соответствующего списка, но быть как можно меньшим (чтобы не перегружать кучу слишком много при ее выделении).

Поскольку уязвимый блок может быть размещен как в PagedPool, так и в NonPagedPoolNx, необходимы два объекта такого типа: один выделен в PagedPool, а другой - в NonPagedPoolNx.

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

Разработанный эксплойт использует эту последнюю технику. Используя массаж кучи и объекты, которые интересно переполнить, 4-байтовый контролируемый поток можно превратить в повышение привилегий, от низкого уровня целостности до SYSTEM.

4.3 Целевые объекты

Выгружаемый пул


После создания пайпа пользователь может добавлять атрибуты в пайп. Атрибуты представляют собой пару "ключ-значение" и хранятся в связанном списке.
Объект PipeAttribute 1 размещается в пуле PagedPool и определяется в ядре структурой, показанной на рисунке 18.

struct PipeAttribute {
LIST_ENTRY list;
char * AttributeName;
uint64_t AttributeValueSize;
char * AttributeValue;
char data[0];
};

Размер выделения и данных полностью контролируется злоумышленником. AttributeName и AttributeValue - это указатели, указывающие на разные параметры поля данных.
Атрибут пайпа может быть создан в пайпе с помощью системного вызова NtFsControlFile и управляющего кода 0x11003C, как показано на рисунке 19.

C:
HANDLE read_pipe;
HANDLE write_pipe;
char attribute[] = "attribute_name \00 attribute_value"
char output[0x100];
CreatePipe(read_pipe , write_pipe , NULL , bufsize);
NtFsControlFile(write_pipe ,
NULL ,
NULL ,
NULL ,
&status ,
0x11003C ,
attribute ,
sizeof(attribute),
output ,
sizeof(output)
);

Затем значение атрибута можно прочитать с помощью управляющего кода 0x110038. Указатель AttributeValue и AttributeValueSize будут использоваться для чтения значения атрибута и возврата его пользователю. Значение атрибутов можно изменить, но это вызовет освобождение предыдущего атрибута PipeAttribute и выделение нового.

Это означает, что если злоумышленник может управлять полями AttributeValue и AttributeValueSize атрибута PipeAttribute, он может читать произвольные данные в ядре, но не может произвольно записывать. Этот объект также идеально подходит для помещения произвольных данных в ядро. Это означает, что его можно использовать для перераспределения уязвимого фрагмента и управления содержимым чанка призрака.

NonPagedPoolNx

Возможность использовать WriteFile в канале - это известный метод распыления NonPagedPoolNx. При записи в канал функция NpAddDataQueueEntry создает структуру, определенную на рисунке 20.

C:
struct PipeQueueEntry
{
LIST_ENTRY list;
IRP *linkedIRP;
__int64 SecurityClientContext;
int isDataInKernel;
int remaining_bytes__;
int DataSize;
int field_2C;
char data[1];
};

Данные и размер PipeQueueEntry контролируются пользователем, поскольку данные хранятся непосредственно за структурой.

При использовании записи в функции NpReadDataQueue ядро просматривает список записей и использует каждую запись для извлечения данных.

C:
if ( PipeQueueEntry ->isDataAllocated == 1 )
data_ptr = (PipeQueueEntry ->linkedIRP ->SystemBuffer);
else
data_ptr = PipeQueueEntry ->data;
[...]
memmove((void *)(dst_buf + dst_len - cur_read_offset), &data_ptr[
PipeQueueEntry ->DataSize - cur_entry_offset], copy_size);

Если поле isDataInKernel равно 1, данные не хранятся непосредственно за структурой, но указатель сохраняется в IRP, на который указывает connectedIRP. Если злоумышленник может полностью контролировать эту структуру, он может установить isDataInKernel в 1 и сделать точку linkedIRP в пользовательском пространстве. Поле SystemBuffer (смещение 0x18) linkedIRP в пользовательском пространстве затем используется для чтения данных из записи.
Это обеспечивает произвольный примитив чтения. Этот объект также идеально подходит для помещения произвольных данных в ядро. Это означает, что его можно использовать для перераспределения уязвимого фрагмента и управления содержимым призрачного чанка.

4.4 Распыление

В этом разделе описываются методы распыления кучи ядра, чтобы получить желаемый макет памяти.

Чтобы получить требуемый макет памяти, представленный в разделе 4.2, необходимо выполнить некоторое распыление кучи. Распыление кучи зависит от размера уязвимого фрагмента, поскольку в конечном итоге он будет выделен другим бэкэндом.

Чтобы облегчить распыление, может быть полезно убедиться, что соответствующий список пуст. Это обеспечит выделение более 256 блоков правильного размера.

Если уязвимый фрагмент меньше 0x200, он будет расположен в бэкэнде LFH. Затем необходимо произвести распыление кусками одного и того же размера по модулю соответствующей степени детализации чанка, чтобы гарантировать, что все они выделены из одного и того же бакета. Как показано в разделе 2.1, когда запрашивается выделение, бэкэнд LFH будет сканировать BlockBitmap по группе максимум из 32 блоков и случайным образом выбирать свободный блок. Выделение более 32 блоков непосредственно перед и после выделения уязвимого блока должно помочь избежать рандомизации.

Если уязвимый фрагмент больше 0x200, но меньше 0x10000, он попадет в бэкэнд с переменным размером. Затем необходимо произвести распыление размером, равным размеру уязвимого участка. Более крупный кусок можно разделить и, таким образом, не справиться с распылением. Сначала выделите тысячи фрагментов выбранного размера, чтобы убедиться, что FreeChunkTree очищается от всех фрагментов, превышающих выбранный размер, а затем распределитель выделит новый подсегмент VS размером 0x10000 байт и поместит его в FreeChunkTree. Затем выделите еще одну тысячу фрагментов, которые окажутся в новом большом свободном фрагменте и, таким образом, будут непрерывными. Затем освободите одну треть последнего выделенного фрагмента, чтобы заполнить FreeChunkTree. Освобождение только одной трети гарантирует, что ни один кусок не будет объединен.
Затем позвольте уязвимому фрагменту быть выделенным. Наконец, освобожденный кусок можно перераспределить, чтобы увеличить вероятность распыления.

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

4.5 Эксплуатация

Демонстрационная установка.

Чтобы продемонстрировать следующий эксплойт, была создана поддельная уязвимость.

Был разработан драйвер ядра Windows, который предоставляет несколько IOCTL, что позволяет:

- Выделить чанк контролируемого размера в PagedPool
- Запускает управляемый memcpy в этом фрагменте, который позволяет полностью контролировать переполнение пула.
- Освободить выделенный кусок


Это, конечно, только для демонстрации и обеспечивает больший контроль, который действительно необходим для работы эксплойта.

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


Кроме того, уязвимый фрагмент размещается в пуле PagedPool. Это важно, поскольку тип пула может изменить объекты, используемые в эксплойтах, а затем оказать большое влияние на сам эксплойт. Однако эксплойт, нацеленный на NonPagedPoolNx, очень похож и использует только PipeQueueEntry для распыления и получения произвольного чтения вместо PipeAttribute.

В этом примере выбранный размер уязвимого фрагмента будет 0x180. Обсуждение размера уязвимого блока и его влияния на эксплойт обсуждается в разделе 4.6.

Создание призрачного чанка
Первым шагом здесь является массаж кучи, чтобы поместить контролируемый объект после уязвимого фрагмента. Объектом в перезаписанном чанке может быть что угодно, единственное требование - контролировать, когда он будет освобожден. Чтобы упростить эксплойт, лучше выбрать объект, который можно распылять, см. Раздел 4.2.

Теперь уязвимость может быть активирована, POOL_HEADER перезаписанного блока заменяется следующими значениями:

PreviousSize: 0x15. Этот размер будет умножен на 0x10. 0x180 - 0x150 = 0x30, смешение поддельного POOL_HEADER в уязвимом фрагменте.
PoolIndex: 0 или любое другое значение, не используется.
BlockSize: 0, или любое другое значение, не используется.
PoolType: PoolType | 4. Бит CacheAligned установлен.

Screenshot_78.png



Поддельный POOL_HEADER должен быть помещен в уязвимый блок с известным смещением. Это делается путем освобождения уязвимого объекта и перераспределения фрагмента с помощью объекта PipeAttribute.

Для демонстрации отключение поддельного POOL_HEADER в уязвимом фрагменте будет 0x30. Поддельный POOL_HEADER имеет следующий вид:

PreviousSize: 0 или любое другое значение не используется.
PoolIndex: 0 или любое другое значение, не используется.
BlockSize: 0x21. Этот размер будет умножен на 0x10 и будет размером освобожденного фрагмента.
PoolType: Тип пула. Биты CacheAligned и PoolQuota НЕ установлены.

Выбранный BlockSize не является случайным, это размер блока, который будет фактически освобожден. Поскольку цель состоит в том, чтобы впоследствии повторно использовать это выделение, необходимо выбрать размер, который будет легко использовать повторно. Поскольку все размеры ниже 0x200 находятся в LFH, таких размеров следует избегать. Наименьший размер, который не является LFH, - это выделение 0x200, то есть фрагмент размером 0x210. Размер 0x210 использует выделение VS и может использовать динамические списки, описанные в разделе 2.1.

Динамический список для размера 0x210 можно включить путем распыления и освобождения фрагментов размером 0x210 байт.

Теперь перезаписанный кусок можно освободить, и это вызовет выравнивание кеша. Вместо освобождения фрагмента по адресу перезаписанного фрагмента он освободит фрагмент с адресом OverwrittenChunkAddress - (0x15 * 0x10), который также является VulnerableChunkAddress + 0x30. POOL_HEADER, используемый для освобождения, является поддельным POOL_HEADER, и вместо освобождения уязвимого фрагмента ядро освобождает фрагмент размером 0x210 и помещает его в верхнюю часть динамического списка. Это показано на рисунке 23.

К сожалению, PoolType поддельного POOL_HEADER не влияет на то, помещается ли освобожденный фрагмент в PagedPool или NonPagedPoolNx.

Screenshot_79.png



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

Теперь перезаписанный фрагмент находится в состоянии "lost"; ядро считает, что оно освобождено, и все ссылки на блок удалены. Он больше не будет использоваться.

Утечка содержимого призрачного чанка.
Призрачный чанк теперь можно перераспределить с помощью объекта PipeAttribute. Структура PipeAttribute перезаписывает значение атрибута, помещенного в уязвимый блок. При чтении значения этого атрибута канала данные могут быть прочитаны, и содержимое атрибута PipeAttribute призрачного чанка утекает. Теперь известен адрес этого фантомного фрагмента и, следовательно, уязвимого фрагмента. Этот шаг представлен на рисунке 24.

Получение произвольного чтения
Уязвимый кусок можно освободить в другой раз и перераспределить с другим атрибутом PipeAttribute. На этот раз данные PipeAttribute перезапишут PipeAttribute призрачного чанка. Таким образом, можно полностью контролировать PipeAttribute призрачного чанк. Новый атрибут PipeAttribute вводится в связанный список, который находится в пользовательском пространстве. Этот шаг представлен на рисунке 25.

Теперь, запрашивая чтение атрибута призрачного чанка PipeAttribute, ядро будет использовать PipeAttribute, который находится в пользовательском пространстве и, таким образом, полностью контролируется. Как было показано ранее, управляя указателем AttributeValue и AttributeValueSize, это обеспечивает произвольный примитив чтения.

Фигура 26 представляет произвольное чтение.

Screenshot_80.png


Screenshot_81.png



Используя первую утечку указателя и произвольное чтение, можно получить указатель на текстовую секцию npfs. Читая таблицу импорта, можно прочитать указатель на текстовую секцию ntoskrnl, которая обеспечивает базу ядра. Оттуда злоумышленник может прочитать значение ExpPoolQuotaCookie и получить адрес структуры EPROCESS для процесса эксплойта и адрес его ТОКЕНА.

Получение произвольного уменьшения

Сначала фальшивая структура EPROCESS создается в области ядра с использованием PipeQueueEntry 3, и ее адрес извлекается с помощью произвольного чтения.

Screenshot_82.png


Затем эксплойт может еще раз освободить и перераспределить уязвимый кусок, чтобы изменить содержимое призрачного чанка и его POOL_HEADER.

POOL_HEADER призрачного чанка перезаписывается следующими значениями:

PreviousSize: 0 или любое другое значение, не используется.
PoolIndex: 0 или любое другое значение, не используется.
BlockSize: 0x21. Этот размер будет умножен на 0x10.
PoolType: 8. Бит PoolQuota установлен.
PoolQuota: ExpPoolQuotaCookie XOR FakeEprocessAddress XOR GhostChunkAddress

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

Это обеспечивает произвольный примитив декремента. Значение уменьшения - это размер блока в PoolHeader, поэтому он выровнен по 0x10 и между 0 и 0xFF0.

От произвольного уменьшения до SYSTEM

В 2012 году Сезар Серрудо [3] описал метод повышения его привилегий, установив поле Privileges.Enabled в структуре TOKEN. Поле Privileges.Enabled содержит привилегии, включенные для этого процесса. По умолчанию токен с низким уровнем целостности имеет для параметра Privileges.Enabled значение 0x0000000000800000, которое дает только SeChangeNotifyPrivilege. После вычитания единицы в этом битовом поле получится 0x000000000007FFFF, что дает гораздо больше привилегий.

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

Эксплойт, описанный в [1], представляет собой перезапись процесса указателя квоты, которая использует произвольное уменьшение для установки SeDebugPrivilege для своего процесса. На рисунке 27 представлена эта техника.

Screenshot_83.png



Однако, начиная с Windows 10 v1607, ядро теперь также проверяет значение поля Privileges.Present токена. Поле Privileges.Present токена - это список привилегий, которые МОЖНО включить для этого токена с помощью AdjustTokenPrivileges API. Таким образом, фактические привилегии TOKEN теперь представляют собой битовое поле, являющееся результатом Privileges.Present & Privileges.Enabled.

По умолчанию токен с низким уровнем целостности имеет значение Privileges.Present, равное 0x602880000. Поскольку 0x602880000 & (1«20) == 0, установки SeDebugPrivilege в Privileges.Enabled недостаточно для получения SeDebugPrivilege.

Идея может заключаться в уменьшении битового поля Privileges.Present, чтобы получить SeDebugPrivilege в битовом поле Privileges.Present. Затем злоумышленник может использовать API AdjustTokenPrivileges для включения SeDebugPrivilege. Однако функция SepAdjustPrivileges выполняет дополнительные проверки, и в зависимости от целостности TOKEN процесс не может активировать какие-либо привилегии, даже если требуемая привилегия находится в битовом поле Privileges.Present. Для высокого уровня целостности процесс может активировать любые привилегии, указанные в битовом поле Privileges.Present. Для среднего уровня целостности процесс может активировать только те привилегии, которые указаны в поле Privileges.Present И в битовом поле 0x1120160684. Для низкого уровня целостности процесс может активировать только те привилегии, которые указаны в поле Privileges.Present И в битовом поле 0x202800000.

Это означает, что этот метод получения SYSTEM из единственного произвольного уменьшения не работает.

Тем не менее, это вполне может быть сделано за два произвольных уменьшения, уменьшив сначала Privileges.Enabled, а затем Privileges.Present.

Призрачный чанк может быть перераспределен, а его POOL_HEADER перезаписан во второй раз, чтобы получить второе произвольное уменьшение.

После получения SeDebugPrivilege эксплойт может открыть любой процесс SYSTEM и внедрить внутри него шелл-код, который выводит оболочку c привилегиями SYSTEM.

4.6 Обсуждение представленного эксплойта

Код представленного эксплойта доступен по адресу [2] вместе с уязвимым драйвером. Этот эксплойт является лишь подтверждением концепции и всегда может быть улучшен.

4.7 Обсуждение размеров уязвимого объекта

В зависимости от размера уязвимого объекта у эксплойта могут быть разные требования.

Представленный выше эксплойт работает только для уязвимого фрагмента размером минимум 0x130. Это связано с размером фантомного фрагмента, который должен быть не менее 0x210. Для уязвимого фрагмента размером менее 0x130 выделение фантомного фрагмента перезапишет фрагмент за перезаписанным фрагментом и вызовет сбой при освобождении. Это поправимо, но оставлено читателю в качестве упражнения.

Есть несколько различий между уязвимым объектом в LFH (фрагменты до 0x200) и уязвимым объектом в сегменте VS (фрагменты> 0x200). В основном, у блока VS есть дополнительный заголовок перед блоком. Это означает, что для управления POOL_HEADER следующего фрагмента в сегменте VS требуется переполнение кучи размером не менее 0x14 байтов. Это также означает, что когда перезаписанный кусок будет освобожден, его _HEAP_VS_CHUNK_HEADER должен быть исправлен. Кроме того, следует позаботиться о том, чтобы не освободить 2 фрагмента, распыленных сразу после перезаписанного фрагмента, потому что механизм освобождения VS может прочитать заголовок VS перезаписанного фрагмента в попытке объединить 3 свободных фрагмента.

Наконец, массажи кучи в LFH и VS совершенно разные, как описано в разделе 4.4.

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

В этом документе описывается состояние внутреннего устройства пула с момента обновления Windows 10 19H1. Сегментная куча была перенесена в ядро, и для правильной работы ей не нужны метаданные фрагментов. Однако старый POOL_HEADER, который был наверху каждого фрагмента, все еще присутствует, но используется по-другому.

Мы продемонстрировали некоторые атаки, которые могут быть выполнены с использованием переполнения кучи в ядре Windows путем атаки на внутренние компоненты, специфичные для пула.

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

Используемая литература

1. Corentin Bayet. Exploit of CVE-2017-6008 with Quota Process Pointer Overwrite attack. https://github.com/cbayet/Exploit-CVE-2017-6008/blob/master/Windows10PoolParty.pdf, 2017.
2. Corentin Bayet and Paul Fariello. PoC exploiting Aligned Chunk Confusion on Windows kernel Segment Heap. https://github.com/synacktiv/Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion, 2020.
3. Cesar Cerrudo. Tricks to easily elevate its privileges. https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf, 2012.
4. Matt Conover and w00w00 Security Development. w00w00 on Heap Overflows. http://www.w00w00.org/files/articles/heaptut.txt, 1999.
5. Tarjei Mandt. Kernel Pool Exploitation on Windows 7. Blackhat DC, 2011.
6. Haroon Meer. Memory Corruption Attacks The (almost) Complete History. Blackhat USA, 2010.
7. Mark Vincent Yason. Windows 10 Segment Heap Internals. Blackhat US, 2016.

Источник: https://www.sstic.org/2020/presentation/pool_overflow_exploitation_since_windows_10_19h1/
Автор перевода: yashechka
Переведено специально для https://xss.pro
 
Последнее редактирование:


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