Saturday, October 19, 2019

Переполнение буфера в реальных условиях (ASLR+DEP @Ellingson@HTB)

Очень немного в русскоязычной блогосфере материалов про реализацию переполнения буфера в условиях, максимально близких к реальным: x64 с включенными ASLR и DEP.

Попробую ситуацию исправить поделившись своим небольшим опытом решения машинки Ellingson на HTB. На момент начала работы над Ellingson@HTB мои познания в реализации RCE через переполнение были сугубо теоретическими, однако по завершении многие вопросы вполне неплохо прояснились, о чем и хочется рассказать. Приведу здесь свои заметки, которые сделал для себя на случай, если снова придется столкнуться с чем-то подобным (ибо в повседневной своей работе, к сожалению, не занимаюсь бинарщиной, а поскольку практики нет, все очень быстро забывается).

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




Предварительно
Надо поставить gdb_peda: https://github.com/longld/peda

Вручную

Для начала копируем к себе garbage и проверяем его формат (можно сразу притащить из libc.so с целевой машины, она нам тоже понадобится). Убеждаемся, что это нормальный ELF x64, значит его можно запустить


Запускаем garbage в отладчике, смотрим checksec


Для приложения активирован DEP (NX: ENABLED). DEP запретит исполнение кода из стека, поэтому мы не сможем просто в него запросить шелл-код и выполнить его, следовательно нам надо надо делать атаку типа “return to libc”, где мы будем передавать управление на уже существующие места в libc и выполнять их.
Кратко о других защитах
CANARY - записывает нечто в регистры до пользовательского ввода и проверяет это после. Если это нечто было изменено - это означет, что стек был поврежден пользовательским вводом: https://en.wikipedia.org/wiki/Stack_buffer_overflow#Stack_canaries
FORTIFY - заменяет некоторые уязвимые вызовы их безопасными эквивалентами, что снижает риск переполнения буфера
PIE (position independent executable) - это фактически ASLR. Здесь PIE выключено - это означает, что все выделения памяти для этого приложения будут одинаковыми при любом запуске, но это не означает, что расположение в памяти всех библиотек, которые использует это приложение будут одинаковыми. Более того, как увидим далее, ASLR на целевой машине будет активирован.
RELRO - Relocation read-only - делает GOT (Global offset table) неперезаписываемо, подробности здесь: https://medium.com/@HockeyInJune/relro-relocation-read-only-c8d0933faef3

Из бинаря strings-ом вытаскивается пароль N3veRF3@r1iSh3r3!, спрашиваемый при запуске. С его помощью можно посмотреть как программа работает, и позасылать 500*”A” на разные вопросы и таким образом убедиться, что переполнения там нет.


Опытным путем устанавливаем, что переполнение возможно как раз при вводе пароля. При отправке 500 символов “А” мы получили Segmentation fault.



Напомним, что на х64 мы не перезаписываем RIP напрямую (в x64 параметры в функцию передаются через регистры (RDI и т.п.), а не через стек), как это делается в случае х32. Видно, последнее, что было в RIP - вызов return (ret), возвращающий программу на код по адресу, находящемуся в RSP (напомню, что RSP -  указывает на стек). Однако, адрес в RSP указывает на наш буфер из “А” (“A” repeats 200 times) и поэтому выполнить это не получается. Нам надо это исправить, записав вместо 200 “А” необходимый нам код, который будет выполнен. Но для начала поймем где происходит переполнение.
Чтобы это сделать воспользуемся классическим подходом с pattern create

и отправкой его в переполняемой буфер

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

Т.е. начиная с байта 136 мы попадаем в область памяти, адрес которой хранится в RSP

Начинаем писать наш эксплоит. Используя pwntools мы заходим по SSH с кредами подломанного пользователя (получение пользователя в этой машине - за пределами этой статьи) и запускаем эксплуатируемое приложение.


Идея следующая. нам надо найти puts внутри нашего бинаря, заставить его выдать нам адрес puts внутри libc - это позволит нам понять адрес libc на момент переполнения, ну и дальше пойти в libc и что-нибудь там позапускать.

Находим вызов puts внутри нашего бинаря


Нам нужны оба адреса:
0х401050 - место puts в PLT (Procedure linkage table)
0x404028 - место puts в GOT (Global offset table)
Мы хотим, что pits из нашего бинаря вызвал себя из GOT в результате чего мы получим адрес из libc, который изменяется при каждом запуске

