Введение 

Попробуем для начала разобраться с самим понятием
снифер. Откуда оно взялось и что это такое? Разберемся с терминологией. Слово sniffer (дословно с английского это можно перевести как «нюхатель») в самом общем смысле суть некое прослушивающее устройство, внедренное в сеть для перехвата передаваемых по ней данных. В некоторой литературе и документации, а также в электронных словарях типа Lingvo, термин sniffer (network sniffer) отождествляется с такими понятиями, как «анализатор сетевого трафика», «анализатор пакетов», «анализатор протоколов», «сетевой анализатор». А в более узком смысле — это ПО, созданное для мониторинга сетевого потока и/или его анализа. Изначально такие программы задумывались в помощь системным администраторам для анализа проблем сети. Но
сниферы, как большинство программного обеспечения для сисадминов, широко распространились и среди хакеров. Ведь такие вещи можно использовать и для перехвата паролей и другой незашифрованной информации, приходящей из сети, и исходящей в Сеть.

Что мы будем делать 

Попробуем разобрать один из способов работы
снифера и напишем такую программу сами. Это будет несложная программа без всяких драйверов, работающая в системе Windows 2000 и выше и требующая прав администратора. Такие ограничения, конечно, не вдохновляют, но все же если писать программу не для корыстных целей, то такие условия достаточно приемлемы. В дальнейшем будет ясно, ЧТО и КАК нас ограничивает. Для перехвата входящего трафика нашему
сниферу не потребуются внешний *.sys и *.dll файлы. Будут использоваться исключительно возможности операционной системы. Условимся сразу, что наш
снифер должен отлавливать приходящие пакеты с самым популярным набором протоколов — таких, как IP, TCP, UDP, ICMP, IGMP, GGP, IPv6, ICMPv6. 

Пишем простой снифер 

Начнем с того, что нам нужно написать снифер, который можно будет настраивать. Сделаем фильтр по протоколу, т.е. не будем пропускать в лог пакеты, не относящиеся к интересующему нас протоколу. Также реализуем возможности записи в лог IP получателя (наш), IP отправителя, имени протокола и длины пакета. В нашей программе, помимо этого, можно будет задавать имя файла лога. Он будет храниться в той же директории, что и наш
снифер. После всего, что мы сделаем, окно нашей программы будет выглядеть так:

Теперь перейдем к рассмотрению кода. Полный исходник нашей программы и ее рабочий вариант можно скачать по ссылке в конце статьи. Напишем программу на Visual C++ 6.0. Выбор этой среды разработки обоснован тем, что она мне по душе.

Начинаем, естественно, с инициализации сокетов. Пусть это будет WinSock 2.2:

WSADATA wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);

Затем создаем сам сокет:

SOCKET s = socket(AF_INET, SOCK_RAW, IPPROTO_IP);

Получаем имя нашего локального хоста и информацию о нем:

CHAR szHostName[16];
gethostname(szHostName, sizeof szHostName);

HOSTENT *phe = gethostbyname(szHostName);

SOCKADDR_IN sa;
ZeroMemory(&sa, sizeof sa);
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = ((struct in_addr*)phe->h_addr_list[0])->s_addr;

Теперь необходимо привязать локальный адрес к нашему сокету:

bind(s, (SOCKADDR*)&sa, sizeof SOCKADDR);

И, наконец, самое главное — включение режима promiscuous, о котором мы говорили ранее:

DWORD flag = TRUE;
ioctlsocket(s, SIO_RCVALL, &flag);

Здесь мы включаем режим приема всех приходящих из Сети пакетов, указанием в качестве команды сокету SIO_RCVALL. Объявление этой константы находится в заголовочном файле Mstcpip.h. Переменная flag включает и выключает режим (TRUE/FALSE). В документации Microsoft про эту команду сказано следующее:

Enables a socket to receive all IP packets on the network. The socket handle passed to the WSAIoctl function must be of AF_INET address family, SOCK_RAW socket type, and IPPROTO_IP protocol. The socket also must be bound to an explicit local interface, which means that you cannot bind to
INADDR_ANY.

