ВЛАДИМИР МЕШКОВ
Анализатор сетевого трафика
Если вы – системный администратор, специалист по безопасности, или вам просто интересно, что происходит в вашей локальной сети, то перехват и анализ нескольких сетевых пакетов может быть полезным упражнением. При помощи небольшой программы на языке С и базовых знаний сетевых технологий вы сможете перехватить данные сетевого траффика, даже если они адресованы не вам. В данной статье мы рассмотрим, как это можно сделать в сети Ethernet – наиболее распространенной на данный момент технологии построения локальных компьютерных сетей.
Обзор технологии Ethernet
Для начала вспомним, как функционирует сеть Ethernet (те из вас, кто уже знаком с данным вопросом, могут пропустить этот параграф). IP-пакеты (дейтаграммы), источником которых является приложение пользователя, инкапсулируются в Ethernet-кадры (пакеты Ethernet-протокола, передаваемые в сети). Каждый кадр содержит исходный IP-пакет и другую информацию, необходимую для доставки его адресату, в частности, 6-ти байтовый Ethernet-адрес (MAC-адрес) назначения, который при помощи протокола ARP ставится в соответствие IP-адресу назначения. Таким образом, сформированный кадр, содержащий пакет, начинает свое путешествие от хоста-отправителя к хосту-получателю через кабельное соединение.
На уровне протокола Ethernet маршрутизация отсутствует. Другими словами, кадр, отправленный хостом-отправителем, не попадает напрямую хосту-получателю, а будет доступен для всех хостов, объединенных в сеть. Каждая сетевая карта принимает кадр и считывает из него первые 6 байт. Эти байты содержат MAC-адрес хоста-получателя, но только одна карта в сети определит его как свой собственный и передаст кадр для дальнейшей обработки сетевому драйверу. Сетевой драйвер проверит поле «Тип протокола» заголовка кадра и, основываясь на этом значении, направит инкапсулированный пакет соответствующей приемной функции данного протокола. В большинстве случаев это протокол IP. Приемная функция изымает IP заголовок из принятой дейтаграммы и передает инкапсулированное сообщение соответствующему модулю протокола транспортного уровня (например, TCP или UDP). Эти протоколы, в свою очередь, обрабатывают свои заголовки и передают данные протоколам прикладного уровня. В течение этой «экскурсии» по различным уровням сетевого стека исходный пакет теряет все служебные поля протоколов и, в конце концов, данные, передаваемые в пакете, принимаются пользовательским приложением.
Пакетные сокеты
При создании сокета стандартным вызовом 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 (SOCK_PACKET, int type, int protocol). Параметр type равен или SOCK_RAW, или SOCK_DGRAM.
Пакеты типа SOCK_RAW передаются драйверу устройства и принимаются от него без всяких изменений данных пакета.
SOCK_DGRAM работает на более высоком уровне. Физический заголовок (MAC-адрес) удаляется перед тем, как пакет отправляется на обработку пользователю.
Пример реализации
Итак, приступим непосредственно к созданию анализатора. Для этого нам необходимо:
- определить необходимые переменные;
- получить параметры сетевого интерфейса (IP-адрес, маску подсети, номер подсети, размер MTU, индекс (номер) интерфейса);
- создать пакетный сокет и привязать его к определенному интерфейсу;
- принять сетевой пакет и проанализировать его структуру. Этим будет заниматься главная функция программы.
Для удобства каждый из пунктов оформим в виде отдельной функции или заголовочного файла.
Переменные
Необходимые заголовочные файлы:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <linux/if.h
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ip.h>
Структура для хранения принятого IP – пакета:
struct ip_packet {
struct iphdr ip;
char *ip_data;
} ip_pack;
Cтруктура для хранения параметров сетевого интерфейса:
struct ifreq ifr;
Структура для получения адресной информации:
struct sockaddr_in s
Структура для хранения заголовка IP-пакета:
struct iphdr *ip;
Структура для хранения заголовка Ethernet-кадра:
struct ethhdr eth;
Вспомогательная структура, содержащая параметры интерфейса:
struct ifparam {
u_long ip;
u_long mask;
u_long subnet;
int mtu;
int index;
} ifp;
int e0_r, – дескриптор сокета
rec; – размер принятого пакета в байтах
char *buff; – буфер для приема пакетов
Переменные разместим в файле ip.h.
Функция получения параметров сетевого интерфейса
#include "ip.h"
#include <sys/ioctl.h>
int getifconfig (struct ifreq *ifr, char *intf, struct ifparam *ifp)
{
int fd; – дескриптор сокета
Создадим сокет:
if (( fd= socket (AF_INET, SOCK_DGRAM, 0)) <0 ) {
perror ( "socket" );
return ( - 1 );
}
Скопируем имя интерфейса в поле ifr_name структуры ifr:
sprintf (ifr->ifr_name, "%s", intf);
Получим IP-адрес интерфейса:
if (ioctl (fd, SIOCGIFADDR, ifr) <0 ) {
perror ("ioctl");
return (-1);
}
memset(&s, 0, sizeof (struct sockaddr_in));
memcpy(&s, &ifr->ifr_addr, sizeof (struct sockaddr));
memcpy(&ifp-ip, &to.sin_addr.s_addr, sizeof (u_long));
Получим маску подсети:
if (ioctl (fd, SIOCGIFNETMASK, ifr) <0 ) {
perror ("ioctl");
return (-1);
}
memset(&s, 0, sizeof (struct sockaddr_in));
memcpy(&s, &ifr->ifr_netmask, sizeof (struct sockaddr));
memcpy(&ifp-mask, &to.sin_addr.s_addr, sizeof (u_long));
Получим номер подсети:
ifp->sunbet = check_subnet(ifp->mask, ifp->ip);
Получим размер MTU:
if (ioctl (fd, SIOCGIFMTU, ifr) <0 ) {
perror ("ioctl");
return (-1);
}
ifp -> mtu = ifr -> ifr_mtu;
Получим индекс (номер) интерфейса:
if ( ioctl (fd, SIOCGIFINDEX, ifr) <0 ) {
perror ("ioctl");
return (-1);
}
ifp -> index = ifr -> ifr_ifindex;
Переведем интерфейс в неразборчивый режим. Для этого получим значение флагов интерфейса:
if ( ioctl (fd, SIOCGIFFLAGS, ifr) <0 ) {
perror ("ioctl");
close (fd);
return (-1);
}
Установим флаг неразборчивого режима:
ifr -> ifr_flags |= IFF_PROMISC;
Установим новое значение флагов интерфейса:
if ( ioctl (fd, SIOCSIFFLAGS, ifr) <0 ) {
perror ("ioctl");
close (fd);
return (-1);
}
return 1;
}
Параметрами функции getifconfig являются структура struct ifreq *ifr, имя интерфейса char *intf и структура struct ifparam *ifp. Номер подсети определяется при помощи вызова функции check_subnet, приведенной ниже.
Установкой флага интерфейса IFF_PROMISC мы добиваемся приема всех пакетов, даже если они не адресованы нашему хосту.
Функция определения номера подсети
BITS 32
GLOBAL check_subnet
SECTION .text
check_subnet:
push ebp – сохраним адрес возврата из функции
mov ebp, esp
mov edx, [ebp+8] – первый параметр - маска подсети
mov eax, [ebp+12] – второй параметр - IP адрес
mov cx, 32 – число разрядов в IP адресе в формате IPv4
push cx – сохраним значение в стеке
xor esi, esi – обнулим счетчик
.label
bt edx, esi – сканируем маску в поисках 1
jnc .msk – выход из цикла при совпадении
inc esi – инкремент счетчика
loop .label – продолжить поиск
.mask
pop cx – извлечь ранее сохраненное значение из стека
sub cx, si – число разрядов в адресе, отведенных под хостовую часть
shl eax, cl – логический сдвиг на это значение
mov esp, ebp
pop ebp – восстановим стек
ret – возврат из функции
Функция check_subnet сканирует первый переданный параметр (маску подсети) до первого появления 1 и запоминает эту позицию. Эта позиция соответствует числу разрядов, отведенных в адресе на сетевую составляющую. Далее, во втором переданном параметре (адресе) происходит логический сдвиг на число разрядов, отведенных под адрес хоста. Таким образом, у нас остается только сетевая составляющая, которая и является адресом подсети.
Функция для создания пакетного сокета.
Заголовочные файлы:
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h >
#include <linux/if_packet.h>
#include <linux/if_ether.h>
int getsock_recv (int index)
{
int fd;
Структура для хранения адресной информации об интерфейсе (см. файл <linux/if_packet.h>):
struct sockaddr_ll s_ll;
Создадим пакетный сокет:
if (( fd= socket (SOCK_PACKET, SOCK_RAW, htons (ETH_P_ALL) )) <0 ) {
perror ( "socket" );
return ( - 1 );
}
Выделим память для структуры struct sockaddr_ll s_ll:
memset (&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; – номер интерфейса
s_ll.sll_pkttype = PACKET_HOST; – тип пакета (для локальной машины)
Привяжем сокет к интерфейсу:
if ((bind (fd, (struct sockaddr *) &s_ll, sizeof (struct sockaddr_ll)) <0 ) {
perror ("bind");
close (fd);
return (-1);
}
Возвратим дескриптор сокета в вызывающую функцию:
return (fd);
}
Функция getsock_recv принимает в качестве параметра индекс интерфейса и возвращает дескриптор пакетного сокета. Значение поля protocol в системном вызове socket равно htons (ETH_P_ALL). Это означает, что мы будем принимать пакеты всех протоколов. Все входящие пакеты будут передаваться пакетному сокету до того, как они будут переданы протоколам, реализованным в ядре. Список возможных протоколов приведен в файле <linux/if_ether.h>.
Для получения пакетов только с определенного интерфейса используется функция bind: таким образом мы соединяем пакетный сокет с интерфейсом, адрес которого указан в структуре struct sockaddr_ll. Если этого не сделать, мы будем получать пакеты со всех сетевых интерфейсов, которые в данный момент активны.
Главная функция
#include "ip.h"
int main ()
{
Получим параметры сетевого интерфейса:
memset (&ifr, 0, sizeof (struct ifreq));
if (getifconfig (&ifr, "eth0", &ifp) <0 ) {
perror ("getifconfig");
exit (1);
}
Выделим память:
buff = (char *) malloc (ifp.mtu + 18);
memset(&ip, 0, sizeof (struct ip_packet));
ip_pack.ip_data = (char *) malloc ( ifp.mtu - sizeof (struct iphdr));
ip=(struct iphdr *)&ip_pack.ip;
Получим дескриптор пакетного сокета:
if ((e0_r = getsock_recv (ifp.index)) <0 ) {
perror ("getsock_recv");
exit(1);
}
Цикл приема пакетов:
for (;;) {
Обнулим буфер:
bzero (buff, ifp.mtu+18);
rec = 0;
Принять пакет:
rec=recvfrom (e0_r, (char *)buff, ifp.mtu+18, 0, NULL, NULL);
if (rec<0) {
perror ("recvfrom");
exit(1);
}
Число принятых байт (длина принятого пакета):
printf (" rec = %d ", rec);
Первые 12 байт в принятом буфере содержат MAC – адреса отправителя и получателя. Заполним структуру struct ethhdr eth адресной информацией:
memcpy ((char *) ð, buff, 12);
По смещению 14 в данном буфере расположены данные Ethernet-кадра – IP-пакет:
memcpy ((char *)&ip.pack, (buff + 14), ifp.mtu );
Проведем анализ принятого Ethernet-кадра.
if ((ip -> version) !=4) continue; – версия IP-протокола
MAC-адрес отправителя:
printf (" %.2x: %.2x: %.2x: %.2x: %.2x: %.2x -> ",
eth.h_source[0], eth.h_source[1], eth.h_source[2],
eth.h_source[3], eth.h_source[4], eth.h_source[5]);
MAC-адрес получателя:
printf (" %.2x: %.2x: %.2x: %.2x: %.2x: %.2x",
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); – длина заголовка IP-пакета
printf ("%d ", ntohs (ip -> tot_len)); – длина всего пакета
printf ("%d ", ip -> protocol); – протокол верхнего уровня
printf ("%s -> ", inet_ntoa (ip -> saddr)); – адрес источника
printf ("%s \n", inet_ntoa (ip -> daddr)); – адрес назначения
}
return (1);
}
Прием пакетов осуществляется с помощью функции recvfrom. Эта функция принимает данные через дескриптор e0_r. Принятое сообщение копируется в структуру ip_pack.
В принятом пакете первым следует заголовок Ethernet-кадра. По смещению 14 расположен IP-пакет. Поле «Версия» указывает тип данного пакета. Для IPv4-пакета данное поле содержит значение 4 в двоичной форме. Значение длины заголовка лежит в диапазоне между 20 и 60 байтами и находится в поле «Длина заголовка». Поле «Протокол» содержит идентификацию протокола следующего, более высокого уровня, содержащегося в разделе данных (т.е. в теле сообщения) данного IP-пакета. В документе RFC 1700 перечислены все значения, которые могут содержаться в поле «Протокол» в заголовке IP-пакета. Поле «Адрес источника» и поле «Адрес назначения» содержат соответственно IP-адрес отправителя пакета и IP-адрес предполагаемого получателя.
Дальнейшая обработка принятого пакета зависит от полей «Длина заголовка» и «Протокол». В принятом буфере по смещению, указанном в поле «Длина заголовка» (с учетом заголовка кадра Ethernet) будет расположен заголовок протокола следующего уровня. Его анализ аналогичен вышеприведенному анализу заголовка IP.
Для получения исполняемого модуля создадим Makefile следующего содержания:
# Компилятор С
CC = gcc
# Компилятор ассемблера
NASM = nasm
# Имя исполняемого модуля
name = ip
IP = ip.o check_snet.o getsock_recv.o getifconf.o
$(name): $(IP)
$(CC) -g -o $(name) $(IP)
ip.o: ip.c
$(CC) -c ip.c
check_snet.o: check_snet.asm
$(NASM) -f elf check_snet.asm
getsock_recv.o: getsock_recv.c
$(CC) -c getsock_recv.c
getifconf.o: getifconf.c
$(CC) -c getifconf.c
clean:
rm -f *.o
В файле ip.c находится главная функция программы. Командой make мы получим исполняемый модуль ip. Команда make clean удалит все объектные файлы. Результаты работы программы будут отображаться на консоли. Иногда это не совсем удобно, поэтому, немного модифицировав программу, результаты можно сохранять в файле.
Компиляцию производим с ключем -g для возможности последующей отладки. Надеюсь, что читателю это не понадобится, но все-таки хочу показать простой прием поиска неисправностей в программе (не только в этой). Иногда программа, хотя и компилируется без ошибок, при запуске выдает сообщение Segmentation fault и завершается. Для нашего примера, если программа работает некорректно, запустите исполняемый файл ip на отладку командой gdb ip. В командной строке отладчика наберите run и изучите информацию, которую выдаст отладчик. Он укажет место, где программа аварийно завершилась, и Вам останется только устранить неисправность. Если все в порядке, то перекомпилируйте программу с ключем -s.
В следующей статье мы рассмотрим, как можно передать принятый пакет, создав, тем самым, простейший шлюз.
Литература:
- Сэтчелл С., Клиффорд Х. LINUX IP Stacks в комментариях: Пер. с англ. - К.: Издательство «ДиаСофт», 2001 г. – 288 с.
- Теренс Чан. Системное программирование на С++ для UNIX: Пер. с англ. - К.: Издательская группа BHV, 1999 г. – 592 с.
- Gianluca Insolvibile. Kernel Korner: The Linux Socket Filter: Sniffing Bytes over the Network: http://www.linuxjournal.com/article.php?sid=4659