Решение задания "Invisible" с ASIS CTF Quals 2020.

Решение задания "Invisible" с ASIS CTF Quals 2020.

https://t.me/hacker_sanctuary

На прошедших выходных можно было поиграть в "ASIS CTF Quals 2020".

Соревнования были организованы командой ASIS из Ирана.

Официальный сайт мероприятия - https://asisctf.com/

Ссылка на событие - https://ctftime.org/event/964

На соревнование были представлены задания из стандартных категорий и сложность была выше средней.

В течение недели выйдет несколько разборов заданий категории PWN и Reverse.

Разбор задания "Baby note" - https://telegra.ph/Reshenie-zadaniya-Baby-note-s-ASIS-CTF-Quals-2020-07-05

Разбор задания "tthttpd" - https://telegra.ph/Reshenie-zadaniya-tthttpd-s-ASIS-CTF-Quals-2020-07-05

Разбор задания "sshâteau" - https://te.legra.ph/Reshenie-zadaniya-ssh%C3%A2teau-s-ASIS-CTF-Quals-2020-07-07

В данном посте разберём задание "Invisible" из категории PWN.

К заданию.

Задание оказалось третим по сложности в категории, сложнее оказались только задачи на эксплуатация модуля ядра Linux и JS-движкка в Safari.

Как и в прошлых задачах - описание ничего не даёт. Скачиваем файл и получаем архив с исполняемым файлом и libc. Сразу определим версию libc.

Версия 2.23, забегая немного вперёд сразу отметим, что в этой версии нет tcache в реализации динамической памяти. Посмотрим защиты исполняемого файла.

Перезаписываем GOT и отсутствие PIE.

Начнём анализ исполняемого файла.

Функция main выглядит очень просто и похожа на типичную функцию в heap-based тасках. Мы можем выбрать одну из 3-х опций: создание нового чанка, удаление чанка и редактирование.

Посмотрим опции по порядку.

Создание нового чанка.

При создании нового чанка необходимо указать индекс, при этом он должен быть не больше 1, т.к. мы можем одновременно иметь только 2 указателя на выделенные объекты. После этого мы указываем размер и он должен быть не больше 0x78. Это сильно ограничивает наши чанки, так что при освобождении они будут автоматически попадать только в fastbin. Это условие сразу указывает нам на атаку fastbin-dup. Дальше мы выделяем чанк и пишем в него данные. Запись производится безопасно. При этом перед чтением размера проверяется не создан ли уже чанк на указанном индексе.

С созданием всё понятно, перейдём к удалению.

Удаление чанка.

Удаление выглядит безопасно. Мы также вводим индекс, он проверяется, а также проверяется, что по такому индексу есть указатель. После чего происходит освобождение и запись нуль байта вместо указателя. Из-за перезаписи указателя в этом месте нам не получится сделать double-free, который необходим в атаке fastbin-dup.

Редактирование чанка.

Редактирование чанка очень похоже на создание, но есть одно различие. После ввода размера происходит вызов realloc, которые работает немного иначе, чем просто malloc.

Как можно заметить, мы можем ввести размер равный нулю (0), для realloc это будет значить, что необходимо вызвать free для переданного указателя. Это можно найти в исходниках

При это указатель не затирается нулём, как в функции удаления. Таким образом после указания размера 0, мы сможем ещё раз освободить чанк через функцию удаления и получить double-free.

Техника fastbin-dup.

Источник - https://github.com/shellphish/how2heap/blob/master/glibc_2.25/fastbin_dup_into_stack.c

Смысл этой техники заключается в том чтобы в fastbin-е оказалось три чанка в порядке a -> b <- a, для того чтобы этого добиться и нужно сделать double-free.

Процесс доставания чанков из fastbin также можно найти в исходниках malloc.c

nb - это размер чанка который нам необходим. Если он оказывается меньше или равен максимальному размеру чанка в fastbin-е, то мы пытаемся высчитать индекс внутри fastbin, где лежат чанки такого размера. После этого мы получаем первый чанк из данного бина. Fastbin-ы хранятся в aren-е для данной кучи. По сути арена это просто общий описатель различных данных в куче. Арена имеет тип "struct malloc_state" внутри этой структуры есть массив fastbinsY, который и является всем fastbin-ом.