Что в буквальном смысле означает: команда SIO_RCVALL позволяет сокету принимать все IP пакеты из Сети. Дескриптор сокета (в нашем случае это s), переданный в функцию WSAIoctl (мы используем просто ioctlsocket), должен быть из семейства адресов AF_INET (internetwork), тип этого сокета должен быть SOCK_RAW и протокол — IPPROTO_IP. Сокет также должен быть связан с явным локальным интерфейсом, т.е. нельзя его связать просто с INADDR_ANY. Соблюдая эти правила, мы и создаем сокет с дескриптором s. Очень важно понимать, что Raw-сокеты могут принимать множество неожиданных пакетов.

Заметим, что режим promiscuous включается только при активной системной плате или при выходе в Internet. В общем, он возможен, только когда есть какая-либо Сеть, будь то Internet или LAN. Если мы вызовем функцию ioctlsocket с параметром SIO_RCVALL на машине без сети, то получим ошибку.

Теперь попытаемся понять, почему наш снифер требует привилегий администратора. Microsoft пишет по этому поводу следующее:

Setting this ioctl requires Administrator privilege on the local computer. SIO_RCVALL is available in Windows 2000 and later versions of Windows. 

Теперь должно быть ясно, что явно требуются привилегии администратора. Также выясняется и то, что команда SIO_RCVALL доступна, начиная с Windows 2000. Если
снифер запустит не администратор, функция bind не даст привязать сокет к локальному адресу. Она возвратит ошибку WSAEACCES (10013 в десятичной системе — не достаточно прав). Причем в MSDN сказано, что можно поднять свои привилегии включением опции сокета SO_EXCLUSIVEADDRUSE:

Possible reason for the WSAEACCES error is that when the bind function is called (on Windows NT 4 SP4 or later), another application, service, or kernel mode driver is bound to the same address with exclusive access. Such exclusive access is a new feature of Windows NT 4 SP4 and later, and is implemented by using the SO_EXCLUSIVEADDRUSE option. 

Т.е. мы могли бы просто вызвать setsockopt с параметром SO_EXCLUSIVEADDRUSE следующим образом:

BOOL bOptVal = TRUE;
setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&bOptVal, sizeof(BOOL));

Но, поэкспериментировав с этим, я не пришел к положительному результату, т.е. по-прежнему нам требуются привилегии администратора.

Итак, режим promiscuous включен. Как же нам получить входящие IP пакеты? Очевидно, нужно постоянно вызывать функцию recv (в нашем случае) или WSARecv. Но создавать отдельный поток и делать бесконечный цикл было бы не очень удобно. Гораздо лучше для нас использовать асинхронные сокеты. Тем более, что у нас GUI-программа. Впишем следующий вызов функции WSAAsyncSelect в обработчик кнопки «Start» (эта кнопка не будет доступна, пока promiscuous mode не включен):

case IDC_BUTTON_START:
CHAR szFileName[_MAX_FNAME];

GetDlgItemText(hwndDlg, IDC_EDIT_LOG_FILENAME, szFileName, _MAX_FNAME);

hLogFile = CreateFile(szFileName, GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);
if (hLogFile == INVALID_HANDLE_VALUE) {
//…
} else {
//…

SetFilePointer(hLogFile, 0, NULL, FILE_END);

//…

//Связываем событие FD_READ с окном
WSAAsyncSelect(s, hwndDlg, WM_RECV, FD_READ);

//…
}
break;

Здесь мы указываем функции, что окну с хендлом hwndDlg нужно отправить сообщение WM_RECV при наступлении события FD_READ для сокета s. Событие FD_READ приходит, когда сокет готов к чтению данных, т.е. когда приходят данные. Сообщение WM_RECV объявим следующим образом:

#define WM_RECV (WM_USER + 1)

В MSDN про вызов функции recv написано следующее:

Once the socket is bound and the ioctl set, calls to the WSARecv or recv functions return IP datagrams passing through the given interface. Note that you must supply a sufficiently large buffer. 

Т.е. мы должны предоставить функции recv буффер достаточного размера. В нашем
снифере он будет равен 64 Кб. Теперь напишем обработчик сообщения
WM_RECV:

case WM_RECV:
if (WSAGETSELECTEVENT(lParam) == FD_READ) {
//
Буфер размера 64 Кб
CHAR btBuffer[65536];

//Получаем входящие данные
if (recv(s, btBuffer, sizeof(btBuffer), 0) >= sizeof(IPHeader)) {
IPHeader* hdr = (IPHeader*)btBuffer;

//Вычисляем размер. Т.к. в сети принят прямой порядок байт,
//
а не обратный, то придется поменять байты местами.
WORD size = (hdr->iph_length << 8) + (hdr->iph_length >> 8);

//Получен пакет?
if (size >= 60 && size <= 1500) {
//
Проверяем протокол
if (IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_ALL) != TRUE) {
if (IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_IP) == TRUE && hdr->iph_protocol == IPPROTO_IP) {
} else if (hdr->iph_protocol == IPPROTO_ICMP && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_ICMP) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_IGMP && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_IGMP) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_GGP && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_GGP) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_TCP && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_TCP) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_PUP && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_PUP) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_UDP && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_UDP) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_IDP && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_IDP) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_IPV6 && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_IPv6) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_ND && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_ND) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_ICLFXBM && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_ICLFXBM) == TRUE) {
} else if (hdr->iph_protocol == IPPROTO_ICMPV6 && IsDlgButtonChecked(hwndDlg, IDC_CHECK_PROTO_ICMPv6) == TRUE) {
} else {
return TRUE;
}
}

DWORD dwWritten;

//Записываем данные
WriteFile(hLogFile, «—Packet begin—\r\n», 18, &dwWritten, 0);

if (IsDlgButtonChecked(hwndDlg, IDC_CHECK_OPTION_LOG_TARGET_IP) == TRUE) {
IN_ADDR ia;

ia.s_addr = hdr->iph_dest;
CHAR *pszTargetIP = inet_ntoa(ia);

WriteFile(hLogFile, «To: «, 4, &dwWritten, 0);
WriteFile(hLogFile, pszTargetIP, lstrlen(pszTargetIP), &dwWritten, 0);
WriteFile(hLogFile, «\r\n», 2, &dwWritten, 0);
}

if (IsDlgButtonChecked(hwndDlg, IDC_CHECK_OPTION_LOG_SENDER_IP) == TRUE) {
IN_ADDR ia;

ia.s_addr = hdr->iph_src;
CHAR *pszSourceIP = inet_ntoa(ia);

WriteFile(hLogFile, «From: «, 6, &dwWritten, 0);
WriteFile(hLogFile, pszSourceIP, lstrlen(pszSourceIP), &dwWritten, 0);
WriteFile(hLogFile, «\r\n», 2, &dwWritten, 0);
}

if (IsDlgButtonChecked(hwndDlg, IDC_CHECK_OPTION_WRITE_PROTO_NAME) == TRUE) {
WriteFile(hLogFile, «Protocol: «, 10, &dwWritten, 0);

switch (hdr->iph_protocol) {
case IPPROTO_IP:
WriteFile(hLogFile, «IP\r\n», 4, &dwWritten, 0);
break;

case IPPROTO_ICMP:
WriteFile(hLogFile, «ICMP\r\n», 6, &dwWritten, 0);
break;

case IPPROTO_IGMP:
WriteFile(hLogFile, «IGMP\r\n», 6, &dwWritten, 0);
break;

case IPPROTO_GGP:
WriteFile(hLogFile, «GGP\r\n», 5, &dwWritten, 0);
break;

case IPPROTO_TCP:
WriteFile(hLogFile, «TCP\r\n», 5, &dwWritten, 0);
break;

case IPPROTO_PUP:
WriteFile(hLogFile, «PUP\r\n», 5, &dwWritten, 0);
break;

case IPPROTO_UDP:
WriteFile(hLogFile, «UDP\r\n», 5, &dwWritten, 0);
break;

case IPPROTO_IDP:
WriteFile(hLogFile, «IDP\r\n», 5, &dwWritten, 0);
break;

case IPPROTO_IPV6:
WriteFile(hLogFile, «IPv6\r\n», 6, &dwWritten, 0);
break;

case IPPROTO_ND:
WriteFile(hLogFile, «ND\r\n», 4, &dwWritten, 0);
break;

case IPPROTO_ICLFXBM:
WriteFile(hLogFile, «ICLFXBM\r\n», 9, &dwWritten, 0);
break;

case IPPROTO_ICMPV6:
WriteFile(hLogFile, «ICMPv6\r\n», 8, &dwWritten, 0);
break;
}
}

