Mary Morton PWN writeup ASIS-CTF 2017
Райтап был написан тиммейтом Purupururin.
Условие:
Решать будем по Unix-way (с помощью реверс-фреймворка radare2 и GDB с аддонами GEF). Не потому, что так удобнее, а потому что мне захотелось освоить эти тулзы.)
Ссылки:
Radare2:https://github.com/radare/radare2, https://www.gitbook.com/book/radare/radare2book/details
GEF: https://github.com/hugsy/gef - расширение GDB, имеющее кучу разных фич;
Pwntools: https://docs.pwntools.com/en/stable/ - либа для питона со всякими приблудами для удобного скриптинга CTF эксплойтов.
Не то, чтобы мы будем использовать какие-то специфичные возможности GEF (кстати, этот таск можно легко решить и без отладчика, если иметь хорошие скилл статического анализа). Но в GEF по дефолту интерфейс приятный, так что я буду павнить с ним. Pwntools помогла простой обёрткой над API для сокетов, но, в принципе, она тут тоже как собаке пятая нога.
Решение:
Intro:
И так, перейдём к самому таску. Для начала скачаем файлик и посмотрим что нас ждёт по адресу, указанному в задании. Юзаем netcat, чтобы подключиться и посмотреть что там, да как.
Как видите, в описании создатели никого не обманули, и они нам сами показали, какие тут есть уязвимости (хоть это пока всего-лишь предположение)).
Тут у нас простенький интерфейс, позволяющий “Выбрать оружие” и выводящий наш последующий ввод обратно.
Что-ж, давайте смотреть, что за файлик мы скачали. Наш сегодняшний поциент: файл «mary_morton_f3555213d54602a8e5a40fe0435adcf84e8eff71».
Юзаем стандартную утилиту file (парсит первые байты файла и пытается сопоставить с известными ей шаблонами). Отлично, создатели решили не заморачиваться и просто скинули нам архив с бинарником (убеждаемся в этом используя разархивируя файл с помощью tar с опцией xf , который, по-видимому, может в этот алгоритм). Кстати, если вы делаете с винды, то можете просто изменить расширение файла на .xz и разархивировать WinRar-ом:
Получили файл mary_morton. Можно юзнуть file и убедиться, что это исполняемый файл, но я сразу загружу его в radare2, ибо он выдаст нам куда больше инфы об этом файле.
Тут много всего, но давайте по порядку. R2 – вызов нашего фреймворка с аргументом в виде имени подопытного, i – всякая информация о файле (iI – общая инфа про исполняемый файл), iz – (вывести строки из data-секции файла), а – анализировать файл, (аааа – полный анализ). Кстати, узнать, что делает любая команда можно введя саму команду и знак вопроса после команды (пример: “aa?”), а с тремя знаками вопроса иногда, вроде, можно даже на примеры использования наткнуться – очень полезные фичи.
По результату первой команды видим, что это – эльф-файл под архитектуру x64(amd64, x86-64 и т.д.), canary – yes (используется защита стека канарейками https://en.wikipedia.org/wiki/Buffer_overflow_protection#Canaries) а так же бинарь stripped (удалены имена функций, и другие отладочные символы, чтоб жизнь мёдом не казалась (хотя на самом деле они и не нужны компьютеру для исполнения программы, и более того, без них даже лучше)).
Далее команда вывода строк. Тут, помимо уже встреченных нами строк есть особо интересная “/bin/cat ./flag”, запомним её, и вернёмся к ней позже.
Кстати, после анализа можно ввести afl и посмотреть, что наш анализ функций выдал.
Как я уже говорил, бинарь пострипан, и функцию main мы сходу бы не нашли, но r2 умный и справился с этим, назвал её! Поэтому мы можем сделать “s main” (s – перевод каретки на указанный адрес, и main – имя функции.)
После s main, как видим, адресок слева изменился, это значит что символ распознан. V для перехода в визуальный режим.
Перейдём к самому интересному – анализу кода!
Когда вы введёте V то увидите бинарь в самом сыром его виде, нажмите кнопку p один раз, чтобы перейти в режим листинга кода. Тогда вы увидите примерно такое чудо:
Сразу извиняюсь за псевдокод (он не мой, мне его подкинули (могу скинуть конфиг, если кому понравился, я его в сети нашел)), хотя для тех, кто слаб в асме будет даже нагляднее. (удобная фича радара, не знаю правда, для каких архитектур помимо x86 она работает).
Разбирать весь листинг я не буду, это явно выходит за рамки райтапа, просто видим, что тут обычный свич по введённой цифре, и запуск соответствующей ей функции. (имена функций пострипаны, поэтому ida, Ой, то есть радар дал им имена типа sub.read_960). Видимо, в этом куске кода использована функция read. И так, если вводим единицу (выбрали Stack Buffer overflow), то попадаем в функцию sub.read_960. Если 2, то sub.read_8eb. Ок, осмотрим её и попробуем посмотреть в отладчике, что там можно выудить.
Установим курсор радара на эту функцию, и наберём pdf (print disassembled function).
Как видим, функция опознала 2 локальные переменные – local_90h, буфер размером 0x88 (0x90 – 0x8), п коду видим, что в нём 0х10 байт заменяется нулями, а потом туда считывается с помощью read 0x100 байт (хмм, да это же переполнение буфера, кто бы мог подумать!)
Но, начиная с четвёртой строки кода мы видим что-то необычное (или обычное в нашу вторую переменную стека запихивается что-то из регистра fs по смещению 0x28. Вспоминаем, что радар нас предупредил про канарейки, и минута в гугле подтверждает нам: FS – указывает на TLS, это не который SSL, а который https://en.wikipedia.org/wiki/Thread-local_storage , структура с данными треда, а по смещению 0x28 в ней находится канарейка (убеждаемся в этом взглянув на последние строчки кода в функции: там значение из переменной подгружается в регистр и XORится снова с этой канарейкой, и если бы мы её каким-либо образом изменили, то jmp (goto) не случится и вызовется функция которая крашнет программу с Segmentation Fault. А если канарейка не задета, то поток перепрыгнет на строчку ниже, там почему-то радар не показал дизассемблинг, но c9 и c3 это опкоды инструкций leave и ret (штатный выход из функции, то, к чему мы стремимся).
Ок, посмотрим, как это выглядит в gdb. На рисунке справа мы подгрузили файл, поставили брейкпоинт на адрес этой функции (его я узнал из радара, а поставить брейкпоинт по имени бы не получилось, т.к. бинарь пострипан).
Пишем run, чтобы запустить прогу, вводим 1, и попадаем на брейкпоинт. Благодарим GEF за удобное отображение контекста, и пишем несколько раз ni (next instruction). Вы увидите примерно такое:
Как видите, красная стрелочка показывает на текущую инструкцию, и после того как я написал ni, в регистр $rax записалось какое-то значение канарейки (подсвечено красным). Можете поиграться дальше, чтобы понять лучше, как работает функция, а я просто ещё раз повторю вышеописанные действия, и обнаружу вот какую неприятную особенность:
Значение канарейки в $rax изменилось с новым запуском программы, то есть там используется рандомизация (это плохо, т.к. если б это была константа, то мы бы просто её узнали и перезаписали эту переменную в стеке корректно). Убираем брейкпоинт (disable breakpoints) он нам пока не понадобится, а так же можем написать continue, чтобы продолжить выполнение программы – она нам ещё понадобится.
Есть несколько методов обхода такой бяки – поиск уязвимости в алгоритме генерации этой рандомной канарейки (вдруг там используется какой-нибудь рандом для бедных, и т.д. Но это слишком сложно для такого дешёвого задания, да и у нас эксплуатация по сети, поэтому – втопку).
Ещё один метод – каким-то образом считать эту канарейку в рантайме и спокойно перезаписать её при переполнении буффера. Эх, была бы у нас какая-нибудь уязвимость, которая бы позволила вызвать эту утечку памяти…
А, ведь мы ещё не проверили вторую функцию -- sub.read_8eb, а ведь она вызывается ! Таким же образом узнаем её адрес из радара, передвигаем на неё курсор (s sub.read_8eb), печатаем листинг через pdf. Видим такое:
В начале и в конце - всё то же самое что и в прошлой функции, но дальше видны различия. Примечаем, что в этой функции подача введённых данных в форматную строку printf (0x00400935: rax := local_90h) . А это – самый, что ни на есть классический Format string bug. Кстати, на 5 строк выше видим, что аргументом к read() посылается 0x7f, т.е. в буфер будет прочитано намного меньше, чем его возможный размер, поэтому, тут его переполнить не удастся.
Эта уязвимость в самом простейшем её способе эксплуатации поможет нам прочесть значение в переменной local_8h, а это, как мы помним, наша канарейка.
Таким образом, дальнейший план: понимаем, эксплуатируем эту уязвимость, получаем значение канарейки, возвращаемся к буферу, думаем как его поломать.
Ок, мы знаем, что нам нужно. Как проэксплуатировать это дело?
Вот картинка иллюстрирующая типичный стековый фрейм, представьте, что вместо func_A() там наша printf(fmt[,arg1,arg2…]). Что происходит, когда запускается printf? Она посимвольно начинает пробегать по своему первому аргументу (param1 на картинке) – форматной строке (которую мы контролируем в данной ситуации), встретив форматирующий символ, например %d, она берёт и выводит свой второй аргумент со стека (param2) и выводит его в нужном нам формате (в моём случае decimal). Скорее всего параметры. При этом никакая проверка целостности не проводится, т.е. следующий форматирующий символ сдвигает указатель стека ещё ниже (это на картинке, а точнее выше, в сторону возрастающих адресов и берёт оттуда следующий параметр), хотя там может что угодно (в нашем случае, первый же параметр будет взят хз откуда, скорее всего с локального стека функции Printf, но нам это и не нужно знать).
Таким образом, мы можем своими форматирующими символами опускать стек всё ниже и ниже и перейти в зелёную область main automatic variables (в нашем случае это sub.read_чтототам automatic variables, но суть та же – мы попали в область локальных переменных функции, которые у нас начинаются с чего? Верно, с нашей форматной строки!
Ок, каким образом мы это сделаем? Метод очень прост: вводим форматную строку вот такого вида:
“AAAAAAAA%llx%llx%llx%llx…” Перебирая количество форматирующих символов %llx (long long hexadecimal – выведется 8 байт в шестнадцатеричном виде) перебирать будет до того момента, пока не увидим в аутпуте “4141414141414141” – первые 8 символов нашей форматной строки. Можно делать это в отладчике, можно по сети, а ещё можно перебирать не в ручную, а в цикле, парся вывод программы.
Быстро находим, что ввод 6 форматирующих символов приводит нас к началу нашей форматирующей строки, то есть дальше дело за малым -- рассчитать, сколько ещё символов таких ввести, чтобы получить значение канарейки (0x90 (размер буфера и канарейки) - 0x8(считали 8 символов из буфера уже)) = 0x88, 0x8C/8… Вроде 16.
И так, нам нужно ввести ввести 17+6 форматирующих символов %llx, чтобы программа выдала нам канарейку. Пробуем, убеждаемся. Как проверить визуально, что это наша local_8h? Перед ней в стеке будет рандомный, но статичный мусор (рандомный он только для нас, пока мы не знаем, что там было раньше в этом месте в стеке, а так он конечно же детерминированный), и он будет при каждом запуске программы одним и тем же, а вот наша канарейка, которая, если вы всё сделали правильно, будет показана в виде последних 16-ти hex символов, будет каждый раз при запуске программы новой.
Можно было проделать все эти шаги по-другому: написать скрипт для отладчика, в котором было бы следующее -- скриптом считываем канарейку с помощью регистра fs, а потом в цикле увеличивая количество форматных символов ждём, пока последние 16 символов вывода не будут равны нашей канарейке, и таким образом узнаём, сколько форматирующих символов надо. Но это навскидку как-то муторно, хз.
И так, мы знаем, как получить канарейку, а значит можем приступить к переполнению буффера. Но что дальше? Можно написать шеллкод, и перезаписав адрес возврата функции на адрес шелкода в стеке выполним его и получим шелл, если стек на сервере исполняемый. Если не исполняемый, то можно попытаться сваять ROP-гаджет(атаку возвратом в библиотеку выполнить не удастся, ибо ASLR, в чём можно убедиться, несколько раз наведя утилиту ldd на бинарник). Но всё это как-то сложно для такого-то нищего таска за 50 баллов. И тут создатели нам снова подсобили – возвращаемся в радар и вспоминаем, что мы упустили.
Если после анализа бинарника (аааа) мы установим курсор на функцию main (s main), а потом перейдём в визуальный режим (V, затем нажать p) а потом прокрутим чуть ниже, то на адресе 0x4008da мы увидим так называемый недостижимый код (unreachable code). Как видим, он запускает /bin/cat ./flag, то есть это то, что нам нужно.
До него поток выполнения не доходит, так как прямо перед ним стоит безусловный прыжок jmp (у меня отображается goto), но нам-то с вами ничто не мешает на него прыгнуть, правда? А теперь переходим к завершающей стадии, разработке эксплойта!
1) Подключаемся к серверу
2) Получаем значение канарейки, эксплуатируя баг форматной строки
3) Перезаписываем адрес возврата (смотрим картинку со стековым фреймом) переполняя буффер, и используя при этом канарейку
4) Забираем флаг
Ну что ж, давайте проверим:
Флаг у нас, все молодцы!
ASIS{An_impROv3d_v3r_0f_f41rY_iN_fairy_lAnds!}
- Автор: drakylar
- Комментарии: 1
- Просмотры: 5596