В свою очередь этот массив типизирован как "mfastbinptr", что на самом деле является структурой malloc_chunk.

Структура malloc_chunk приведена ниже.

Нас интересуют поля fd и bk, которые есть у освобождённого чанка. Если мы освобождаем чанк, то он попадает в fastbin, освобождая следующий чанк аналогичного размера, он также попадёт в этот же fastbin и мы получим, что для второго чанка fd станет указывать на первый чанк. А второй чанк также не будет иметь указателей. Стоит отметить, что fastbin является односвязным списком, поэтому указатель bk всегда будет нулевым у всех добавляемых чанков.

Пример того как это работает.

Возьмём обычный код который выделяет два чанка размером 0x60

Освободим чанки последовательно. Сразу после первого free поставим брейк-поинт и посмотрим на указатели чанков.

Первый чанк на данный момент уже освобождён и помещён в fastbin для чанков размером 0x70 (это происходит потому что реальный размер чанка в памяти больше на 16 байт из-за добавления метаданных, таким как размер предыдущего чанка, если он свободен и размер данного чанка, а также три флага, которые сообщают о чанке информацию свободен ли прошлый чанк, выделен ли данный чанк с помощью mmap и находится ли этот чанк в не главной aren-e).

Как мы видим, чанк находится в fastbin-e с индексом 5. Этот fastbin предназначен для чанков размером 0х70. Смотря на текущее состояние чанка, видно, что fd и bk равны нулю. То есть чанк единственный в fastbin-e.

Теперь перейдём на место после освобождения второго чанка.

Теперь в fastbin-е у нас два чанка, второй чанк стал указывать на первый, а первый так и остался с нулями. Теперь посмотрим, что будет с тремя чанками.

Теперь у нас есть третий чанк, который указывает на второй, а второй на первый.

Посмотрим, что произойдёт при аллоцировании чанка такого же размера. Перед вызовом имеет расклад кучи как указано на рисунке выше, после выделения нового чанка получаем следующее расположение в бине.

В регистре rax содержится адрес выделенного чанка. Как можно заметить, это последний чанк, который был добавлен в fastbin. В момент выделения нового чанка и получения его из fastbin-a мы производим действие по записи указателя на fd для выделяемого чанка в заголовок fastbin-a. Это можно увидеть посмотрев состояние памяти где лежит fastbin-ы.

Как можно заметить, следующий чанк, который будет выделен это второй чанк. Когда он выделиться, то в это-же место будет записан его fd, который указывает на первый чанк. И следующим чанком будет выделен первый. Таким образом, если мы сможем переписать fd одному из чанков, который ещё не выделен, но уже лежит в fastbin-е, а после выделить чанк, то заголовок fastbin-а будет перезаписан нашим значением, после чего выделяя очередной чанк, мы можем выделить его в том месте, которое указали.

Double free в fastbin-e.

Теперь рассмотрим вариант, когда у нас есть double-free и что он нам даёт.

Освобождая чанк, он помещается в начало fastbin-a и его указатели нулевые. Теперь мы освобождаем очередной чанк и он становится началом списка и его fd начинает указывать на первый чанк.

***Освобождая ещё раз первый чанк, мы снова добавляем его в fastbin и теперь он становится началом списка и в его fd помещается второй чанк, а второй чанк в fd начинает указывать на первый чанк и таким образом получается зацикливание***

То есть по сути у нас в fastbin-e находятся чанки в виде: a -> b <- a

Что произойдёт, когда мы решим выделить чанк из fastbin-a?

Т.к. текущий указатель начала списка является чанком a, то выделив мы получим его и в начало списка будет назначен чанк b, т.к. он был у a в fd. Теперь мы можем редактировать чанк a, т.к. выделили его и можем перезаписать его fd. Если мы перезапишем его fd, то после выделения очередного чанка, мы получим чанк b, а fd чанка b станет началом списка. Т.к. fd чанка b являлся чанк a, то он опять становится началом списка.

*И вот теперь при выделении нового чанка мы получим опять чанк a и fd чанка будет записано в начало списка, то есть туда попадёт изменённое значение, а если мы его поменяли на контролируемую нами область, то новый чанк, будет выделен там*

