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

Techniques Сегментирование кучи Windows: Атака на аллокатор VS

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 ---> 0x5B1f2Ac9cF5616D9d7F1819d1519912e85eb5C09 для поднятия ноды ETHEREUM и тестов


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

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

Краткий обзор их характеристик:
Цель переполнения _HEAP_VS_CHUNK_HEADER.Sizes.UnsafeSize
Минимальный требуемый размер переполнения 3 байта
Требования к данным переполнения Нет, они могут быть случайными
Уязвимый распределитель чанков ПРОТИВ
Распределитель перекрывающихся фрагментов ПРОТИВ
Сложность обработки кучи Средний/жесткий
Динамическая обратная зависимость Нет (а)
Применимость кучи сегментов режима ядра/пользователя Да (б)

[a]: Динамический lookaside может быть полезен для целей эксплуатации (например, см. [1]), или, по крайней мере, не является большим препятствием. Тем не менее, динамический lookaside отключен в некоторых кучах (например, в пуле сессий). В этих случаях мы не можем использовать техники, которые зависят от динамического lookaside.
: Эти техники не зависят от конкретных конфигураций/оптимизаций сегментной кучи или нижележащего уровня. Поэтому они должны быть применимы к реализации сегментной кучи как в режиме kernelmode, так и в режиме usermode.
Ниже Здесь мы обсудим некоторые специфические аспекты аллокатора VS, который мы будем использовать в дальнейшем. Следует также отметить, что детали, представленные здесь, будут относиться к реализации/конфигурации сегментной кучи в ядре, если явно не указано иное.

Поток запросов динамической памяти

Можно сказать, что запросы динамической памяти проходят через два уровня функций. В функции первого уровня (например, ExAllocateHeapPool /ExFreeHeapPool) мы выполняем некоторые общие операции по обслуживанию/оптимизации, а главная цель - передать запрос памяти соответствующей функции ядру, если явно не указано обратное.

Поток запросов динамической памяти

На высоком уровне можно сказать, что запросы динамической памяти проходят через два уровня функций. В функции первого уровня (например, ExAllocateHeapPool/ExFreeHeapPool) мы выполняем некоторые общие операции по обслуживанию/оптимизации, а главная цель - передать запрос памяти соответствующей функции второго уровня. Функция второго уровня является, по сути, точкой входа для специализированного аллокатора и, по сути, местом реализации сегментации кучи. Сегментированная куча состоит из нескольких аллокаторов, включая аллокатор VS, каждый из которых оптимизирован под диапазон запросов, которые они обслуживают. За исключением некоторых особых случаев (например, специальный пул, включенный для запроса), выбор соответствующего аллокатора функцией первого уровня основывается на размере запроса. Следует отметить, что, кроме различий в конфигурации сегментной кучи, ожидается, что функции первого уровня будут иметь большую часть различий между реализациями сегментной кучи в режимах usermode/kernelmode, в то время как функции второго уровня должны быть очень похожи. Как упоминалось ранее, функции первого уровня отвечают за некоторые операции по обслуживанию, для которых могут потребоваться метаданные. Эти метаданные могут храниться либо в соответствующем сегменте кучи, либо в глобальной таблице, в зависимости от типа запроса. Если метаданные встроены в блок, обработчик первого уровня корректирует размер запроса, чтобы обеспечить достаточно места для дополнительных метаданных перед передачей запроса обработчику второго уровня (например, добавляет 16 байт к размеру запроса для учета _POOL_HEADER). Метаданные обычно добавляются в начало блока, возвращаемого функцией второго уровня, и функции более высокого уровня не должны обращать внимания на метаданные, используемые функциями более низкого уровня.

На двух диаграммах ниже показано представление в памяти чанков со встроенными и отделенными метаданными функций первого уровня:

Встроенные метаданные:х


1670262807366.png


Метаданные лежат отдельно:

1670262880585.png


Запросы, обслуживаемые аллокатором VS

Реализация ядра распределителя VS отвечает за регулярные запросы с размерами, удовлетворяющими одному из следующих условий:

Код:
a. 0 <= size <= 0x1e0 && LFH[size]==disabled
b. 0x1e1 <= size <= 0xfe0
c. 0x1001 <= size < 0x10000 && size % 0x1000 <= 0xfc0

Состояния чанков

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

Использованные чанки - это чанки, которые были выделены, но еще не освобождены(!). Используемые чанки всегда возглавляются структурой _HEAP_VS_CHUNK_HEADER

Код:
1: kd> dt _HEAP_VS_CHUNK_HEADER
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 _HEAP_VS_CHUNK_HEADER_SIZE
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

Краткое описание заголовка:

Размеры: это поле представляет собой структуру _HEAP_VS_CHUNK_HEADER_SIZE и кодируется в памяти. Если предположить, что vs_chunk_header является переменной, которая хранит адрес _HEAP_VS_CHUNK_HEADER, это поле кодируется следующим образом: vs_chunk_header->Sizes.HeaderBits ^ vs_chunk_header ^ RtlpHpHeapGlobals.HeapKey

По крайней мере, HeapKey является 8-байтовым случайным значением, не зависящим от других значений XOR, поэтому все поля в разделе Sizes будут содержать случайное значение независимо от их декодированного содержимого.

Ниже описашим поля в _HEAP_VS_CHUNK_HEADER_SIZE (после декодирования):
MemoryCost: Это поле указывает на количество страниц, занимаемых текущим чанком, кроме страницы, на которой находится заголовок vs чанк. MemoryCost для текущего чанка не обновляется во время выделения, поэтому его значение может быть неточным в некоторых случаях. Это происходит, например, в случае, если выделенный чанк был частью большего чанка, который был разделен, чтобы удовлетворить запрос меньшего размера. В этом случае MemoryCost текущего чанка будет равна стоимости памяти изначально большего чанка. Используется в основном при выделении из FreeChunkTree в качестве разделителя в случае, если существует несколько чанков одинакового размера. В этом случае возвращается чанк с наименьшей MemoryCost.
UnsafeSize: Размер чанка, деленный на 0x10. Например, чанк с размером 0x400 будет иметь 0x40 для UnsafeSize. Это поле является частью значения, используемого во время выделения FreeChunkTree для сравнения различных узлов и поиска подходящего чанка в соответствии с запрошенным размером. Другое использование этого поля - во время объединения, где оно используется для поиска следующего соседнего чанка. UnsafePrevSize: Это размер предыдущего чанка, деленный на 0x10. Одно из применений этого поля - выделение чанка из FreeChunkTree. В этом случае UnsafePrevSize используется для нахождения EncodedSegmentPageOffset предыдущего чанка и определения начала подсегмента vs. Чтобы найти это значение, необходимо пройти через предыдущий чанк, поскольку чанк из FreeChunkTree не будет иметь поля EncodedSegmentPageOffset в своем заголовке (помните, что эти чанки освобождаются). UnsafePrevSize также используется во время коалесценции для определения границ предыдущего соседнего чанка. Allocated: Указывает, выделен/используется чанк или нет. Например, это поле используется во время выполнения процедуры объединения, когда чанк выделяется, чтобы определить, свободны ли предыдущие/следующие чанки, и объединить их, если да. Оно также используется для определения того, имеет ли чанк действительный EncodedSegmentPageOffset во время выделения из FreeChunkTree. Значение этого поля должно быть 0 для свободных и 1 для используемых, но на практике все, что отлично от 0, считается используемым, что в некоторых случаях, как мы увидим позже, может быть удобно для эксплуатации. EncodedSegmentPageOffset: это поле также кодируется. Если предположить, что vs_chunk_header содержит адрес _HEAP_VS_CHUNK_HEADER, то это поле кодируется следующим образом: (vs_chunk_header->EncodedSegmentPageOffset ^ vs_chunk_header ^ RtlpHpHeapGlobals.HeapKey) & 0xff.

Это поле содержит расстояние в страницах от текущего чанка до начала подсегмента VS.
UnusedBytes: указывает, есть ли в чанке неиспользованные байты. Это может произойти, например, когда чанк выделен из FreeChunkTree и больше запрашиваемого размера, но оставшийся чанк был слишком мал для разделения (например, меньше 0x20 - максимального размера заголовка в освобожденном и используемом состояниях). Если это значение равно единице, то неиспользованный размер кодируется в последних двух байтах чанка. Неиспользуемые байты, потенциально добавляемые в конец чанка, похоже, не используются больше нигде в текущей реализации сегментной кучи.

Освобожденные чанки

Чанки, которые были освобождены, можно разделить на три категории:

Чанки, помещенные в динамический lookaside.
Чанки, временно помещенные в список освобождения задержки.
Чанки, размещенные в дереве FreeChunkTree

Динамический Lookaside

Динамический lookaside, вероятно, является оптимизацией функции первого уровня (в режиме ядра), используемой для быстрого перераспределения блоков с горячими размерами, которые попадают в определенный диапазон размеров.
Динамический lookaside сгруппирован в так называемые buckets (_RTL_LOOKASIDE), и каждый bucket отвечает за обслуживание определенного диапазона размеров. Каждый bucket имеет указатель на заголовок односвязного списка, который содержит чанки, являющиеся частью ассоциированного bucket. Структура односвязного списка хранится в части пользовательских данных чанка, возвращаемого распределителем VS (адрес возврата функции 2-го уровня). Во многих случаях это место совпадает с местом, где размещается _POOL_HEADER (интересно, что команда windbg !pool подавится, если заголовок пула поврежден, например, когда чанк является частью lookaside). Для ведения списка используется структура _SINGLE_LIST_ENTRY. Максимальный размер этого списка вычисляется динамически на основе спроса на соответствующий размер в течение предопределенного периода, при этом максимальная глубина составляет 0x100 элементов. Ведро активируется только тогда, когда этот размер не равен нулю.
Во время распределения функция 1-го уровня проверяет, попадает ли размер запроса в допустимый диапазон динамического lookaside, который составляет 0x1e1-0xf70. Если да, то она проверит, есть ли в bucket, связанном с этим размером, доступные фрагменты. Если да, то он не будет проходить через аллокатор VS (функция второго уровня), вместо этого он вернет первый доступный чанк из bucket. Сопоставление размера запроса с размером bucket совпадает с сопоставлением размера запроса 1-го уровня с размером запроса VS allocator, вычисляемым функцией 1-го уровня перед пересылкой запроса, и показано ниже:

Rounded↑ Request SizeBucket Size
0x1f0 - 0x3f0size+0x10
0x400 - 0x7f0ROUND_UP(size+0x10, 0x40)
0x800 - 0xf70ROUND_UP(size+0x10, 0x80)

Код:
#define ROUND_UP(x, n) (x+n-1)&~(n-1)

Так, например, запрос на выделение размером 0x800 будет управляться bucket размером 0x880. Если во время распределения связанное bucket будет включено, но в нем не будет чанков, то распределитель будет проактивно выделять чанки bucket_maximum_list_size/2 и добавлять их в bucket.
Когда чанк освобождается, функция первого уровня будет использовать его метаданные (т.е. _POOL_HEADER.BlockSize), чтобы найти размер чанка первого уровня. Затем она проверит, попадает ли этот размер в диапазон динамического lookaside, а затем посмотрит, есть ли в списке связанного bucket место для нового чанка. Если bucket не достиг своей максимальной глубины, то чанк будет добавлен в динамический lookaside, в противном случае будет выполнена функция второго уровня для надлежащего освобождения чанка.

Список свободных задержек

Список свободных задержек - это специфическая оптимизация VS аллокатора. Это односвязный список, который ведется сразу после _HEAP_VS_CHUNK_HEADER скоро освобождаемого чанка. Максимальная глубина списка установлена на 0x20 записей. Когда этот предел достигнут, все чанки в этом списке освобождаются одновременно. Эта оптимизация потенциально используется для уменьшения фрагментации. Она также ускоряет 97% (0x20/0x21) вызовов освобождения пула, поскольку им не нужно проходить через всю процедуру освобождения. С другой стороны, большая часть прироста производительности преобразуется в накладные расходы для остальных 3% вызовов освобождения пула. Эта оптимизация применяется только для размеров чанков меньше 0x1000, и чанки не могут быть перераспределены в течение периода, когда они являются частью отложенного списка освобождения.

FreeChunkTree

FreeChunkTree представляет собой красно-черное дерево и является основной структурой, используемой функцией VS specific для хранения свободных чанков во время деаллокации, когда запрос больше не подходит под предыдущие оптимизации.
Для итерации красно-черного дерева используется значение узла: UnsafeSize<<16 | MemoryCost , поэтому на практике стоимость памяти используется в качестве решающего фактора, когда существует несколько свободных блоков одинакового размера.

Заголовок, используемый для чанков внутри FreeChunkTree - это _HEAP_VS_CHUNK_FREE_HEADER:

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

Код:
1: kd> dt _RTL_BALANCED_NODE
nt!_RTL_BALANCED_NODE
   +0x000 Children         : [2] Ptr64 _RTL_BALANCED_NODE
   +0x000 Left             : Ptr64 _RTL_BALANCED_NODE
   +0x008 Right            : Ptr64 _RTL_BALANCED_NODE
   +0x010 Red              : Pos 0, 1 Bit
   +0x010 Balance          : Pos 0, 2 Bits
   +0x010 ParentValue      : Uint8B

Как мы видим, чанки здесь по-прежнему имеют заголовки_HEAP_VS_CHUNK_HEADER.Sizes (OverlapsHeader), но в отличие от использованных чанков, они используют пространство после для поддержания заголовка узла дерева черно-красного.

Во время деаллокации чанка, затраты памяти на чанк обновляются, и чанк добавляется в FreeChunkTree. FreeChunkTree используется только во время деаллокации, когда чанк не может быть частью динамического lookaside или когда происходит очистка списка delay free.

