PWN — одна из наиболее самодостаточных категорий тасков на CTF-соревнованиях. Такие задания быстро готовят к анализу кода в боевых условиях, а райтапы по ним чаще всего описывают каждую деталь, даже если она уже была многократно описана. Мы рассмотрим таск Useless Crap с апрельского TG:HACK 2020. Сам автор оценил его сложность как Hard. Задание очень интересное, во время соревнования я потратил на него около двенадцати часов.
 

Подготовка

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

Я предпочел не выбирать однозначно между IDA и Ghidra и использую один или другой дизассемблер в зависимости от ситуации, но в тасках категории PWN хороший псевдокод чаще выдает IDA.

«Ванильный» GDB невозможно использовать без очень серьезной психологической подготовки, так что чаще всего его юзают в сочетании с одним из плагинов: PEDA, GEF или pwndbg. Из них PEDA — самый старый (классический!) вариант, но я до сих пор не переехал на один из новых, так что использую его.

Также, пока весь мир полностью переезжает на Python 3, разработчики эксплоитов и не думают о том, чтобы покидать любимый Python 2. Дело в очень неприятной обработке raw bytes в третьей ветке Python, приходится постоянно держать в голове ее особенности и тратить лишнее время на исправление возникающих багов.

Полезные дополнительные инструменты:

  • pwntools как самый удобный API на Python для взаимодействия с исполняемыми файлами;
  • checksec — для определения защитных механизмов бинарника;
  • patchelf как инструмент для патчинга libc и исполняемых файлов.
 

Первоначальный осмотр

Итак, организаторы дали нам бинарник и файлы серверной libc и линковщика. Также точно указан путь до флага; опытные игроки в CTF сразу могут предположить, что придется писать свой шелл-код.

Очевидно, самое первое, что нужно сделать, — это просто выполнить бинарник и примерно оценить сложность, быстренько просмотрев security mitigations в checksec.

Чтобы исполняемый файл использовал нужную libc, пропатчим в нем путь до линкера и укажем ее в переменной окружения LD_PRELOAD.

patchelf --set-interpreter ld-2.31.so ./crap
LD_PRELOAD=./libc-2.31.so ./crap

Нас встречает незамысловатая менюшка, появляется надежда на быстрое и простое решение. Живет эта надежда, правда, недолго, примерно до открытия checksec.

У нас включены на максимум все защитные механизмы. Вот их краткое описание.

NX — делает стек неисполняемым. Около двадцати лет назад большинство уязвимостей переполнения буфера эксплуатировали запись шелл-кода на стек с последующим прыжком на него. NX делает такую технику невозможной, однако сейчас она еще жива в мире IoT.

Stack canary — определенное секретное значение на стеке, записанное перед RBP и return pointer и, таким образом, защищающее их от перезаписи через уязвимость переполнения буфера.

Full RELRO — делает сегмент GOT доступным только для чтения и размещает его перед сегментом BSS. Техники эксплуатации через перезапись GOT не сложны, но выходят за рамки этой статьи, так что предлагаю читателю самому разобраться с ними. О том, что такое Global Offset Table, можно прочитать, например, в Википедии.

 

Как работают ASLR и PIE?

ASLR — это защитный механизм, который значительно усложняет эксплуатацию. Его основная задача — рандомизация базовых адресов всех регионов памяти, кроме секций, принадлежащих самому бинарнику.

По сути, ASLR работает следующим образом. В диапазоне адресов, который на несколько порядков превышает размер рандомизируемого региона памяти, выбирается начальная точка отсчета, базовый адрес. К нему есть два требования:

  • последние три ниббла («полубайта») этого адреса должны быть равны 000;
  • весь рандомизированный регион не должен конфликтовать с другими регионами и выходить за рамки предложенного диапазона.

Главная проблема атакующего в том, что это действительно работает. Можно взять конкретный пример: нужно найти адрес функции system в libc, при этом никакой информации о нем не известно. Давай примерно представим, сколько времени на это понадобится. Первый байт любого адреса библиотеки почти обязан быть равен 0x7f. Последние три ниббла мы знаем, так как независимо от выбора базового адреса они остаются теми же при каждом запуске программы. Достаточно несложная школьная задачка по комбинаторике:

2^8 * 2^8 * 2^8 * 2^4 = 2^28 = 268435456