Берем эти адреса и копируем в наш эксплоит. Адреса должны быть преобразованы в х64-форма (в частности, у нас в адресе указано 3 байта, p64() дополнит как надо до 8 и решит проблему с big/little-endian)


Еще нам нужен вызов (есть модное слово - гаджет) “pop rd; ret”. pop rdi - извлекает значение из стека в регистр RDI, ret - выходит. По основным командам ассемблера попалась шпаргалка: https://eax.me/assembler-basics/
В двоичных кодах pop rdi; ret выглядит как 5f c3 поэтому искать эту последовательность можно прямо вручную:

Получаем адрес в бинаре: 0x1790 + 0xb = 0x179b. запишем это в наш эксплоит (за одно поправим ошибку в слове offset 🙂) 



Перед тем как посылать эти команды нам надо выполнить переполнение. Для этого мы посылаем 136*”А”, и можно конструировать нагрузку

Что тут происходит.
Посылаем junk - вызываем переполнение и добираемся о стека. Далее выполняется команда в pop_rdi, которая кладет в RDI содержимое стека (помним, что в RDI хранится аргумент для функции). Этим аргументом является got_put - адрес puts в GOT, а самой функцией является - puts, адрес которой в plt_puts.
При отработке этого кода мы должны в выводе получить адрес puts внутри libc в момент краша.

Запустим наш эксплоит с отладкой и посмотрим, что там видно


Видно, что после “access denied.\n” возвращается какая-то информация - 7f f1 93 ad 59 c0 (у нас little-endian, поэтому записал наоборот) - это и есть искомый динамический адрес. Можно убедиться, что он изменяется от запуска к запуску.

Нам надо, чтобы программа после переполнения не умирала, а продолжила исполнение, поэтому после получения адреса, надо вернуть ее в main.
Сделаем это.

Найдем по какому адресу у нас располагается main

Скорректируем нашу нагрузку соответственно



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


При многократном запуске можно убедиться, что leaked puts@GLIBC возвращает разные адреса. Например, в моем случае:


и т.д.

Замечаем на опыте, что возвращается всегда 6 байт, вместо 8. 8 - ой байт 0x45 соответсвует символу “E” от фразы "Enter access password:”, выводимой уже из main. А 0a - это перевод строки. Творчески скорректируем эксплоит.


После этого утекающие адреса будут выглядеть более правдоподобно


и т.д.

Итак, мы сделали переполнение, получили адрес puts в libc и вернусь в main как ни в чем не бывало. Переходим ко второй части - вызову system(“/bin/sh)

Для вычисления фактического положения libc нам понадобится относительное положение puts в libc. это можно получить из бинаря.


Нам понадобится расположение вызова system там же:


Также понадобится расположение строки "/bin/sh”, которая также есть в libc:


Кроме того, поскольку нам надо повыситься через это приложение, нам надо вручную вызвать setuid(0) , благо setuid также есть в libc:


Соберем теперь все эти знания вместе для написания пейлоада для второй стадии


Сначала мы посылаем jink и доходим до стека (область памяти, куда ссылается RSP).
Затем вызываем pop rdi; ret (в переменной pop_rdi), чтобы положить в RDI аргумент (0х0) для вызова setuid.
Затем вызываем сам setuid (в переменной setuid), который берет из RDI для своего исполнения аргумент (0x0)
Затем мы снова вызываем pop_rdi, чтобы на этот раз положить в RDI строку ‘/bin/sh’, располагающуюся по адресу в переменной sh.
Затем вызываем system по адресу в переменной sys с аргументом из RDI.

Лично для меня было любопытно, что программа будучи setuid-ной 

требует при своей работе ручного вызова setuid(0), что мы несложно реализовали последовательностью: pop_rdi + p64(0x0) + setuid

Запускаем наш эксплоит и получаем root-а


можно выключить отладку, тогда вывод будет поприличней. 




Общий код выглядит вот так:



Автоматизированно
Мы проделали полезное для понимания упражнение, вычисляя все нужные нам гаджеты и адреса вручную. pwntools может это все автоматизировать.
Чтобы pwntools сама искала в бинарях нужные нам последовательности вызовов и строки, скопируем их локально:


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



Вывод с включенной отладкой





В выводе полезно обратить внимание на адрес libc - 0x7f7d912ff000. Адрес с окончанием на 0x00 - характерен для libc. Так можно косвенно проверить, что мы правильно вычислили рандомизированный адрес libc.

Оба варианта эксплоитов и само приложение приложены.

Всм счастья!

No comments: