Анализатор сетевого трафика и пакетный фильтр своими руками (Часть 1)
Недавно, будучи не совсем системным администратором, а скажем, просто любителем, я задался вопросом “как же, все-таки, защитить сеть от беспредельщиков?”. Благо информации на эту тему “пруд пруди”, плюс когда-то изучал эту тему, поэтому остались наработки. И вот так появились циклы статей про сетевые анализаторы, сканеры, а в будущем будет материал и про детекторы.
К чему я виду? А к тому, что люди читают посты, комментируют их, порой задают вполне конкретные и адекватные вопросы. Так и произошло в очередной раз. Просматривая новые комментарии, увидел такое: “…а есть ли сетевые анализаторы под Windows?..”. Сказать нет нельзя (и я думаю человек – это понимал, задавая вопрос), но такого просто не может быть. На вопрос я “мало-мальски” ответил, еще добавил, что можно написать самому, имея “рыбу” – в сети хватает. А потом промелькнула мысль поискать скрипт такого анализатора и наткнулся на материал 2005 года, но вполне показательный. Сайт источник http://www.oszone.net (см. www.oszone.net), но и это не оригинал как оказалось, то есть где-то в закоулках интернета, в архивах opennet-а скрываются-таки, действительно ценные вещи!
Рассмотрим, наконец-то, пример программной реализации такого анализатора под Linux, который представляет собой программный код на языке С. Разобраться в котором без наличия определенных навыков, да это и понятно, будет сложно. Кроме знаний программирования нужно знать основы построения локальных сетей на протоколе Ethernet. И, само собой, разумеется, знать технологию перехвата пакетов, чтоб реализовать ее в программе. Начнем!
Как говорит источник, есть много способов перехватить данные сетевого трафика. Самым оптимальным является использование специализированных библиотек, например, libpcap. Именно эту библиотеку используют в своей работе tcpdump и snort. Мы же в нашем примере не будем опираться на библиотеки, а рассмотрим, как перехватить данные сетевого трафика, используя для этого пакетный сокет.
При создании сокета стандартным вызовом socket (int domain, int type, int protocol) параметр domain определяет коммуникационный домен, в котором будет использоваться сокет. Обычно используются значения PF_UNIX для соединений, ограниченных локальной машиной, и PF_INET, для соединений, базирующихся на протоколе IPv4. Аргумент type определяет тип создаваемого сокета и имеет несколько значений. Значение SOCK_STREAM указывается при создании сокета для работы в режиме виртуальных соединений (протокол TCP), а значение SOCK_DGRAM — для работы в режиме пересылки дейтаграмм (протокол UDP). Последний параметр protocol определяет используемый протокол (в соответствии с IEEE 802.3).
В версиях ядра Linux, начиная с 2.2, появилась поддержка нового типа сокетов — пакетных сокетов. Пакетные сокеты используются для отправления и приема пакетов на уровне драйверов устройств. Сокеты данного типа создаются вызовом socket (PF_PACKET, int type, int protocol). Параметр type принимает значение SOCK_RAW или SOCK_DGRAM. Пакеты типа SOCK_RAW передаются драйверу устройства и принимаются от него без всяких изменений данных пакета. SOCK_DGRAM работает на более высоком уровне. Физический заголовок (MAC-адрес) удаляется перед тем, как пакет отправляется на обработку пользователю.
Алгоритм работы таков:
- после запуска на выполнение анализатор определяет параметры сетевого интерфейса eth0, такие как IP адрес, маска подсети, размер MTU, индекс, и переводит интерфейс в неразборчивый режим (promiscuous mode). В этом режиме интерфейс принимает все пакеты, циркулирующие в сети, даже если они не адресованы данному хосту;
- создается пакетный сокет и выполняется его привязка к выбранному сетевому интерфейсу (eth0). Далее анализатор в бесконечном цикле выполняет прием сетевых пакетов и отображает данные об этом пакете – MAC-адреса и IP-адреса отправителя и получателя, размер пакета, размер IP заголовка, тип транспортного протокола (TCP/UDP), порт отправителя и получателя. Выход из цикла осуществляется по приходу сигнала SIGINT (генерируется комбинацией клавиш Ctrl-C);
- получив сигнал SIGINT, анализатор прерывает цикл приема пакетов, снимает флаг неразборчивого режима с сетевого интерфейса и завершает выполнение.
Определять параметры сетевого интерфейса и переключать его режимы будет функция getifconf(). Прототип данной функции выглядит следующим образом:
int getifconf(__u8 *, struct ifparam *, int) |
Функция принимает три параметра:
- указатель на строку, содержащую символьное имя сетевого интерфейса;
- указатель на структуру, в которой будут сохранены параметры сетевого интерфейса. Определение этой структуры будет рассмотрено ниже;
- флаг, определяющий режим работы интерфейса.
Создавать пакетный сокет будет функция getsock_recv():
int getsock_recv (int) |
Параметром функции является индекс сетевого интерфейса, к которому будет привязан сокет. Определение структуры struct ifparam, в которой будут храниться параметры сетевого интерфейса, разместим в файле analizator.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* Листинг 1. Файл analizator.h */ #include <linux/types.h> #define PROMISC_MODE_ON 1 // флаг включения неразборчивый режим #define PROMISC_MODE_OFF 0 // флаг выключения неразборчивого режима struct ifparam { __u32 ip; // IP адрес __u32 mask; // маска подсети int mtu; // размер MTU int index; // индекс интерфейса } ifp; |
Рассмотрим подробно каждую функцию.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | /* * Листинг 2. Функция определения параметров сетевого интерфейса и * переключения режимов (файл getifconf.c) */ #include <linux/socket.h> #include <linux/ioctl.h> #include <linux/if.h> #include <linux/in.h> #include "analizator.h" int getifconf(__u8 *intf, struct ifparam *ifp, int mode) { int fd; struct sockaddr_in s; struct ifreq ifr; // см. <linux/if.h> memset((void *)&ifr, 0, sizeof(struct ifreq)); if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0) return (-1); sprintf(ifr.ifr_name,"%s",intf); /* * Проверяем флаг режима. Если он установлен в 0, неразборчивый режим * необходимо отключить, поэтому сразу выполняется переход на метку setmode */ if(!mode) goto setmode; /* * Определяем IP адрес сетевого интерфейса */ if(ioctl(fd, SIOCGIFADDR, &ifr) < 0) { perror("ioctl SIOCGIFADDR"); return -1; } memset((void *)&s, 0, sizeof(struct sockaddr_in)); memcpy((void *)&s, (void *)&ifr.ifr_addr, sizeof(struct sockaddr)); memcpy((void *)&ifp->ip, (void *)&s.sin_addr.s_addr, sizeof(__u32)); /* * Определяем маску подсети */ if(ioctl(fd, SIOCGIFNETMASK, &ifr) < 0) { perror("ioctl SIOCGIFNETMASK"); return -1; } memset((void *)&s, 0, sizeof(struct sockaddr_in)); memcpy((void *)&s, (void *)&ifr.ifr_netmask, sizeof(struct sockaddr)); memcpy((void *)&ifp->mask, (void *)&s.sin_addr.s_addr, sizeof(u_long)); /* * Определяем размер MTU */ if(ioctl(fd, SIOCGIFMTU, &ifr) < 0) { perror("ioctl SIOCGIFMTU"); return -1; } ifp->mtu = ifr.ifr_mtu; /* * Индекс интерфейса */ if(ioctl(fd, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl SIOCGIFINDEX"); return -1; } ifp->index = ifr.ifr_ifindex; /* * Устанавливаем заданный режим работы сетевого интерфейса */ setmode: /* * Получаем значение флагов */ if(ioctl(fd, SIOCGIFFLAGS, &ifr) < 0) { perror("ioctl SIOCGIFFLAGS"); close(fd); return -1; } /* * В зависимости от значения третьего параметра функции, устанавливаем * или снимаем флаг неразборчивого режима */ if(mode) ifr.ifr_flags |= IFF_PROMISC; else ifr.ifr_flags &= ~(IFF_PROMISC); /* * Устанавливаем новое значение флагов интерфейса */ if(ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) { perror("ioctl SIOCSIFFLAGS"); close(fd); return (-1); } return 0; } |
Значения SIOCGIFADDR, SIOCGIFNETMASK, SIOCGIFMTU и др. определены в файле <linux/sockios.h>. Этот файл не включен в список заголовочных файлов, т.к. он уже определен в <linux/socket.h>.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /* * Листинг 3. Функция создания пакетного сокета (файл getsock_recv.c) */ #include <linux/socket.h> #include <linux/types.h> #include <linux/if_ether.h> #include <linux/if_packet.h> int getsock_recv(int index) { int sd; // дескриптор сокета /* * При работе с пакетными сокетами для хранения адресной информации * сетевого интерфейса вместо структуры sockaddr_in используется структура * sockaddr_ll (см. <linux/if_packet.h>) */ struct sockaddr_ll s_ll; /* * Cоздаем пакетный сокет. Т.к. MAC-адреса мы тоже собираемся обрабатывать, * параметр type системного вызова socket принимает значение SOCK_RAW */ sd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if(sd < 0) return -1; memset((void *)&s_ll, 0, sizeof(struct sockaddr_ll)); /* * Заполним поля адресной структуры s_ll */ s_ll.sll_family = PF_PACKET; // тип сокета s_ll.sll_protocol = htons(ETH_P_ALL); // тип принимаемого протокола s_ll.sll_ifindex = index; // индекс сетевого интерфейса /* * Привязываем сокет к сетевому интерфейсу. В принципе, делать это не * обязательно, если на хосте активен только один сетевой интерфейс. * При наличии двух и более сетевых плат пакеты будут приниматься сразу со всех * активных интерфейсов, и если нас интересуют пакеты только из одного сегмента * сети, целесообразно выполнить привязку сокета к нужному интерфейсу */ if(bind(sd, (struct sockaddr *)&s_ll, sizeof(struct sockaddr_ll)) < 0) { close(sd); return -1; } return sd; } |
Цикл приема сетевых пакетов и отображение результатов будет выполняться в главной функции main().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | /* * Листинг 4. Главная функция (файл analizator.c) */ #include <stdio.h> #include <signal.h> #include <sys/socket.h> #include <linux/if_ether.h> #include <linux/in.h> #include <linux/ip.h> #include <linux/tcp.h> #include "analizator.h" /* * В буфере buff будут сохранятся принятые сетевые пакеты. * Значение ETH_FRAME_LEN равно максимальной длине кадра Ethernet (1514) * и определено в <linux/if_ether.h> */ __u8 buff[ETH_FRAME_LEN]; /* * Функция, которая заменит стандартный обработчик сигнала SIGINT. * Задача этой функции - по приходу сигнала SIGINT вывести интерфейс из * состояния "Promiscuous mode" в обычный режим */ void mode_off() { if(getifconf("eth0", &ifp, PROMISC_MODE_OFF) < 0) { perror("getifconf"); exit(-1); } return; } /* * Главная функция */ int main() { __u32 num = 0; int eth0_if, rec = 0, ihl = 0; struct iphdr ip; // структура для хранения IP заголовка пакета struct tcphdr tcp; // TCP заголовок struct ethhdr eth; // заголовок Ethernet-кадра static struct sigaction act; /* * Получаем параметры сетевого интерфейса eth0 и переводим его * в неразборчивый режим */ if(getifconf("eth0", &ifp, PROMISC_MODE_ON) < 0) { perror("getifconf"); return -1; } /* * Отобразим полученные параметры сетевого интерфейса */ printf("IP адрес - %s\n",inet_ntoa(ifp.ip)); printf("Маска подсети - %s\n",inet_ntoa(ifp.mask)); printf("MTU - %d\n", ifp.mtu); printf("Индекс - %d\n", ifp.index); /* * Получим дескриптор пакетного сокета */ if((eth0_if = getsock_recv(ifp.index)) < 0) { perror("getsock_recv"); return -1; } /* * Определим новый обработчик сигнала SIGINT - функцию mode_off */ act.sa_handler = mode_off; sigfillset(&(act.sa_mask)); sigaction(SIGINT, &act, NULL); /* * Запускаем бесконечный цикл приема пакетов */ for(;;) { memset(buff, 0, ETH_FRAME_LEN); rec = recvfrom(eth0_if, (char *)buff, ifp.mtu + 18, 0, NULL, NULL); if(rec < 0 || rec > ETH_FRAME_LEN) { perror("recvfrom"); return -1; } memcpy((void *)ð, buff, ETH_HLEN); memcpy((void *)&ip, buff + ETH_HLEN, sizeof(struct iphdr)); if((ip.version) != 4) continue; memcpy((void *)&tcp, buff + ETH_HLEN + ip.ihl * 4, sizeof(struct tcphdr)); /* * MAC-адреса отправителя и получателя */ printf("\n%u\n", num++); printf("%.2x:%.2x:%.2x:%.2x:%.2x:%.2x\t->\t", eth.h_source[0],eth.h_source[1],eth.h_source[2], eth.h_source[3],eth.h_source[4],eth.h_source[5]); printf("%.2x:%.2x:%.2x:%.2x:%.2x:%.2x\n", eth.h_dest[0],eth.h_dest[1],eth.h_dest[2], eth.h_dest[3],eth.h_dest[4],eth.h_dest[5]); printf("Длина заголовка - %d, ", (ip.ihl * 4)); printf("длина пакета - %d\n", ntohs(ip.tot_len)); /* * Если транспортный протокол - TCP, отобразим IP адреса и порты * получателя и отправителя */ if(ip.protocol == IPPROTO_TCP) { printf("%s (%d)\t->\t",inet_ntoa(ip.saddr), ntohs(tcp.source)); printf("%s (%d)\n",inet_ntoa(ip.daddr), ntohs(tcp.dest)); printf("TCP пакет\n"); } } return 0; } |
Для сборки исполняемого файла создадим Makefile следующего содержания:
.PHONY = clean analizator: analizator.o getifconf.o getsock_recv.o gcc $^ -g -o $@ %.o: %.c gcc -c $^ clean: rm -f *.o rm -f ./analizator |
После запуска на выполнение программа переводит интерфейс в неразборчивый режим и запускает бесконечный цикл прослушивания сетевого трафика и отображения результатов. Факт включения неразборчивого режима интерфейса будет зафиксирован в файле /var/log/messages:
eth0: Promiscuous mode enabled. device eth0 entered promiscuous mode |
Бесконечный цикл работы анализатора будет прерван после нажатия комбинации клавиш Ctrl-C. Программа получит сигнал SIGINT, и обработчик этого сигнала функция mode_off() снимет флаг неразборчивого режима с интерфейса. При этом в файле /var/log/messages появится запись:
device eth0 left promiscuous mode |
В следующей статье мы рассмотрим пакетную фильтрацию, а точнее способ ее программной реализации. Успехов Всем!..
Твитнуть
Когда-то пытался писать подобное, но под Windows но все было куда более замутно с перехватом ядерных апи NDIS. Сейчас пользуюсь в основном линукс, интересно будет почитать продолжение статьи. спасибо! )