При распределении и если динамический lookaside не смог выполнить запрос, используется FreeChunkTree. Распределитель VS итерирует красно-черное дерево и использует стратегию наилучшего соответствия, чтобы определить подходящий чанк для запроса на распределение. Если размер идентифицированного блока больше, чем размер запроса, то, как правило, блок разделяется, а оставшаяся часть блока добавляется обратно в FreeChunkTree. Исключением является случай, когда остаток чанка меньше 0x20 байт, в этом случае параметр _HEAP_VS_CHUNK_HEADER.UnusedBytes устанавливается в 1, и последние два байта чанка устанавливаются в соответствии с фактическим неиспользованным размером.

VS_CONTEXT.Config.Flags.PageAlignLargeAllocs

Дальше мы вспомним про флаг PageAlignLargeAllocs. Когда этот флаг включен, пользовательские данные аллокатора VS будут выравниваться по страницам при больших выделениях. В данном контексте "большое распределение" - это распределение, охватывающее несколько страниц. Метаданные распределителя VS будут размещены на предыдущей странице, а после заголовка VS распределитель добавит дополнительную 16-байтовую прокладку. Дополнительная прокладка, скорее всего, добавляется для того, чтобы весь _HEAP_VS_CHUNK_FREE_HEADER поместился на одной странице. Заголовок должен всегда находиться в памяти, поэтому если он занимает две страницы, то обе страницы должны оставаться в памяти. Таким образом, эта прокладка позволяет аллокатору выделить потенциально еще одну страницу на чанк при освобождении чанка.

Пример того, как может выглядеть "большое" распределение в памяти:

Код:
PAGE OFFSETS:  0             0xfe0                  0xff0      0x1000
               ↓               ↓                       ↓         ↓           
               | other chunks  | _HEAP_VS_CHUNK_HEADER | PADDING | USER_DATA |

После освобождения он будет выглядеть как на следующей иллюстрации, где видно, что весь _HEAP_VS_CHUNK_FREE_HEADER может поместиться на одной странице благодаря вставкам:

Код:
PAGE OFFSETS: 0xfe0                       0x1000
                ↓                            ↓           
                | _HEAP_VS_CHUNK_FREE_HEADER | USER_DATA |
                                                   ^
                            no headers in this page, it can be decommited if user data>PAGE

Из-за реализации оптимизации декоммита, когда включена функция PageAlignLargeAllocs, аллокатор всегда будет искать в FreeChunkTree чанк, который может вместить подложку заголовка (т.е. дополнительные 0x10 байт). Это происходит независимо от того, был ли запрос на выделение для большого chunk. После нахождения подходящего чанка, проверяется, начинается ли чанк со смещения 0xfe0 от его страницы. Если да, то набивка остается, независимо от того, был ли запрос на большой чанк или нет. В противном случае правильный размер чанка передается в RtlpHpVsChunkSplit, который может разделить чанк в зависимости от количества неиспользуемых байт.

Таким образом, FreeChunkTree всегда итерируется для размера чанка, потенциально на 0x10 байт превышающего размер запроса. Побочные эффекты такого поведения могут добавить сложности в некоторые сценарии эксплуатации. Например, допустим, мы создали следующую схему памяти, запросив у аллокатора первого уровня чанки с размерами: 0x7f0, 0x3f0:

Код:
0: kd> !poolview  0xffffc18757802c70 //see [7]
  Address              Size       (Status)      Tag    Type
  ---------------------------------------------------------
  0xffffc18757802050   0x7f0      (Allocated)   NpFr   Vs
  0xffffc18757802860   0x3f0      (Allocated)   NpFr   Vs
* 0xffffc18757802c70   0x370      (Free)               Vs

Учитывая приведенную выше схему, чтобы чанк 0xffffc18757802c70 был потенциально выделен нам обратно, мы должны запросить максимальный размер (помните, алгоритм наилучшего соответствия) 0x360 вместо 0x370. При запросе размера 0x360 распределитель добавит дополнительные 0x10 для заполнения и найдет чанк 0xffffc18757802c70. Затем он вычтет 0x10 из необходимых байт, поскольку смещение страницы адреса не 0xfe0, и передаст чанк для разбиения. Функция разбиения посчитает, что оставшийся размер мал для разбиения, поэтому разбиения не произойдет, а параметр UnusedBytes чанка 0xffffc18757802c70 будет установлен в true.

Другой пример, предположим, что мы находимся в следующем состоянии:

Код:
0: kd> !poolview  0xffffc18757802c70
  Address              Size       (Status)      Tag    Type
  ---------------------------------------------------------
  xffffc18757802050   x7f0      (Allocated)   NpFr   Vs
  xffffc18757802860   x3f0      (Allocated)   NpFr   Vs
* xffffc18757802c70   x370      (Allocated)   NpFr   Vs

Теперь, если мы захотим, например, освободить 0xffffc18757802860 (например, чтобы создать место для уязвимого чанка) и перераспределить его, мы снова столкнемся с проблемой заполнения:

Код:
0: kd> !poolview  0xffffc18757802860
  Address              Size       (Status)      Tag    Type
  ---------------------------------------------------------
  0xffffc18757802050   0x7f0      (Allocated)   NpFr   Vs
* 0xffffc18757802860   0x3f0      (Free)               Vs
  0xffffc18757802c70   0x370      (Allocated)   NpFr   Vs

Чтобы получить обратно чанк 0xffffc18757802860, мы должны выделить максимум 0x3e0 байт вместо исходного 0x3f0.

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

Рвссмотрим некоторые проблемы с которыми мы можем столкнуться с осложнениями при использовании этого подхода:

Динамический lookaside (dl) целевого освобожденного чанка отключен, поэтому он помещается в FreeChunkTree. Если dl меньшего размера запроса отличается от dl исходного размера (например, в случае 0x3e0 и 0x3f0), а dl меньшего размера включен, то перераспределение освобожденного чанка из FreeChunkTree может быть затруднено.
Целевой освобожденный чанк попадает в динамический lookaside исходного размера чанка. Теперь, если меньший размер чанка попадет в другой lookaside, перераспределить нужный освобожденный чанк будет невозможно.
Неиспользуемые байты в конце чанка должны быть учтены при расчете размера переполнения. Например, в описанном выше чанке 0xffffc18757802860 размер переполнения должен быть 0x13, поскольку в конце чанка есть дополнительные 0x10 байт. Если уязвимый чанк будет выделен без дополнения, то переполнение составит всего 3 байта. Этот двойной потенциальный размер переполнения может обеспечить небольшую гибкость, но чтобы им воспользоваться, нужно быть очень точным и внимательно отнестись к пунктам (1) и (2).
Когда дело доходит до распределения чанков, сначала объединяется с потенциально свободными соседними чанками. Если включена функция PageAlignLargeAllocs, и если чанк после освобожденного чанка был свободен, то проверяется и чанк после него. Адреса кандидатов на объединение вычисляются следующим образом:

