Мы прекрасно провели время, эксплуатируя уязвимости виртуальной машины
Protostar, но теперь пришло время двигаться дальше и поискать более сложные задачи.
Protostar, но теперь пришло время двигаться дальше и поискать более сложные задачи.
Мы прекрасно провели время, эксплуатируя уязвимости виртуальной машины
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
, но теперь пришло время двигаться дальше и поискать более сложные задачи. И такие задачи присутствуют в виртуальной машине Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
, где предусмотрены более продвинутые уровни, связанные с эксплуатацией брешей в бинарных файлах. Главное отличие заключается в том, что эти уровни связаны с сетевыми службами, а, значит, нам предстоит написать эксплоиты для удаленного выполнения.В этом руководстве, являющимся частью нашей
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
, мы проанализируем процесс нулевого уровня в отладчике GDB, напишем эксплоит и подготовимся к более сложным испытаниям. На первый взгляд анализ исходного кода может отнять много времени, но в дальнейшем эти затраты многократно окупятся, и ваше путешествие будет более беспроблемным. Начинаем!Шаг 1: Установка Fusion
Установка Fusion во многом схожа с установкой Protostar. Единственное отличие заключается в том, что система для Fusion должна быть сконфигурирована как «Ubuntu (32-bit)» в той среде виртуализации, с которой вы работаете. В случае с Protostar достаточно конфигурации под стандартную Linux-среду. Если же вы сделаете подобные настройки для Fusion (вместо Ubuntu 32-bit), то у вас могут возникнуть проблемы.
Ссылку для загрузки Fusion можно найти на
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
сайта Exploit Exercises.После загрузки и установки Fusion включите виртуальную машину, наденьте камуфляжную форму и приготовьтесь к битве J.
Шаг 2: Анализ исходного кода
Как и случае с Protostar,
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
для каждого уровня в Fusion доступен в онлайне. Если мы научимся анализировать исходный код, то сэкономим себе много времени в дальнейшем, поэтому я решил уделить этой теме чуть больше внимания. Начнем с главной функции и далее перейдем ко всем вложенным методам.При анализе исходного кода на предмет присутствия уязвимостей, первое, что нам нужно сделать – найти места, где программа принимает пользовательские входные данные, и в какие переменные сохраняется полученная информация. Эти места для нас важны, поскольку через эти точки входа мы можем влиять на логику работы программы. Если в приложении отсутствуют места, где принимаются пользовательские данные, эксплуатация сильно усложняется, но все равно остается возможной.
Предполагая, что пользовательские данные у нас предусмотрены, нам нужно найти, в какие переменные сохраняется эта информация, и что хранится рядом с этими переменными. Если мы знаем, какие данные хранятся рядом, то сможем также переполнить соответствующие ячейки памяти. Таким образом, у нас будет больше возможностей влиять на логику работы приложения, помимо простой перезаписи регистра
EIP
.В главной функции есть две переменные:
fd
– целочисленная и p
– символьный указатель. На первый взгляд не видно, что какая-то из этих переменных хранит пользовательские данные. В строке 41 мы видим, что в переменной fd
сохраняется результат работы функции serve_forever(PORT)
. Поскольку у нас нет кода этой функции, мы можем лишь предположить, что этот метод делает приложение доступным в качестве сетевой службы.С учетом вышесказанного, кажется, что переменная fd не представляет для нас особой пользы, как, впрочем, и переменная p, которая вообще не упоминается где-либо в коде. Если бы мы были знакомы с программистом, написавшим этот код, то можно было бы спросить, зачем вообще нужна эта переменная.
Однако изучение функции main все же принесло некоторые результаты. В строке 44 вызывается функция
parse_http_request
. Поскольку у нас есть исходных код этой функции, будет хорошей идеей заглянуть во внутрь.Здесь уже есть кое-что интересное. Внутри функции есть три локальные переменные:
buffer
– символьный массив размером 1024 (какое интересное совпадение), path
– символьный указатель, q
– еще один символьный указатель.Следует отметить, что в строке 17 присутствует умышленная информационная утечка. Программист был достаточно добр и показал нам, где в памяти находится этот буфер. Данная информация очень пригодится нам в дальнейшем.
Далее мы видим серию странных условных выражений. В строке 19 программа проверяет, есть ли возможность считывания информации с удаленного хоста. Если считать нельзя, функция errx выводит ошибку и завершает работу программы. Здесь мы видим первые следы пользовательских входных данных: то, что отсылает пользователь через сеть, помещается в буфер. Здесь сразу же возникает вопрос, сможем осуществить переполнение этого буфера? Скорее всего, нет. Обратите внимание на последний аргумент -
sizeof(buffer)
, означающий, что в буфер будет скопировано только первые 1024 байта. Двигаемся дальше.Условное выражение в строке 20 – довольно интересное. Функция errx выводит сообщение «Not a GET request», если условие не равно 0. И здесь сразу приходит в голову очень важная тонкость, связанная с эксплуатацией сетевых уязвимостей, а именно - форматирование.
В основном сетевые службы требуют, чтобы присылаемая информация была в определенном формате. Если формат не соответствует ожидаемому, программа отклоняет полученные данные. Именно эта логика реализована в строке 20. В условном выражении происходит сравнение первых четырех байтов буфера со строкой «GET » (вместе конечным пробелом). Если первые четыре байта буфера не соответствуют строке «GET », весь процесс завершается. Сей факт означает, что наш буфер должен начинаться с «GET », иначе мы не продвинемся слишком далеко.
В строке 22 мы видим, что переменной path присваивается некое значение. Чтобы понять, что присваивается этой переменной, нужно вспомнить об указателях. Как мы знаем, указатели хранят адрес в памяти. Значение, на которое ссылается этот адрес, соответствует типу указателя. Если указатель указывает на символ, соответственно, в указателе будет храниться адрес символа или серии символов. Как раз этот адрес и присваивается переменной
path
. Символ &
перед переменной buffer
говорит о том, что мы хотим сохранить адрес символа с индексом 4 в переменную path
. Нам не нужен сам символ, а нужно местонахождение этого символа.Вспоминаем, что в первых четырех символах буфера (с индексом от 0 до 3) должна храниться строка «GET ». Соответственно, в переменной path будет находиться адрес символа, следующий за строкой «GET », а по сути – адрес буфера, следующего за строкой «GET ».
В случае с переменной q картина примерно та же. Функция
strchr
присваивает переменной q
адрес первого пробела, который встречается в переменной path
. Затем в строке 25 этот пробел преобразуется в пустой байт. То есть переменная path
будет указывать только на символы между строкой «GET» и этим пустым байтом. А в переменной q
будет храниться адрес символов, находящихся после пустого байта.Далее находится еще одна конструкция
if/errx
, которая говорит о том, что, если q
не указывает на что-то (или другими словами пробел в содержимом, на которое указывает path
, отсутствует), выводится сообщение «Invalid protocol». В строке 26 находим ответ на вопрос, о каком протоколе идет речь. Здесь происходит сравнение значения, на которое указывает q
, со строкой «HTTP/1.1». Что-то подсказывает мне, что эта информация также пригодится при написании эксплоита.Собираем все воедино.
Мы провели анализ кода, и теперь, надеюсь, вы понимаете примерную логику работы подопытной программы. На данный момент имеем следующее:
- Первые четыре символа нашего запроса должны совпадать со строкой «GET» (с завершающим пробелом).
- Затем программа сохраняет адрес остальной части пользовательских данных в переменную
path
. - В третьей переменной
q
хранится ссылка на все остальные символы после пробела в буфере, на который указываетpath
. Пробел заменяется на пустой байт. Кроме того, в буфере, на который указывает q, должна быть строка «HTTP/1.1».
Таким образом, шаблон нашего запроса будет выглядеть примерно так:
GET <строка переменной path> HTTP/1.1
Если вы знакомы с протоколом HTTP, то должны знать, что именно так выглядит GET-запрос к веб-серверу.
По результатам наших исследований можно сделать вывод, что нужно обратить особое внимание на переменную
path
, поскольку это единственная строка, которой мы можем управлять. В строке 28 переменная path
передается в функцию fix_path
, для которой доступен исходный код. Приступаем к анализу кода этой функции.Поиск уязвимости
Функция
fix_path
выглядит так:В строке 5 объявляется новая локальная переменная resolved размером 128 байт (опять видим неожиданное совпадение). Эта переменная вместе с переменной
path
, приходящей из функции parse_http_request
, передаются в функцию realpath
. Если Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
или обраться к Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
, то можно найти следующую информацию:Прототип:
char *realpath(const char *path, char *resolved_path);
Описание:
realpath()
преобразует все символические ссылки и указатели на компоненты /./, /../
и дополнительные символы /
в строке с пустым завершающим байтом, именуемой переменной path
, для создания абсолютного канонизированого имени пути. Результирующее имя пути хранится в строке с завершающим пустым байтом, размером вплоть до PATH_MAX
байт, в буфере, на который указывает переменная resolved_path
. В результирующем пути символических ссылок и компонентов /./
или /../
не будет.По сути,
realpath()
преобразует переменную path
в канонический вариант, используемый в Linux, и копирует результат в переменную resolved
. Несмотря на то, что в описании упоминается ограничение на размер PATH_MAX
, в реальности максимальное значение нигде не указывается. Таким образом, мы можем скопировать переменную path
размером более 128 байт в переменную resolved
и вызвать переполнение буфера.Шаг 3: Зачем нужен анализ исходного кода
Не секрет, что анализ исходного кода не всегда приносит нужные результаты. Кто-то может найти сходство между этим процесс и переводом книги на несколько языков. Однако в реальности анализ исходного кода жизненно важен при разработке эксплоитов. Если бы мы не провели подобный тщательный анализ, то скорее всего начали бы просто засовывать бесконечное количество символов в буфер до тех пор, пока не произошло бы аварийное завершение (segmentation fault), которое, на самом деле, никогда бы не случилось.
Даже если бы мы знали, что вначале нужна строка «GET», а в конце – «HTTP/1.1», попытка переслать любое сообщение размером больше 1024 байт не увенчалась бы успехом. Написать качественный эксплоит можно только после того, как мы нашли уязвимую переменную resolved и место, где в эту переменную приходят пользовательские данные. Таким образом, теперь наш запрос будет выглядеть примерно так:
GET <переменная path больше 128 байт> HTTP/1.1
Теперь понятно, что нам нужна переменная path больше 128 байт, но в то же время общий размер буфера должен быть меньше 1024 байт. Едем дальше.
Шаг 4: Авторизация в Fusion
Для разработки эксплоита нам понадобится два терминала. Один – для разработки, второй – для отладки. В отладочном терминале мы будем подключаться к Fusion. Вначале нужно узнать IP-адрес виртуальной машины, а затем ввести следующую команду (или эквивалент, если вы пользуетесь другим SSH-клиентом):
Bash:
ssh fusion@<ip of virtual machine>
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
. Если вы пользуетесь напрямую SSH-клиентом под Windows, нужно создать вторую SSH-сессию, которая будет использоваться при разработке эксплоита. Создавать эксплоит можно в домашней директории пользователя fusion
.После подключения появится приглашение для ввода пароля. Пароль –
godmode
.Шаг 5: Исследование программы в GDB
Между Fusion и Protostar есть одно больше отличие. В Fusion программы, для которых мы пытаемся написать эксплоит, уже работают, и вместо создания новых процессов, нам придется отлаживать уже запущенный процесс. Несмотря на то, что нам понадобится чуть лучше изучить функционал
GDB
, в целом задача не представляет особых сложностей.Прежде чем мы погрузимся в
GDB
, нужно выяснить идентификатор процесса level00
. Вводим следующую команду:
Bash:
ps -A | grep level00
level00 – 1485
.Теперь, когда мы знаем идентификатор процесса
level00
, настало время загрузить бинарный файл в GDB
. Вводим следующую команду:
Bash:
sudo gdb /opt/fusion/bin/level00
GDB
, поскольку нам потребуются точки останова в определенных местах кода. Именно поэтому в команде указан полный путь к приложению. Поскольку процесс запущен от имени другого пользователя, нам нужны права суперпользователя для подключения к этому процессу. Получить нужные привилегии можно при помощи команды sudo
. После запуска этой команды появится приглашение о повторном вводе пароля godmode
.После того как мы оказались в
GDB
, нужно позаботиться о нескольких вещах. Первое – мы хотим подключиться к уже запущенному процессу level00
. Вводим следующую команду (номер 1484 нужно заменить на идентификатор вашего экземпляра):
Bash:
attach 1485
GDB
выдаст несколько сообщений о том, что подключение к процессу произошло успешно. Если вы не смогли подключиться к процессу, еще раз проверьте, что при запуске GDB
использовалась команда sudo
.Еще одна интересная особенность заключается в том, что программа
level00
не работает исключительно внутри одного процесса, а порождает несколько подчиненных процессов, что вполне логично, если мы имеем дело с веб-сервером, который должен обрабатывать несколько подключений одновременно. Здесь могут возникнуть некоторые сложности, поскольку по умолчанию GDB
подключен только к родительскому процессу. Чтобы учесть подчиненные процессы, введите следующую команду:
Bash:
set follow-fork-mode child
GDB
о том, что при порождении нового процесса, к этому процессу также нужно подключаться.Теперь нам необходимо поставить точки останова. Поскольку, благодаря анализу исходного кода, у нас уже есть общее понимание схемы эксплуатации программы, мы будем использовать
GDB
для выяснения, насколько большое переполнение нам нужно. Поскольку переполнение происходит в функции fix_path
, вполне логично поставить точку останова внутри этой функции. Если говорить о конкретном месте, то лучше сразу же после объявления переменной resolved
, которая напрямую связана с уязвимостью. Строка 6 будет хорошим кандидатом для точки останова. Выполняем следующую команду:
C:
break 6
C:
c
Наш процесс
level00
готов к тому, чтобы прервать свою работу в тот момент, когда выполнение дойдет до точек останова. И теперь настало время отправить входные данные. В нашем терминале, который используется для разработки эксплоита, мы будем подключаться к службе при помощи netcat
. Вводим следующую команду:
Bash:
nc <IP-адрес, используемый для подключения к Fusion> 20000
Как уже упоминалось ранее, разработчик оказался ОЧЕНЬ добрый и показал нам начальный адрес буфера. То есть нам не нужно мастерить большую вереницу NOP’ов. Мы просто добавим шелл-код в эксплоит, и дело в шляпе. Вернемся к этому вопросу позднее.
В
GDB
пока не сработала ни одна точка останова, поскольку мы еще ничего не отправили процессу. Вначале не будем пытаться переполнять буфер, а просто отправим тестовый запрос, удовлетворяющий формату, о котором мы узнали при анализе кода:GET /test HTTP/1.1
Строка, показанная выше, удовлетворяет всем требованиям. Первые четыре символа – «GET», и третья часть запроса – «HTTP/1.1». Отправим запрос и посмотрим, что получится.
Прекрасно. Как мы и предполагали,
GDB
переключился на подчиненный процесс и остановился на точке останова в строке 6. Теперь нам нужно примерно прикинуть, насколько переполнять переменную resolved
для перезаписи регистра EIP
. Нам потребуется запустить две команды. Первая:
C:
p &resolved
resolved
».На рисунке выше шестнадцатеричный адрес переменной
resolved
выделен красным. В вашем случае это значение может отличаться. Как только мы нашли стартовую точку отсчета, определим направление движения. Нам понадобится адрес регистра EIP
. Вводим следующую команду:
Bash:
info frame
EIP
.Как видно на рисунке выше, регистр
EIP
находится не очень далеко от переменной resolved
. Рассчитаем точное расстояние по следующей формуле:
C:
p 0xbffff8dc - 0xbffff860
EIP
. Теперь настало время смастерить эксплоит.Шаг 6: Планирование структуры эксплоита
Открываем текстовый редактор и начинаем разбирать эксплоит построчно.
В первой строке указано, как нужно интерпретировать наш файл. В нашем случае мы хотим, чтобы файл был интерпретирован как скрипт, написанный на Python. То есть мы указываем полный путь к интерпретатору Python.
Далее идут три импорта. Нам нужен пакет sys для обработки аргументов из командной строки, пакет struct для упаковки адреса, который мы будем использовать для перезаписи регистра EIP, и пакет socket для установки соединения с удаленным хостом. Идем дальше.
Прежде всего необходимо инициализировать объект socket. Эту операцию мы будем выполнять внутри функции exploit без каких-либо аргументов. В первой строчке переменной host присваивается первый аргумент из командной строки, который представляет собой IP-адрес целевого хоста. Затем в переменную port заносится второй аргумент из командной строки. На этом порту работает уязвимая служба.
Затем определяем новый объект типа socket под именем evilSock. Несмотря на то, что мы пишем эксплоит исключительно в образовательных целях, подобное имя вполне уместно.
Вначале присваиваем переменной
evilSock
объект типа socket.socket
. Далее вызываем функцию connect
этого объекта и в качестве параметров передаем переменные host
и port
, в которых будут находиться IP-адрес и порт соответственно. Поскольку функция connectработает определенным образом, аргументы нужно передавать в форме кортежа (упорядоченного набора фиксированной длины), где значения разделены запятыми. Кортеж должен заключаться в скобки, поэтому с каждой стороны по две скобки.Переключаемся на следующую передачу. В процессе работы программы периодически нам понадобится выводить сообщения на экран. То есть несколько раз выполнить повторяющиеся участки кода. Для решения этой задачи вполне логично создать еще одну функцию.
Функция
getMsg
в качестве аргумента принимает один объект типа socket
, который по очевидным причинам называется aSock
. Сама функция довольно проста. Мы получаем сообщение от целевого хоста через функцию recv
. Аргумент 1024 ограничивает максимальное количество байт, которое мы хотим принять. Мы знаем, что первое присылаемое сообщение содержит начальный адрес буфера, которые не очень большого размера, и 1024 байт вполне достаточно. После получения сообщения, выводим на печать переменную, где хранится полученное сообщение.Опытные программисты могут возразить, что нет смысла создавать отдельную переменную, и можно упростить функцию, если воспользоваться конструкцией
print(aSock.recv(1024))
. Однако с целью сделать код более удобочитаемым мы из одной строки сделали две и добавили новую переменную.Добавляем в эксплоит еще несколько строк, и теперь код должен выглядеть так:
Мы добавили два вызова функции
getMsg
. Первый – после подключения к целевому хосту, второй – после отправки запроса. Между этими вызовами мы создали переменную payload, которая будет использоваться при переполнении, и присвоили этой переменной строку из 144 символов A. Несмотря на то, что расстояние между переменной resolved и регистром EIP составляет 140 байт, всегда важно подстраховаться и убедиться в том, что EIP переполнен полностью.После создания полезной нагрузки, формируем запрос по формату, который мы выявили ранее. Первая часть – «GET» с пробелом, затем наша полезная нагрузка и строка «HTTP/1.1». Затем пересылаем сформированный запрос при помощи функции
send
объекта evilSock
.Кроме того, важно сделать так, чтобы функция
exploit()
запускалась в конце скрипта.Теперь мы готовы приступить к тестированию эксплоита. Повторите ранее описанные шаги для того, чтобы GDB оказался в рабочем состоянии, и запустите эксплоит при помощи следующих команд:
Bash:
chmod +x exploit00.py
./exploit00.py <ip address of Fusion> 20000
Как видно из рисунка выше, наши усилия по анализу исходного кода полностью окупились, и мы получили аварийное завершение программы. То есть регистр EIP успешно перезаписан.
Шаг 7: Наброски полной версии эксплоита
Мы выполнили главную задачу и перезаписали EIP. Но что теперь делать, спросите вы? Конечно же, заняться развертыванием шеллов.
Для решения этой задачи нам понадобится шелл-код. Но сначала нужно понять, где будет размещаться шелл-код. У нас есть три варианта:
- Мы можем хранить шелл-код непосредственно перед регистром EIP в переменной resolved.
- Мы можем хранить шелл-код сразу же после регистра EIP в переменной resolved.
- Мы можем хранить шелл-код в конце буфера (переменная buffer).
Прежде чем мы выберем один из вышеперечисленных вариантов для размещения шелл-кода, нам нужно вспомнить о размерах переменных. Буфер переменной resolved – 128 байт. Однако у нас еще есть 800 байт внутри переменной buffer. Если мы решим поместить шелл-код после EIP, то в целом размер можно не принимать во внимание, однако этот путь не имеет для нас особого смысла, учитывая, что мы знаем адрес буфера и можем легко вычислить, куда поместить шелл-код.
С учетом вышесказанного, финальная версия запроса будет выглядеть так:
Как обычно, запрос начинается со строки «GET». Затем мы переполняем переменную resolved при помощи 139 байт и оказывается на у границы регистра EIP, который мы будем переполнять адресом нашего шелл-кода. Далее нам нужно добавить строку HTTP/1.1, чтобы запрос соответствовал формату, после которой идет уже сам шелл-код.
Шаг 8: Написание эксплоита
Финальная версия эксплоита должна выглядеть примерно так:
По сравнению с предыдущей версией мы внесли два изменения. Во-первых, добавили переменную
address
, содержащую адрес шелл-кода. Но откуда был взят этот адрес?Вспоминаем сообщение, в котором говорится, что адрес переменной
buffer - 0xbffff8f8
. Размер строки «GET » - 4 байта, размер полезной нагрузки – 139 байт, адрес шелл-кода – 4 байта и размер строки " HTTP/1.1" – 9 байт. Адрес, по которому будет размещаться шелл-код, вычисляется при помощи следующей команды:
C:
p/x 0xbffff8f8 + 4 + 139 + 4 + 9
/x
говорит о том, что информация должна выводится в шестнадцатеричном формате. В результате выполнения команды должно получиться значение 0xbffff994
.Далее мы добавили большого монстра в виде переменной
shellcode
. Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
был взят со специализированного сайта, который, в том числе, используется для хранения подобного рода программ. Конкретно этот шелл-код заточен под разворачивание шелла на целевой машине на порту 1337. Затем мы можем подключиться к этому порту при помощи netcat для выполнения команд.Шаг 9: Тестирование эксплоита
Час X наступил. Нашей хакерской натуре уже не терпится попробовать на практике то, что мы смастерили. Вводим следующую команду:
Bash:
./exploit00.py <ip address of Fusion> 20000
Хм. Мне очень интересно, почему такое произошло. Переключаемся в отладочный терминал, запущенный на виртуальной машине, для выяснения причин. Чтобы понять, запустился ли шелл-код, мы можем проверить, используется ли 1337 порт, при помощи следующей команды:
Bash:
sudo netstat -tulpn
Как мы и предполагали, процесс
level00
сейчас работа на 1337 порту. Вновь переключаемся в терминал, где запущен эксплоит.Эксплоит до сих пор работает, и для завершения нажмите Ctrl-C. Чтобы подключиться к новому шеллу, воспользуемся следующей командой:
Bash:
nc <ip address of Fusion> 1337
ls
:Наконец-то, появился свет в конце тоннеля, и теперь у нас есть собственный шелл. Весь процесс прошел со скрипом, но тем не менее, результат мы получили.
Заключение:
Написание эксплоита для
level00
в Fusion
оказалось чуть сложнее, чем для уровней в Protostar. То, что мы провели предварительный анализ кода, впоследствии сэкономило нам кучу времени.Вы можете возразить, мол, изучение кода отнимает много времени. На что я отвечу: «Успокойтесь и расслабьтесь». Ваши временные затраты неминуемо окупятся, поскольку при разработке эксплоита вы не будете тратить время на тестирование догадок и гипотез. У нас было несколько переменных, но во время анализа мы безошибочно выбрали ту, которая напрямую связана с уязвимостью. Кроме того, мы смогли узнать формат запроса, который был необходим для успешной работы эксплоита.