Programming with pcap
Tim Carstens
timcarst at yahoo dot com
Further editing and development by Guy Harris
guy at alum dot mit dot edu
Итак , для понимания существа дела нужны базовые знания по С.
Совсем необязательно быть гуру.Все будет достаточно хорошо разжевано.
Вам также помогут базовые знания по сетям, поскольку в данной статье речь пойдет о пакетном сниффере.
Представленный код был протестирован на FreeBSD 4.3.
Яковлев С :Для начала заберите библиотеку libpcap-1.0.0 и соберите ее.
Вы получите бинарник libpcap.a , который надо будет положить в каталог с примерами ,
иначе вы их не сможете собрать.
Getting Started: The format of a pcap application
-
Для начала нужно определиться с интерфейсом.
На линуксе это может быть eth0, на BSD это может быть xl1.
Устройство мы будем хранить в форме строки,или же можно попросить pcap,
чтобы оно само определило это устройство.
- Инициализация pcap.
Мы конкретно укажем pcap , что хотим сниффить сетевой интерфейс.
Если надо . то сразу несколько интерфейсов.
Различать мы их будем с помощью file handles.
Для работы с таким файлом нужно установить соответственную "сессию".
-
Наш снифинг будет распространяться только на TCP/IP-пакеты, проходящие через порт 23,
для этого нужно составить набор правил.
Правило хранится в строке и конвертируется во внутренний формат pcap.
-
Мы запускаем основной цикл сниффинга.pcap ловит достаточную порцию пакетов.
Каждый раз . когда он получает новый пакет , он вызывает соответственную функцию.
Она может распечатать пакет , сохранить его в файле.
-
Мы закрываем сессию и приложение
На самом деле все очень просто.5 шагов , один из которых - 3 - опционален.
Давайте глянем на реализацию :
Setting the device
Существует 2 подхода для инициализации девайса .
В первом случае пользователь набирает его в командной строке в качестве параметра программы :
#include <stdio.h>
#include <pcap.h>
int main(int argc, char *argv[])
{
char *dev = argv[1];
printf("Device: %s\n", dev);
return(0);
}
Теперь 2-й вариант :
#include <stdio.h>
#include <pcap.h>
int main(int argc, char *argv[])
{
char *dev, errbuf[PCAP_ERRBUF_SIZE];
dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
return(2);
}
printf("Device: %s\n", dev);
return(0);
}
В этом случае pcap сам устанавливает интерфейс.
Если при инициализации произойдет ошибка , она будет сохранена в строке errbuf.
Opening the device for sniffing
Создание сессии снифинга - достаточно простая задача.
Для этого мы используем pcap_open_live().
Её прототип :
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms,
char *ebuf)
Первый аргумент - устройство,второй - челое число, определяет максимальное количество байт,
которое может быть захвачено pcap,
третий аргумент - если он true , то переводит интерфейс в promiscuous mode.
Четвертый аргумент - определяет тайм-аут в миллисекундах, время ,
необходимое для фиксации определенного числа пакетов.
Пятый аргумент - строка ошибки.
Функция возвращает session handler.
Рассмотрим фрагмент :
#include <pcap.h>
...
pcap_t *handle;
handle = pcap_open_live(somedev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", somedev, errbuf);
return(2);
}
Устройство здесь хранится в переменной "somedev", количество байт - в BUFSIZ (см. pcap.h).
Устанавливаем устройство в promiscuous mode, делаем отлов пакетов до появления ошибки,
которая хранится в errbuf.
Несколько слов о promiscuous / non-promiscuous sniffing:
Это 2 различные техники.
При non-promiscuous sniffing, мы отловим только те пакеты , которые предназначены именно нам.
Все остальное будет просеяно.
При Promiscuous mode будет отловлен весь сетевой трафик.
В этом случае можно определить аналогичные узлы , которые занимаются тем же самым :-)
Promiscuous mode работает при условии non-switched (хаб или свитч не прокатят).
При интенсивном сетевом трафике Promiscuous mode может нагрузить машину по полной программе.
Filtering traffic
Сниффер можно использовать для специфических задач.
Например , прослушивание порта 23 (telnet) может дать информацию о паролях.
Через порт 21 (FTP) пересылаются файлы.
DNS traffic идет через порт 53 UDP.
Для захвата всего сетевого трафика нужно вызвать pcap_compile() и pcap_setfilter().
Далее мы вызываем pcap_open_live() и работаем с сессией.
Мы будем использовать BPF driver напрямую.
Перед использованием фильтра его нужно "скомпилировать".
Фильтовочное выражение хранится в строке (char array).
Для компиляции мы вызываем pcap_compile().
Прототип:
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize,
bpf_u_int32 netmask)
Первый аргумент - session handle (pcap_t *handle ).
Второй аргумент - ссылка на память , где будет храниться откомпилированная версия фильтра.
Дальше идет само выражение в виде строки
Следующий параметр - либо 0 , либо 1(оптимизация).
Последний параметр - маска сети.
В случае ошибки возвращается -1.
После этого идет функция pcap_setfilter():
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
1-й аргумент - session handler, 2-й - ссылка на скомпилированную версию выражения.
Код:
#include <pcap.h>
...
pcap_t *handle; /* Session handle */
char dev[] = "rl0"; /* Device to sniff on */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
struct bpf_program fp; /* The compiled filter expression */
char filter_exp[] = "port 23"; /* The filter expression */
bpf_u_int32 mask; /* The netmask of our sniffing device */
bpf_u_int32 net; /* The IP of our sniffing device */
if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
fprintf(stderr, "Can't get netmask for device %s\n", dev);
net = 0;
mask = 0;
}
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", somedev, errbuf);
return(2);
}
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
Этот пример захватывает весь трафик , проходящий через порт 23, в promiscuous mode, на устройстве rl0.
В этом примере функция pcap_lookupnet() в качестве входящего параметра берет имя устройства
и возвращает его IP и маску.
The actual sniffing
А теперь поговорим о захвате пакетов.
Есть 2 техники захвата : мы можем либо захватить один пакет за раз ,
либо группу пакетов в течение какого-то временного цикла.
Сначала рассмотрим первый способ , потом второй , для этого будем использовать pcap_next().
Прототип pcap_next():
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
1-й аргумент - session handler. 2-й - указатель на структуру , хранящую информацию о пакете :
время , длина пакета , длина порции или фрагмента .
pcap_next() возвращает указатель u_char на пакет.
Пример , показывающий использование pcap_next() для захвата пакета:.
#include <pcap.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pcap_t *handle; /* Session handle */
char *dev; /* The device to sniff on */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
struct bpf_program fp; /* The compiled filter */
char filter_exp[] = "port 23"; /* The filter expression */
bpf_u_int32 mask; /* Our netmask */
bpf_u_int32 net; /* Our IP */
struct pcap_pkthdr header; /* The header that pcap gives us */
const u_char *packet; /* The actual packet */
/* Define the device */
dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
return(2);
}
/* Find the properties for the device */
if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
fprintf(stderr, "Couldn't get netmask for device %s: %s\n", dev, errbuf);
net = 0;
mask = 0;
}
/* Open the session in promiscuous mode */
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", somedev, errbuf);
return(2);
}
/* Compile and apply the filter */
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
/* Grab a packet */
packet = pcap_next(handle, &header);
/* Print its length */
printf("Jacked a packet with length of [%d]\n", header.len);
/* And close the session */
pcap_close(handle);
return(0);
}
Устройство определяется с помощью pcap_lookupdev() в режиме promiscuous mode.
Находится 1-й пакет на 23-м порту (telnet) и выводится размер пакета в байтах.
Новая функция - pcap_close() - про нее попозже.
Другая техника захвата посложнее и возможно , более полезная.
Чаще вместо pcap_next() используется pcap_loop() или pcap_dispatch().
Для их понимания нужно усвоить , что такое callback function.
Принципиально в Callback functions нет ничего такого особенно нового.
Концепция такова : пусть у меня есть программа , которая ожидает событие от какого-то порта.
Допустим , мы ждем . когда пользователь нажмет на клавишу.
Каждый раз при нажатии клавиши вызывается функция - callback function.
Аналогичные функции используются в pcap,
но вместо ожидания нажатия они ждут , когда произойдет захват пакета.
Называются они pcap_loop() и pcap_dispatch().
Прототип pcap_loop() :
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
1-й аргумент - session handle.
2-й - указывает , сколько пакетов для pcap_loop() войдет в одну порцию.
3-й - имя самой callback function , без параметров.
4-й - обычно NULL.
Функция pcap_dispatch() почти индентична.
Разница лишь в том , что pcap_dispatch() получает лишь первую порцию пакетов ,
в то время как pcap_loop() будет продолжать до тех пор ,
пока счетчик не обнулится.
Перед тем как показать пример использования pcap_loop(),
нужно проверить формат callback function.
Нельзя произвольно определять прототип callback's prototype.
Прототип нашей callback function:
void got_packet(u_char *args, const struct pcap_pkthdr *header,
const u_char *packet);
Тип у функции - void.
1-й аргумент соответствует последнему аргументу функции pcap_loop() и передается оттуда каждый раз.
2-й аргумент - pcap header.
Структура pcap_pkthdr определена в pcap.h :
struct pcap_pkthdr {
struct timeval ts; /* time stamp */
bpf_u_int32 caplen; /* length of portion present */
bpf_u_int32 len; /* length this packet (off wire) */
};
Наиболее интересен в got_packet последний аргумент.
Это еще один указатель на u_char, и он указывает на первый байт данных пакета,
который захвачен функцией pcap_loop().
Он представляет из себя набор структур - Ethernet header, IP header, TCP header.
Этот указатель указывает на сериализованную версию этих структур.
Чтобы использовать эти структуры , надо использовать преобразование - typecasting.
Определения этих структур :
/* Ethernet addresses are 6 bytes */
#define ETHER_ADDR_LEN 6
/* Ethernet header */
struct sniff_ethernet {
u_char ether_dhost[ETHER_ADDR_LEN]; /* Destination host address */
u_char ether_shost[ETHER_ADDR_LEN]; /* Source host address */
u_short ether_type; /* IP? ARP? RARP? etc */
};
/* IP header */
struct sniff_ip {
u_char ip_vhl; /* version << 4 | header length >> 2 */
u_char ip_tos; /* type of service */
u_short ip_len; /* total length */
u_short ip_id; /* identification */
u_short ip_off; /* fragment offset field */
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
u_char ip_ttl; /* time to live */
u_char ip_p; /* protocol */
u_short ip_sum; /* checksum */
struct in_addr ip_src,ip_dst; /* source and dest address */
};
#define IP_HL(ip) (((ip)->ip_vhl) & 0x0f)
#define IP_V(ip) (((ip)->ip_vhl) >> 4)
/* TCP header */
struct sniff_tcp {
u_short th_sport; /* source port */
u_short th_dport; /* destination port */
tcp_seq th_seq; /* sequence number */
tcp_seq th_ack; /* acknowledgement number */
u_char th_offx2; /* data offset, rsvd */
#define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4)
u_char th_flags;
#define TH_FIN 0x01
#define TH_SYN 0x02
#define TH_RST 0x04
#define TH_PUSH 0x08
#define TH_ACK 0x10
#define TH_URG 0x20
#define TH_ECE 0x40
#define TH_CWR 0x80
#define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
u_short th_win; /* window */
u_short th_sum; /* checksum */
u_short th_urp; /* urgent pointer */
};
Кстати:
Тут автор пишет , что на Slackware Linux 8 box (stock kernel 2.2.19) (какая древность :-))
это вообще не компилится.
Проблема оказалась в include/features.h.
Для разрешения проблемы нужно было определить
#define _BSD_SOURCE 1
Можно также использовать альтернативную TCP header structure, которая лежит тут :
here.
У меня на 10-й сузе все собралось на ура.
Эти хидеры фигурируют в данных пакета.
Как выудить данные из TCP пакета ?
/* ethernet headers - длина всегда 14 байт */
#define SIZE_ETHERNET 14
const struct sniff_ethernet *ethernet; /* The ethernet header */
const struct sniff_ip *ip; /* The IP header */
const struct sniff_tcp *tcp; /* The TCP header */
const char *payload; /* Packet payload */
u_int size_ip;
u_int size_tcp;
И теперь магическое преобразование :
ethernet = (struct sniff_ethernet*)(packet);
ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);
size_ip = IP_HL(ip)*4;
if (size_ip < 20) {
printf(" * Invalid IP header length: %u bytes\n", size_ip);
return;
}
tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);
size_tcp = TH_OFF(tcp)*4;
if (size_tcp < 20) {
printf(" * Invalid TCP header length: %u bytes\n", size_tcp);
return;
}
payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
u_char - это указатель на адрес памяти.
Если значение этого указателя равно X , то адрес структуры sniff_ethernet
тоже равно X, отсюда мы можем найти адрес следующей структуры -
X плюс длина Ethernet header,равная 14 ,или SIZE_ETHERNET.
Далее аналогично получаем адрес IP header, но он
не имеет фиксированной длины;
длина равна числу 4-байтных слов.
Это число умножаем на 4 и получаем длину следующей структуры .
Минимальная длина - 20 байт.
TCP header также имеет переменную длину;его длина - это "data offset" TCP header,
и минимум это тоже 20 байт.
Таблица :
Variable |
Location (in bytes) |
sniff_ethernet |
X |
sniff_ip |
X + SIZE_ETHERNET |
sniff_tcp |
X + SIZE_ETHERNET + {IP header length} |
payload |
X + SIZE_ETHERNET + {IP header length} + {TCP header length} |
Структура sniff_ethernet = X.
sniff_ip = X + 14 байт, или SIZE_ETHERNET).
sniff_tcp = X +14 + (4 * IP header length).
Теперь мы знаем , как установить callback function, вызвать ее,
и вычислить атрибуты захваченного пакета.
Исходники лежат тут
|
Galaran | большое спасибо за перевод) 2009-08-21 07:31:42 | doctor | отличная статья, спасибо 2011-06-14 16:29:16 | дима | спасибо, то что надо! 2012-06-15 17:05:37 | Alatar | Статья хорошая в качестве вводной, но следует отметить, что код магического преобразования очень условный -
он подразумевает, что Вы точно уверены, что это фрейм Ethernet DIX, он содержит именно IP, а он, в свою очередь, - именно TCP.
Надо помнить, что на самом деле SIZE_ETHERNET тоже не константа, а между эзернетом и IP могут прятаться инкапсулирующие протоколы,
например VLAN_TAG. 2013-08-29 19:02:49 | |
|