if (IsDlgButtonChecked(hwndDlg, IDC_CHECK_OPTION_WRITE_PACKET_LEN) == TRUE) {
CHAR szTemp[17];

WriteFile(hLogFile, «Packet length: «, 15, &dwWritten, 0);
wsprintf(szTemp, «%d\r\n», size);
WriteFile(hLogFile, szTemp, lstrlen(szTemp), &dwWritten, 0);
}

WriteFile(hLogFile, «Contents:\r\n\r\n», 13, &dwWritten, 0);
WriteFile(hLogFile, &btBuffer[sizeof(IPHeader) * 2], size — sizeof(IPHeader) * 2, &dwWritten, 0);
WriteFile(hLogFile, «\r\n—Packet end—\r\n», 18, &dwWritten, 0);

SetDlgItemInt(hwndDlg, IDC_EDIT_LOG_SIZE, GetFileSize(hLogFile, NULL), FALSE);
}
}
return TRUE;

Здесь, помимо приема пакета, у нас реализован фильтр по протоколу. Нам не составило бы труда реализовать и фильтр по размеру пакета, по IP адресам, но реализацию этих фильтров я оставляю читателю. Заголовок пакета описывается структурой IPHeader. Она выглядит следующим образом:

//Структура заголовка IP-пакета
typedef struct IPHeader {
UCHAR iph_verlen; // версия и длина заголовка
UCHAR iph_tos; // тип сервиса
USHORT iph_length; // длина всего пакета
USHORT iph_id; // Идентификация
USHORT iph_offset; // флаги и смещения
UCHAR iph_ttl; // время жизни пакета
UCHAR iph_protocol; // протокол
USHORT iph_xsum; // контрольная сумма
ULONG iph_src; // IP-адрес отправителя
ULONG iph_dest; // IP-адрес назначения
}IPHeader;

Тестирование снифера 

Теперь посмотрим на получившуюся программу в работе. Для тестирования ее работы я решил использовать подключение к интернету, а также виртуальную локальную сеть. Virtual LAN удобно использовать
когда нет реальной LAN, а работу своей программы необходимо протестировать. Виртуальную LAN довольно удобно настраивать с помощью VMware Workstation (у меня версия 5.5.0). Смоделируем локальную сеть путем установки на виртуальную машину системы Windows 2000 Pro. На хост-машине пусть будет Windows XP SP2. После настройки LAN менеджером виртуальных сетей в VMware, получаем локальную сеть из 2-х компьютеров.

Перед тестированием нужно обязательно не забыть выключить Firewall, т.к. он блокирует входящие пакеты. Убедиться в этом можно, пропинговав машины в обе стороны.

Меня всегда интересовало, как выглядит сообщение при его отправке по локальной сети командой net send. Но нельзя забывать о том, что эта команда работает лишь при включенной службе Messenger. Попробуем послать с виртуальной машины (Windows 2000) сообщение на хост машину (Windows XP SP2) «This is my message!»…

…и посмотрим в созданные нашим снифером логи:

log1.log

Среди прочего мусора, мы видим, что сообщение прибыло в 3-ем пакете. А в 4-ом оно, видимо, завершилось.

Теперь попробуем вариант с Internet’ом. Зайдем на сайт www.google.ru и проверим почтовый ящик, находящийся на pop.mail.ru:

log2.log

Из данного лога видно все HTML-содержимое главной страницы www.google.ru + содержимое *.gif файлов. Также мы видим ответы smtp- и pop3-сервера.

Итог 

Мы рассмотрели один из самых простых способов создания программ перехвата входящего трафика. Полученные в результате прочтения данной статьи знания можно использовать в различных целях, будь то тестирование своих сетевых программ, наблюдение за кем-либо или просто ради интереса. Теперь мы точно знаем, что Microsoft предоставляет возможности для создания программ данного типа, хотя и с урезанными привилегиями. Но в большинстве случаев этого бывает достаточно для достижения поставленных целей.

Снифер
Исходник

2 комментария

  1. 09.03.2015 at 23:27

    А как сделать такую же программу, только на c#, и что бы она перехватывала html-трафик?

  2. Fakir

    11.10.2015 at 17:20

    Здравствуйте ! Возникла небольшая проблема. Принимает пакеты только по UDP протоколу. Мб это не только у меня одного ?

Оставить мнение

Check Also

Идеальная форма. Обрабатываем сложные формы на Python с помощью WTForms

Обработка HTML-форм в веб-приложениях — несложная задача. Проблемы начинаются, когда форма…