Код:
Previous Chunk:
prev_chunk_addr = freed_chunk_addr + freed_chunk_sizes.UnsafePrevSize*16

Next Chunk:
next_chunk_addr = freed_chunk_addr + freed_chunk_sizes.UnsafeSize*16

Next Next Chunk:
next_next_chunk_addr = next_chunk_addr + next_chunk_sizes.UnsafeSize*16

Поле _HEAP_VS_CHUNK_HEADER.Sizes.Allocated каждого кандидата используется для определения того, выделены или нет соседние чанки. Если это значение отлично от нуля, то чанк считается выделенным, иначе он свободен. После процедуры объединения мы получаем потенциально объединенный чанк. Если этот чанк пересекает границы страницы, то он разделяется на два чанка, как показано ниже:

Код:
|   PAGE BOUNDARY    |   PAGE BOUNDARY    |   PAGE BOUNDARY    |

  ...  |         MERGED CHUNK BOUNDARIES        |   ...

Is broken down into:

|   PAGE BOUNDARY    |   PAGE BOUNDARY    |   PAGE BOUNDARY    |

  ...  |   CHUNK 1   |          CHUNK 2         |   ...


Первым чанком, который проходит через этот процесс, является самый базовый подсегмент. Когда запрос достигает распределителя VS, а в дереве FreeChunkTree нет подходящего чанка, распределитель VS выделяет новый подсегмент. Размер нового подсегмента будет зависеть от размера пропущенного запрошенного размера. Затем распределитель будет использовать начало подсегмента для хранения его заголовков (т.е. _HEAP_VS_SUBSEGMENT), а оставшаяся часть будет добавлена в FreeChunkTree. Учитывая, что подсегмент VS будет выровнен по странице и что пользовательские данные будут размещены по адресу, выровненному на 16 байт после заголовка _HEAP_VS_SUBSEGMENT, мы знаем, что освобожденный чанк подсегмента будет начинаться по адресу: xxxxxxx030 и его размер будет не менее 0x10000 байт. Поскольку он будет пересекать границы страниц, он будет разбит на два куска, один из которых будет иметь размер: 0xfb0 (0xfe0-0x30), а другой - оставшееся количество страниц.

Резюме

В таблице ниже показаны различные категории чанков, обслуживаемых распределителем VS, а также некоторые их характеристики:

Код:
a. 0 <= size <= 0x1e0 && LFH[size]==disabled
b. 0x1e1 <= size <= 0xfe0
c. 0x1001 <= size < 0x10000 && size % 0x1000 <= 0xfc0


GroupRounded↑ Request SizeActual VS Request SizeLFH DisabledPOOL_HEADERDynamic LookasideDelay Free List
A0 - 0x1e0size+0x10xx x
B0x1f0 - 0x3f0size+0x10n/axxx
C0x400 - 0x7f0ROUND_UP(size+0x10, 0x40)n/axxx
D0x800 - 0xf70ROUND_UP(size+0x10, 0x80)n/axxx
E0xf80 - 0xfd0size+0x10n/ax ~x
F0xfe0size+0x10n/ax
Gall the restsizen/a

Код:
define ROUND_UP(x, n) (x+n-1)&~(n-1)

Техники эксплуатации

Терминология

Уязвимый кусок: это кусок, который в какой-то момент будет переполнен уязвимым приложением. Bruce Banner Chunk: это чанк сразу после уязвимого чанка, но до переполнения. UnsafeSize этого чанка будет перезаписан после переполнения. Наша цель состоит в том, чтобы этот чанк выглядел больше, чем он есть на самом деле. Мы также будем называть его BB чанк.

Изменённый Чанк : это чанк Брюса Баннера после переполнения.

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

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

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


1670266754052.png



Обзор

Целью переполнения кучи будет закодированное поле \_HEAP_VS_CHUNK_HEADER.Sizes.UnsafeSize. Обращаясь к полю UnsafeSize, мы, по сути, изменяем границы чанка, воспринимаемые аллокатором VS. Наша цель - увеличить размер чанка BB, превратить его в измененного и добиться того, чтобы его границы перекрывались с соседним чанком (перезаписываемым чанком). Мы можем запустить эту атаку, когда BB чанк находится в трех различных состояниях:

Allocated Chunk Attack: чанк BB выделен. Чтобы выполнить атаку:
Используя уязвимость повреждения памяти, перезапишите поле UnsafeSize чанка BB.
Освободите чанк BB. Это заставляет аллокатор VS использовать переполненный UnsafeSize, что приводит к преобразованию BB-чанка в мутантный чанк. В этот момент мутантный чанк будет добавлен в FreeChunkTree.

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

Соображения: После освобождения чанка BB на шаге (2), процедура коалесцирования попытается найти, свободны ли соседние чанки, чтобы объединить их. Чтобы найти чанк, следующий за освобождаемым, функция использует UnsafeSize освобожденного чанка, поле, которое мы испортили. Таким образом, адрес следующего чанка будет находиться на случайном смещении от начала освобожденного чанка. Мало того, чтобы проверить, свободен чанк или нет, функция коалесцирования будет использовать \_HEAP_VS_CHUNK_HEADER.Sizes.Allocated, который также закодирован. К счастью, если поле Allocated имеет любое значение, отличное от нуля, то считается, что чанк выделен. Таким образом, даже если мы не имеем контроля над следующим чанком, шансы (255/256), что следующий чанк будет идентифицирован как выделенный, и слияния не произойдет. Также отмечается, что мы можем избежать слияния, если размер мутантного чанка (шаг 2) больше, чем границы нижележащего подсегмента VS, что является вероятным сценарием при атаке MSB. Неспособность предотвратить слияние двух чанков, скорее всего, приведет к проверке ошибки.

Атака узла RB: чанк BB освобожден и находится в дереве FreeChunkTree.
Используя уязвимость повреждения памяти, перезапишите поле UnsafeSize освобожденного BB-чанка. Это должно немедленно превратить BB-чанк в мутант.

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

Соображения: Здесь мы изменяем значение узла, который уже находится в красно-черном дереве. Уникальным преимуществом этого подхода является то, что BB чанк превращается в мутантный чанк немедленно, не требуя никаких промежуточных действий, как, например, деаллокация BB чанка, которая требовалась в атаке "Выделенный чанк". Это позволяет нам избежать вероятности неудачи 1/256, налагаемой другими атаками, из-за проверки свободных чанков, обсуждавшейся в предыдущей атаке. С другой стороны, мы увеличили сложность успешного выполнения атаки. Например, если начальное значение узла BB chunk было x, и x было меньше значения родителя, а после переполнения x стало больше x, то мы не должны быть в состоянии утверждать, что мутантный chunk. С цифрами, допустим, значение родительского узла равно 0x300, значение чанка BB было 0x200, а после переполнения он стал мутантом с размером 0x400. Чанк BB будет левым дочерним узлом родительского узла, так как его значение меньше значения родительского узла. Поэтому после переполнения левая ветвь родителя не будет достигнута, если размер запроса равен 0x400, так как она займет правую дочернюю ветвь родителя.

