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



Дата публикации: 2020-04-22
Автор: Перевод – Анонимный переводчик, ред. @N3M351D4
Теги: , ,

Источник: https://www.fuzzysecurity.com/tutorials/mr_me/1.html, https://www.fuzzysecurity.com/tutorials/mr_me/2.html

Введение

Всем привет! Вы, наверное, думаете: Что мне дадут туториалы по эксплуатации кучи от b33f’а? Я надеюсь, это короткое введение ответит на все вопросы. Как вы наверняка знаете, я очень интересуюсь разработкой эксплойтов.

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

Именно поэтому, когда я увидел этот твит от mr_me, я понял, что я должен сделать что-то ради сохранения его туториалов по эксплуатации кучи Windows.

Я решил, что не буду продлять мой старый домен http://t.co/gvVS4gCKCG, поэтому, если вы интересуетесь переполнением буфера, я рекомендую вам выкачивать содержимое прямо сейчас. — mr_me (@ae0n_) Июнь 25, 2015

Я написал письмо mr_me. Он сказал, было бы круто, если бы я вместе с ним опубликовал его посты на FuzzySec. Все туториалы серии “Переполнение буфера для людей” были сохранены в оригинальном виде. Единственное, что я добавил, это возможность интегрировать их в сайт. Это нужно для того чтобы они коррелировали с общей темой FussySec.

Переполнение буфера кучи. Часть первая

Ранее, изучая переполнение стека, мы получали контроль над регистром инструкций (EIP – Extended Instruction Pointer – расширенный указатель на инструкцию) с помощью обработчика прерывания или напрямую. Сегодня мы рассмотрим техники, проверенные временем. Они позволяют получить контроль за выполнением программы без прямого использования EIP или SEH (Structured Exception Handling – структурированная обработка исключений). С их помощью, перезаписывая некоторые ячейки в памяти определёнными значениями, мы можем добиться перезаписи любого значения DWORD (DOUBLE WORD – двойное машинное слово. Обычно 32 бита) в памяти.

Если вы не знакомы с принципами переполнения буфера стека на среднем или продвинутом уровне, я рекомендую вам сперва сфокусироваться на этой теме. То, что мы будем обсуждать, уже давно устарело, поэтому, если вы ищете новые техники эксплуатации менеджера кучи Windows, не тратьте здесь время 😉

Что вам понадобится:

– Windows XP с установленным SP1

– Отладчик (Olly Debugger, Immunity Debugger, windbg и т.д.)

– Компилятор C/C++ (Dev C++, lcc-32, MS visual C++ 6.0 (если вы сможете его достать)).

– Удобный для вас скриптовый язык (Я использую python, вы можете пользоваться perl)

– Мозги (и/или настойчивость)

– Некоторые знания Ассемблера, C. Также умение использовать плагин HideDbg для Olly или !hidedebug в Immunity debugger

– Время.

Давайте сфокусируемся на базовых понятиях и основах. Скорее всего техники, которые мы рассмотрим уже устарели для использования в “реальном мире”, однако вы должны всегда помнить: если вы хотите двигаться вперёд, вам нужно знать прошлое.

Что такое куча и как она работает в XP?

Куча — это часть оперативной памяти, в которой процесс может хранить данные. Каждый процесс динамически запрашивает (аллоцирует (allocate)) и освобождает из неё кусочки памяти в зависимости от требований приложения. Важно отметить, что стэк растёт сверху вниз, то есть в сторону адреса 0x00000000. Куча в свою очередь растёт снизу-вверх – в сторону адреса 0xFFFFFFFF. Когда процесс дважды вызывает HeapAllocate(), второй вызов вернёт указатель, находящийся выше первого. Таким образом любое переполнение в первом блоке затрагивает второй.

Каждый процесс, пользуется ли он стандартной кучей процесса или динамически аллоцированной, имеет несколько структур данных. Одной из них является массив из 128 структур LIST_ENTRY, которые отслеживают свободные блоки. Он называется FreeLists. Каждый элемент содержит два указателя в начале массива и расположен по смещению 0x178 относительно базовой структуры кучи. Когда куча создаётся, обоим указателям присваивается адрес FreeLists[0]. Они указывают на первый свободный блок доступной памяти.

Давайте рассмотрим это поподробнее. Допустим, у нашей кучи был базовый адрес 0x00650000 и первый доступный блок расположен по адресу 0x00650688. Тогда у нас есть четыре следующих адреса:

0x00650178 (Freelist[0].Flink) указатель на значение 0x00650688 (1-й свободный блок)

0x0065017c (FreeList[0].Blink) указатель на значение 0x00650688 (1-й свободный блок)

0x00650688 (1й свободный блок) указатель на значение 0x00650178 (FreeList[0])

0x0065068c (1й свободный блок) указатель на значение 0x00650178 (FreeList[0])

При аллокации указатели FreeList[0].Flink и FreeList[0].Blink обновляются и указывают на следующий свободный блок. Далее указатели на FreeList перемещаются на конец свежеаллоцированного блока. Они обновляются при каждой аллокации или освобождении памяти (unlink). Таким образом вся работа с памятью отслеживается в двусвязном списке.

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

Эксплуатация переполнения буфера кучи с использованием векторной обработки исключений (VEH)

Для начала взглянем на файл heap-veh.c:

 

Из кода выше видно, что мы используем обработку исключений с использованием блока __try .. __catch. Скомпилируйте этот файл в Windows XP SP1.

Запустив приложение в командной строке, обратите внимание, что для срабатывания исключения потребовалось больше 260 символов (байт).

Самое время запустить его в отладчике. Контроль за исполнением мы получаем при второй аллокации (потому что freelist[0] перезаписан атакующей строкой при первой аллокации).

MOV DWORD PTR DS:[ECX],EAX

MOV DWORD PTR DS:[EAX+4],ECX

Эти инструкции говорят: “Сделай текущее значение EAX указателем на ECX, а текущее значение ECX указателем на ячейку, которая находится через 4 байта после EAX”. Отсюда мы узнали, каким образом мы освобождаем (freeing, unlinking) первый блок аллоцированной памяти.

EAX (что мы пишем) : Blink

ECX (куда мы пишем) : Flink

Так что же такое векторная обработка исключений?

Векторная обработка исключений появилась в Windows XP и хранит структуры зарегистрированных исключений в куче, в отличие от традиционной фреймовой обработки исключений, известной как SEH. SEH хранит эти структуры на стеке. Этот тип исключений вызывается до запуска любых других обработчиков, основанных на фреймах. Ниже представлено объявление этой структуры:

 

Всё, что нужно знать, это что m_pNextNode указывает на следующую структуру _VECTORED_EXCEPTION_NODE. Таким образом мы должны переписать этот указатель. Но какой адрес нам нужен? Давайте взглянем на код, работающий с _VECTORED_EXCEPTION_NODE:

77F7F49E 8B35 1032FC77 MOV ESI,DWORD PTR DS:[77FC3210]

77F7F4A4 EB 0E JMP SHORT ntdll.77F7F4B4

77F7F4A6 8D45 F8 LEA EAX,DWORD PTR SS:[EBP-8]

77F7F4A9 50 PUSH EAX

77F7F4AA FF56 08 CALL DWORD PTR DS:[ESI+8]