Это примерная оценка, так как не учитывается определенное количество адресов вверху диапазона, которые брать нельзя, иначе остальной регион памяти тогда не уместится; тем не менее она достаточно точная. Допустим, на каждый запуск эксплоита в среднем мы тратим три секунды. Тогда полный перебор займет примерно 25 лет, что нас явно не устраивает, ведь CTF идет всего 48 часов.

Ну и наконец, PIE — это, по сути, ASLR для сегментов памяти самого исполняемого файла. В отличие от базового ASLR, который работает на уровне ОС, PIE — это опциональный защитный механизм, он может и не присутствовать в бинарнике.

 

Реверс-инжиниринг программы

Есть легенда, что если реверсера разбудить среди ночи и дать ноутбук, то он первым делом откроет IDA и нажмет F5. Не знаю, насколько это правда, но всегда поступаю именно так, когда хочу разобраться, как работает неизвестный исполняемый файл.

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

Рассмотрим функции по порядку.

 

1. init

Здесь нет ничего по-настоящему интересного, просто отключается буферизация ввода и вывода и устанавливается время работы программы (после 0x3c секунд произойдет прыжок на handler, функцию, которая состоит из одной строки: exit(0);).

 

2. sandbox

Эта функция намного интереснее предыдущей: оказывается, в программе достаточно жестко настроен seccomp. Давай разберемся, какие системные вызовы разрешены. Найти таблицу соответствий названий сисколлов с их номерами не представляет труда.

Итак, абсолютно точно разрешены exit, mprotect, open и close. Немного, но уже становится понятен финальный этап эксплуатации: нужно будет сделать один из регионов памяти доступным для чтения, записи и исполнения, записать туда шелл-код на чтение файла с флагом и прыгнуть на него.

Также доступны системные вызовы read и write, но не полностью. IDA не показывает аргументы seccomp_rule_add после четвертого, а ведь основные правила настройки для заданных сисколлов именно там. Нажав правой кнопкой мыши на название функции, можно выбрать опцию Set call type и, таким образом, дописать еще несколько __int64, чтобы увидеть больше аргументов.

По опыту работы с seccomp могу сказать, что IDA не совсем правильно определила седьмой аргумент, который равен SCMP_CMP_EQ (это 4), но становится ясно, что программа может читать только из нулевого дескриптора (stdin), а писать только в первый дескриптор (stdout). Пока что не совсем понятно, как тогда написать шелл-код, ведь читать нужно в любом случае из дескриптора открытого файла, который точно не равен нулю. Но об этом позже.

 

3. menu

Это меню, которое выводится каждую итерацию цикла в main.

 

4. get_num

Получение номера выбранной функции происходит безопасно, здесь нет ничего интересного для нас.

 

5. do_read

Автор таска предоставляет нам чистый arbitrary read. Не могу сказать, что это очень редкое и уникальное решение, но такие задания чаще всего крайне интересны. Мы можем прочитать что угодно откуда угодно не более двух раз, по крайней мере так будет считать программа. Переменная read_count глобальная, а значит, хранится не на стеке, а в BSS. В дальнейшем понимание этого может облегчить эксплуатацию.

 

6. do_write

Похожим образом работает do_write. У нас появляется возможность записывать что угодно куда угодно, пока write_count меньше единицы или равен ей. Сразу можно придумать обход механизма проверки: после каждой записи через следующий do_write присваивать write_count значение -1, таким образом получить полный, ничем не ограниченный arbitrary write и схожим образом arbitrary read.

Мы еще не успели прочитать весь код, а уже имеем серьезный контроль над потоком выполнения программы.

 

7. leave_feedback

При выборе третьей опции меню вместо незамедлительного выхода программа попросит оставить обратную связь. Это происходит следующим образом:

  • проверяется, равен ли нулю глобальный указатель feedback;
  • если нет, то программа аллоцирует чанк размером 0x501 и даст нам непосредственный ввод в него;
  • затем пользователя спрашивают: хочет ли он, чтобы его фидбек был сохранен;
  • если он введет n, то чанк будет освобожден, но указатель feedback не обнулится.

Пока что эта ошибка некритична, но в дальнейшем может быть очень полезна.

Чтобы вызвать эту функцию, нужно ввести цифру 4, упоминания о которой нет в меню. Функция view_feedback выводит то, что находится по указателю feedback, не проверяя состояние чанка, который может быть освобожден. Такой тип уязвимостей называется Use-After-Free. Подразумевается, что по адресу указателя должен лежать пользовательский ввод, но чуть позже мы увидим, что для освобожденных чанков это не всегда так.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Оставить мнение