Coalescing Attack: BB чанк освобождается и помещается в FreeChunkTree. Чтобы выполнить атаку:
Используя уязвимость повреждения памяти, перезапишите поле UnsafeSize освобожденного BB-куска.
Освободите либо предыдущий, либо следующий после BB-чанка чанк. Это должно вызвать выполнение процедуры coalescing, которая объединит освобожденный здесь чанк с чанком BB. Объединенный чанк будет изменённым чанком.

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

Примечания: Требования к компоновке кучи немного более строгие, что увеличивает сложность атаки. Независимо от сложности, сильной стороной этого подхода является то, что мы используем процедуру объединения для "исправления" проблемы поврежденного узла дерева rb, которую мы имели в атаке на узел RB. После шага 2 ошибочно размещенный измененного мутантный чанк будет удален из rb-дерева и снова правильно вставлен в FreeChunkTree на основе его нового размера объединенного чанка. С другой стороны, исправляя поврежденный узел rb-дерева, мы снова вводим процедуру слияния для проверки свободного чанка, что добавляет вероятность неудачи 1/256.

Техника LSB: Переполнение наименее значимого байта

Описание

Изучая эту технику мы стремимся устранить непредсказуемость, добавляемую кодировкой UnsafeSize, устанавливая соответствующую компоновку кучи и нацеливаясь только на наименьший значащий байт (lsb) чанка BB. По сути, мы искусственно ограничиваем потенциальные размеры изменённого чанка после переполнения и удерживаем их в границах чанков, которые находятся под нашим контролем.

Если говорить немного подробнее, то для того, чтобы все работало, мы начнем с того, что чанк BB должен быть больше страницы. Например, допустим, мы выбрали размер 0xc040. Внутри его UnsafeSize мы должны иметь кодировку 0xc04 (помните, что в этом поле хранится размер чанка/16). Наша цель после уязвимости повреждения памяти - перезаписать только первый байт UnsafeSize. Таким образом, теперь чанк будет иметь любой размер между 0xc00-0xcff, в переводе на реальный размер это 0xc000-0xcff0. Учитывая это, если мы будем держать конец переполненного чанка как можно ближе к содержащей его странице (например, по смещению 0x40-headers=0x20 в данном случае), то наши шансы создать достаточно большой перекрывающийся чанк довольно высоки. Например, чтобы перезаписать первые 16 байт перезаписываемого куска пользовательских данных, мы имеем 249/256, 97% успеха (т.е. любой размер >=0x70). Даже в тех случаях, когда нам не удается получить достаточно большой измененный чанк, мы сможем повторить попытку.

Рассмотрим различные этапы этого подхода более подробно. Для этого мы будем продолжать использовать 0xc040 в качестве размера уязвимого чанка. Мы также будем атаковать UnsafeSize уязвимого чанка, пока он находится в выделенном состоянии, поэтому мы будем выполнять ранее описанную "Allocated Chunk Attack".

Схема кучи до переполнения:

1670268049634.png


Важные моменты:

Чанк BB должен быть больше страницы, а конец этого чанка должен быть расположен очень близко к началу связанной с ним страницы. Если промежуток от начала страницы слишком велик, то мы уменьшаем вероятность того, что после переполнения останется достаточно большой измененный чанк. Уязвимый чанк должен находиться близко, в идеале рядом с чанком BB, и мы должны иметь возможность переполнить всего один байт из UnsafeSize чанка BB. Здесь мы должны помнить о потенциальных неиспользуемых байтах в уязвимом чанке.
Перезаписываемый чанк должен занимать всю оставшуюся часть содержащей его страницы. Это важно, поскольку мутантный чанк может быть расширен, например, до размера 0xcff0. Поэтому если мы не выделим хотя бы весь остаток конечной страницы чанка BB, и этот чанк будет передан другому процессу, то после перезаписи этого чанка мы, скорее всего, вызовем неустранимую ошибку.
После перезаписывания чанка мы хотим выделить чанк, который уже под нашим контролем, в данном случае резервный чанк. Этот чанк важен по двум причинам:
Если мы позаботимся о пункте (3), то заголовок VS нашего резервного чанка будет начинаться со смещения 0xfe0 от страницы, когда включена функция PageAlignLargeAllocs. Теперь, если UnsafeSize будет повернут на 0xcff0, это приведет к повреждению VS-заголовка резервного чанка. Так что если мы не будем контролировать это, то очень вероятно, что система упадет в какой-то момент после того, как чанк будет либо выделен, либо освобожден другим процессом. Тем не менее, этот случай довольно маловероятен, 1/256 (т.е. вероятность получить размер 0xcff).
Даже если заголовки свободного блока не повреждены, у нас все равно может возникнуть проблема. Допустим, мы вызвали переполнение, и нам удалось перезаписать часть перезаписываемого чанка. В этот момент заголовок VS перезаписываемого блока должен быть поврежден. В этот момент, если свободный чанк был освобожден, помещен в дерево FreeChunkTree и повторно выделен другим процессом, то произойдет сбой системы, поскольку распределитель vs попытается выяснить, выделен ли предыдущий чанк (т.е. перезаписанный чанк), чтобы использовать его EncodedSegmentPageOffset и вычислить расстояние до нижележащего подсегмента VS. Но поскольку весь VS-заголовок перезаписанного чанка будет поврежден, его поле Allocated, скорее всего, укажет, что чанк выделен (по сути, ненулевое значение с коэффициентами 255/256). Это заставит распределитель VS использовать EncodedSegmentPageOffset перезаписанного чанка, который будет содержать случайное значение (помните, что оно кодируется аналогично UnsafeSize), что приведет к краху системы. Этот сценарий гораздо более вероятен, чем первый, поэтому очень важно выделить резервный чанк.

Теперь посмотрим на состояние памяти после переполнения:

1670268474845.png


Итак, мы задействовали уязвимость и смогли перезаписать lsb размера чанка BB нулем. Следует отметить, что значение перезаписываемого байта не имеет значения, поскольку мы нацелились на закодированное поле. Переписанный байт будет превращен в случайное значение после декодирования поля для использования, и мы будем иметь дело с этой случайностью, используя шаги, описанные в этом разделе. Итак, теперь, после переполнения, хотя переполненный чанк будет занимать 0xc040 байт в памяти, когда аллокатор VS будет обрабатывать этот чанк, он будет считать, что это 0xc5d0 байт из-за измененного UnsafeSize. Так что теперь остается только задействовать VS-распределитель, деаллоцировав чанк. Теперь у нас должен быть измененный чанк с размером 0xc5d0 в FreeChunkTree.

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

1670268555739.png


