From: Bob <ubob at mail.ru>
Newsgroups: email
Date: Mon, 23 Jun 2004 14:31:37 +0000 (UTC)
Subject: Пишем анализатор сетевого трафика и пакетный фильтр
АНАЛИЗАТОР СЕТЕВОГО ТРАФИКА
1. Введение
-----------
Если вы - системный администратор, специалист по безопасности, или вам
просто интересно, что происходит в вашей локальной сети, то перехват и
анализ нескольких сетевых пакетов может быть полезным упражнением. При
помощи небольшой программы на языке С и базовых знаний сетевых
технологий вы сможете перехватить данные сетевого траффика, даже если
они адресованы не вам. В данной статье рассмотрено, как это можно
сделать в сети Ethernet, наиболее распространенной на данный момент
технологии построения локальных компьютерных сетей.
2. Обзор технологии Ethernet
----------------------------
Для начала вспомним, как функционирует сеть Ethernet (те из вас, кто
знаком с данным вопросом, могут пропустить этот параграф). IP-пакеты
(дейтаграммы), источником которых является приложение пользователя,
инкапсулируются в Ethernet-кадры (пакеты Ethernet-протокола,
передаваемые в сети). Каждый кадр содержит исходный IP-пакет и другую
информацию, необходимую для доставки его адресату, в частности, 6-ти
байтовый Ethernet-адрес (MAC-адрес) назначения, который при помощи
протокола ARP ставится в соответствие IP-адресу назначения. Таким
образом, сформированный кадр, содержащий пакет, начинает свое
путешествие от хоста-отправителя к хосту-получателю через кабельное
соединение.
На уровне протокола Ethernet маршрутизация отсутствует. Другими словами,
кадр, отправленный хостом-отправителем, не попадает напрямую
хосту-получателю, а будет доступен для всех хостов, объединенных в сеть.
Каждая сетевая карта принимает кадр и считывает из него первые 6 байт.
Эти байты содержат MAC-адрес хоста-получателя, но только одна карта в
сети определит его как свой собственный, и передаст кадр для дальнейшей
обработки сетевому драйверу. Сетевой драйвер проверит поле "Тип
протокола" заголовка кадра, и, основываясь на этом значении, направит
инкапсулированный пакет соответствующей приемной функции данного
протокола. В большинстве случаев это протокол IP. Приемная функция
изымает IP заголовок из принятой дейтаграммы и передает
инкапсулированное сообщение соответствующему модулю протокола
транспортного уровня (например, TCP или UDP). Эти протоколы, в свою
очередь, обрабатывают свои заголовки и передают данные протоколам
прикладного уровня. В течении этой "экскурсии" по различным уровням
сетевого стека исходный пакет теряет все служебные поля протоколов и в
конце концов данные, передаваемые в пакете, принимаются пользовательским
приложением.
3. Пакетные сокеты
------------------
Есть много способов перехватить данные сетевого трафика. Самым
оптимальным является использование специализированных библиотек,
например, 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-адрес) удаляется перед тем, как пакет
отправляется на обработку пользователю.
4. Пример программной реализации анализатора
--------------------------------------------
Разработаем сетевой анализатор, который будет функционировать по следующему
алгоритму:
- после запуска на выполнение анализатор определяет параметры сетевого
интерфейса 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. Файл 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;
Рассмотрим подробно каждую функцию.
/*
* Листинг 2. Функция определения параметров сетевого интерфейса и
* переключения режимов (файл getifconf.c)
*/
/*
* Проверяем флаг режима. Если он установлен в 0, неразборчивый режим
* необходимо отключить, поэтому сразу выполняется переход на метку setmode
*/
if(!mode) goto setmode;
/*
* В зависимости от значения третьего параметра функции, устанавливаем
* или снимаем флаг неразборчивого режима
*/
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>.
/*
* Листинг 3. Функция создания пакетного сокета (файл getsock_recv.c)
*/
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;
/*
* Заполним поля адресной структуры 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().
/*
* Листинг 4. Главная функция (файл analizator.c)
*/
/*
* В буфере 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;
}
После запуска на выполнение программа переводит интерфейс в неразборчивый режим
и запускает бесконечный цикл прослушивания сетевого трафика и отображения
результатов. Факт включения неразборчивого режима интерфейса будет зафиксирован
в файле /var/log/messages:
Бесконечный цикл работы анализатора будет прерван после нажатия комбинации
клавиш Ctrl-C. Программа получит сигнал SIGINT, и обработчик этого сигнала
функция mode_off() снимет флаг неразборчивого режима с интерфейса. При этом
в файле /var/log/messages появится запись:
Во время работы анализатор будет принимать все пакеты, циркулирующие в сети,
и отображать информацию о каждом принятом пакете. Если нас интересуют пакеты,
принадлежащие определенным хостам, необходимо анализировать адресную часть
каждого принятого пакета. Это может делать сам анализатор при помощи оператора
условия if. Но лучше возложить эту функцию на ядро ОС.
Отсеять лишний трафик можно, используя средства Linux Socket Filter (LSF).
LSF позволяет создать и подключить непосредственно к сокету фильтр, который
представляет собой последовательность инструкций следующего формата
(см. <linux/filter.h>):
LSF является наследником Berkeley Packet Filter (BPF) - языка,
разработанного Стивом Маккеном (Steve McCanne) и Ван Якобсоном (Van Jacobson).
Коды инструкций LSF идентичны BPF, подробную информацию обо всех инструкциях
можно получить, обратившись к BSD bfp.4 manpage, либо на opennet.ru.
Опишем основные типы и приведем примеры использования инструкций LSF.
BPF_LD
Инструкция BFP_LD служит для загрузки в аккумулятор следующих величин:
- константы (BPF_IMM)
- значения, расположенного в блоке данных по определенному смещению
(BPF_ABS, BPF_IND)
- значения, находящегося в ячейке памяти (BPF_MEM)
Для значений BPF_IND и BPF_ABS размер загружаемых данных должен быть задан как
слово (BPF_W), полуслово (BPF_H), байт (BPF_B). Здесь имеется ввиду машинное
слово, которое равно числу разрядов в регистрах общего назначения. Для 32-х
разрядных процессоров это значение составляет 4 байта.
Примеры использования данной инструкции:
- загрузить из блока данных в аккумулятор 4/2/1 байт. Смещение в блоке данных
задается значением k:
- загрузить из блока данных в аккумулятор 4/2/1 байт. Смещение в блоке данных
задается суммой значений k и значения, находящегося в индексном регистре:
- загрузить в аккумулятор значение, находящееся в ячейке памяти с адресом K:
{(BPF_LD + BPF_MEM), 0, 0, k} A <- M[k]
BPF_LDX
Инструкция BPF_LDX служит для загрузки в индексный регистр следующих величин:
- константы (BPF_IMM)
- значения, находящегося в ячейке памяти (BPF_MEM)
- длины заголовка IP пакета (BPF_MSH)
Примеры использования данной инструкции:
- загрузить в индексный регистр константу K:
{(BPF_LDX + BPF_IMM), 0, 0, k} X <- k
- загрузить в индексный регистр длину заголовка принятого IP пакета:
Длина заголовка находится в младших 4 битах первого байта IP пакета и содержит
количество 32-х битных слов в заголовке. Поскольку минимальный размер заголовка
равен 20 байт, то минимальное значение этого поля равно 5 (т.е. пять 32-х
разрядных слов). Старшие 4 бита содержат версию протокола, и для IPv4 это
значение равно 4. Итак, предположим, что первый байт содержит значение 0x45.
Выполним следующие арифметико-логические действия:
(0x45 & 0x0F) * 4 = 0x14
В итоге получаем 0x14, в десятичном представлении - 20. Это и есть искомая длина
IP заголовка.
BPF_JMP
Инструкция BPF_JMP изменяет порядок выполнения программы фильтрации. Данная
инструкция может осуществлять как условный, так и безусловный переход. При
безусловном переходе смещение задается 32-х разрядным значением, при условном -
8-разрядным. Смещение представляет собой число инструкций, которое должна
пропустить программа фильтрации.
Примеры использования данной инструкции:
- безусловный переход по смещению, заданному значением K:
{(BPF_JMP + BPF_JA), 0, 0, k} pc += k
- сравнение значений аккумулятора и константы K. Условный переход по смещению,
заданному в поле jt при выполнении условия A > k:
{(BPF_JMP + BPF_JGT + BPF_K), jt, jf, k} pc += (A > k)?jt:jf
BPF_RET
Программа фильтрации выполняется для каждого пакета, поступающего на сетевой
интерфейс. Результатом работы фильтра является целое положительное число,
показывающее, сколько байт в принятом пакете будет доступно для дальнейшей
обработки приложению пользователя. Если принятый пакет не удовлетворяет условиям
фильтрации, программа фильтрации вернет нулевое значение.
Пример использования данной инструкции:
- вернуть для дальнейшей обработки приложению K байт
{(BPF_RET + BPF_K), 0, 0, k}
Все значения BPF_xxx определены в файле <linux/filter.h>
6. Пример программной реализации пакетного фильтра
--------------------------------------------------
Модифицируем код функции getsock_recv(), подключив к создаваемому сокету
фильтр. Подключаемый фильтр описывает следующая структура (см. <linux/filter.h>):
struct sock_fprog
{
unsigned short len; /* Number of filter blocks */
struct sock_filter *filter;
};
Здесь struct sock_filter *filter - это программа фильтрации, представляющая
собой массив структур struct sock_filter.
Перед тем, как составлять программу фильтрации, определим условия фильтрации:
принимать пакеты IP протокола, адрес отправителя - 192.168.1.2, транспортный
протокол - TCP, порт - telnet.
При составлении программы фильтрации воспользуемся следующими
макроопределениями (см. <linux/filter.h>):
/*
* В принятом Ethrnet-кадре по смещению, равному 12 байт (6 байт MAC-адреса
* отправителя + 6 байт MAC-адреса получателя) находится 2-х байтовый
* идентификатор протокола сетевого уровня. Эти 2 байта мы загружаем в
* аккумулятор
*/
BPF_STMT(BPF_LD+BPF_H+BPF_ABS, 12),
/*
* Проверяем соответствие значения, загруженного в аккумулятор, идентификатору
* IP протокола (ETH_P_IP). При выполнении условия равенства этих значений
* переходим к следующей инструкции (jt = 0). В противном случае смещаемся
* на 8 инструкций вниз (jf = 8) и выходим из программы фильтрации с возвратом
* нулевого значения
*/
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_IMM, ETH_P_IP, 0, 8),
/*
* Загружаем в аккумулятор 4-х байтовое значение, находящееся по смещению 26
* в принятом пакете. Это значение соответствует IP адресу источника
*/
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, 26),
/*
* Проверяем соответствие значения, загруженного в аккумулятор, IP адресу
* 192.168.1.2, в шестнадцатеричном представлении - 0xC0A80102. В сетевом
* формате этот адрес выглядит как 0x0201A8C0. Это связано с порядком передачи
* данных в сети - передача начинается с байта младшего разряда. Если адреса не
* совпали - выходим из программы фильтрации
*/
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_IMM, 0xC0A80102, 0, 6),
/*
* Загружаем в аккумулятор 1 байт, находящийся по смещению 23. Этот байт
* содержит идентификатор протокола транспортного уровня
*/
BPF_STMT(BPF_LD+BPF_B+BPF_ABS, 23),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_IMM, IPPROTO_TCP, 0, 4),
/*
* Определяем длину IP заголовка
*/
BPF_STMT(BPF_LDX+BPF_B+BPF_MSH, ETH_HLEN),
/*
* Значение длины IP заголовка загружено в индексный регистр. Порт источника
* находится по смещению, определяемому суммой длин IP и Ethernet заголовков
*/
BPF_STMT(BPF_LD+BPF_H+BPF_IND, ETH_HLEN),
В приведенном примере есть существенный недостаток - исходные данные для
фильтрации (IP адрес и порт) вводятся непосредственно в текст программы.
Модифицируем функцию getsock_recv() для возможности гибкой настройки на новые
условия фильтрации. Параметры фильтрации - IP адрес и порт - будут передаваться
из главной функции. Прототип функции getsock_recv() принимает следующий вид:
int getsock_recv (int, __u8 *, int)
Первый параметр не изменился - это индекс сетевого интерфейса. Второй параметр -
указатель на строку, содержащую IP адрес, третий параметр - значение порта.
В массиве BPF_code третий и восьмой элементы перепишем в следующем виде:
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_IMM, 0, 0, 6), // 3-й элемент
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_IMM, 0, 0, 1), // 8-й
Здесь константы, содержащие значение IP адреса и порта, обнулены.
Заполним их значениями, переданными из главной функции:
Строковое значение IP адреса мы преобразуем в сетевой формат, а затем при
помощи макроопределения __swab32 (см. файл <linux/byteorder/swab.h>) меняем
местами байты - нулевой с третьим, второй с первым.
В итоге функция getsock_recv() будет выглядеть следующим образом:
/*
* Листинг 6. Функция создания пакетного сокета и подключения
* пакетного фильтра (файл getsock_recv.c)
*/
/* Цикл приема пакетов и отображения результатов */
....
}
При запуске анализатора в командной строке задаются параметры - IP адрес
отправителя и порт.
Составление программы фильтрации можно существенно упростить, если
воспользоваться услугами tcpdump. Например, составим программу фильтрации,
принимающую пакеты, источником которых является хост 192.168.1.1,
транспортный протокол - TCP, порт - 23. Вводим команду: