Анализ встроенных механизмов защиты от переполнения кучи в Windows XP SP2.

Анализ встроенных механизмов защиты от переполнения кучи в Windows XP SP2.

В статье приведен подробный анализ нового механизма защиты кучи, встроенного в Microsoft Windows XP Service Pack 2, а так же описываются найденные уязвимости защиты, которые позволяют при определенных условиях перезаписать любую область памяти и как следствие выполнить код.Помимо этого, приводится метод эксплуатации, позволяющий размещать зловредный код в любой области памяти, благодаря которому можно обойти технологию DEP (Data Execution Prevention).

Alexander Anisimov, Positive Technologies.

( aanisimov[at]ptsecurity.ru, http://www.ptsecurity.ru )

Введение

Защита памяти (DEP)

Предотвращение выполнения данных (DEP) — это набор программных и аппаратных технологий, позволяющих выполнять дополнительную проверку содержимого памяти и предотвращать запуск злонамеренного кода. Windows XP с пакетом обновления 2 (SP2) и Windows XP Tablet PC Edition 2005 позволяют выполнять указанную проверку как программным, так и аппаратным образом.

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

Аппаратная реализация компонента DEP использует возможности процессора, чтобы присвоить определенным областям памяти специальный атрибут, показывающий, что из этих областей не может запускаться код. DEP работает на уровне страниц виртуальной памяти и, как правило, отмечает какую-либо область памяти, изменяя один бит (NX) элемента таблицы страниц (PTE).

Sandboxing

Для дополнительной защиты, добавила программные проверки переполнения буферов в стеке и в куче (“sandboxing”). Для защиты буферов в стеке, все бинарные файлы в системе были перекомпилированы и добавлены проверки целостности стекового фрейма. Т.е. пролог функции теперь добавляет в стек некоторое число (“cookie”) и перед тем как выйти из функции эпилог проверяет целостность этого числа. Эта защита присутствует только в системных файлах и другим приложениям и библиотекам нужно по-прежнему самим беспокоиться о возможных уязвимостях переполнения буфера.

Помимо этого, изменился и алгоритм работы кучи, теперь каждый выделяемый блок памяти в куче также содержит специальный маркер (“cookie”). В отличие от защиты стека, защита кучи работает с любыми приложениями и runtime библиотеками, которые динамически выделяют память. Изменения коснулись только диспетчера кучи, поэтому все приложения, которые запускаются в Windows XP c установленным SP2 защищены от потенциальных уязвимостей переполнения буфера в куче.

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

Дизайн Кучи

Куча (heap) - это область зарезервированного адресного пространства размером в одну или более страниц, из которой диспетчер куч может динамически выделять память меньшими порциями. Диспетчер куч (heap manager) представляет собой набор функций для выделения и освобождения памяти которые локализованы в двух местах: в ntdll.dll и ntoskrnl.exe.

Каждый процесс получает при создании стандартную кучу (default heap), размер которой по умолчанию равен 1 Мб и по мере необходимости автоматически увеличивается. Стандартную кучу процесса используют не только Win32-приложения, но и многие функции runtime библиотек, которым нужны блоки временной памяти. Процесс может создавать дополнительные закрытые кучи вызовом HeapCreate, а также, вызвав HeapDestroy, освободить занимаемое ею виртуальное адресное пространство. Выделение и освобождение блоков памяти соответственно происходит с помощью вызовов HeapAlloc и HeapFree.

[*] Подробнее о функциях управления кучами см. справочную документацию по Win32 API.

Память в куче выделяется участками, называемыми единичными областями

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

Для управления памятью для каждого блока создается заголовок, который также кратен размеру единичной области (рис 1, 2.). Поэтому истинный размер блока при выделении будет высчитываться исходя из запрашиваемой величины памяти и размера заголовка.

Рис.1. Заголовок выделенного блока памяти.

Рис.2. Заголовок свободного блока памяти.

Где:

Size – размер блока памяти (реальный размер блока памяти с заголовком / 8);

Previous Size – размер предыдущего блока (реальный размер блока памяти с заголовком / 8);

Segment Index – индекс сегмента в котором находится блок памяти;

Flags – флаги:

- 0x01 - HEAP_ENTRY_BUSY
- 0x02 - HEAP_ENTRY_EXTRA_PRESENT
- 0x04 - HEAP_ENTRY_FILL_PATTERN
- 0x08 - HEAP_ENTRY_VIRTUAL_ALLOC
- 0x10 - HEAP_ENTRY_LAST_ENTRY
- 0x20 - HEAP_ENTRY_SETTABLE_FLAG1
- 0x40 - HEAP_ENTRY_SETTABLE_FLAG2
- 0x80 - HEAP_ENTRY_SETTABLE_FLAG3

Unused – количество неиспользуемых байт (количество байт дополнения);

Tag Index – индекс метки;

Flink – указатель на следующий свободный блок памяти;

Blink – указатель на предыдущий свободный блок памяти.

Размер блока в единицах выделения важен для управления списком свободных блоков, сортируемых по размеру и информация о которых хранится в массиве 128 двунаправленных списков внутри заголовка кучи (Рис. 3, 4.). Свободные блоки от 2 до 127 единиц выделения хранятся в списках, соответствующих их размеру (индексу). Например, все свободные блоки размером в 24 единицы хранятся в списке с индексом 24, т.е. в Freelist[24]. Список с индексом 1 (Freelist[1]) не используется, т.к. блоки размером в 8 байт не могут существовать, а список с индексом 0 используется для хранения блоков памяти больше 127 единиц (больше 1016 байт).

Рис. 3.

Если при создании кучи флаг HEAP_NO_SERIALIZE был сброшен, а так же флаг HEAP_GROWABLE был установлен (что является установками по умолчанию), то для ускоренного выделения и освобождения небольших блоков памяти (до 1016 байт) создается еще 128 дополнительных ассоциативных списка (lookaside lists, рис. 3, 4.). Которые предназначены для хранения только небольших блоков до 127 единиц выделения, т.е. индексы 0 и 1 не используются. Первоначально ассоциативные списки пусты и разрастаются по мере освобождения блоков памяти.

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

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

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

Вероятность попадания свободного блока (до 1016 байт) в lookaside список довольно большая - 97% (в приложениях, которые сильно нагружают кучу).

Двусвязный список, freelist[n]

Односвязный список, lookaside[n]

Рис. 4.

Переполнение буфера в куче.

Рассмотрим довольно простой пример уязвимой функции:

HANDLE h = HeapCreate(0, 0, 0); // default flags

DWORD vulner(LPVOID str)
{
	LPVOID mem = HeapAlloc(h, 0, 128);

	// <..>
	strcpy(mem, str);
	// <..>


return 0;
}
Как мы видим, в функции vulner() копируется строка по указателю str в выделенный блок памяти buf, без проверки на размер. Передав этой функции строку, размером больше чем 127 байт, мы тем самым выйдем за пределы буфера и затрем соседние данные в памяти.

Как правило, сценарий эксплуатирования уязвимости переполнения кучи развивается следующим образом:

Если, в момент переполнения буфера, присутствует соседний блок и он свободен, то подменяются указатели на следующий (Flink) и предыдущий (Blink) свободные блоки памяти (Рис. 5).

Рис. 5.

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

mov dword ptr [ecx],eax
mov dword ptr [eax+4],ecx

EAX - Flink
ECX - Blink

Например, указатель Blink можно подменить адресом фильтра необработанный исключений (UEF - UnhandledExceptionFilter), а Flink адресом инструкции, которая передаст управление шеллкоду.

В Windows XP SP2 алгоритм выделения изменился - прежде чем удалить свободный блок из двунаправленного списка происходит проверка достоверности указателей на предыдущий и последующий блок памяти (safe unlinking, рис. 6.):

Рис. 6.

  1. Free_entry2 -> Flink -> Blink == Free_entry2 -> Blink -> Flink
  2. Free_entry2 -> Blink -> Flink == Free_entry2
7C92AE22   mov         edx,dword ptr [ecx]
7C92AE24   cmp         edx,dword ptr [eax+4]
7C92AE27   jne         7C927FC0
7C92AE2D   cmp         edx,esi
7C92AE2F   jne         7C927FC0
7C92AE35   mov         dword ptr [ecx],eax
7C92AE37   mov         dword ptr [eax+4],ecx

Помимо этого изменился и заголовок блока памяти, теперь появилось новое поле "cookie" размером в один байт, которое содержит заранее определенное уникальное значение и является своеобразным проверочным маркером, который может сигнализировать о повреждении заголовка (рис. 7.).

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

(&Block_header >> 3) xor (&(Heap_header + 0x04))

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

Если хоть одна из проверок завершается неудачей, то куча считается разрушенной и генерируется исключение.

Рис. 7.

Первый недостаток, который был выявлен при анализе диспетчера кучи - это наличие проверки “cookie” только тогда, когда свободный блок выделяется, при освобождении никаких проверок нет. Правда, в этой ситуации практически ничего сделать нельзя, кроме как изменить размер блока и тем самым поместить его в произвольный freelist.

Второй недостаток - при работе с lookaside списками не реализовано никаких проверок целостности заголовка - нет даже проверки достоверности значения "cookie".

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

Сценарий эксплуатирования этого недостатка может развиваться следующим образом:

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

Далее, если произойдет выделение этого блока памяти, то подмененный указатель Flink будет скопирован в заголовок ассоциативного списка и при следующем выделении, HeapAlloc() возвратит этот подмененный указатель.

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

На практике эта техника была успешно опробована при тестировании уязвимости переполнения буфера в куче в компоненте Windows winhlp32.exe, опубликованной командой xfocus:

http://www.xfocus.net/flashsky/icoExp/index.html

Результат успешной атаки:

1) Перезапись произвольной области памяти (до 1016 байт) - appendix A;

2) Выполнение кода - appendix A ;

3) Обход DEP - appendix B.

Хронология

10/09/2004 Обнаружена проблема

12/21/2004 Отправлено сообщение в Microsoft с описанием проблемы

12/22/2004 Получен ответ

12/22/2004 Отправлен “Proof-of-Concept” код

Решение

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

http://www.ptsecurity.ru/ptmshorp.asp

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

Внимание: включение этого флага может снизить производительность приложения.

Где кванты и ИИ становятся искусством?

На перекрестке науки и фантазии — наш канал

Подписаться