Что касается перекрывающегося чанка, то в моих заметках важным моментом является выделение всего измененного чанка перекрывающимся чанком (т.е. без остаточного чанка). К сожалению, я не упомянул почему, но при ближайшем рассмотрении я думаю, что это в основном относится к случаю, когда мы выполняем RB Node Attack. В этом случае мы можем избежать 1/256 вероятности провала проверки смежного чанка в процедуре объединения, как было объяснено ранее.

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

Настройка компоновки кучи

В теории подход кажется хорошим, давайте посмотрим, как можно получить соответствующую компоновку памяти при включенном флаге LargeAllocsPageAlign (например, в реализации kernelmode). Мы будем продолжать использовать размер чанка 0xc040 BB, который мы использовали в предыдущих примерах, но опять же это не является обязательным условием. Мы также предположим, что размер уязвимого чанка равен 0x360, но тот же подход может быть использован для большинства размеров < 0x1000.

Ниже приведена диаграмма, показывающая различные состояния атаки для нашего примера, а также описание соответствующих шагов:

1670268704717.png



Выделение чанков BB: распыление памяти чанками 0x200 размером 0xc040 Каждый раз, когда распределителю не удается найти подходящий чанк в FreeChunkTree, он выделяет новый подсегмент 0x20000 и разбивает его на два чанка размером 0xfb0 и 0x1f020, как указано в описании выделения пропущенных чанков. Чанк 0x1f020 будет разделен для обслуживания запроса 0xc040. После выделения чанка 0xc040, оставшийся чанк будет снова разделен на 0xfc0 и 0x12020.

Примечания:

Чтобы получить чанк размером 0xc040 vs, запросите 0xc020 у распределителя 1-го уровня. Как упоминалось ранее, распределитель VS добавит 0x10 для заголовка VS и 0x10 для подбивки в этом случае.
В подсегмент 0x20000 поместятся два чанка 0xc040. Поэтому для 0x200 выделений у нас будет около 0x100 новых подсегментов и, таким образом, мы получим 0x100 чанков размером 0xfb0 и 0xfc0.
На следующем шаге мы разделим 0xfb0 соответствующим образом, чтобы уязвимый чанк был смежным с переполненным чанком. Поскольку этот чанк в конечном итоге будет содержать уязвимый чанк, мы будем называть его уязвимым суперчанком.
0xfc0 в конечном итоге станет перезаписываемым чанком.
Самый первый чанк 0x12020 станет в итоге запасным чанком.
Несмотря на то, что мы использовали 0xc020 в качестве размера запроса первого уровня, мы могли бы также использовать 0xc010, что также немного увеличило бы наши шансы на успех. Единственный размер, которого нам следует избегать, это 0xc030, так как в итоге перезаписываемый чанк и уязвимый суперчанк будут иметь одинаковый размер, и мы не сможем отличить их друг от друга для проведения атаки. Отмечается, что мы не можем использовать размер 0xc000, так как он будет обслуживаться другим распределителем второго уровня (сегментным распределителем).

Восстановление перезаписанных чанков. Мы выделяем 0x200 чанков размером 0xfa0 (1-й уровень выделения). Распределитель vs добавит 0x10 для своего заголовка и 0x10 для "проактивной" прокладки на случай, если чанк начнется со смещения 0xfe0 от своей страницы. Отметим, что 0xfa0 - это максимальный размер, который позволяет перераспределить чанк 0xfc0 vs, и это связано с оптимизацией decommit, о которой мы говорили ранее. В данном случае 0xfa0 также является единственным размером, который полностью захватывает перезаписанный чанк, не вызывая его разделения (т.е. размер оставшегося чанка меньше 0x20).
Уязвимое вырезание чанков. Итак, как мы уже упоминали, механизм распределения FreeChunkTree следует стратегии наилучшего соответствия (по крайней мере, в принципе - не обращая внимания на побочные эффекты проблемы decommit). Теперь у нас есть чанк 0xfb0, и мы хотим, чтобы его конец был выделен как уязвимый чанк. Если уязвимый чанк близок к 0xfb0, наша работа сделана, мы просто утверждаем это с соответствующим размером alloc первого уровня. Но давайте возьмем более сложный размер, например 0x360, и посмотрим, как мы можем сделать его смежным с чанком BB. Стратегия проста. Мы создадим несколько циклов разбиения. В каждом цикле разбиения мы будем запрашивать определенный размер чанка, который приведет к разбиению уязвимого суперчанка (т.е. чанка 0xfb0 в первом цикле). Таким образом, в конце каждого раунда мы получим меньший уязвимый суперчанк, и наша цель - в конечном итоге создать сам уязвимый чанк. Итак, давайте посмотрим, как эта стратегия может быть реализована для уязвимого чанка 0x360:

Splitting RoundSuperchunk Size Pre-Split1st Layer Alloc SizeVS Alloc SizeSuperchunk Size Post-Split
10xfb00x7f00x8100x7a0
20x7a00x3f00x4100x390

В таблице выше видно, что к концу второго раунда FreeChunkTree должно содержать кучу чанков 0x390, примыкающих к чанкам BB. Теперь, когда будет запрошен следующий аллокатор первого уровня размером 0x360, для его обслуживания будет использован один из 0x390 чанков (0x360+0x10 заголовок пула+0x10 заголовок vs + 0x10 padding). Отметим, что в этом сценарии проблема decommit затронет только уязвимый чанк. Уязвимый чанк будет иметь 0x10 неиспользованных байт, которые мы должны будем учесть при расчете того, сколько байт должно быть переполнение.

Примечание: формула для расчета "Superchunk Size Post-Split" (ssps) должна быть похожа на эту: Superchunk Size Post Split = (Superchunk Size Pre-Split) % (Vs Alloc Size) Это важная деталь, в приведенной выше таблице размер vs alloc вписывается только один раз в начальные суперчанки, поэтому расчет размера суперчанки после разделения является простым вычитанием, но это не всегда так. Например, при размере vs alloc 0x300 во втором раунде разбиения мы получим после разбиения суперчанк размером 0x1a0 вместо 0x4a0.
В этот момент у нас должно быть около 0x100 чанков с размером уязвимого чанка (т.е. 0x390) в FreeChunkTree. В качестве меры предосторожности мы можем выделить 0x50 чанков с тем же размером, что и уязвимый чанк, чтобы очистить окружение от потенциального шума (например, уже существующих чанков в динамическом lookaside, мешающих деаллокаций от других процессов). Затем мы запускаем выделение фактического уязвимого блока. Этот чанк должен быть выделен в одном из вырезанных чанков 0x390 предыдущего шага.
Срабатывает переполнение и освобождает чанк BB. В этот момент измененный чанк будет добавлен в FreeChunkTree, и его границы, скорее всего, будут пересекаться с перезаписанным чанком.
Запустите выделение, достаточно большое, по крайней мере, для перезаписи необходимых частей перезаписываемого куска. Например, выделение размером 0xc080 перезапишет 0x20 байт из пользовательских данных перезаписанного куска. В случае атаки RB-узла мы можем избежать проверки свободного чанка, которая происходит во время объединения, если перекрывающийся чанк будет покрывать весь измененный чанк. Это позволит нам избежать 1/256 источника сбоя, о котором говорилось в обзоре. Для этого, используя уже представленный сценарий, мы начинаем выделять чанки размером 0xcff0, уменьшая размер выделения на 0x10 байт в каждой итерации. После каждой итерации мы пытаемся проверить, удалось ли нам успешно переписать перезаписываемый чанк. Если к итерации не удалось создать достаточно большой перекрывающийся чанк с размером около 0xc040, атака не удалась, и мы должны иметь возможность повторить попытку.

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

Техника MSB: Переполнение старшего значащего байта

Описание

В подходе LSB мы обходили случайность, добавляемую кодированием размера, путем ограничения возможных размеров измененного чанка в пределах контролируемой нами области (т.е. BB чанк+перезаписанный чанк). Здесь мы действуем в противоположном направлении, и вместо этого перезаписываем оба байта UnsafeSize BB, потенциально расширяя чанк далеко за пределы контролируемых нами чанков. Учитывая, что даже некоторые наиболее значимые биты поля UnsafeSize остаются неиспользуемыми, перезаписывая оба байта, мы с очень большой вероятностью создадим огромный измененный чанк, который почти наверняка покроет перезаписанный чанк. Но проблема здесь в том, что в итоге мы получим огромный чанк в FreeChunkTree, который, скорее всего, будет охватывать несколько границ чанков, которые, вероятно, будут принадлежать либо разным процессам, либо самому (vs) аллокатору. Если измененный чанк будет использоваться для обслуживания случайных запросов к памяти, то мы безвозвратно испортим состояние других процессов. Кроме того, нам придется запускать другие процессы при попытке выделить перекрывающийся чанк. Мы решаем эти две проблемы следующим образом:

Мы хотим быть первыми и, в идеале, последними, кто использует измененныйй чанк для удовлетворения запросов на выделение памяти. Чтобы максимизировать вероятность использования измененного чанка первыми, мы вводим в FreeChunkTree то, что в дальнейшем будем называть заборными чанками. Эти чанки должны иметь размер выше всех запросов к памяти, которые ожидаются в течение времени атаки, и ниже размера, при котором мы пересекаем границы соседних чанков (т.е. размер BB чанка + размер перезаписываемого чанка). Учитывая, что FreeChunkTree следует стратегии наилучшего подбора, все запросы к памяти с размером, меньшим, чем размер заборных чанков, будут удовлетворены самими заборными чанками, если меньшие чанки не доступны. Единственный способ получить доступ к измененному чанку - создать запрос на выделение большего размера, чем заборные чанки. Здесь предполагается, что заборный чанк будет меньше, чем измененный чанк, что, скорее всего, верно, учитывая возможный диапазон значений размера. Наконец, отмечается, что размер заборного чанка может в небольшой степени повлиять на вероятность неудачи. Это связано с тем, что размер заборного чанка становится минимальным размером, который нам нужно расширить чанк BB, чтобы иметь возможность выделить перекрывающийся чанк. Я не изучал этот вопрос, но устранение этого потенциального источника сбоев не должно быть сложным.
После того, как заборный чанк будет разделен во время выделения перекрывающегося чанка, его оставшийся чанк будет доступен в FreeChunkTree для будущих выделений. Мы хотим минимизировать вероятность того, что этот чанк будет использован каким-либо другим процессом, поскольку он может пересекать границы других чанков вне нашего контроля. Для этого мы используем чанки-ловушки для инъекций. Эти чанки предназначены для перехвата и выполнения запросов памяти, чтобы избежать наступления на мину, которой в нашем случае будет выделение оставшегося чанка. Несколько замечаний о чанках-ловушках:
Перед выделением перекрывающегося чанка мы должны быть осторожны, чтобы установить только те ловушки, которые меньше, чем заградительные чанки. В противном случае, скорее всего, мы будем создавать новые заградительные чанки.
После выделения перекрывающего чанка, особенно если чанки funce были недостаточно большими (например, >0xa000), мы хотим установить несколько больших чанков ловушек. Оставшийся чанк, вероятно, будет очень большим, поэтому мы хотим защитить его этими ловушками.
Для повышения надежности мы можем установить и меньшие ловушки, по своему усмотрению.

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

измененный чанк < перекрывающийся чанк маловероятно, но мы потерпели неудачу, попробуйте еще раз. Следует отметить, что перекрывающийся чанк должен быть больше, чем заборный чанк, и его размер должен быть как минимум равен минимальному допустимому размеру переполнения, например, 0x380 байт в данном сценарии.
Измененный чанк <= оригинальный переполненный + перезаписанный чанк В этом случае мы находимся в аналогичной ситуации с техникой LSB, измененный чанк содержится в пределах контролируемой области, поэтому здесь не нужно многого делать.
Оставшийся чанк <= 0x10000 Оставшийся чанк - это то, что осталось после того, как мутантный чанк был разделен для обслуживания перекрывающегося чанка. Грубо говоря, если этот чанк меньше 0x10000, то мы можем использовать различные размеры ловушечных чанков, чтобы минимизировать вероятность повторного использования этого чанка аллокатором.
остаточный чанк > 0x10000 Это наиболее вероятный сценарий. Здесь чанк остатка, вероятно, пересечет границы самого подсегмента. Чтобы избежать этого, мы вводим в аллокатор VS большие чанки-ловушки. Мы можем установить, например, ловушечные чанки размером 0xdf00, но мы должны сделать это после выделения перекрывающегося чанка, чтобы избежать вмешательства в заградительные чанки. Ловушечные чанки для этого сценария важны, когда заградительные чанки относительно малы.

Настройка компоновки кучи

Здесь мы рассмотрим подход к установке схемы памяти, необходимой для атаки. В целом, для удобства наша стратегия будет очень похожа на ту, что используется в технике LSB. Тем не менее, следует отметить, что требования здесь не такие строгие, поэтому в зависимости от аппетита к сбоям могут быть применены различные/более простые стратегии. Единственным строгим требованием является последовательное выделение уязвимого куска, куска BB и перезаписываемого куска, не обязательно смежных друг с другом, но содержащихся в области кучи с кусками под нашим контролем. В идеале нам также нужен запасной чанк по причинам, описанным в подходе LSB. Учитывая это, для данного примера мы будем считать, что размер 1-го запроса на выделение для уязвимого чанка равен 0x350, BB чанка - 0x300, а перезаписываемого чанка - 0xc020.

Ниже приведена диаграмма и описание каждого шага атаки:

1670269955888.png



Выделите чанки для ограждения. Выделите куски 0x200 с размером 0xc100.

