Привет всем бинарщикам и людям, которые хотят попрактиковаться с эксплуатацией уязвимости переполнения буфера. Мы для своего киберполигона придумываем различные сценарии атак и насыщаем его различными мисконфигурациями и многочисленным уязвимым ПО для тренировок “синих” и “красных”. И рассматриваемое ПО в данной статье, как раз одно из таких. В этой статье мы разберем весь процесс от идентификации уязвимости до получения обратной оболочки со скомпрометированного хоста.
Специально для этого примера я скомпилировал уязвимый TCP-сервер, который принимает подключения через порт 3301 (
Что нам понадобится:
1. Атакуемая машина. Лаба на базе Windows 7 (взял ее, чтобы не запускать уязвимый сервер на основной ОС).
2. Средства отладки (Immunity Debugger, mona.py (github.com/corelan/mona) - скрипт Python, который можно использовать для автоматизации и ускорения определенных поисков при разработке эксплойтов).
3. Уязвимый TCP-сервер (скачать можно с моего Github).
4. Атакующая машина Kali Linux.
Наш сервер состоит из двух файлов: Server_B0f.exe и lib.dll.
Давайте запустим его:
Для имитации формата “blackbox” просканируем наш целевой хост с атакующей машины Kali:
Убеждаемся, что порт 3301 открыт и доступен. Приступаем к подключению:
После ввода команды HELP мы видим список команд, которые мы можем использовать. Теперь нам нужно определить, при использовании какой из команд мы можем вызвать переполнение. Для этого воспользуемся техникой фаззинга. Фа́ззинг (англ. fuzzing) — техника тестирования программного обеспечения, часто автоматическая или полуавтоматическая, заключающаяся в передаче приложению на вход неправильных, неожиданных или случайных данных.
Мы будем использовать в каждой команде аргумент длинной в 3000 символов. Например, символов "А". Напишем небольшой скрипт на Python:
В ходе проверки каждой из команд выяснилось, что переданный аргумент длиной в 3000 символов в команде BRUN привел к “падению” сервера:
Теперь давайте перезапустим сервер, откроем отладчик, “приаттачимся” к процессу и запустим наш скрипт снова:
Видим, что регистр EIP был перезаписан символами "А". Таким образом, с помощью простого фаззера нам удалось обнаружить уязвимость в приложении! Когда происходит перезапись, это приводит к переполнению стека. Мы воспользуемся этим, чтобы обмануть центральный процессор и заставить его выполнить любой код, который нам нужен.
Теперь нам нужно найти место для нашего шеллкода. Следующим нашим шагом будет определение 4-х байт, которые перезаписывают регистр EIP. Чтобы это сделать, мы будем отправлять уникальную строку вместо символов А, которую мы можем сгенерировать с помощью следующей команды:
Модифицируем наш скрипт, вставив ранее сгенерированную строку:
Перезапускаем сервер и запускаем скрипт:
В EIP видим новое значение - “43396F43”. Воспользуемся командой msf-pattern_offset, чтобы определить смещение в нашей строке:
Отлично! Смещение составляет 2007 байт. Чтобы убедиться, что мы контролируем регистр EIP, давайте перепишем код нашего эксплойта следующим образом: мы попробуем перезаписать EIP 4-мя байтами “B”, а также дозаполним оставшимися байтами “C”:
Запустим эксплойт:
Отлично! Также мы видим, что регистр ESP указывает на начало буфера “C” , который мы поместили в полезную нагрузку сразу после 4-ех байтов “B”.
На данный момент мы имеем контроль над регистром EIP. Наша следующая задача - найти способ перенаправить поток выполнения на шеллкод, расположенный по адресу памяти, на который указывает регистр ESP во время сбоя. Наиболее грамотный подход заключается в том, чтобы попробовать заменить символы B, которые перезаписывают EIP, на адрес, который появляется в регистре ESP в момент сбоя. Однако, как мы уже говорили, значение ESP меняется от сбоя к сбою. Поэтому жесткое кодирование определенного адреса стека не будет надежным способом.
Мы можем хранить наш шеллкод по адресу, на который указывает ESP, но нам нужен способ, чтобы этот код выполнился. Одним из решений является использование инструкции JMP ESP, которая, как следует из названия, при выполнении "перепрыгивает" на адрес, указанный в ESP. Если мы сможем найти адрес, содержащий эту инструкцию, мы можем направить EIP на этот адрес, и в момент сбоя будет выполнена инструкция JMP ESP. Этот переход приведет поток выполнения в наш шеллкод. Многие библиотеки в Windows содержат эту часто используемую инструкцию, но адреса, используемые в библиотеке, должны быть статическими, что исключают библиотеки, скомпилированные с поддержкой ASLR. И адрес инструкции не должен содержать никаких "плохих" символов, которые могли бы сломать наш эксплойт (о них мы поговорим чуть позже), так как адрес будет частью нашего входного буфера.
P.S. Для упрощения задачи я отключил на уровне системы ASLR и DEP. Обход ASLR/DEP - это отдельная тема, которая заслуживает написание отдельной статьи.
Итак, сначала мы запросим информацию обо всех библиотеках DLL, загруженных нашим уязвимым сервером в память процесса, с помощью команды “!mona modules”:
Находим lib.dll (этот файлик как раз лежит рядом с Server_B0f.exe). Попробуем найти в нем JMP ESP:
Выберем, например, инструкцию JMP ESP по адресу 0x62501559. Перейдем по адресу и убедимся в "существовании" инструкции:
Обновим наш эксплойт и перезапишем EIP адресом инструкции JMP ESP в формате Little endian:
P.S. Как правило, формат, используемый для хранения адресов в памяти, зависит от архитектуры операционной системы. Little endian в настоящее время является наиболее распространенным форматом, который используется в архитектуре x86.
Запустим эксплойт и убедимся, что мы корректно перезаписали регистр EIP:
Следующим нашим шагом будет проверка на наличие “плохих” символов. В зависимости от приложения, типа уязвимости и используемых протоколов могут существовать определенные символы, которые считаются "плохими" и не должны использоваться в нашем передаваемом буфере или шеллкоде. Одним из примеров распространенного плохого символа является нулевой байт 0x00. Этот символ считается плохим, потому что нулевой байт также используется для завершения строки в языках низкого уровня, таких как C/C++. Это приведет к тому, что наш буфер обрежется на первом попавшемся нулевом байте. Чтобы проверить наличие “плохих” символов, мы отправим в приложение символы от 01 до FF (за исключением 00, так как мы уже определили, что он “плохой”) и вручную проверим результаты в отладчике.
Перезапустим сервер, подключим отладчик и установим точку останова (F2) на ранее найденной инструкции JMP ESP:
Запустим эксплойт:
Как мы видим, в отладчике никакие символы не искажаются.
Перейдем к генерации шеллкода и обновления нашего шеллкода. Для генерации шеллкода мы воспользуемся командой msfvenom. Мы будем использовать полезную нагрузку Meterpreter, укажем IP-адрес и порт нашего слушателя. Стандартным методом выхода в шеллкоде Metasploit после его выполнения является функция ExitProcess. Этот метод завершит весь процесс службы. Мы можем попытаться избежать это, используя ExitThread, который завершит только затронутый поток программы. Это позволит нашему эксплойту работать, не прерывая обычную работу уязвимого сервера. В качестве “плохого” символа мы укажем “0x00” (так как не были найдены иные). А также мы будем кодировать нашу полезную нагрузку и для этого укажем кодировщик “xor_dynamic” (XOR-кодировщик с динамическим размером ключа):
Наш шеллкод составил 421 байт.
Обновим наш эксплойт с добавлением NOP’ов (для уверенности добавим 32 NOP’а, можно и меньше):
Из-за кодировки (в нашем случае, xor_dynamic) шеллкод не является исполняемым и поэтому дополнен "заглушкой" декодера. Работа этой "заглушки" заключается в декодировании байтов в исходную исполняемую форму. В нашем случае шеллкод использует шифрование, и дешифратору необходимо получить абсолютный адрес зашифрованной части. Для этого используется GetPC-код. GetPC код (Get Program Counter) - код, вычисляющий свое расположение в адресном пространстве исполняемого процесса:
Процедура GetPC в конечном итоге искажает по крайней мере пару байтов, близких к адресу, на который указывает регистр ESP. К сожалению, это небольшое изменение в стеке является проблемой для нас, потому что декодер начинается точно с адреса, на который указывает регистр ESP. Короче говоря, выполнение процедуры GetPC приводит к изменению нескольких байт самого декодера (и возможно, закодированного шеллкода), что в конечном итоге приводит к сбою процесса декодирования и краху целевого процесса.
Один из способов решить эту проблему - создать широкую "посадочную площадку" для нашего JMP ESP. Так что когда выполнение приземляется в любом месте этой площадки, оно переходит к нашей полезной нагрузке. Это может показаться сложным, но мы просто предваряем нашу полезную нагрузку серией инструкций No Operation (или NOP), которые имеют значение опкода 0x90. Как следует из названия, эти инструкции ничего не делают, а просто передают выполнение следующей инструкции. Это позволяет процессору "скользить" через NOP, пока не будет достигнута полезная нагрузка.
Теперь наш эксплойт полностью готов. Давайте запустим слушателя для получения обратной оболочки с целевого хоста:
И запустим эксплойт:
Отлично! Мы создали полностью рабочий эксплойт для уязвимости переполнения буфера с нуля и получили обратную оболочку Meterpreter!
Всем спасибо за внимание! Также будем рады видеть вас на нашем курсе по тестированию на проникновение! Если вы дочитали до конца, найти, думаю, будет не сложно!
Все используемые скрипты в статье также будут находиться в архиве с уязвимым сервером!