То есть по сути мы подменяем fd чанка и заставляем fastbin думать, что новый чанк будет находится там, куда мы указали. Однако, при выделнии нового чанка есть также ещё одна проверка, которая заключается в том, что на адресе, куда выделяется чанк должен быть корректный размер. То есть если мы хотим выделить чанк из fastbin-a 0x70, то в контролируемой области должен быть соотвествующий размер (точнее размер от 0x70 до 0x7f). Не всегда этого получается добиться. Проверку можно увидеть в исходном коде.

То есть мы проверяем соответсвтие размера чанка на том месте, куда мы хотим его выделить. По этому размеру мы должны получить верный индекс fastbin-а из которого мы этот чанк достали. Чтобы получить чанк из fastbin-a ох70, нам нужно иметь размер от 0x70 до 0x7f.

Почему такой большой диапазон, ведь чанк ровно 0x70?

Ответ опять находится в исходниках. Давайте посмотрим на функцию проверки очень подробно.

Сначало мы получаем размер чанка.

Итак мы берём поле размера и накладываем на него битовую маску, которая определяет три флага, про которые было сказано выше. Значение этих флагов 1, 2, 4. Посмотрим как это работает с помощью Python.

Берём самый большой размер 0x7f и получим 0x78. Отлично, теперь посмотрим на функцию fastbin_index, которая определяет индекс по размеру.

Обратите внимание, что размер преобразуется к типу unsigned int, который занимает 4 байта, то есть на самом деле размер определяется 4 байтами, а не size_t (то есть 8 байтами на х86-64) как в структуре чанка. Далее выбирается размер сдвига в взависимости от архитектуры. Если size_t на х86-64, то он равен 8 байта, а на х32 4 байтам. Опять проверим на Python.

Как можно понять, для 0x78 и для 0x7f всегда получается индекс 5, который соответствует fastbin-у для чанков 0x70.

Вроде бы мы рассмотрели все важные моменты, необходимые для понимания решения данного задания, поэтому можно вернуться к задаче.

Обратно к заданию.

Как мы уже выяснили, нам нужно сделать double-free и потом получить чанк, который ещё будет находится в fastbin-e. Однако чтобы заспавнить чанк на стеке, нам нужно знаеть его адрес, а мы не имеем никаких утечек адресов стека, однако мы можем заспавнить чанк на GOT-таблице. Самое главное, это найти верный размер, то есть чтобы младшие 4 байта выглядели как 0x0000007* . Для предоставленной libc это можно сделать, т.к. функция printf всегда имеет младший байт равный нулю.

Таким образом, подобрав верный оффсет начала чанка, мы можем верно аллоцировать его из fastbin-a 0x70.

То есть адрес чанка должен быть 0x60202d и тогда мы получим чанк размером 0x78 прямо в GOT. Приведённый ниже код осуществляет double-free после чего переписывает fd для дважды освобождённого чанка и доходит до момента, когда следующий чанк будет выделен на GOT.

Остановимся прям после удаления чанка.

Как можно заметить, мы освободили ptr[1] и теперь можем выделить ещё один чанк, а fastbin как раз указывает на GOT. То есть создание следующего чанка будет прямо на GOT.

Так и произошло, теперь мы можем писать в GOT. Теперь, чтобы дальше всё работало также, нам надо корректно перезаписать GOT, для этого мы запишем вместо реальных адресов внтури libc (т.к. мы их не знаем) адреса из plt, чтобы функции верно вызывались и отрабатывали, а тажке мы заблокируем вызов realloc просто поменяв его на ret (чтобы напрямую редактировать чанк), и перепишем atoi на printf, чтобы получить уязвимость форматной строки.

Теперь всё что нам осталось это использовать форматную строку для получения адреса libc, после чего заменить atoi на one_gadget.

Даже с перезаписью atoi на printf мы всё равно можем возвращать корретные числа из функции чтения числа, т.к. printf возвращает количестве выведенных символов (по этому мы передаём "22" и используется функци редактирования, которая в меню обозначена как 2).

Запускаем и в конце набираем любое число.

Получаем шелл.

Задание было очень интересным. Также довольно полезно познакомиться с атаками на fastbin, вроде бы fastbin-dup неплохо получилось разобрать в данном посте.

Report Page