Фактический размер не так важен, но он должен быть достаточно большим, чтобы удовлетворить запросы памяти, поступающие во время временного окна, в котором мы проводим атаку.
Они не должны быть слишком большими, т.е. больше 0x330 + 0xc040 = 0xc370, так как они будут перекрывать свободный чанк.
Размер заборных чанков должен быть меньше, чем перекрывающийся чанк, иначе при попытке выделить перекрывающийся чанк мы получим обратно инъекционные чанки вместо измененного чанка.
Чтобы предотвратить объединение с соседними чанками после их освобождения, мы также запрашиваем предыдущий и следующий чанки заборных чанков. В данном случае это чанки с размерами 0xfb0 (предыдущий чанк - начало подсегмента) и 0xee0 (предыдущий/следующий чанк - остаток чанков от выделения 0xc100) Примечание: для простоты в этом примере мы использовали большие заградительные чанки, чтобы избежать дополнительной сложности, связанной с требованием хотя бы некоторых инъекционных чанков-ловушек.

Как отмечалось ранее, в подходе MSB мы резервируем большой чанк для перезаписываемого чанка. Поэтому мы начинаем с распыления памяти кусками 0x200 по 0xc040 байт. В итоге мы снова получаем 0x100 кусков размером 0xfb0 и 0xfc0. В данном случае:

Здесь чанк 0xfb0 будет использоваться в качестве уязвимого и BB суперчанка.
Чанк 0xfc0 будет запасным чанком.

Восстановление запасных чанков: создайте 0x300 запросы на 0xfa0 байт из 1-го уровня alloc.
Вырезание уязвимых/BB чанков. Стратегия разбиения здесь намного проще по сравнению с LSB. В нашем примере у нас есть уязвимый чанк с vs размером 0x370 (0x350+0x10 заголовок пула+0x10 заголовок vs). Поэтому мы выбираем размер vs alloc для первого раунда разбиения так, чтобы его оставшийся чанк был достаточно большим, чтобы вместить только один чанк размером 0x370. Во втором раунде разбиения мы используем уязвимый размер чанка. Остатком последнего раунда разбиения будет чанк BB, размер которого не должен быть особенно важен. Когда речь заходит о состоянии чанка BB перед переполнением, у нас есть два варианта:

Выделить его. Это потребует выделения чанка BB после уязвимого чанка, что будет проблематично в том случае, если выделение уязвимого чанка и возникновение повреждения памяти связаны между собой. Это проблематично, поскольку уязвимый кусок выделяется до BB-куска (поэтому состояние BB-куска будет свободным, а не выделенным, как мы предполагали для этого сценария). Предположим, что выделение и срабатывание уязвимости не могут быть разделены, поскольку их разделение решило бы нашу проблему. В этом случае мы могли бы временно запросить группу чанков того же размера, что и уязвимый чанк во время раунда разделения 1, а затем выделить чанк BB. После этого мы можем освободить группу временно выделенных чанков, что должно оставить в FreeChunkTree/Dynamic Lookaside около 0x100 возможных кандидатов на уязвимый чанк.

Сохраните его свободным и запустите соответствующую атаку в соответствии с его состоянием (например, Coalescing Attack). Чтобы эта опция была доступна, у нас должен быть остаточный чанк после второго раунда разбиения размером не менее 0x20 байт (минимальный размер vs размер чанка). Этот остаточный чанк будет использоваться в качестве BB чанка. В идеале, но не очень важно, вы хотите, чтобы BB чанк попадал в пределы LFH (куча с низкой фрагментацией), чтобы исключить возможность того, что распределитель vs передаст наши чанки другим процессам. Преимущество этого подхода в том, что мы можем напрямую выделить уязвимый чанк после первого раунда разбиения. У нас также нет проблемы decommit при выделении уязвимого блока, которая существовала, когда блок BB находился в состоянии выделения. К концу первого раунда разбиения у нас должно быть не менее 0x100 возможных кандидатов на уязвимый чанк.

Здесь мы используем вариант (a) и будем считать, что выделение уязвимого чанка и срабатывание уязвимости связаны друг с другом Информация о раундах разбиения показана ниже:


Splitting RoundSuperchunk Size Pre-Split1st Layer Alloc SizeVS Alloc SizeSuperchunk Size Post-Split
10xfb00x8f00x9100x6a0
20x6a00x3500x3700x330

Выделение чанков BB: создайте запросы на выделение 0x200 памяти размером 0x300. Они должны восстановить чанки, созданные после разделения раунда 2 предыдущего шага. Обратите внимание, что мы изменили размер 1-го запроса на 0x300 вместо 0x310 для захвата чанка размером 0x330 против размера 0x300, чтобы учесть проблему с декомитом.
Установите заборные чанки: деаллокация чанков, выделенных на шаге (1)
Освободите группу чанков, имеющих тот же размер, что и уязвимый чанк, временно выделенный в шаге 3
Создайте запросы на выделение 0x50 с тем же размером, что и размер уязвимого чанка, чтобы очистить среду. После этого вызовите выделение уязвимого чанка/переполнение кучи.
Теперь мы должны быть в состоянии после переполнения, показанном на диаграмме выше. Освободите группу чанков BB, выделенных в шаге (3). Теперь у нас должен быть измененный чанк в дереве FreeChunkTree. При отладке вы можете найти этот чанк, выполнив следующую команду:

dx @$scriptContents.vs_freechunktree_stats(0x10000) //это вернет все чанки размером более 0x10000 байт из текущей выбранной кучи, см. [8].
Выделите перекрывающийся чанк: запросите чанк размером 0xc110 (близко, но больше, чем чанки ограждения). Затем проверьте, удалось ли нам разделить переполненный чанк. Если нет, повторите этот шаг, так как наш запрос мог быть выполнен другим чанком. Предположим, что атака не удалась после примерно 0x200 попыток.

Если мы успешно выполнили шаг (10), то у нас должно быть что-то похожее на конечное состояние в нашем примере диаграммы. Мы успешно выделили перекрывающийся чанк поверх перезаписанного чанка. В этот момент мы хотим повысить привилегии и исправить состояние кучи для изящного выхода.

  1. @OnlyTheDuck, @paulfariello: https://github.com/synacktiv/Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion
  2. @yarden_shafir: https://i.blackhat.com/USA21/Wednes...ked-Pool-The-Good-The-Bad-And-The-Encoded.pdf
  3. @scwuaptx: https://speakerdeck.com/scwuaptx/windows-kernel-heap-segment-heap-in-windows-kernel-part-1
  4. @MarkYason: https://www.blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals-wp.pdf
  5. @_vepe: https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation
  6. @alexjplaskett: https://research.nccgroup.com/2021/...ting-the-windows-kernel-ntfs-with-wnf-part-2/
  7. @yarden_shafir: https://github.com/yardenshafir/Poo...5d45d4f5a70bb8bc6e65de2/PoolData/PoolData.cpp
  8. @_vepe: https://github.com/vp777/exploit-dev/segment_heap.js
 
Пожалуйста, обратите внимание, что пользователь заблокирован
 


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