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



Дата публикации: 2020-10-10
Теги: , ,

Источник: https://www.fuzzysecurity.com/tutorials/mr_me/6.html
Перевод: Анонимный переводчик

Ранее мы рассмотрели различные техники обхода механизмов защиты Windows при переполнении кучи. Сегодня же у меня для вас сюрприз. Сегодня мы познакомимся не с очередной техникой атаки переполнением, а узнаем, как использовать уязвимость типа “двойное освобождение” при переполнении.
Многие скажут вам, что двойное освобождение трудно эксплуатировать, но я вас уверяю: зачастую эксплуатация этого бага легче, чем обычное переполнение кучи. Мне всегда было любопытно, как можно эксплуатировать эти уязвимости, ведь нередко такие уязвимости обозначены как критические.

На вашей машине нужно установить:

  • Windows XP с установленным SP2/SP3
  • Immunity Debugger
  • pyparser
  • graphviz
  • heaper.py – плагин к immunity debugger
  • Компилятор C/C++ (Dev C++, lcc-32, MS visual C++ 6.0 (если вы сможете его достать))
  • Удобный для вас скриптовый язык (Я использую python, вы можете пользоваться perl)
  • Мозги (и/или настойчивость)
  • Некоторые знания Ассемблера и языка C. Также умение использовать плагин HideDbg для Olly или !hidedebug для Immunity debugger
  • Некоторые знания о внутренних механизмах работы кучи
  • Время

К сожалению, когда я проводил свой анализ, Immunity Debugger не всегда работал корректно, а несколько раз даже падал. Но это нас не остановит 😉

Что такое двойное освобождение?

Определение из не очень популярной OWASP wiki (https://owasp.org/www-community/vulnerabilities/Doubly_freeing_memory):
“Ошибка двойного освобождения возникает, когда одно и то же значение указателя передается free() более одного раза”
При обсуждении этого определения я пришёл к выводу, что эта уязвимость на самом деле не является переполнением буфера (хотя OWASP говорит именно об этом). Если у атакующего появляется возможность освободить один и тот же блок памяти дважды, то он может использовать это, изменив висящий указатель на чанк в куче и модифицировать метаданные, чтобы произвести определенную атаку.
Допустим у вас есть освобождённый чанк размером 0x24. И пусть последним элементом lookaside[4] является один из указателей, который будет освобождаться дважды. Таким образом вы достаточно легко получите два идентичных указателя, хранящихся в разных аллокаторах кучи.
Если атакующий владеет *некоторым* (на самом деле минимальными) упорством, это может открыть широкий спектр атак, которые мы рассматривали ранее:

  • Перезапись чанка в lookaside при освобождении памяти дважды в lookaside[n] и повторном размещении одного из элементов. Затем перезапись головы списка с висящим указателем и возвращение зловредного flink с целью перезаписи указателя на функцию.
  • Освобождение одного чанка в lookaside, а другого во FreeList. Таким образом, при аллокации чанка из lookaside происходит перезапись flink/blink в элементе freelist. С помощью этой техники можно:
    • Заставить flink указывать на зловредный (поддельный) чанк и вставить его во FreeList[0] после его разделения и возврата после relink. Оставшийся чанк будет вставлен перед поддельным и перезапишет blink поддельного. После этого blink будет указывать на зловредный flink (Брет Мур – FreeList[0] relink атака).
    • Заставить flink указывать на поддельный чанк. В этом случае при запросе чанка возвращается поддельный (Брет Мур – FreeList[0] атака поиском)
    • Заставить flink указывать на элемент Lookaside или таблицу функций. Blink при этом должен указывать на другую функцию, тем самым контролируя адрес, куда указывает вставленный чанк. Это может дать возможность создавать чанки в Lookaside[n] и доставать их оттуда, пока не будет получен зловредный flink (Брет Мур – FreeList[0] атака вставкой)
    • Изменить размер и разместить чанк из элемента списка (после очистки lookaside). Затем в битмапе нужно изменить бит, соответствующий перезаписанному размеру. (Николас Вайзман 2008).
  • Напрямую вернуть чанк, адресом которого является сдвиг от базы кучи. С его помощью можно перезаписать управляющие структуры и получить контроль над кучей (Николас Вайзман 2008)
  • Освободить два чанка во FreeList[n] и аллоцировать один из них. Перезаписать его размер и указатели flink/blink (предполагается, что этот чанк не единственный в элементе FreeList[n]) на другие элементы. Аллоцировать соответствующий размер, что изменит значение бита FreeListInUse соответствующего размера. Это даст возможность атакующему вернуть чанк, адрес которого является сдвигом от базы кучи (Николас Вайзман 2008)

Пример эксплуатации уязвимости двойного освобождения

Начнём с кода. Скомпилируйте его (замечание: я использую ассемблерные вставки(AT&T) для поиска по vtable и вызовы функций):

При внимательном рассмотрении вы заметите, что мы используем указатели на функции приложения и что мы перезаписываем функцию в vtable. Откройте скомпилированную программу в отладчике и установить брейкпоинт сразу после вызова HeapCreate() (0x00401364). Также вызовите следующую команду:

Теперь запустите приложение. После остановки на брейкпоинте с помощью heaper повесьте в куче несколько хуков на размещение и освобождение.

Мы собираемся отследить размещение и освобождения, и, попытаться понять как они работают в процессе.

Вы заметите, что часть кода примера подсвечена. Это таблица виртуальных функций. По сути приложения на С++ имеют vtable для всех объектов (она хранится как первый DWORD по указателю на объект) и содержит функции-члены (методы) этого объекта. В приведённом примере нет объекта, только vtable, содержащая указатели, которые мы позже будем использовать для захвата управления процессом.
Заметьте, что vtable расположена по адресу 0x00404080. Это значение записывается в ECX, происходит вызов функцию по смещению +0x10 от него и управление передается этой функции. Это будет важно далее.
Следующее, что нужно сделать – установить брейкпоинты на последних двух вызовах HeapFree() и вывести дамп по адресу 0x00404080 (vtable). Вы увидите несколько DWORD адресов, которые используют для вызова определенных функций по смещению.

Давайте запустим приложение и проследим за вызовами HeapAlloc() и HeapFree():

Заметьте, если на этом этапе просмотреть содержимое lookaside[4], мы увидим 3 чанка, а во FreeList[4] не будет ни одного (нам ещё предстоит вызвать HeapFree дважды).

Перепрыгнув HeapFree(), мы обнаружим, что lookaside[4] теперь содержит чанк 0x00491e88, хотя этот адрес является некорректным. В lookaside должен был быть помещён 0x00491ef0. Я не знаю точно, почему это происходит в Immunity Debugger. Так или иначе у нас есть другие пути отслеживания действий приложения, поэтому эта проблема нам не помешает.


Теперь, остановившись на втором вызове HeapFree() (вызывающим двойное освобождение) мы видим следующие аргументы:

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

Всё, что осталось сделать – доставать чанки из lookaside[4] до тех пор, пока мы не получим тот, что вызывает двойное освобождение. Несмотря на четыре последовательных размещения из lookaside[4], чанк всё ещё находится в списке lookaside[4]. Нам нужно переписать всего 0x4 байта в чанке, чтобы изменить указатель flink, тем самым косвенно создавая поддельный чанк в lookaside.

Посмотрим на изменённый lookaside[4]:

Помните vtable? Если прокрутить ниже в окне дизассемблера, вы заметите вызов [vtable+0xc] после двойного освобождения. Мы знаем, что указатели на функции занимают DWORD (4 байта), значит всё, что нам нужно – перезаписать 3-й элемент в vtable.

Тут мы просто изменили первые четыре байта в элементе lookaside[4] (теперь выглядит так, что он содержит указатель на первый чанк) и поменять значение EAX на указатель на функцию 0x00404080+0xc.

Если мы сейчас продолжим выполнение, нам вернётся 0040408c как валидный адрес чанка.

Теперь мы можем поместить в него шеллкод. Если мы просто перезапишем первые 4 байта, то при вызове функции будет происходить вызов по контролируемому нами указателю на функцию, передавая управление нашему шеллкоду. Стоит заметить, что раз наша vtable хранится в ECX, мы можем попробовать сместить стек на эту область памяти и таким образом обойти DEP (Data Execution Protection ( предотвращение выполнения данных) – техника защиты, которая запрещает исполнять код из области памяти, помеченной как «только для данных»). Например, так: mov esp, ecx; pop r32; pop r32; pop r32; pop r32; retn.

В заключение

Как я сказал во введении, вы можете использовать практически любой вектор атаки на кучу для получения контроля с использованием двойного освобождения (в зависимости от уровня вашего упорства). Поскольку указатель является висячим, атакующий может переписать 0x4-0x8 байтов с указателями flink/blink (больше в случае, если чанк находится в lookaside) через аллокацию висячего чанка. Это открывает дорогу множеству видов атак на приложение, которые способны обойти механизмы защиты Windows XP SP3. Здесь я просто представил самый простой способ добиться исполнения кода. Однако этого без сомнений можно добиться с помощью двойного освобождения в lookaside и двойной аллокации.
К сожалению, я запомнил, как недавно в IRC один человек пытался меня троллить. В итоге ему удалось показать, что я не знаю, как использовать двойное освобождение. Этот пост посвящается тому плохому человеку (ред.) всем тем, кто постоянно спрашивает, что такое двойное освобождение и как его можно эксплуатировать.

Ссылки:

Уязвимости двойного освобождения // Часть 1 [Мэтью Коновер] – тут
Уязвимости двойного освобождения // Часть 2 [Мэтью Коновер] – тут
Эксплуатация Freelist[0] на XP SP2 [InsomniaSec] – тут