Сперва мы перемещаем в ESI указатель _VECTORED_EXCEPTION_NODE и через пару команд вызываем ESI+8. Если мы сделаем так, что указатель на следующую структуру в _VECTORED_EXCEPTION_NODE будет содержать [#адрес_нашего_шеллкода-0x08], тогда мы легко передадим ему управление. Где искать указатель на наш шеллкод? Вот же он, на стеке:

Тут видно адрес нашего шеллкода на стеке. Давайте, особо не напрягаясь, попробуем значение прямо из отладчика – 0x0012ff40. Не забыли, что вызывается esi+8? Чтобы вызвать наш код, нужно сместиться на 8 байт: 0x0012ff40 – 0x08 = 0x0012ff38. Отлично! Тогда ECX будет присвоено 0x0012ff40. Как же нам найти m_nextNode (указатель на следующую структуру _VECTORED_EXCEPTION_NODE)? Сделаем несколько шагов в отладчике дальше, пока не будет подготовлен вызов первого _VECTORED_EXCEPTION_NODE. Тут мы и получим указатель:

77F60C2C BF 1032FC77 MOV EDI,ntdll.77FC3210

77F60C31 393D 1032FC77 CMP DWORD PTR DS:[77FC3210],EDI

77F60C37 0F85 48E80100 JNZ ntdll.77F7F485

В EDI записывается m_pNextNode (тот указатель, что нам нужен). Замечательно, присвоим EAX это значение. Мы пришли к тому, что ECX = 0x77fc3210, EAX = 0x0012ff38. Разумеется, нам нужны смещения для EAX и ECX. Чтобы их найти, сгенерируем msf последовательность и скормим её приложению. Ниже небольшая напоминалочка:

Вычисляем смещения, включив режим скрытия отладки, и дожидаясь срабатывания исключения.

Ок, вот скелет нашего PoC эксплойта:

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

Отлично, в данный момент мы добрались до брейкпойта на “\xcc” и можем просто заменить его каким-то шеллкодом. Помните, что он должем быть меньше 272 байт, потому что это единственное место, где мы его можем разместить.

 

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

Фильтр необработанных исключений (Unhandled Exception Filter – UEF) является последним исключением, которое отрабатывает перед завершением приложения. Именно он ответственен за вывод часто встречающегося сообщения “An unhandled error occurred” (в русской винде “Произошла неизвестная ошибка”), выводящееся при внезапном падении программы. На данном этапе мы уже контролируем содержимое EAX и ECX и знаем смещения для обоих регистров:

 

В отличие от предыдущего примера, наш heap-uef.c не содержит ни следа от обработчика исключений. Это значит, что мы будем эксплуатировать UEF. Ниже содержимое файла heap-uef.c:

 

При отладке этого типа переполнения важно включать анти-дебаггинг в отладчике. Это нужно, чтобы вызывался UEF, а также не менялись наши смещения для регистров. В первую очередь нам нужно понять, где мы должны писать наш dword. Это будет указатель на UEF. Его мы получим, внимательно рассмотрев вызов SetUnhandledExceptionFilter().

Инструкция MOV записывает значение по адресу UnhandledExceptionFilter (0x77ed73b4):

При вызове SetUnhandledExceptionFilter(), в регистр ECX будет записано значение указателя, которое хранится в UnhandledExceptionFilter. Сперва это может запутать, ведь освобождение памяти приводит к присвоению регистра EAX содержимого ECX, но тут особый случай. Мы просто используем побочный эффект функции SetUnhandledExceptionFilter() при присваивании значения UnhandledExceptionFilter. Теперь мы можем с уверенностью сказать, что ECX содержит указатель на наш шеллкод. Следующие строчки должны развеять любые сомнения:

77E93114 A1 B473ED77 MOV EAX,DWORD PTR DS:[77ED73B4]

77E93119 3BC6 CMP EAX,ESI

77E9311B 74 15 JE SHORT kernel32.77E93132

77E9311D 57 PUSH EDI

77E9311E FFD0 CALL EAX

По сути, значение UnhandledExceptionFilter() записывается в EAX и через пару команд происходит вызов по адресу в EAX. То есть UnhandledExceptionFilter() –> [указатель атакующего], затем этот указатель помещается в EAX и получает управление. Его получает наш шеллкод или инструкция, приводящая нас к шеллкоду.

Если мы взглянем на EDI, мы заметим, что указатель смещён на 0x78 байт от конца пэйлоада.

Если мы просто вызовем этот указатель – мы запустим шеллкод. Таким образом EAX должен указывать на инструкцию вида:

call dword ptr ds:[edi+74]

Её легко можно найти во многих модулях XP SP1.

Запишем эти значения в наш PoC и взглянем, куда они нас приведут:

 

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

 

Бум!

Заключение

Мы продемонстрировали две техники эксплуатации unlink() в примитивной форме в Windows XP SP1. Возможно применение и других техник, таких как RtlEnterCriticalSection или TEB (Thread Information Block) Exception Handler. В следующем туториале я покажу, как эксплуатировать unlink() (HeapAlloc/HeapFree) в Windows SP2 и SP3 и обходить защиту кучи.

POC’s

– http://www.exploit-db.com/exploits/12240/

– http://www.exploit-db.com/exploits/15957/

Ссылки

– The shellcoder’s handbook (Chris Anley, John Heasman, FX, Gerardo Richarte)

– David Litchfield (http://www.blackhat.com/presentations/win-usa-04/bh-win-04-litchfield/bh-win-04-litchfield.ppt)