Содержание статьи
Здравствуйте, дорогие любители эксплойтостроения! Сегодня мы познакомимся с немецкой компанией MAGIX AG и ее продуктом Music Maker 16. В процессе нашего знакомства мы найдем уязвимость нулевого дня, а также напишем эксплойт, который будет обходить DEP и ASLR.
Предыстория
Началась эта история с того, что автор, как обычно, на работе с чашкой чая и пончиком в зубах читал новостную ленту (вот так работают в Digital Security). Мое внимание привлек заголовок «MAGIX AG угрожает судебным преследованием специалисту по безопасности». Суть была примерно в следующем: шведский парень Acidgen из тусовки Corelan Team нашел уязвимость в продукте компании MAGIX AG под названием Music Maker 16.
После чего он написал письмо разработчикам, где сообщил всю инфу о баге и сказал, что после патча опубликует ее вместе с PoC. Вроде бы ситуация классическая, и такие истории происходят каждый день с самыми разными компаниями. Однако немцы не оценили добрые намерения шведского хакера и вместо того, чтобы сказать парню спасибо, обратились в суд. По всей видимости, работники MAGIX AG не очень-то в курсе того, как следует себя вести в таких ситуациях. Что ж, им же хуже: сейчас я покажу, как даже без PoC любой баг-хантер может разнести их программу на куски.
Ищем 0day
Скачав триал Music Maker 16, можно начать искать баги. Как известно, обычно уязвимости ищут фаззингом или статическим анализом. Но прежде чем начать использовать артиллерию, следует просто взглянуть на то, как работает программа. Итак, данный продукт предназначен для монтирования и сведения аудиоматериалов. Каждый проект объединяет в себе несколько аудиофайлов, их привязку к дорожкам, времени и так далее.
Собственно, файл проекта, имеющий расширение .mmm, и является первой нашей фаззинговой мишенью. Прежде всего, откроем файл демо-проекта _Demo.mmm в любом HEX-редакторе (например, 010-Editor). Простого взгляда достаточно, чтобы увидеть, как файлы с контентом (звуковые файлы) поступают в проект. Строки с путями и именами файлов просто разделены нулевыми байтами. Поскольку нигде не указан размер этих строк, то можно предположить, что Music Maker определяет конец строки по нулевому байту. Логично предположить, что если ПО не считает размер строки при копировании, а тупо идет до нулевого байта, то возможна классическая уязвимость — переполнение буфера. Чтобы быстро проверить эту гипотезу, руками затрем нули после имени файла, заменив их, к примеру, на 'а'. Выполнив это простое редактирование, можно попытаться открыть файл проекта в программе и… она с треском упадет.
Что ж, теперь еще раз повторим открытие проекта. Только на этот раз присоединимся дебагером (я использую Immunity Debuger) к процессу Music Maker до открытия файла. В результате дебагер покажет причину падения и исключительную ситуацию — чтение по несуществующему адресу. Так как возник Access Violation, это заставит программу перейти к обработчику исключительной ситуации, указатель на который находится в стеке, который… мы тоже переписали значением байта 'a' из .mmm файла. Вот, в общем, мы и нашли 0day-уязвимость без применения тяжелой артиллерии, буквально за пару-тройку минут.
Как я понял, шведский хакер Acidgen нашел другую уязвимость, зато нашу уязвимость параллельно и независимо нашел лидер команды Corelan — Corelancod3r, автор известной примочки pvefindaddr. Но у него свой путь, а у нас — свой. Вообще, неудивительно, что одну и ту же уязвимость находят несколько человек, особенно такую простую и очевидную :).
Эксплойт
Найдя уязвимость, я выложил скриншот бага, чтобы донести до создателей софта простую мысль: для нахождения уязвимости совершенно не нужен никакой PoC, достаточно абстрактного указания «в софтине Music Maker есть бага». Кстати, опубликованный скриншот с ошибкой никаким образом нельзя считать вредоносным кодом, поэтому я абсолютно чист перед законами ФРГ и РФ. Однако очевидно, что информации со скриншота достаточно, чтобы любой другой человек нашел уязвимость. Так и получилось: наш читатель, известный мне под ником @ontrif, без труда докопался до сути проблемы и даже написал эксплойт! Суть его проста: перезаписываем SEH-указатель на адрес инструкции pop REG/pop REG/retn, который предварительно нашли в программе, в какой-нибудь подгруженной ДЛЛ-ке без SafeSEH (что несложно, так как вендор не потрудился включить защиту safeSEH). Это приведет к тому, что когда случится Access Violation, управление перейдет по данному указателю… а там у нас pop/pop/retn. Это значит, что 8 байт из стека уйдут, и указатель ESP опустится на 8 байт выше («опустится выше» — добро пожаловать в матрицу).
А восемью байтами ниже, по правилам игры, должен находиться указатель на наш переписанный SEH-заголовок, только на четыре байта выше, где должен быть указатель на следующее звено SEH-цепочки. Таким образом, RETN возьмет указатель на наш переписанный указатель и вернет ЕМУ управление, то есть EIP будет указывать на переписанный нами стек на то место, где должен быть указатель на следующий SEH. Поэтому вместо указателя надо писать туда инструкцию JMP +0x12 (это для того, чтобы перепрыгнуть указатель на SEH-дескриптор, который у нас, если ты помнишь, четырьмя байтам дальше). Таким образом, имя «файла» во входном .mmm-проекте должно иметь следующий вид:
aaaa…aaaaXXXXYYYYZZZZZZZZZZZ…
- aaaa…aaaa — буфер
- XXXX — указатель на следующий SEH, а на самом деле — 0x909010EB (JMP +0x12/nop/nop)
- YYYY — указатель на SEH-дескриптор, а на самом деле — адрес любой pop/pop/retn инструкции
- ZZZZ — куча NOP и шеллкод
В этом случае ход выполнения программы такой:
- Грузим файл;
- Access Violation;
- Переход по YYYY;
- POP/POP/RETN => Исполняется XXXX;
- XXXX = JMP +0x12;
- Исполняется ZZZZZ, т.е шеллкод.
Все здорово, только эксплойт у меня не заработал. Причин несколько. Поскольку у меня Windows 7 x64, то я защищен DEP и ASLR. Из-за этого выбранное ontrif значение YYYY указывало на dll'ку, которая прыгала в памяти из-за ASLR и BaseFixUP. А даже если поменять YYYY на то, что надо, YYYY и ZZZZ не будут исполнятся, так как DEP не даст исполнится коду из неисполняемого участка памяти — стека. Таким образом, эксплойт работает только в среде Windows XP. Чуть позже ontrif случайно сделал еще одну версию эксплойта, исправив один байт из YYYY на нулевой. В таком варианте Access Violation не происходило, так как программа воспринимала ввод как две строчки (из-за нулевого байта).
При этом переписывалось значение адреса возврата из функции на указатель кучи, причем ровно на место YYYY! Удивительное, магическое везение, как потом описывал данное событие ontrif.
В этом варианте после выхода из функции программа передавала управление в кучу на место YYYY. Это позволяет не думать об ASLR, но не решает проблему с DEP.
ROP-эксплойт
Самое время вспомнить о возвратно-ориентированном программировании. Год назад я уже писал о таких эксплойтах, время повторить изученное, но на более сложном примере. Дело в том, что наш буфер в стеке обрезанный. Хоть мы и переполняем буфер в стеке, мы ограничены длиной, которую программа считывает из файла. После перезаписи SEH у нас остается в стеке ровно 508 байт! Сюда поместится РОП-программа или шеллкод, но и то и то вместе не поместится. Corelancod3r сделал РОП-программу + egg-hunter-шеллкод (он маленький и помещается после РОПпрограммы в оставшийся объем, но он работает до-о-олго, пока ищет в куче основной шеллкод по восьмибайтной метке). Я же мазохист, и мне интересно мгновенное срабатывание шеллкода.
Покопавшись по содержимому стека, я увидел следующее: в стеке до SEH байт 100. В стеке по определенному смещению содержится указатель на кучу, на результат конкатенации пути + имени файла. Причем, если в стеке у нас начало имени файла попорче но, то в куче нет, тем не менее, длина также ограничена.
Моя идея состояла в следующем: переписываем SEH (YYYY)указателем на ROP-инструкцию для выравнивания указателя на стек так, чтобы ESP указывал не куда-то там, а точно на нашу ROP-программу из стека. То есть YYYY должен указывать на ROPгаджет, который меняет указатель ESP, а потом делает RETN, чтобы передать управление следующему ROP-гаджету и ROP-программе в целом. Для поиска гаджетов я использовал уже упомянутую примочку Corelancod3r'а. Все мои гаджеты из двух не поддерживающих ASLR библиотек — LTKRN14N.dll и LTDIS14n.dll. Таким образом, я нашел гаджет ADD “ESP,4F8 # RETN 4” по адресу 0x20012026 (всегда постоянные, так как модуль не поддерживает ASLR). В результате после Access Violation программа переходила по этому адресу и меняла указатель стека, после чего он указывал в зону aaaa…aaaa.
Таким образом, RETN 4 передавала управление по адресу из аааа… аааа, поэтому туда я также добавил адреса, но уже с меньшим сдвигом — ADD ESP, 40 # RETN. В результате указатель стека рос, пока не попадал в зону ZZZZ, где я и расположил РОП-программу.
ROP-программа
ROP-программа представляет собой указатели на РОП-гаджеты, а также некоторые параметры. Главное ограничение — запрет на использование нулевых байт и размер. Так как шеллкод в стек не поместить, моя РОП-программа брала на сохраненный до aaaa…aaaa указатель на кучу, где хранится путь и имя файла, соединенные в одну строку:
PPPP…PPPP/FFFFF…FFFaaaa…aaaaXXXXYYYYZZZZZZ…
- PPPP…PPPP – путь
- FFFF…FFFF – начало имени файла, через которое мы атакуем
- aaaa…aaaa – ROP: 0x20012026
- YYYY - SEH-ROP: 0x20012026
- ZZZZ - ROP-программа
Примерно так. Замечу еще раз, что в стеке у нас поместилась только часть этого буфера: aaaa…aaaaXXXXYYYYZZZZZZ… именно ее мы используем для ROP-программы. Вышеупомянутый указатель указывает на FFFF, так что ROP-программа будет использовать найденную кучу для того, чтобы записать в FFFF параметры для вызова VirtualAlloc. Чтобы выполнить шеллкод, нам надо сделать память исполняемой. Для этого годится, например, вызов VirtualProtect, который может менять флаги доступа на страницы памяти и может сделать ее исполняемой. Но в указанных библиотеках я не нашел вызовы этой функции, зато нашел вызовы VirtualAlloc, которая выделяет память, однако ее можно использовать для «перевыделения» памяти, задав при этом флаг доступа на исполнение, что позволит нам выполнить шеллкод из кучи.
Сама функция VirtualAlloc находится в kernel32.dll, ее адрес не известен из-за ASLR, но так как LTDIS14n.dll используют этот вызов, они сохраняют этот адрес в своей .data-секции, которая постоянна, так как эти ДЛЛ-ки не поддерживают ASLR. В итоге по адресу 0x1FFAF160 хранится указатель на VirtualAlloc. Его я записал в FFFFзону, так как там будет хранится небольшая РОП-программа номер два. РОП-программа «один» (ZZZZZ) вычисляет нахождение РОП«два» (FFFF), считает параметры, сохраняет так же в FFFF, после чего меняет ESP на FFFFF, после чего исполняется ROP-«два», которая делает FFFF исполняемой и передает управление куче, в самом конце FFFF. В конце FFFF-шеллкод также не поместится, поэтому его я решил схоронить в PPPP…PPPP. Таким образом, там можно поместить шеллкод размером до 750 байт, что достаточно для большинства задач. FFFF…FFFF можно условно разбить так:
QQ…QQ1111222233334444WW…WWJJJJJJ…
- QQ…QQ - РОП-программа 2
- 1111222233334444 - место для параметров VA, сюда пишет РОП 1
- WW…WW - вызов VA, передача управления на кучу дальше
- JJJJJJ… - stage 0 шеллкод, прыжок на PPPP…PPPP
Последняя часть нужна, так как выяснилось, что иногда PPPP лежит в другой странице памяти, поэтому WW..WW делает исполняемой только FFFF-часть, а PPPP — нет. Поэтому stage 0 шеллкод вычисляет по сдвигу PPPP, еще раз вызывает VA, делает уже страницу с PPPP исполняемой, после чего передает управление на шеллкод. Ну вот, с теорией и покончено. Как видно, написать эксплойт намного сложнее, чем найти дыру :). Теперь давай перейдем к самому эксплойту, который я написал в виде модуля для Metasploit.
Реализация эксплойта
aaa_data = aaa_header # Заголовок MMM-файла
aaa_data << "x00"1680
aaa_data << aaa_list
aaa_data << "x00"25
Первая строка — путь к файлу
aaa_data << "C:\aaa\"
7. Шеллкод из метасплойта
aaa_data << shellcode
Оставшееся место заполняем
aaa_data << "a"(target['Size']-shellcode.length)
aaa_data << "a"328
Разделитель
aaa_data << "x00"*16
Вторая строка — имя файла
aaa_data << "x"*320
4. Тут начинается РОП-2
aaa_data << rop_gadgets2
5. Тут stage 0 шеллкод
aaa_data << shell_jmp
aaa_data << "a"*61
Продолжение имени файла, эта часть будет в стеке
триггер уязвимости
2. ROP-гаджет: ADD ESP, 40 / RETN
aaa_data << rop_jmp32
aaa_data << "a"16
1. SEH, переписанный YYYY
aaa_data << [target.ret].pack('V')
3. РОП 1 — RETN
aaa_data << rop_nop*10
4. РОП 1 — программа
aaa_data << rop_gadgets
aaa_data << "a"*31337
Цифры в комментариях — порядок выполнения при атаке. Остальной код я не буду приводить в журнале — значительно проще взять готовый модуль с нашего диска и изучать напрямую его.
Послесловие
Как видишь, даже в условиях с ограничениями по размеру данных в стеке можно написать рабочий эксплойт, который обойдет все защиты. Можно было бы воспользоваться и вариантом egg-hunter, но это сильно замедляет атаку. Corelancod3r обещал придумать, как ускорить свой вариант эксплойта с egg-hunter'ом :). Наш же вариант ограничен по размеру шеллкода в 750 байт, зато работает моментально.
Если тебе интересна проактивная, агрессивная сторона безопасности и не только, ты хочешь поделиться своим опытом и набраться чужого, пообщаться с такими же, как и ты, то рекомендую принять участие в первой в России официальной DEFCON-группе. Мы находимся по адресу defcon-russia.ru, и проводим локальные мероприятия (пока только в Питере). На наших встречах будут мастер-классы по различным вопросам ИБ, включая и эксплойт-девелопмент, искусство поиска уязвимостей, вопросы WEB-безопасности, состязания, пиво, общение и прочие составляющие того, без чего не может быть ни одной хакерсокой тусовки. Наша цель — создать локальный hackerspace, который объединяет людей с общими интересами — ИТ, хакинг, программирование и т.д. Хочу сообщить, что в конце года мы поддержим создание первой в России, открытой, НЕЗАВИСИМОЙ и ДЕМОКРАТИЧНОЙ международной конференции по безопасности.
Присоединяйся!